diff --git a/.clang-tidy b/.clang-tidy index 7b682391..f8817f1d 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -30,10 +30,12 @@ Checks: "*, -cppcoreguidelines-avoid-do-while, -bugprone-easily-swappable-parameters, -misc-non-private-member-variables-in-classes, - -llvm-header-guard," + -llvm-header-guard, + + -misc-include-cleaner" CheckOptions: cppcoreguidelines-pro-type-member-init.IgnoreArrays: true readability-implicit-bool-conversion.AllowPointerConditions: true readability-braces-around-statements.ShortStatementLines: 3 + misc-include-cleaner.MissingIncludes: false FormatStyle: none - diff --git a/.env.example b/.env.example index 69b57979..5e131ed9 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,4 @@ API_URL=localhost:8000 WEBHOOK_PORT=8443 WEBHOOK_HOST=https://mydomain.com WEBHOOK_SECRET=secret +WEBHOOK_CERTIFICATE_PATH=cert.pem diff --git a/.github/actions/build_deps/action.yml b/.github/actions/build_deps/action.yml index dc59d791..9c92417c 100644 --- a/.github/actions/build_deps/action.yml +++ b/.github/actions/build_deps/action.yml @@ -17,7 +17,7 @@ runs: path: | ~/.conan2 /tmp/deps - key: ${{ runner.os }}-conan-cpp23-${{ hashFiles('Frontend/conanfile.txt') }}-tgbotstater-0.4.1 + key: ${{ runner.os }}-conan-cpp23-${{ hashFiles('Frontend/conanfile.txt') }}-tgbotstater-0.4.2 restore-keys: | ${{ runner.os }}-conan-cpp23- @@ -54,9 +54,9 @@ runs: run: | mkdir -p /tmp/deps cd /tmp/deps - wget https://github.com/Makcal/TgBotStater/archive/refs/tags/v0.4.1.tar.gz -O tgbotstater.tar.gz + wget https://github.com/Makcal/TgBotStater/archive/refs/tags/v0.4.2.tar.gz -O tgbotstater.tar.gz tar -xf tgbotstater.tar.gz - cd TgBotStater-0.4.1 + cd TgBotStater-0.4.2 conan create . --build=missing - name: Install project dependencies diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc202ab9..a0943f81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,4 @@ + name: ci pipeline for frontend on: @@ -23,7 +24,6 @@ jobs: working-directory: . run: | make build-release - clang-format-check: runs-on: ubuntu-latest needs: build @@ -40,7 +40,6 @@ jobs: run: | find src -type f \( -name '*.hpp' -or -name '*.cpp' \) \ -exec clang-format-19 --dry-run --Werror {} \+ - clang-tidy-check: runs-on: ubuntu-latest needs: build @@ -56,7 +55,7 @@ jobs: sudo apt-get update sudo apt-get install --no-install-recommends -y clang-tidy-19 parallel echo "/usr/lib/llvm-19/bin" >> $GITHUB_PATH - + - name: Restore clang-tidy cache id: cache-restore uses: actions/cache/restore@v4 @@ -65,8 +64,9 @@ jobs: key: ${{ runner.os }}-clang-tidy-${{ github.ref }}-${{ github.run_id }} restore-keys: | ${{ runner.os }}-clang-tidy-${{ github.ref }}- - ${{ runner.os }}-clang-tidy-refs/heads/main - + ${{ runner.os }}-clang-tidy-refs/heads/dev- + ${{ runner.os }}-clang-tidy-refs/heads/main- + - name: Copy original cache file if exists run: | if [ -f .clang-tidy-cache.json ]; then @@ -150,7 +150,6 @@ jobs: - name: Run clang-tidy with caching run: | bash ./run-clang-tidy-cached.sh - - name: Check if cache file changed id: cache-changed run: | @@ -166,7 +165,6 @@ jobs: echo "No original cache file, assuming changed" echo "changed=true" >> $GITHUB_OUTPUT fi - - name: Debug cache state run: | echo "Cache hit: ${{ steps.cache-restore.outputs.cache-hit }}" @@ -177,7 +175,6 @@ jobs: else echo "No cache file found" fi - - name: Save clang-tidy cache if: always() && steps.cache-changed.outputs.changed == 'true' uses: actions/cache/save@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3774c9ee..71798432 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,10 @@ include(src/handlers/CMakeLists.txt) include(src/render/CMakeLists.txt) include(src/utils/CMakeLists.txt) +# settings +target_compile_definitions(main PRIVATE + TGBOTSTATER_NOT_LOG_HANDLERS_CALLS) + # setup target_include_directories(main PRIVATE ${CMAKE_SOURCE_DIR}/src diff --git a/Dockerfile b/Dockerfile index 64d456b5..c801bb53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,10 +17,10 @@ RUN conan profile detect \ RUN conan install --requires=boost/1.83.0 --build=missing WORKDIR /deps -RUN wget https://github.com/Makcal/TgBotStater/archive/refs/tags/v0.4.1.tar.gz -O tgbotstater.tar.gz \ +RUN wget https://github.com/Makcal/TgBotStater/archive/refs/tags/v0.4.2.tar.gz -O tgbotstater.tar.gz \ && tar -xf tgbotstater.tar.gz \ && rm tgbotstater.tar.gz \ - && cd TgBotStater-0.4.1 \ + && cd TgBotStater-0.4.2 \ && conan create . --build=missing WORKDIR /app diff --git a/Makefile b/Makefile index d1bc165d..4212e21b 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ build/Release/CMakeCache.txt: conanfile.txt build-debug: build/Debug/CMakeCache.txt cmake --build --preset=conan-debug build-debug-j5: build/Release/CMakeCache.txt - cmake --build . --preset=conan-debug -- -j5 + cmake --build . --preset=conan-debug -- -j2 build-release: build/Release/CMakeCache.txt cmake --build --preset=conan-release diff --git a/conanfile.txt b/conanfile.txt index 6627a1fe..e93360e5 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,5 +1,5 @@ [requires] -tgbotstater/0.4.1 +tgbotstater/0.4.2 cpp-httplib/0.19.0 boost/1.83.0 diff --git a/src/backend/CMakeLists.txt b/src/backend/CMakeLists.txt index 3141c2cb..ea1b03ce 100644 --- a/src/backend/CMakeLists.txt +++ b/src/backend/CMakeLists.txt @@ -1,15 +1,20 @@ target_sources(main PRIVATE src/backend/api/base.cpp + src/backend/api/publicity_filter.cpp + src/backend/api/storages.cpp src/backend/api/ingredients.cpp src/backend/api/users.cpp src/backend/api/recipes.cpp src/backend/api/shopping_lists.cpp + src/backend/api/moderation.cpp src/backend/models/storage.cpp src/backend/models/ingredient.cpp src/backend/models/user.cpp src/backend/models/recipe.cpp src/backend/models/shopping_list.cpp + src/backend/models/publication_request_status.cpp + src/backend/models/moderation.cpp ) diff --git a/src/backend/api/api.hpp b/src/backend/api/api.hpp index 2b83d07a..f1992290 100644 --- a/src/backend/api/api.hpp +++ b/src/backend/api/api.hpp @@ -1,10 +1,11 @@ #pragma once -#include "backend/api/ingredients.hpp" -#include "backend/api/recipes.hpp" -#include "backend/api/shopping_lists.hpp" -#include "backend/api/storages.hpp" -#include "backend/api/users.hpp" +#include "ingredients.hpp" +#include "moderation.hpp" +#include "recipes.hpp" +#include "shopping_lists.hpp" +#include "storages.hpp" +#include "users.hpp" #include @@ -20,13 +21,16 @@ class ApiClient { IngredientsApi ingredients; RecipesApi recipes; ShoppingListApi shoppingList; + ModerationApi moderation; public: explicit ApiClient(const std::string& apiAddress) - : api{apiAddress}, users{api}, storages{api}, ingredients{api}, recipes{api}, shoppingList{api} {} + : api{apiAddress}, users{api}, storages{api}, ingredients{api}, recipes{api}, shoppingList{api}, + moderation{api} {} ApiClient(const ApiClient&) = delete; ApiClient(ApiClient&& other) noexcept - : api{std::move(other.api)}, users{api}, storages{api}, ingredients{api}, recipes{api}, shoppingList{api} {} + : api{std::move(other.api)}, users{api}, storages{api}, ingredients{api}, recipes{api}, shoppingList{api}, + moderation{api} {} ApiClient& operator=(const ApiClient&) = delete; ApiClient& operator=(ApiClient&& other) noexcept { if (&other == this) @@ -37,6 +41,7 @@ class ApiClient { ingredients = IngredientsApi{api}; recipes = RecipesApi{api}; shoppingList = ShoppingListApi{api}; + moderation = ModerationApi{api}; return *this; } ~ApiClient() = default; @@ -80,6 +85,16 @@ class ApiClient { operator const ShoppingListApi&() const { // NOLINT(*-explicit-*) return shoppingList; } + + [[nodiscard]] const ModerationApi& getModerationApi() const { + return moderation; + } + + operator const ModerationApi&() const { // NOLINT(*-explicit-*) + return moderation; + } }; +using ApiClientRef = const api::ApiClient&; + } // namespace cookcookhnya::api diff --git a/src/backend/api/common.hpp b/src/backend/api/common.hpp deleted file mode 100644 index c160cfd1..00000000 --- a/src/backend/api/common.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -enum struct filterType : std::uint8_t { All, Custom, Public }; - -inline std::string filterStr(filterType value) { - static const std::unordered_map map = { - {filterType::All, "All"}, {filterType::Custom, "Custom"}, {filterType::Public, "Public"}}; - - auto it = map.find(value); - if (it != map.end()) { - return it->second; - } - throw std::invalid_argument("Invalid filterType value"); -} diff --git a/src/backend/api/ingredients.cpp b/src/backend/api/ingredients.cpp index 639c06ad..3071f137 100644 --- a/src/backend/api/ingredients.cpp +++ b/src/backend/api/ingredients.cpp @@ -1,6 +1,6 @@ #include "backend/api/ingredients.hpp" -#include "backend/api/common.hpp" +#include "backend/api/publicity_filter.hpp" #include "backend/id_types.hpp" #include "backend/models/ingredient.hpp" #include "utils/parsing.hpp" @@ -23,11 +23,11 @@ Ingredient IngredientsApi::get(UserId user, IngredientId ingredient) const { // GET /storages/{storageId}/ingredients std::vector -IngredientsApi::getStorageIngredients(UserId user, StorageId storage, std::size_t size, std::size_t offset) const { +IngredientsApi::getStorageIngredients(UserId user, StorageId storage, std::size_t count, std::size_t offset) const { return jsonGetAuthed>( user, std::format("/storages/{}/ingredients", storage), - {{"size", utils::to_string(size)}, {"offset", utils::to_string(offset)}}); + {{"size", utils::to_string(count)}, {"offset", utils::to_string(offset)}}); } // PUT /storages/{storageId}/ingredients/{ingredientId} @@ -46,7 +46,7 @@ void IngredientsApi::deleteMultipleFromStorage(UserId user, const std::vector& ingredients) const { httplib::Params params = {}; for (auto id : ingredients) - params.insert({"ingredient", utils::to_string(id)}); + params.emplace("ingredient", utils::to_string(id)); jsonDeleteAuthed(user, std::format("/storages/{}/ingredients/", storage), params); } @@ -54,43 +54,51 @@ void IngredientsApi::deleteMultipleFromStorage(UserId user, IngredientSearchForStorageResponse IngredientsApi::searchForStorage(UserId user, StorageId storage, std::string query, - std::size_t threshold, - std::size_t size, - std::size_t offset) const { + std::size_t count, + std::size_t offset, + unsigned threshold) const { return jsonGetAuthed(user, "/ingredients-for-storage", {{"query", std::move(query)}, {"storage-id", utils::to_string(storage)}, - {"size", utils::to_string(size)}, + {"size", utils::to_string(count)}, {"threshold", utils::to_string(threshold)}, {"offset", utils::to_string(offset)}}); } -// GET /public/ingredients -IngredientSearchResponse -IngredientsApi::publicSearch(std::string query, std::size_t threshold, std::size_t size, std::size_t offset) const { - return jsonGet("/public/ingredients", - {}, - {{"query", std::move(query)}, - {"threshold", utils::to_string(threshold)}, - {"size", utils::to_string(size)}, - {"offset", utils::to_string(offset)}}); -} - // GET /ingredients IngredientSearchResponse IngredientsApi::search(UserId user, + PublicityFilterType filter, std::string query, - std::size_t threshold, - std::size_t size, + std::size_t count, std::size_t offset, - filterType filter) const { + unsigned threshold) const { return jsonGetAuthed(user, "/ingredients", {{"query", std::move(query)}, - {"size", utils::to_string(size)}, + {"size", utils::to_string(count)}, {"offset", utils::to_string(offset)}, {"threshold", utils::to_string(threshold)}, - {"filter", filterStr(filter)}}); + {"filter", utils::to_string(filter)}}); +} + +// GET /ingredients +IngredientList +IngredientsApi::getList(UserId user, PublicityFilterType filter, std::size_t count, std::size_t offset) const { + auto result = search(user, filter, "", count, offset, 0); + return {.page = std::move(result.page), .found = result.found}; +} + +// GET /ingredients +CustomIngredientList IngredientsApi::customIngredientsSearch( + UserId user, std::string query, std::size_t threshold, std::size_t count, std::size_t offset) const { + return jsonGetAuthed(user, + "/ingredients", + {{"query", std::move(query)}, + {"size", utils::to_string(count)}, + {"offset", utils::to_string(offset)}, + {"threshold", utils::to_string(threshold)}, + {"filter", utils::to_string(PublicityFilterType::Custom)}}); } // GET /public/ingredients/{ingredientId} @@ -109,17 +117,13 @@ void IngredientsApi::deleteFromRecipe(UserId user, RecipeId recipe, IngredientId } // GET /ingredients-for-recipe -IngredientSearchForRecipeResponse IngredientsApi::searchForRecipe(UserId user, - RecipeId recipe, - std::string query, - std::size_t threshold, - std::size_t size, - std::size_t offset) const { +IngredientSearchForRecipeResponse IngredientsApi::searchForRecipe( + UserId user, RecipeId recipe, std::string query, std::size_t count, std::size_t offset, unsigned threshold) const { return jsonGetAuthed(user, "/ingredients-for-recipe", {{"query", std::move(query)}, {"threshold", utils::to_string(threshold)}, - {"size", utils::to_string(size)}, + {"size", utils::to_string(count)}, {"offset", utils::to_string(offset)}, {"recipe-id", utils::to_string(recipe)}}); } @@ -129,6 +133,11 @@ IngredientId IngredientsApi::createCustom(UserId user, const IngredientCreateBod return utils::parse(postWithJsonAuthed(user, "/ingredients", body)); } +// DELETE /ingredients/{ingredientId} +void IngredientsApi::deleteCustom(UserId user, IngredientId ingredient) const { + jsonDeleteAuthed(user, std::format("/ingredients/{}", ingredient)); +} + // POST /recipes/{ingredientId}/request-publication void IngredientsApi::publishCustom(UserId user, IngredientId ingredient) const { jsonPostAuthed(user, std::format("/ingredients/{}/request-publication", ingredient)); diff --git a/src/backend/api/ingredients.hpp b/src/backend/api/ingredients.hpp index f918a55a..4ec684e4 100644 --- a/src/backend/api/ingredients.hpp +++ b/src/backend/api/ingredients.hpp @@ -1,9 +1,9 @@ #pragma once -#include "backend/api/base.hpp" #include "backend/id_types.hpp" #include "backend/models/ingredient.hpp" -#include "common.hpp" +#include "base.hpp" +#include "publicity_filter.hpp" #include @@ -22,54 +22,63 @@ class IngredientsApi : ApiBase { [[nodiscard]] models::ingredient::Ingredient get(UserId user, IngredientId ingredient) const; [[nodiscard]] std::vector - getStorageIngredients(UserId user, StorageId storage, std::size_t size = 2, std::size_t offset = 0) const; + getStorageIngredients(UserId user, + StorageId storage, + std::size_t count = 200, // NOLINT(*magic-number*) + std::size_t offset = 0) const; void putToStorage(UserId user, StorageId storage, IngredientId ingredient) const; - void deleteFromStorage(UserId user, StorageId storage, IngredientId ingredient) const; - void - deleteMultipleFromStorage(UserId user, StorageId storage, const std::vector& ingredients = {}) const; + void deleteMultipleFromStorage(UserId user, StorageId storage, const std::vector& ingredients) const; [[nodiscard]] models::ingredient::IngredientSearchForStorageResponse searchForStorage(UserId user, StorageId storage, std::string query = "", - std::size_t threshold = 50, // NOLINT(*magic*) - std::size_t size = 2, - std::size_t offset = 0) const; + std::size_t count = 50, // NOLINT(*magic-number*) + std::size_t offset = 0, + unsigned threshold = 50) const; // NOLINT(*magic-number*) [[nodiscard]] models::ingredient::IngredientSearchResponse - publicSearch(std::string query = "", - std::size_t threshold = 50, // NOLINT(*magic*) - std::size_t size = 2, - std::size_t offset = 0) const; - - [[nodiscard]] models::ingredient::IngredientSearchResponse search(UserId user, - std::string query = "", - std::size_t threshold = 50, // NOLINT(*magic*) - std::size_t size = 2, - std::size_t offset = 2, - filterType filter = filterType::All) const; + search(UserId user, + PublicityFilterType filter = PublicityFilterType::All, + std::string query = "", + std::size_t count = 50, // NOLINT(*magic-number*) + std::size_t offset = 0, + unsigned threshold = 50) const; // NOLINT(*magic-number*) + + [[nodiscard]] models::ingredient::IngredientList getList(UserId user, + PublicityFilterType filter = PublicityFilterType::All, + std::size_t count = 50, // NOLINT(*magic-number*) + std::size_t offset = 0) const; + + [[nodiscard]] models::ingredient::CustomIngredientList + customIngredientsSearch(UserId user, + std::string query, + std::size_t threshold, + std::size_t count = 50, // NOLINT(*magic-number*) + std::size_t offset = 0) const; [[nodiscard]] models::ingredient::Ingredient getPublicIngredient(IngredientId ingredient) const; void putToRecipe(UserId user, RecipeId recipeId, IngredientId ingredient) const; - void deleteFromRecipe(UserId user, RecipeId recipeId, IngredientId ingredient) const; [[nodiscard]] models::ingredient::IngredientSearchForRecipeResponse searchForRecipe(UserId user, RecipeId recipe, std::string query = "", - std::size_t threshold = 50, // NOLINT(*magic*) - std::size_t size = 2, - std::size_t offset = 0) const; + std::size_t count = 50, // NOLINT(*magic-number*) + std::size_t offset = 0, + unsigned threshold = 50) const; // NOLINT(*magic-number*) IngredientId createCustom(UserId user, // NOLINT(*-nodiscard) const models::ingredient::IngredientCreateBody& body) const; - + void deleteCustom(UserId user, IngredientId ingredient) const; void publishCustom(UserId user, IngredientId ingredient) const; }; +using IngredientsApiRef = const api::IngredientsApi&; + } // namespace cookcookhnya::api diff --git a/src/backend/api/moderation.cpp b/src/backend/api/moderation.cpp new file mode 100644 index 00000000..fe41e50a --- /dev/null +++ b/src/backend/api/moderation.cpp @@ -0,0 +1,25 @@ +#include "moderation.hpp" + +#include "backend/models/moderation.hpp" + +#include + +#include +#include + +namespace cookcookhnya::api { + +using namespace models::moderation; + +// GET /publication-requests?size={}&offset={} +[[nodiscard]] std::vector +ModerationApi::getAllPublicationRequests(UserId user, std::size_t size, std::size_t offset) const { + return jsonGetAuthed>(user, + "/publication-requests", + { + {"size", utils::to_string(size)}, + {"offset", utils::to_string(offset)}, + }); +} + +} // namespace cookcookhnya::api diff --git a/src/backend/api/moderation.hpp b/src/backend/api/moderation.hpp new file mode 100644 index 00000000..70ffb0c8 --- /dev/null +++ b/src/backend/api/moderation.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "backend/models/moderation.hpp" +#include "base.hpp" + +#include + +#include +#include + +namespace cookcookhnya::api { + +class ModerationApi : ApiBase { + friend class ApiClient; + + explicit ModerationApi(httplib::Client& api) : ApiBase{api} {} + + public: + [[nodiscard]] std::vector getAllPublicationRequests( + UserId user, std::size_t size = 30, std::size_t offset = 0) const; // NOLINT(*magic-number*) +}; + +using ModerationApiRef = const ModerationApi&; + +} // namespace cookcookhnya::api diff --git a/src/backend/api/publicity_filter.cpp b/src/backend/api/publicity_filter.cpp new file mode 100644 index 00000000..d7481d46 --- /dev/null +++ b/src/backend/api/publicity_filter.cpp @@ -0,0 +1,18 @@ +#include "publicity_filter.hpp" + +#include +#include +#include +#include + +namespace cookcookhnya::utils { + +std::string to_string(PublicityFilterType value) { + static constexpr std::array names = {"All", "Custom", "Public"}; + + if (static_cast(value) >= names.size()) + throw std::invalid_argument("Invalid FilterType value"); + return names[static_cast(value)]; +} + +} // namespace cookcookhnya::utils diff --git a/src/backend/api/publicity_filter.hpp b/src/backend/api/publicity_filter.hpp new file mode 100644 index 00000000..7cf91f77 --- /dev/null +++ b/src/backend/api/publicity_filter.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +enum struct PublicityFilterType : std::uint8_t { All, Custom, Public }; + +namespace cookcookhnya::utils { + +std::string to_string(PublicityFilterType value); + +} // namespace cookcookhnya::utils diff --git a/src/backend/api/recipes.cpp b/src/backend/api/recipes.cpp index 737df48b..bd9d15c3 100644 --- a/src/backend/api/recipes.cpp +++ b/src/backend/api/recipes.cpp @@ -1,6 +1,6 @@ #include "backend/api/recipes.hpp" -#include "backend/api/common.hpp" +#include "backend/api/publicity_filter.hpp" #include "backend/id_types.hpp" #include "backend/models/recipe.hpp" #include "utils/parsing.hpp" @@ -19,37 +19,48 @@ namespace cookcookhnya::api { using namespace models::recipe; // GET /suggested-recipes -RecipesList RecipesApi::getSuggestedRecipesList(UserId user, - const std::vector& storages, - size_t size, - size_t offset) const { +RecipesListWithIngredientsCount RecipesApi::getSuggestedRecipes(UserId user, + const std::vector& storages, + std::size_t size, + std::size_t offset) const { httplib::Params params = {{"size", utils::to_string(size)}, {"offset", std::to_string(offset)}}; - for (auto id : storages) - params.insert({"storage-id", utils::to_string(id)}); - return jsonGetAuthed(user, "/suggested-recipes", params); + for (const StorageId& id : storages) + params.emplace("storage-id", utils::to_string(id)); + return jsonGetAuthed(user, "/suggested-recipes", params); } // GET /recipes -RecipeSearchResponse RecipesApi::getRecipesList(UserId user, - std::string query, - std::size_t threshold, - std::size_t size, - std::size_t offset, - filterType filter) const { +RecipeSearchResponse RecipesApi::search(UserId user, + PublicityFilterType filter, + std::string query, + std::size_t size, + std::size_t offset, + unsigned threshold) const { return jsonGetAuthed(user, "/recipes", {{"query", std::move(query)}, {"threshold", utils::to_string(threshold)}, {"size", utils::to_string(size)}, {"offset", utils::to_string(offset)}, - {"filter", filterStr(filter)}}); + {"filter", utils::to_string(filter)}}); +} + +// GET /recipes +RecipesList RecipesApi::getList(UserId user, PublicityFilterType filter, std::size_t size, std::size_t offset) const { + auto result = search(user, filter, "", size, offset, 0); + return {.page = std::move(result.page), .found = result.found}; } // GET /recipes/{recipeId} -RecipeDetails RecipesApi::getIngredientsInRecipe(UserId user, RecipeId recipe) const { +RecipeDetails RecipesApi::get(UserId user, RecipeId recipe) const { return jsonGetAuthed(user, std::format("/recipes/{}", recipe)); } +// GET /recipes/{recipeId} +SuggestedRecipeDetails RecipesApi::getSuggested(UserId user, RecipeId recipe) const { + return jsonGetAuthed(user, std::format("/recipes/{}", recipe)); +} + // POST /recipes RecipeId RecipesApi::create(UserId user, const RecipeCreateBody& body) const { return utils::parse(postWithJsonAuthed(user, "/recipes", body)); @@ -60,14 +71,15 @@ void RecipesApi::delete_(UserId user, RecipeId recipeId) const { jsonDeleteAuthed(user, std::format("/recipes/{}", recipeId)); } -// GET /recipes/{recipeId} -CustomRecipeDetails RecipesApi::get(UserId user, RecipeId recipe) const { - return jsonGetAuthed(user, std::format("/recipes/{}", recipe)); +// POST /recipes/{recipeId}/publication-requests +void RecipesApi::publishCustom(UserId user, RecipeId recipe) const { + jsonPostAuthed(user, std::format("recipes/{}/publication-requests", recipe)); } -// POST /recipes/{recipeId}/request-publication -void RecipesApi::publishCustom(UserId user, RecipeId recipe) const { - jsonPostAuthed(user, std::format("my/recipes/{}/request-publication", recipe)); +// GET /recipes/{recipeId}/publication-requests +std::vector RecipesApi::getRecipeRequestHistory(UserId user, RecipeId recipe) const { + return jsonGetAuthed>( + user, std::format("/recipes/{}/publication-requests", recipe)); } } // namespace cookcookhnya::api diff --git a/src/backend/api/recipes.hpp b/src/backend/api/recipes.hpp index 8b42dc58..63750150 100644 --- a/src/backend/api/recipes.hpp +++ b/src/backend/api/recipes.hpp @@ -1,9 +1,9 @@ #pragma once -#include "backend/api/base.hpp" #include "backend/id_types.hpp" #include "backend/models/recipe.hpp" -#include "common.hpp" +#include "base.hpp" +#include "publicity_filter.hpp" #include @@ -19,28 +19,37 @@ class RecipesApi : ApiBase { explicit RecipesApi(httplib::Client& api) : ApiBase{api} {} public: - [[nodiscard]] models::recipe::RecipesList getSuggestedRecipesList(UserId user, - const std::vector& storages, - size_t size = 2, - size_t offset = 0) const; - - [[nodiscard]] models::recipe::RecipeSearchResponse getRecipesList(UserId user, - std::string query, - std::size_t threshold, - std::size_t size, - std::size_t offset, - filterType filter) const; - - [[nodiscard]] models::recipe::RecipeDetails getIngredientsInRecipe(UserId user, RecipeId recipe) const; - - [[nodiscard]] RecipeId create(UserId user, // NOLINT(*-nodiscard) - const models::recipe::RecipeCreateBody& body) const; - + [[nodiscard]] models::recipe::RecipesListWithIngredientsCount + getSuggestedRecipes(UserId user, + const std::vector& storages, + std::size_t size = 500, // NOLINT(*magic-number*) + std::size_t offset = 0) const; + + [[nodiscard]] models::recipe::SuggestedRecipeDetails getSuggested(UserId user, RecipeId recipeId) const; + + [[nodiscard]] models::recipe::RecipeSearchResponse search(UserId user, + PublicityFilterType filter = PublicityFilterType::All, + std::string query = "", + std::size_t size = 100, // NOLINT(*magic-number*) + std::size_t offset = 0, + unsigned threshold = 50) const; // NOLINT(*magic-number*) + + [[nodiscard]] models::recipe::RecipesList getList(UserId user, + PublicityFilterType filter = PublicityFilterType::All, + std::size_t size = 100, // NOLINT(*magic-number*) + std::size_t offset = 0) const; + + [[nodiscard]] models::recipe::RecipeDetails get(UserId user, RecipeId recipeId) const; + + RecipeId create(UserId user, // NOLINT(*-nodiscard) + const models::recipe::RecipeCreateBody& body) const; void delete_(UserId user, RecipeId recipe) const; - [[nodiscard]] models::recipe::CustomRecipeDetails get(UserId user, RecipeId recipe) const; - void publishCustom(UserId user, RecipeId recipe) const; + [[nodiscard]] std::vector getRecipeRequestHistory(UserId user, + RecipeId recipe) const; }; +using RecipesApiRef = const api::RecipesApi&; + } // namespace cookcookhnya::api diff --git a/src/backend/api/shopping_lists.cpp b/src/backend/api/shopping_lists.cpp index 7b4cbfc1..03a9a817 100644 --- a/src/backend/api/shopping_lists.cpp +++ b/src/backend/api/shopping_lists.cpp @@ -4,6 +4,7 @@ #include "backend/models/shopping_list.hpp" #include "utils/to_string.hpp" +#include #include #include #include @@ -13,15 +14,16 @@ namespace cookcookhnya::api { using namespace models::shopping_list; // GET /shopping-list -std::vector ShoppingListApi::get(UserId user) const { - return jsonGetAuthed>(user, "/shopping-list"); +std::vector ShoppingListApi::get(UserId user, std::size_t count, std::size_t offset) const { + return jsonGetAuthed>( + user, "/shopping-list", {{"size", utils::to_string(count)}, {"offset", utils::to_string(offset)}}); } // PUT /shopping-list void ShoppingListApi::put(UserId user, const std::vector& ingredients) const { httplib::Params params; for (const IngredientId id : ingredients) - params.insert({"ingredient-id", utils::to_string(id)}); + params.emplace("ingredient-id", utils::to_string(id)); jsonPutAuthed(user, "/shopping-list", params); } @@ -29,15 +31,16 @@ void ShoppingListApi::put(UserId user, const std::vector& ingredie void ShoppingListApi::remove(UserId user, const std::vector& ingredients) const { httplib::Params params; for (const IngredientId id : ingredients) - params.insert({"ingredient-id", utils::to_string(id)}); + params.emplace("ingredient-id", utils::to_string(id)); jsonDeleteAuthed(user, "/shopping-list", params); } // PUT /shopping-list/buy -void ShoppingListApi::buy(UserId user, const std::vector& ingredients) const { +void ShoppingListApi::buy(UserId user, StorageId storage, const std::vector& ingredients) const { httplib::Params params; for (const IngredientId id : ingredients) - params.insert({"ingredient-id", utils::to_string(id)}); + params.emplace("ingredient-id", utils::to_string(id)); + params.emplace("storage-id", utils::to_string(storage)); jsonPutAuthed(user, "/shopping-list/buy", params); } diff --git a/src/backend/api/shopping_lists.hpp b/src/backend/api/shopping_lists.hpp index 37990964..143437d4 100644 --- a/src/backend/api/shopping_lists.hpp +++ b/src/backend/api/shopping_lists.hpp @@ -6,6 +6,7 @@ #include +#include #include namespace cookcookhnya::api { @@ -16,13 +17,16 @@ class ShoppingListApi : ApiBase { explicit ShoppingListApi(httplib::Client& api) : ApiBase{api} {} public: - [[nodiscard]] std::vector get(UserId user) const; + [[nodiscard]] std::vector + get(UserId user, std::size_t count = 500, std::size_t offset = 0) const; // NOLINT(*magic-number*) void put(UserId user, const std::vector& ingredients) const; void remove(UserId user, const std::vector& ingredients) const; - void buy(UserId user, const std::vector& ingredients) const; + void buy(UserId user, StorageId storage, const std::vector& ingredients) const; }; +using ShoppingListApiRef = const api::ShoppingListApi&; + } // namespace cookcookhnya::api diff --git a/src/backend/api/storages.cpp b/src/backend/api/storages.cpp index cf9e1cb4..ae3693e4 100644 --- a/src/backend/api/storages.cpp +++ b/src/backend/api/storages.cpp @@ -6,6 +6,7 @@ #include "utils/parsing.hpp" #include +#include #include namespace cookcookhnya::api { @@ -54,8 +55,12 @@ InvitationId StoragesApi::inviteMember(UserId user, StorageId storage) const { } // POST /invitations/{invitationHash}/activate -void StoragesApi::activate(UserId user, InvitationId invitation) const { - postAuthed(user, std::format("/invitations/{}/activate", invitation)); +std::optional StoragesApi::activate(UserId user, InvitationId invitation) const { + try { + return jsonPostAuthed(user, std::format("/invitations/{}/activate", invitation)); + } catch (...) { + return std::nullopt; + } } } // namespace cookcookhnya::api diff --git a/src/backend/api/storages.hpp b/src/backend/api/storages.hpp index 95f677d1..c10c1d42 100644 --- a/src/backend/api/storages.hpp +++ b/src/backend/api/storages.hpp @@ -7,6 +7,7 @@ #include +#include #include namespace cookcookhnya::api { @@ -18,15 +19,21 @@ class StoragesApi : ApiBase { public: [[nodiscard]] std::vector getStoragesList(UserId user) const; + [[nodiscard]] models::storage::StorageDetails get(UserId user, StorageId storage) const; + StorageId create(UserId user, // NOLINT(*-nodiscard) const models::storage::StorageCreateBody& body) const; void delete_(UserId user, StorageId storage) const; + [[nodiscard]] std::vector getStorageMembers(UserId user, StorageId storage) const; void addMember(UserId user, StorageId storage, UserId member) const; void deleteMember(UserId user, StorageId storage, UserId member) const; + [[nodiscard]] InvitationId inviteMember(UserId user, StorageId storage) const; - void activate(UserId user, InvitationId invitation) const; + [[nodiscard]] std::optional activate(UserId user, InvitationId invitation) const; }; +using StorageApiRef = const api::StoragesApi&; + } // namespace cookcookhnya::api diff --git a/src/backend/api/users.hpp b/src/backend/api/users.hpp index 21c22299..e353d6ec 100644 --- a/src/backend/api/users.hpp +++ b/src/backend/api/users.hpp @@ -17,4 +17,6 @@ class UsersApi : ApiBase { const models::user::UpdateUserInfoBody& body) const; }; +using UserApiRef = const api::UsersApi&; + } // namespace cookcookhnya::api diff --git a/src/backend/models/ingredient.cpp b/src/backend/models/ingredient.cpp index 771502fe..7d658981 100644 --- a/src/backend/models/ingredient.cpp +++ b/src/backend/models/ingredient.cpp @@ -1,8 +1,10 @@ #include "backend/models/ingredient.hpp" +#include "backend/models/publication_request_status.hpp" #include #include #include +#include namespace cookcookhnya::api::models::ingredient { @@ -15,6 +17,26 @@ Ingredient tag_invoke(json::value_to_tag /*tag*/, const json::value& }; } +CustomIngredient tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { + const auto& status = j.at("moderationStatus"); + if (status.is_object()) { + return { + .id = value_to(j.at("id")), + .name = value_to(j.at("name")), + .moderationStatus = value_to(status.at("type")), + .reason = status.as_object().if_contains("reason") + ? value_to(status.at("reason")) + : std::nullopt, + }; + } + return { + .id = value_to(j.at("id")), + .name = value_to(j.at("name")), + .moderationStatus = moderation::PublicationRequestStatus::NO_REQUEST, + .reason = std::nullopt, + }; +} + void tag_invoke(json::value_from_tag /*tag*/, json::value& j, const IngredientCreateBody& body) { j = {{"name", body.name}}; } @@ -52,10 +74,26 @@ IngredientSearchForRecipeResponse tag_invoke(json::value_to_tag(j.at("found")), }; } + IngredientSearchResponse tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { return { .page = value_to(j.at("results")), .found = value_to(j.at("found")), }; } + +IngredientList tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { + return { + .page = value_to(j.at("results")), + .found = value_to(j.at("found")), + }; +} + +CustomIngredientList tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { + return { + .page = value_to(j.at("results")), + .found = value_to(j.at("found")), + }; +} + } // namespace cookcookhnya::api::models::ingredient diff --git a/src/backend/models/ingredient.hpp b/src/backend/models/ingredient.hpp index 977ee6bb..483263f6 100644 --- a/src/backend/models/ingredient.hpp +++ b/src/backend/models/ingredient.hpp @@ -1,11 +1,13 @@ #pragma once #include "backend/id_types.hpp" +#include "backend/models/publication_request_status.hpp" #include #include #include +#include #include #include @@ -18,6 +20,15 @@ struct Ingredient { friend Ingredient tag_invoke(boost::json::value_to_tag, const boost::json::value& j); }; +struct CustomIngredient { + IngredientId id; + std::string name; + moderation::PublicationRequestStatus moderationStatus = moderation::PublicationRequestStatus::NO_REQUEST; + std::optional reason = std::nullopt; + + friend CustomIngredient tag_invoke(boost::json::value_to_tag, const boost::json::value& j); +}; + struct IngredientCreateBody { std::string name; @@ -66,4 +77,19 @@ struct IngredientSearchResponse { const boost::json::value& j); }; +struct IngredientList { + std::vector page; + std::size_t found; + + friend IngredientList tag_invoke(boost::json::value_to_tag, const boost::json::value& j); +}; + +struct CustomIngredientList { + std::vector page; + std::size_t found; + + friend CustomIngredientList tag_invoke(boost::json::value_to_tag, + const boost::json::value& j); +}; + } // namespace cookcookhnya::api::models::ingredient diff --git a/src/backend/models/moderation.cpp b/src/backend/models/moderation.cpp new file mode 100644 index 00000000..c68b3986 --- /dev/null +++ b/src/backend/models/moderation.cpp @@ -0,0 +1,31 @@ +#include "moderation.hpp" + +#include "publication_request_status.hpp" +#include "utils/parsing.hpp" + +#include +#include +#include + +#include + +namespace cookcookhnya::api::models::moderation { + +namespace json = boost::json; + +PublicationRequest tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { + return { + .name = value_to(j.at("name")), + .requestType = value_to(j.at("requestType")), + .status = j.as_object().if_contains("status") + ? value_to(j.at("status")) + : PublicationRequestStatusStruct{.status = PublicationRequestStatus::NO_REQUEST, + .reason = std::nullopt}, + .created = utils::parseIsoTime(value_to(j.at("createdAt"))), + .updated = j.as_object().if_contains("updatedAt") + ? std::optional{utils::parseIsoTime(value_to(j.at("updatedAt")))} + : std::nullopt, + }; +} + +} // namespace cookcookhnya::api::models::moderation diff --git a/src/backend/models/moderation.hpp b/src/backend/models/moderation.hpp new file mode 100644 index 00000000..812c567e --- /dev/null +++ b/src/backend/models/moderation.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "publication_request_status.hpp" + +#include +#include + +#include +#include + +namespace cookcookhnya::api::models::moderation { + +struct PublicationRequest { + std::string name; + std::string requestType; + PublicationRequestStatusStruct status; + std::chrono::system_clock::time_point created; + std::optional updated; + + friend PublicationRequest tag_invoke(boost::json::value_to_tag, const boost::json::value& j); +}; + +} // namespace cookcookhnya::api::models::moderation diff --git a/src/backend/models/publication_request_status.cpp b/src/backend/models/publication_request_status.cpp new file mode 100644 index 00000000..a21cfb40 --- /dev/null +++ b/src/backend/models/publication_request_status.cpp @@ -0,0 +1,48 @@ +#include "publication_request_status.hpp" + +#include "utils/utils.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace cookcookhnya::api::models::moderation { + +PublicationRequestStatus tag_invoke(boost::json::value_to_tag /*tag*/, + const boost::json::value& j) { + if (j == "pending") + return PublicationRequestStatus::PENDING; + if (j == "accepted") + return PublicationRequestStatus::ACCEPTED; + if (j == "rejected") + return PublicationRequestStatus::REJECTED; + return PublicationRequestStatus::NO_REQUEST; +}; + +PublicationRequestStatusStruct tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value& j) { + if (j.is_object()) { + return {.status = j.as_object().if_contains("type") ? value_to(j.at("type")) + : PublicationRequestStatus::NO_REQUEST, + .reason = j.as_object().if_contains("reason") + ? value_to(j.at("reason")) + : std::nullopt}; + } + return {.status = PublicationRequestStatus::NO_REQUEST, .reason = std::nullopt}; +} + +} // namespace cookcookhnya::api::models::moderation + +namespace cookcookhnya::utils { + +std::string to_string(api::models::moderation::PublicationRequestStatus status) { + static constexpr std::array statusStr = { + u8"🟡 На рассмотрении", u8"🟢 Принят", u8"🔴 Отклонен", u8"⚪️ Вы еще не отправили запрос"}; + return utf8str(statusStr[static_cast(status)]); +} + +} // namespace cookcookhnya::utils diff --git a/src/backend/models/publication_request_status.hpp b/src/backend/models/publication_request_status.hpp new file mode 100644 index 00000000..6ad5ac68 --- /dev/null +++ b/src/backend/models/publication_request_status.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace cookcookhnya::api::models::moderation { + +enum class PublicationRequestStatus : std::uint8_t { PENDING, ACCEPTED, REJECTED, NO_REQUEST }; + +PublicationRequestStatus tag_invoke(boost::json::value_to_tag, const boost::json::value& j); + +struct PublicationRequestStatusStruct { + PublicationRequestStatus status = PublicationRequestStatus::NO_REQUEST; + std::optional reason; + + friend PublicationRequestStatusStruct tag_invoke(boost::json::value_to_tag, + const boost::json::value& j); +}; + +} // namespace cookcookhnya::api::models::moderation + +namespace cookcookhnya::utils { + +std::string to_string(api::models::moderation::PublicationRequestStatus status); + +} // namespace cookcookhnya::utils diff --git a/src/backend/models/recipe.cpp b/src/backend/models/recipe.cpp index c0b91955..73570a59 100644 --- a/src/backend/models/recipe.cpp +++ b/src/backend/models/recipe.cpp @@ -1,13 +1,18 @@ -#include "backend/models/recipe.hpp" +#include "recipe.hpp" + +#include "utils/parsing.hpp" #include #include #include #include +#include + namespace cookcookhnya::api::models::recipe { namespace json = boost::json; +using moderation::PublicationRequestStatus; RecipeSummary tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { return { @@ -16,6 +21,17 @@ RecipeSummary tag_invoke(json::value_to_tag /*tag*/, const json:: }; } +RecipeDetails tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { + return { + .ingredients = value_to(j.at("ingredients")), + .name = value_to(j.at("name")), + .link = value_to(j.at("sourceLink")), + // Deal with optionals using ternary operator + .creator = j.as_object().if_contains("creator") ? value_to(j.at("creator")) + : std::nullopt, + }; +} + RecipeSummaryWithIngredients tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { return { @@ -26,10 +42,11 @@ RecipeSummaryWithIngredients tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { +RecipesListWithIngredientsCount tag_invoke(json::value_to_tag /*tag*/, + const json::value& j) { return { - .page = value_to(j.at("results")), - .found = value_to(j.at("found")), + .page = value_to(j.at("results")), + .found = value_to(j.at("found")), }; } @@ -45,57 +62,41 @@ IngredientInRecipe tag_invoke(json::value_to_tag /*tag*/, co }; } -RecipeCreator tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { +SuggestedRecipeDetails tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { return { - .id = value_to(j.at("id")), - .fullName = value_to(j.at("fullName")), + .ingredients = value_to(j.at("ingredients")), + .name = value_to(j.at("name")), + .link = value_to(j.at("sourceLink")), + // Deal with optionals using ternary operator + .creator = j.as_object().if_contains("creator") ? value_to(j.at("creator")) + : std::nullopt, + .moderationStatus = + j.as_object().if_contains("moderationStatus") + ? value_to(j.at("moderationStatus")) + : moderation::PublicationRequestStatusStruct{.status = PublicationRequestStatus::NO_REQUEST, + .reason = std::nullopt}, }; } -RecipeDetails tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { - return { - .ingredients = value_to(j.at("ingredients")), - .name = value_to(j.at("name")), - .link = value_to(j.at("sourceLink")), - .creator = value_to(j.at("creator")), - }; -} - -CustomRecipeSummary tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { - return { - .id = value_to(j.at("recipeId")), - .name = value_to(j.at("name")), - .link = value_to(j.at("sourceLink")), - }; -} - -CustomRecipesList tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { - return { - .page = value_to(j.at("results")), - .found = value_to(j.at("found")), - }; -} - -CustomRecipeDetails tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { +RecipeSearchResponse tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { return { - .ingredients = value_to(j.at("ingredients")), - .name = value_to(j.at("name")), - .link = value_to(j.at("sourceLink")), + .page = value_to(j.at("results")), + .found = value_to(j.at("found")), }; } -IngredientInCustomRecipe tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { +RecipePublicationRequest tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { return { - .id = value_to(j.at("id")), - .name = value_to(j.at("name")), + .status = j.as_object().if_contains("status") + ? value_to(j.at("status")) + : moderation::PublicationRequestStatusStruct{.status = PublicationRequestStatus::NO_REQUEST, + .reason = std::nullopt}, + .created = utils::parseIsoTime(value_to(j.at("createdAt"))), + .updated = j.as_object().if_contains("updatedAt") + ? std::optional{utils::parseIsoTime(value_to(j.at("updatedAt")))} + : std::nullopt, }; } -RecipeSearchResponse tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { - return { - .page = value_to(j.at("results")), - .found = value_to(j.at("found")), - }; -} } // namespace cookcookhnya::api::models::recipe diff --git a/src/backend/models/recipe.hpp b/src/backend/models/recipe.hpp index b0f6ec23..4128bc77 100644 --- a/src/backend/models/recipe.hpp +++ b/src/backend/models/recipe.hpp @@ -1,12 +1,16 @@ #pragma once #include "backend/id_types.hpp" -#include "backend/models/user.hpp" +#include "backend/models/ingredient.hpp" +#include "publication_request_status.hpp" +#include "storage.hpp" +#include "user.hpp" -#include "tg_types.hpp" #include #include + #include +#include #include #include @@ -19,6 +23,15 @@ struct RecipeSummary { friend RecipeSummary tag_invoke(boost::json::value_to_tag, const boost::json::value& j); }; +struct RecipeDetails { + std::vector ingredients; + std::string name; + std::optional link; + std::optional creator; + + friend RecipeDetails tag_invoke(boost::json::value_to_tag, const boost::json::value& j); +}; + struct RecipeSummaryWithIngredients { RecipeId id; std::string name; @@ -32,70 +45,43 @@ struct RecipeSummaryWithIngredients { struct IngredientInRecipe { IngredientId id; std::string name; - std::vector inStorages; + std::vector inStorages; friend IngredientInRecipe tag_invoke(boost::json::value_to_tag, const boost::json::value& j); }; -struct RecipeCreator { - tg_types::UserId id; - std::string fullName; - - friend RecipeCreator tag_invoke(boost::json::value_to_tag, const boost::json::value& j); -}; - -struct RecipeDetails { +struct SuggestedRecipeDetails { std::vector ingredients; std::string name; - std::string link; - user::UserDetails creator; - - friend RecipeDetails tag_invoke(boost::json::value_to_tag, const boost::json::value& j); -}; - -struct IngredientInCustomRecipe { - IngredientId id; - std::string name; - - friend IngredientInCustomRecipe tag_invoke(boost::json::value_to_tag, - const boost::json::value& j); -}; - -struct CustomRecipeDetails { - std::vector ingredients; - std::string name; - std::string link; + std::optional link; + std::optional creator; + moderation::PublicationRequestStatusStruct moderationStatus; - friend CustomRecipeDetails tag_invoke(boost::json::value_to_tag, const boost::json::value& j); + friend SuggestedRecipeDetails tag_invoke(boost::json::value_to_tag, + const boost::json::value& j); }; struct RecipesList { - std::vector page; + std::vector page; std::size_t found; friend RecipesList tag_invoke(boost::json::value_to_tag, const boost::json::value& j); }; -struct CustomRecipeSummary { - RecipeId id; - std::string name; - std::string link; - - friend CustomRecipeSummary tag_invoke(boost::json::value_to_tag, const boost::json::value& j); -}; - -struct CustomRecipesList { - std::vector page; +struct RecipesListWithIngredientsCount { + std::vector page; std::size_t found; - friend CustomRecipesList tag_invoke(boost::json::value_to_tag, const boost::json::value& j); + friend RecipesListWithIngredientsCount tag_invoke(boost::json::value_to_tag, + const boost::json::value& j); }; + struct RecipeCreateBody { std::string name; std::vector ingredients; std::string link; - friend void tag_invoke(boost::json::value_from_tag /*tag*/, boost::json::value& j, const RecipeCreateBody& body); + friend void tag_invoke(boost::json::value_from_tag, boost::json::value& j, const RecipeCreateBody& body); }; struct RecipeSearchResponse { @@ -106,4 +92,13 @@ struct RecipeSearchResponse { const boost::json::value& j); }; +struct RecipePublicationRequest { + moderation::PublicationRequestStatusStruct status; + std::chrono::system_clock::time_point created; + std::optional updated; + + friend RecipePublicationRequest tag_invoke(boost::json::value_to_tag, + const boost::json::value& j); +}; + } // namespace cookcookhnya::api::models::recipe diff --git a/src/backend/models/storage.cpp b/src/backend/models/storage.cpp index 22b10292..05f2c3b5 100644 --- a/src/backend/models/storage.cpp +++ b/src/backend/models/storage.cpp @@ -12,7 +12,6 @@ StorageSummary tag_invoke(json::value_to_tag /*tag*/, const json return { .id = value_to(j.at("id")), .name = value_to(j.at("name")), - .ownerId = value_to(j.at("ownerId")), }; } diff --git a/src/backend/models/storage.hpp b/src/backend/models/storage.hpp index 9cd16e9b..2aa6dec1 100644 --- a/src/backend/models/storage.hpp +++ b/src/backend/models/storage.hpp @@ -13,7 +13,6 @@ namespace cookcookhnya::api::models::storage { struct StorageSummary { StorageId id; std::string name; - tg_types::UserId ownerId; friend StorageSummary tag_invoke(boost::json::value_to_tag, const boost::json::value& j); }; diff --git a/src/backend/models/user.hpp b/src/backend/models/user.hpp index 702f4dc0..ccf8e647 100644 --- a/src/backend/models/user.hpp +++ b/src/backend/models/user.hpp @@ -19,6 +19,7 @@ struct UpdateUserInfoBody { struct UserDetails { tg_types::UserId userId; + // Note: current backend doesn't have alias field (19.07.2025) std::optional alias; std::string fullName; diff --git a/src/handlers/CMakeLists.txt b/src/handlers/CMakeLists.txt index b0f6c93e..54dfd8d6 100644 --- a/src/handlers/CMakeLists.txt +++ b/src/handlers/CMakeLists.txt @@ -1,38 +1,55 @@ target_sources(main PRIVATE - src/handlers/initial/start.cpp + src/handlers/commands/start.cpp + src/handlers/commands/wanna_eat.cpp + src/handlers/commands/shopping_list.cpp + src/handlers/commands/personal_account.cpp + src/handlers/commands/my_storages.cpp src/handlers/main_menu/view.cpp src/handlers/personal_account/ingredients_list/create.cpp src/handlers/personal_account/ingredients_list/publish.cpp src/handlers/personal_account/ingredients_list/view.cpp + src/handlers/personal_account/ingredients_list/delete.cpp src/handlers/personal_account/recipes_list/create.cpp - src/handlers/personal_account/recipes_list/recipe/search_ingredients.cpp - src/handlers/personal_account/recipes_list/recipe/view.cpp src/handlers/personal_account/recipes_list/view.cpp + src/handlers/personal_account/recipe/search_ingredients.cpp + src/handlers/personal_account/recipe/view.cpp + src/handlers/personal_account/recipe/moderation_history.cpp + src/handlers/personal_account/view.cpp + src/handlers/personal_account/publication_history.cpp - src/handlers/recipe/add_storage.cpp - src/handlers/recipe/view.cpp + src/handlers/cooking_planning/add_storage.cpp + src/handlers/cooking_planning/view.cpp src/handlers/recipes_suggestions/view.cpp src/handlers/shopping_list/create.cpp + src/handlers/shopping_list/search.cpp + src/handlers/shopping_list/storage_selection_to_buy.cpp src/handlers/shopping_list/view.cpp src/handlers/storage/ingredients/view.cpp + src/handlers/storage/ingredients/delete.cpp src/handlers/storage/members/add.cpp src/handlers/storage/members/delete.cpp src/handlers/storage/members/view.cpp src/handlers/storage/view.cpp + src/handlers/storage/delete.cpp src/handlers/storages_list/create.cpp - src/handlers/storages_list/delete.cpp src/handlers/storages_list/view.cpp src/handlers/storages_selection/view.cpp + + src/handlers/recipes_search/view.cpp + + src/handlers/recipe/view.cpp + + src/handlers/cooking/ingredients_spending.cpp ) diff --git a/src/handlers/commands/my_storages.cpp b/src/handlers/commands/my_storages.cpp new file mode 100644 index 00000000..58ffe49e --- /dev/null +++ b/src/handlers/commands/my_storages.cpp @@ -0,0 +1,17 @@ +#include "my_storages.hpp" + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" +#include "render/storages_list/view.hpp" +#include "states.hpp" + +namespace cookcookhnya::handlers::commands { + +using namespace render::storages_list; + +void handleMyStoragesCmd(MessageRef m, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + renderStorageList(false, m.from->id, m.chat->id, bot, api); + stateManager.put(StorageList{}); +}; + +} // namespace cookcookhnya::handlers::commands diff --git a/src/handlers/commands/my_storages.hpp b/src/handlers/commands/my_storages.hpp new file mode 100644 index 00000000..87719af2 --- /dev/null +++ b/src/handlers/commands/my_storages.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::commands { + +void handleMyStoragesCmd(MessageRef m, BotRef bot, SMRef stateManager, api::ApiClientRef api); + +} // namespace cookcookhnya::handlers::commands diff --git a/src/handlers/commands/personal_account.cpp b/src/handlers/commands/personal_account.cpp new file mode 100644 index 00000000..38c1087c --- /dev/null +++ b/src/handlers/commands/personal_account.cpp @@ -0,0 +1,18 @@ +#include "personal_account.hpp" + +#include "handlers/common.hpp" +#include "message_tracker.hpp" +#include "render/personal_account/view.hpp" +#include "states.hpp" + +namespace cookcookhnya::handlers::commands { + +using namespace render::personal_account; + +void handlePersonalAccountCmd(MessageRef m, BotRef bot, SMRef stateManager) { + message::deleteMessageId(m.from->id); + renderPersonalAccountMenu(m.from->id, m.chat->id, bot); + stateManager.put(PersonalAccountMenu{}); +}; + +} // namespace cookcookhnya::handlers::commands diff --git a/src/handlers/commands/personal_account.hpp b/src/handlers/commands/personal_account.hpp new file mode 100644 index 00000000..ac3861c0 --- /dev/null +++ b/src/handlers/commands/personal_account.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::commands { + +void handlePersonalAccountCmd(MessageRef m, BotRef bot, SMRef stateManager); + +} // namespace cookcookhnya::handlers::commands diff --git a/src/handlers/commands/shopping_list.cpp b/src/handlers/commands/shopping_list.cpp new file mode 100644 index 00000000..2a3f19fc --- /dev/null +++ b/src/handlers/commands/shopping_list.cpp @@ -0,0 +1,27 @@ +#include "shopping_list.hpp" + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" +#include "message_tracker.hpp" +#include "render/shopping_list/view.hpp" +#include "states.hpp" + +#include + +namespace cookcookhnya::handlers::commands { + +using namespace render::shopping_list; + +void handleShoppingListCmd(MessageRef m, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + const auto userId = m.from->id; + + const bool hasStorages = !api.getStoragesApi().getStoragesList(userId).empty(); + auto items = api.getShoppingListApi().get(userId); + + auto newState = ShoppingListView{.items = std::move(items), .canBuy = hasStorages}; + message::deleteMessageId(userId); + renderShoppingList(newState, userId, m.chat->id, bot); + stateManager.put(newState); +}; + +} // namespace cookcookhnya::handlers::commands diff --git a/src/handlers/commands/shopping_list.hpp b/src/handlers/commands/shopping_list.hpp new file mode 100644 index 00000000..dcb89c7e --- /dev/null +++ b/src/handlers/commands/shopping_list.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::commands { + +void handleShoppingListCmd(MessageRef m, BotRef bot, SMRef stateManager, api::ApiClientRef api); + +} // namespace cookcookhnya::handlers::commands diff --git a/src/handlers/commands/start.cpp b/src/handlers/commands/start.cpp new file mode 100644 index 00000000..05d32231 --- /dev/null +++ b/src/handlers/commands/start.cpp @@ -0,0 +1,75 @@ +#include "start.hpp" + +#include "backend/api/api.hpp" +#include "backend/id_types.hpp" +#include "handlers/common.hpp" +#include "message_tracker.hpp" +#include "render/main_menu/view.hpp" +#include "render/recipe/view.hpp" +#include "utils/parsing.hpp" +#include "utils/uuid.hpp" + +#include +#include +#include +#include + +namespace cookcookhnya::handlers::commands { + +using namespace render::main_menu; +using namespace render::recipe; +using namespace api::models::user; +using namespace std::literals; + +void handleStartCmd(MessageRef m, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + const auto userId = m.from->id; + const auto chatId = m.chat->id; + + std::string fullName = m.from->firstName; + if (!m.from->lastName.empty()) { + fullName += ' '; + fullName += m.from->lastName; + } + + std::optional alias; + if (!m.from->username.empty()) + alias = m.from->username; + + api.getUsersApi().updateInfo(userId, {.alias = std::move(alias), .fullName = std::move(fullName)}); + + if (!m.text.starts_with("/start ")) { + // default case + renderMainMenu(false, std::nullopt, userId, chatId, bot, api); + stateManager.put(MainMenu{}); + return; + } + const std::string_view payload = std::string_view{m.text}.substr("/start "sv.size()); + if (payload.starts_with("invite_")) { + const std::string_view hash = payload.substr("invite_"sv.size()); + auto storage = api.getStoragesApi().activate(userId, api::InvitationId{hash}); + if (!storage) + return; + renderMainMenu(false, storage->name, userId, chatId, bot, api); + stateManager.put(MainMenu{}); + return; + } + + if (payload.starts_with("recipe_")) { + const auto mRecipeId = utils::parseSafe(payload.substr("recipe_"sv.size())); + if (!mRecipeId) + return; + auto recipe = api.getRecipesApi().get(userId, *mRecipeId); + message::deleteMessageId(userId); + renderRecipeView(recipe, *mRecipeId, userId, chatId, bot); + stateManager.put(RecipeView{.prevState = std::nullopt, .recipe = std::move(recipe), .recipeId = *mRecipeId}); + return; + } +}; + +void handleNoState(MessageRef m, BotRef bot) { + if (m.text.starts_with("/start")) + return; + bot.sendMessage(m.chat->id, "Use /start please"); +}; + +} // namespace cookcookhnya::handlers::commands diff --git a/src/handlers/initial/start.hpp b/src/handlers/commands/start.hpp similarity index 51% rename from src/handlers/initial/start.hpp rename to src/handlers/commands/start.hpp index acde30a5..cccae238 100644 --- a/src/handlers/initial/start.hpp +++ b/src/handlers/commands/start.hpp @@ -1,11 +1,12 @@ #pragma once +#include "backend/api/api.hpp" #include "handlers/common.hpp" -namespace cookcookhnya::handlers::initial { +namespace cookcookhnya::handlers::commands { -void handleStartCmd(MessageRef m, BotRef bot, SMRef stateManager, ApiClientRef api); +void handleStartCmd(MessageRef m, BotRef bot, SMRef stateManager, api::ApiClientRef api); void handleNoState(MessageRef m, BotRef bot); -} // namespace cookcookhnya::handlers::initial +} // namespace cookcookhnya::handlers::commands diff --git a/src/handlers/commands/wanna_eat.cpp b/src/handlers/commands/wanna_eat.cpp new file mode 100644 index 00000000..52deb48f --- /dev/null +++ b/src/handlers/commands/wanna_eat.cpp @@ -0,0 +1,42 @@ +#include "wanna_eat.hpp" + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" +#include "message_tracker.hpp" +#include "render/main_menu/view.hpp" +#include "render/recipes_suggestions/view.hpp" +#include "render/storages_selection/view.hpp" +#include "states.hpp" +#include "utils/utils.hpp" + +#include +#include +#include + +namespace cookcookhnya::handlers::commands { + +using namespace render::select_storages; +using namespace render::main_menu; +using namespace render::recipes_suggestions; +using namespace std::views; + +void handleWannaEatCmd(MessageRef m, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + auto storages = api.getStoragesApi().getStoragesList(m.from->id); + if (storages.empty()) { + bot.sendMessage(m.chat->id, utils::utf8str(u8"😔 К сожалению, у вас пока что нет хранилищ.")); + renderMainMenu(false, std::nullopt, m.from->id, m.chat->id, bot, api); + stateManager.put(MainMenu{}); + } else if (storages.size() == 1) { + message::deleteMessageId(m.from->id); + renderRecipesSuggestion({storages[0].id}, 0, m.from->id, m.chat->id, bot, api); + stateManager.put(SuggestedRecipesList{ + .prevState = SuggestedRecipesList::FromMainMenuData{{}, std::move(storages[0])}, .pageNo = 0}); + } else { + message::deleteMessageId(m.from->id); + auto newState = StoragesSelection{.prevState = MainMenu{}, .selectedStorages = {}}; + renderStorageSelection(newState, m.from->id, m.chat->id, bot, api); + stateManager.put(newState); + } +}; + +} // namespace cookcookhnya::handlers::commands diff --git a/src/handlers/commands/wanna_eat.hpp b/src/handlers/commands/wanna_eat.hpp new file mode 100644 index 00000000..ff67da45 --- /dev/null +++ b/src/handlers/commands/wanna_eat.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::commands { + +void handleWannaEatCmd(MessageRef m, BotRef bot, SMRef stateManager, api::ApiClientRef api); + +} // namespace cookcookhnya::handlers::commands diff --git a/src/handlers/common.hpp b/src/handlers/common.hpp index 5ad0cb3b..dd9c61fd 100644 --- a/src/handlers/common.hpp +++ b/src/handlers/common.hpp @@ -1,10 +1,5 @@ #pragma once -#include "backend/api/api.hpp" -#include "backend/api/ingredients.hpp" -#include "backend/api/recipes.hpp" -#include "backend/api/storages.hpp" -#include "backend/api/users.hpp" #include "states.hpp" #include @@ -22,13 +17,16 @@ using states::PersonalAccountMenu; using states::CustomIngredientConfirmation; using states::CustomIngredientCreationEnterName; +using states::CustomIngredientDeletion; using states::CustomIngredientPublish; using states::CustomIngredientsList; using states::StorageList; -using states::RecipeStorageAddition; -using states::RecipeView; +using states::TotalPublicationHistory; + +using states::CookingPlanning; +using states::CookingPlanningStorageAddition; using states::StorageCreationEnterName; using states::StorageDeletion; @@ -38,33 +36,34 @@ using states::StorageMemberAddition; using states::StorageMemberDeletion; using states::StorageMemberView; +using states::StorageIngredientsDeletion; using states::StorageIngredientsList; using states::StoragesSelection; -using states::SuggestedRecipeList; +using states::SuggestedRecipesList; using states::ShoppingListCreation; +using states::ShoppingListIngredientSearch; +using states::ShoppingListStorageSelectionToBuy; using states::ShoppingListView; using states::CreateCustomRecipe; using states::CustomRecipeIngredientsSearch; +using states::CustomRecipePublicationHistory; using states::CustomRecipesList; -using states::RecipeCustomView; +using states::CustomRecipeView; -// Type aliases -using ApiClientRef = const api::ApiClient&; -using UserApiRef = const api::UsersApi&; -using StorageApiRef = const api::StoragesApi&; -using IngredientsApiRef = const api::IngredientsApi&; -using RecipesApiRef = const api::RecipesApi&; +using states::RecipesSearch; + +using states::RecipeView; + +using states::CookingIngredientsSpending; +// Type aliases using BotRef = const TgBot::Api&; using SMRef = const states::StateManager&; using MessageRef = const TgBot::Message&; using CallbackQueryRef = const TgBot::CallbackQuery&; using InlineQueryRef = const TgBot::InlineQuery&; -using NoState = tg_stater::HandlerTypes::NoState; -using AnyState = tg_stater::HandlerTypes::AnyState; - } // namespace cookcookhnya::handlers diff --git a/src/handlers/cooking/ingredients_spending.cpp b/src/handlers/cooking/ingredients_spending.cpp new file mode 100644 index 00000000..25818c59 --- /dev/null +++ b/src/handlers/cooking/ingredients_spending.cpp @@ -0,0 +1,70 @@ +#include "ingredients_spending.hpp" + +#include "backend/api/api.hpp" +#include "backend/id_types.hpp" +#include "handlers/common.hpp" +#include "render/cooking/ingredients_spending.hpp" +#include "render/cooking_planning/view.hpp" +#include "states.hpp" +#include "utils/parsing.hpp" +#include "utils/utils.hpp" + +#include +#include +#include +#include + +namespace cookcookhnya::handlers::cooking { + +using namespace render::cooking_planning; +using namespace render::cooking; +using states::helpers::SelectableIngredient; +using namespace std::literals; +using namespace std::views; +using std::ranges::to; + +void handleCookingIngredientsSpendingCQ( + CookingIngredientsSpending& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + auto chatId = cq.message->chat->id; + auto userId = cq.from->id; + + if (cq.data == "back") { + bot.answerCallbackQuery(cq.id); + renderCookingPlanning(state.prevState.availability, state.prevState.recipeId, userId, chatId, bot, api); + stateManager.put(auto{std::move(state.prevState)}); + return; + } + + if (cq.data == "remove") { + if (!state.storageId) + return; + auto selected = state.ingredients | filter(&SelectableIngredient::selected) | + transform(&SelectableIngredient::id) | to(); + api.getIngredientsApi().deleteMultipleFromStorage(userId, *state.storageId, selected); + bot.answerCallbackQuery(cq.id, utils::utf8str(u8"Успешно удалено из выбранного хранилища")); + return; + } + + if (cq.data == "to_shopping_list") { + auto selected = state.ingredients | filter(&SelectableIngredient::selected) | + transform(&SelectableIngredient::id) | to(); + api.getShoppingListApi().put(userId, selected); + bot.answerCallbackQuery(cq.id, utils::utf8str(u8"Успешно добавлено")); + return; + } + + if (cq.data.starts_with("ingredient_")) { + auto mIngredientId = + utils::parseSafe(std::string_view{cq.data}.substr("ingredient_"sv.size())); + if (!mIngredientId) + return; + auto ingredientIter = std::ranges::find(state.ingredients, *mIngredientId, &SelectableIngredient::id); + if (ingredientIter == state.ingredients.end()) + return; + ingredientIter->selected = !ingredientIter->selected; + renderIngredientsSpending(state.ingredients, state.storageId.has_value(), userId, chatId, bot); + return; + } +} + +} // namespace cookcookhnya::handlers::cooking diff --git a/src/handlers/cooking/ingredients_spending.hpp b/src/handlers/cooking/ingredients_spending.hpp new file mode 100644 index 00000000..93361715 --- /dev/null +++ b/src/handlers/cooking/ingredients_spending.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::cooking { + +void handleCookingIngredientsSpendingCQ( + CookingIngredientsSpending& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); + +} // namespace cookcookhnya::handlers::cooking diff --git a/src/handlers/cooking_planning/add_storage.cpp b/src/handlers/cooking_planning/add_storage.cpp new file mode 100644 index 00000000..af68e142 --- /dev/null +++ b/src/handlers/cooking_planning/add_storage.cpp @@ -0,0 +1,100 @@ +#include "add_storage.hpp" + +#include "backend/api/api.hpp" +#include "backend/id_types.hpp" +#include "backend/models/storage.hpp" +#include "handlers/common.hpp" +#include "render/cooking_planning/add_storage.hpp" +#include "render/cooking_planning/view.hpp" +#include "states.hpp" +#include "utils/ingredients_availability.hpp" +#include "utils/parsing.hpp" + +#include +#include +#include +#include + +namespace cookcookhnya::handlers::cooking_planning { + +using namespace render::cooking_planning; +using namespace api::models::storage; + +void handleCookingPlanningStorageAdditionCQ( + CookingPlanningStorageAddition& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + bot.answerCallbackQuery(cq.id); + const std::string& data = cq.data; + auto chatId = cq.message->chat->id; + auto userId = cq.from->id; + + if (data == "back") { + renderCookingPlanning(state.prevState.availability, state.prevState.recipeId, userId, chatId, bot, api); + stateManager.put(auto{std::move(state.prevState)}); + return; + } + + if (data[0] == '-') { + auto newStorageId = utils::parseSafe(std::string_view{data}.substr(1)); + if (newStorageId) { + auto newStorageDetails = api.getStoragesApi().get(userId, *newStorageId); + const StorageSummary newStorage = {.id = *newStorageId, .name = newStorageDetails.name}; + state.prevState.addedStorages.push_back(newStorage); + utils::addStorage(state.prevState.availability, newStorage); + + using StoragesList = std::vector; + auto selectedStorages = state.prevState.getStorages(); + const StoragesList* selectedStoragesPtr = nullptr; + // very optimized decision! (no) + if (auto* storagesVal = std::get_if(&selectedStorages)) + selectedStoragesPtr = storagesVal; + else if (auto* storagesRef = std::get_if>(&selectedStorages)) + selectedStoragesPtr = &storagesRef->get(); + else + return; + + renderStoragesSuggestion(state.prevState.availability, + *selectedStoragesPtr, + state.prevState.addedStorages, + state.prevState.recipeId, + userId, + chatId, + bot, + api); + return; + } + } + + if (data[0] == '+') { + auto newStorageId = utils::parseSafe(std::string_view{data}.substr(1)); + if (newStorageId) { + auto newStorageDetails = api.getStoragesApi().get(userId, *newStorageId); + const StorageSummary newStorage = {.id = *newStorageId, .name = newStorageDetails.name}; + state.prevState.addedStorages.erase(std::ranges::find( + state.prevState.addedStorages, newStorageId, &api::models::storage::StorageSummary::id)); + utils::deleteStorage(state.prevState.availability, newStorage); + + using StoragesList = std::vector; + auto selectedStorages = state.prevState.getStorages(); + const StoragesList* selectedStoragesPtr = nullptr; + // very optimized decision! (no) + if (auto* storagesVal = std::get_if(&selectedStorages)) + selectedStoragesPtr = storagesVal; + else if (auto* storagesRef = std::get_if>(&selectedStorages)) + selectedStoragesPtr = &storagesRef->get(); + else + return; + + renderStoragesSuggestion(state.prevState.availability, + *selectedStoragesPtr, + state.prevState.addedStorages, + state.prevState.recipeId, + userId, + chatId, + bot, + api); + return; + } + } +} + +} // namespace cookcookhnya::handlers::cooking_planning diff --git a/src/handlers/cooking_planning/add_storage.hpp b/src/handlers/cooking_planning/add_storage.hpp new file mode 100644 index 00000000..d5e0207c --- /dev/null +++ b/src/handlers/cooking_planning/add_storage.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::cooking_planning { + +void handleCookingPlanningStorageAdditionCQ( + CookingPlanningStorageAddition& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); + +} // namespace cookcookhnya::handlers::cooking_planning diff --git a/src/handlers/cooking_planning/view.cpp b/src/handlers/cooking_planning/view.cpp new file mode 100644 index 00000000..ae5773e6 --- /dev/null +++ b/src/handlers/cooking_planning/view.cpp @@ -0,0 +1,136 @@ +#include "view.hpp" + +#include "backend/api/api.hpp" +#include "backend/id_types.hpp" +#include "backend/models/ingredient.hpp" +#include "backend/models/storage.hpp" +#include "handlers/common.hpp" +#include "render/cooking/ingredients_spending.hpp" +#include "render/cooking_planning/add_storage.hpp" +#include "render/main_menu/view.hpp" +#include "render/recipes_search/view.hpp" +#include "render/recipes_suggestions/view.hpp" +#include "render/shopping_list/create.hpp" +#include "states.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace cookcookhnya::handlers::cooking_planning { + +using namespace render::recipes_suggestions; +using namespace render::shopping_list; +using namespace render::cooking_planning; +using namespace render::cooking; +using namespace render::recipes_search; + +using namespace api::models::ingredient; +using namespace api::models::storage; +using IngredientAvailability = states::CookingPlanning::IngredientAvailability; +using AvailabilityType = states::CookingPlanning::AvailabilityType; +using states::helpers::SelectableIngredient; + +using namespace std::views; +using std::ranges::to; + +namespace { + +std::optional getTheOnlyStorage(const CookingPlanning& state) { + // what a pattern matching hell (c) Team lead + if (const auto* prevState = std::get_if(&state.prevState)) { + if (const auto* prevPrevState = std::get_if(&prevState->prevState)) + return prevPrevState->storageId; + if (const auto* prevPrevState = std::get_if(&prevState->prevState)) + return prevPrevState->second.id; + if (const auto* prevPrevState = std::get_if(&prevState->prevState)) + if (prevPrevState->selectedStorages.size() == 1) + return prevPrevState->selectedStorages.front().id; + } else if (const auto* prevState = std::get_if(&state.prevState)) { + if (prevState->second.size() == 1) + return prevState->second[0].id; + return {}; + } + return {}; +} + +} // namespace + +void handleCookingPlanningCQ( + CookingPlanning& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + bot.answerCallbackQuery(cq.id); + const std::string data = cq.data; + auto chatId = cq.message->chat->id; + auto userId = cq.from->id; + + if (data == "start_cooking") { + auto ingredients = + state.availability | + transform([](const auto& ia) { return SelectableIngredient{{ia.ingredient.id, ia.ingredient.name}}; }) | + to(); + std::optional theOnlyStorage = getTheOnlyStorage(state); + renderIngredientsSpending(ingredients, theOnlyStorage.has_value(), userId, chatId, bot); + CookingIngredientsSpending newState{ + .prevState = std::move(state), .storageId = theOnlyStorage, .ingredients = std::move(ingredients)}; + stateManager.put(std::move(newState)); + return; + } + + if (data == "shopping_list") { + std::vector selectedIngredients; + std::vector allIngredients; + for (const auto& av : state.availability) { + if (av.available == AvailabilityType::NOT_AVAILABLE) { + selectedIngredients.push_back({.id = av.ingredient.id, .name = av.ingredient.name}); + } + allIngredients.push_back({.id = av.ingredient.id, .name = av.ingredient.name}); + } + renderShoppingListCreation(selectedIngredients, allIngredients, userId, chatId, bot); + stateManager.put(ShoppingListCreation{ + .prevState = std::move(state), + .selectedIngredients = selectedIngredients, + .allIngredients = allIngredients, + }); + return; + } + + if (data == "back") { + if (auto* prevState = std::get_if(&state.prevState)) { + renderRecipesSuggestion(prevState->getStorageIds(), prevState->pageNo, userId, chatId, bot, api); + stateManager.put(auto{std::move(*prevState)}); + } else if (auto* prevState = std::get_if(&state.prevState)) { + if (auto& mSearchState = prevState->first.prevState) { + renderRecipesSearch(mSearchState->pagination, mSearchState->page, userId, chatId, bot); + stateManager.put(auto{std::move(*mSearchState)}); + } else { + render::main_menu::renderMainMenu(true, std::nullopt, userId, chatId, bot, api); + stateManager.put(MainMenu{}); + } + } + return; + } + + if (data == "add_storages") { + using StoragesList = std::vector; + auto selectedStorages = state.getStorages(); + const StoragesList* selectedStoragesPtr = nullptr; + // very optimized decision! (no) + if (auto* storagesVal = std::get_if(&selectedStorages)) + selectedStoragesPtr = storagesVal; + else if (auto* storagesRef = std::get_if>(&selectedStorages)) + selectedStoragesPtr = &storagesRef->get(); + else + return; + + renderStoragesSuggestion( + state.availability, *selectedStoragesPtr, state.addedStorages, state.recipeId, userId, chatId, bot, api); + stateManager.put(CookingPlanningStorageAddition{.prevState = std::move(state)}); + return; + } +} + +} // namespace cookcookhnya::handlers::cooking_planning diff --git a/src/handlers/cooking_planning/view.hpp b/src/handlers/cooking_planning/view.hpp new file mode 100644 index 00000000..387a8ab4 --- /dev/null +++ b/src/handlers/cooking_planning/view.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::cooking_planning { + +void handleCookingPlanningCQ( + CookingPlanning& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); + +} // namespace cookcookhnya::handlers::cooking_planning diff --git a/src/handlers/handlers_list.hpp b/src/handlers/handlers_list.hpp index 1ef3c95f..99142f0f 100644 --- a/src/handlers/handlers_list.hpp +++ b/src/handlers/handlers_list.hpp @@ -1,72 +1,107 @@ #pragma once // Handler callbacks -#include "initial/start.hpp" +#include "commands/my_storages.hpp" +#include "commands/personal_account.hpp" +#include "commands/shopping_list.hpp" +#include "commands/start.hpp" +#include "commands/wanna_eat.hpp" #include "main_menu/view.hpp" +#include "personal_account/ingredients_list//delete.hpp" #include "personal_account/ingredients_list/create.hpp" #include "personal_account/ingredients_list/publish.hpp" #include "personal_account/ingredients_list/view.hpp" +#include "personal_account/recipe/moderation_history.hpp" +#include "personal_account/recipe/search_ingredients.hpp" +#include "personal_account/recipe/view.hpp" + #include "personal_account/recipes_list/create.hpp" -#include "personal_account/recipes_list/recipe/search_ingredients.hpp" -#include "personal_account/recipes_list/recipe/view.hpp" #include "personal_account/recipes_list/view.hpp" +#include "personal_account/publication_history.hpp" #include "personal_account/view.hpp" -#include "recipe/add_storage.hpp" -#include "recipe/view.hpp" +#include "cooking_planning/add_storage.hpp" +#include "cooking_planning/view.hpp" #include "recipes_suggestions/view.hpp" #include "shopping_list/create.hpp" +#include "shopping_list/search.hpp" +#include "shopping_list/storage_selection_to_buy.hpp" #include "shopping_list/view.hpp" +#include "storage/ingredients/delete.hpp" #include "storage/ingredients/view.hpp" #include "storage/members/add.hpp" #include "storage/members/delete.hpp" #include "storage/members/view.hpp" +#include "storage/delete.hpp" #include "storage/view.hpp" #include "storages_list/create.hpp" -#include "storages_list/delete.hpp" #include "storages_list/view.hpp" #include "storages_selection/view.hpp" -#include "handlers/common.hpp" +#include "recipes_search/view.hpp" + +#include "recipe/view.hpp" + +#include "cooking/ingredients_spending.hpp" #include #include -namespace cookcookhnya::handlers { - -using namespace initial; -using namespace main_menu; -using namespace personal_account; -using namespace personal_account::ingredients; -using namespace personal_account::recipes; -using namespace recipe; -using namespace shopping_list; -using namespace storage; -using namespace storage::ingredients; -using namespace storage::members; -using namespace storages_list; -using namespace storages_selection; -using namespace recipes_suggestions; +namespace cookcookhnya::handlers::bot_handlers { + +using namespace handlers::commands; +using namespace handlers::main_menu; +using namespace handlers::personal_account; +using namespace handlers::personal_account::ingredients; +using namespace handlers::personal_account::recipe; +using namespace handlers::personal_account::recipes_list; +using namespace handlers::shopping_list; +using namespace handlers::cooking_planning; +using namespace handlers::storage; +using namespace handlers::storage::ingredients; +using namespace handlers::storage::members; +using namespace handlers::storages_list; +using namespace handlers::storages_selection; +using namespace handlers::recipes_suggestions; +using namespace handlers::recipes_search; +using namespace handlers::recipe; +using namespace handlers::cooking; using namespace tg_stater; -namespace bot_handlers { +using NoState = tg_stater::HandlerTypes::NoState; +using AnyState = tg_stater::HandlerTypes::AnyState; -// Init +using noStateHandler = Handler; + +// Commands constexpr char startCmd[] = "start"; // NOLINT(*c-arrays) using startCmdHandler = Handler; // NOLINT(*decay) -using noStateHandler = Handler; + +constexpr char myStoragesCmd[] = "my_storages"; // NOLINT(*c-arrays) +using myStoragesCmdHandler = Handler; // NOLINT(*decay) + +constexpr char shoppingListCmd[] = "shopping_list"; // NOLINT(*c-arrays) +using shoppingListCmdHandler = + Handler; // NOLINT(*decay) + +constexpr char personalAccountCmd[] = "personal_account"; // NOLINT(*c-arrays) +using personalAccountCmdHandler = + Handler; // NOLINT(*decay) + +constexpr char wannaEatCmd[] = "wanna_eat"; // NOLINT(*c-arrays) +using wannaEatCmdHandler = Handler; // NOLINT(*decay) // MainMenu using mainMenuCQHandler = Handler; @@ -78,16 +113,17 @@ using customIngredientCreationEnterNameMsgHandler = using customIngredientCreationEnterNameCQHandler = Handler; using customIngredientConfirmationCQHandler = Handler; +using handleCustomIngredientDeletionCQHandler = Handler; using customIngredientPublishCQHandler = Handler; -// StorageListCreate +// StorageList using storageListCQHandler = Handler; using storageCreationEnterNameMsgHandler = Handler; using storageCreationEnterNameCQHandler = Handler; -using storageDeletionCQHandler = Handler; // StorageView using storageViewCQHandler = Handler; +using storageDeletionCQHandler = Handler; // StorageViewMembers using storageMemberViewCQHandler = Handler; @@ -98,23 +134,32 @@ using storageMemberDeletionCQHandler = Handler; -// SuggestedRecipeList -using suggestedRecipeListCQHandler = Handler; +// SuggestedRecipesList +using suggestedRecipeListCQHandler = Handler; // StorageIngredientsList using storageIngredientsListCQHandler = Handler; using storageIngredientsListIQHandler = Handler; -// RecipeView -using recipeViewCQHandler = Handler; -using recipeStorageAdditionCQHandler = Handler; +// StorageIngredientsDeletion +using storageIngredientsDeletionCQHandler = Handler; + +// Cooking planning +using cookingPlanningCQHandler = Handler; +using cookingPlanningStorageAdditionCQHandler = + Handler; using shoppingListCreationCQHandler = Handler; // Shopping list using shoppingListViewCQHandler = Handler; +using shoppingListStorageSelectionToBuyCQHandler = + Handler; +using shoppingListIngredientSearchCQHandler = Handler; +using shoppingListIngredientSearchIQHandler = Handler; // Personal account using personalAccountMenuCQHandler = Handler; +using totalPublicationHistoryCQHandler = Handler; // Custom Recipes List using customRecipesListCQHandler = Handler; @@ -125,7 +170,17 @@ using createCustomRecipeCQHandler = Handler; using customRecipeIngredientsSearchCQHandler = Handler; using customRecipeIngredientsSearchIQHandler = Handler; +using customRecipePublicationHistoryCQHandler = + Handler; + +// Recipes search +using recipesSearchCQHandler = Handler; +using recipesSearchIQHandler = Handler; + +// Recipe +using recipeViewCQHandler = Handler; -} // namespace bot_handlers +// Cooking +using cookingIngredientsSpendingCQHandler = Handler; -} // namespace cookcookhnya::handlers +} // namespace cookcookhnya::handlers::bot_handlers diff --git a/src/handlers/initial/start.cpp b/src/handlers/initial/start.cpp deleted file mode 100644 index eb4ec88c..00000000 --- a/src/handlers/initial/start.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "start.hpp" - -#include "backend/id_types.hpp" -#include "backend/models/user.hpp" -#include "handlers/common.hpp" -#include "render/main_menu/view.hpp" -#include "states.hpp" - -#include -#include -#include -#include - -namespace cookcookhnya::handlers::initial { - -using namespace render::main_menu; -using namespace std::literals; - -void handleStartCmd(MessageRef m, BotRef bot, SMRef stateManager, ApiClientRef api) { - auto userId = m.from->id; - renderMainMenu(false, m.from->id, m.chat->id, bot, api); - stateManager.put(MainMenu{}); - std::string fullName = m.from->firstName; - if (!m.from->lastName.empty()) { - fullName += ' '; - fullName += m.from->lastName; - } - std::optional alias; - if (!m.from->username.empty()) - alias = m.from->username; - - api.getUsersApi().updateInfo( - userId, - api::models::user::UpdateUserInfoBody{.alias = std::move(m.from->username), .fullName = std::move(fullName)}); - - auto startText = m.text; - const int hashPos = "/start "sv.size(); - if (startText.size() > hashPos - 1) { - auto hash = std::string(m.text).substr(hashPos); - api.getStoragesApi().activate(userId, hash); - } -}; - -void handleNoState(MessageRef m, BotRef bot) { - if (m.text.starts_with("/start")) - return; - bot.sendMessage(m.chat->id, "Use /start please"); -}; - -} // namespace cookcookhnya::handlers::initial diff --git a/src/handlers/main_menu/view.cpp b/src/handlers/main_menu/view.cpp index fd0a9f26..a51a07ac 100644 --- a/src/handlers/main_menu/view.cpp +++ b/src/handlers/main_menu/view.cpp @@ -1,15 +1,15 @@ #include "view.hpp" -#include "backend/api/storages.hpp" -#include "backend/id_types.hpp" +#include "backend/api/api.hpp" #include "handlers/common.hpp" #include "render/personal_account/view.hpp" +#include "render/recipes_search/view.hpp" #include "render/recipes_suggestions/view.hpp" #include "render/shopping_list/view.hpp" #include "render/storages_list/view.hpp" #include "render/storages_selection/view.hpp" -#include +#include #include namespace cookcookhnya::handlers::main_menu { @@ -19,35 +19,50 @@ using namespace render::recipes_suggestions; using namespace render::select_storages; using namespace render::shopping_list; using namespace render::personal_account; +using namespace render::recipes_search; +using namespace std::views; -void handleMainMenuCQ(MainMenu& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, ApiClientRef api) { +void handleMainMenuCQ(MainMenu& state, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, api::ApiClientRef api) { bot.answerCallbackQuery(cq.id); auto userId = cq.from->id; auto chatId = cq.message->chat->id; auto storages = api.getStoragesApi().getStoragesList(userId); + if (cq.data == "storage_list") { renderStorageList(true, userId, chatId, bot, api); stateManager.put(StorageList{}); return; } + if (cq.data == "wanna_eat") { if (storages.size() == 1) { - auto storageId = {storages[0].id}; - renderRecipesSuggestion(storageId, 0, userId, chatId, bot, api); - stateManager.put(SuggestedRecipeList{.pageNo = 0, .storageIds = storageId, .fromStorage = false}); + renderRecipesSuggestion({storages[0].id}, 0, userId, chatId, bot, api); + stateManager.put(SuggestedRecipesList{ + .prevState = SuggestedRecipesList::FromMainMenuData{state, std::move(storages[0])}, .pageNo = 0}); return; } renderStorageSelection({}, userId, chatId, bot, api); - stateManager.put(StoragesSelection{.storageIds = std::vector{}}); + stateManager.put(StoragesSelection{.prevState = state, .selectedStorages = {}}); return; } + if (cq.data == "shopping_list") { + const bool canBuy = !storages.empty(); auto items = api.getShoppingListApi().get(userId); - stateManager.put( - ShoppingListView{{{std::make_move_iterator(items.begin()), std::make_move_iterator(items.end())}}}); - renderShoppingList(std::get(*stateManager.get()).items.getAll(), userId, chatId, bot); + + auto newState = ShoppingListView{.items = std::move(items), .canBuy = canBuy}; + renderShoppingList(newState, userId, chatId, bot); + stateManager.put(std::move(newState)); return; } + + if (cq.data == "recipes_search") { + auto newState = RecipesSearch{}; + renderRecipesSearch(newState.pagination, newState.page, userId, chatId, bot); + stateManager.put(std::move(newState)); + return; + } + if (cq.data == "personal_account") { renderPersonalAccountMenu(userId, chatId, bot); stateManager.put(PersonalAccountMenu{}); diff --git a/src/handlers/main_menu/view.hpp b/src/handlers/main_menu/view.hpp index 9bc51877..56a9086f 100644 --- a/src/handlers/main_menu/view.hpp +++ b/src/handlers/main_menu/view.hpp @@ -1,9 +1,10 @@ #pragma once +#include "backend/api/api.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::main_menu { -void handleMainMenuCQ(MainMenu& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, ApiClientRef api); +void handleMainMenuCQ(MainMenu&, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, api::ApiClientRef api); } // namespace cookcookhnya::handlers::main_menu diff --git a/src/handlers/personal_account/ingredients_list/create.cpp b/src/handlers/personal_account/ingredients_list/create.cpp index 02f328fc..87f110d8 100644 --- a/src/handlers/personal_account/ingredients_list/create.cpp +++ b/src/handlers/personal_account/ingredients_list/create.cpp @@ -1,22 +1,29 @@ #include "create.hpp" +#include "backend/api/ingredients.hpp" #include "backend/models/ingredient.hpp" #include "handlers/common.hpp" #include "message_tracker.hpp" #include "render/personal_account/ingredients_list/create.hpp" #include "render/personal_account/ingredients_list/view.hpp" -#include "states.hpp" +#include "render/personal_account/recipe/search_ingredients.hpp" +#include "render/storage/ingredients/view.hpp" #include "utils/utils.hpp" +#include + namespace cookcookhnya::handlers::personal_account::ingredients { +using namespace render::storage::ingredients; +using namespace render::personal_account::recipe; using namespace render::personal_account::ingredients; +using namespace std::views; void handleCustomIngredientCreationEnterNameMsg(CustomIngredientCreationEnterName& /*unused*/, MessageRef m, BotRef& bot, SMRef stateManager, - IngredientsApiRef api) { + api::IngredientsApiRef api) { auto name = m.text; auto userId = m.from->id; auto chatId = m.chat->id; @@ -26,7 +33,7 @@ void handleCustomIngredientCreationEnterNameMsg(CustomIngredientCreationEnterNam if (messageId) { bot.editMessageText(text, chatId, *messageId); } - renderCustomIngredientConfirmation(name, userId, chatId, bot, api); + renderCustomIngredientConfirmation(false, name, userId, chatId, bot, api); stateManager.put(CustomIngredientConfirmation{name}); } @@ -34,7 +41,7 @@ void handleCustomIngredientCreationEnterNameCQ(CustomIngredientCreationEnterName CallbackQueryRef cq, BotRef& bot, SMRef stateManager, - IngredientsApiRef api) { + api::IngredientsApiRef api) { bot.answerCallbackQuery(cq.id); auto userId = cq.from->id; auto chatId = cq.message->chat->id; @@ -45,19 +52,40 @@ void handleCustomIngredientCreationEnterNameCQ(CustomIngredientCreationEnterName } void handleCustomIngredientConfirmationCQ( - CustomIngredientConfirmation& state, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, IngredientsApiRef api) { + CustomIngredientConfirmation& state, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, api::ApiClientRef api) { bot.answerCallbackQuery(cq.id); auto userId = cq.from->id; auto chatId = cq.message->chat->id; auto name = state.name; + if (cq.data == "confirm") { - api.createCustom(userId, api::models::ingredient::IngredientCreateBody{name}); + api.getIngredientsApi().createCustom(userId, api::models::ingredient::IngredientCreateBody{name}); renderCustomIngredientsList(true, userId, chatId, bot, api); stateManager.put(CustomIngredientsList{}); } + if (cq.data == "back") { + const std::size_t numOfIngredientsOnPage = 5; + + if (state.recipeFrom.has_value() && state.ingredients.has_value()) { + auto newState = + CustomRecipeIngredientsSearch{state.recipeFrom.value(), state.ingredients.value() | as_rvalue, ""}; + renderRecipeIngredientsSearch(newState, numOfIngredientsOnPage, userId, chatId, bot); + stateManager.put(std::move(newState)); + return; + } + + if (state.storageFrom.has_value()) { + auto ingredients = api.getIngredientsApi().getStorageIngredients(userId, state.storageFrom.value()); + auto newState = StorageIngredientsList{state.storageFrom.value(), ingredients | as_rvalue, ""}; + renderIngredientsListSearch(newState, userId, chatId, bot); + stateManager.put(std::move(newState)); + return; + } + renderCustomIngredientsList(true, userId, chatId, bot, api); stateManager.put(CustomIngredientsList{}); + return; } } diff --git a/src/handlers/personal_account/ingredients_list/create.hpp b/src/handlers/personal_account/ingredients_list/create.hpp index 27a10e34..9bd09f74 100644 --- a/src/handlers/personal_account/ingredients_list/create.hpp +++ b/src/handlers/personal_account/ingredients_list/create.hpp @@ -1,5 +1,7 @@ #pragma once +#include "backend/api/api.hpp" +#include "backend/api/ingredients.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::personal_account::ingredients { @@ -8,18 +10,18 @@ void handleCustomIngredientCreationEnterNameMsg(CustomIngredientCreationEnterNam MessageRef m, BotRef& bot, SMRef stateManager, - IngredientsApiRef api); + api::IngredientsApiRef api); void handleCustomIngredientCreationEnterNameCQ(CustomIngredientCreationEnterName& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, - IngredientsApiRef api); + api::IngredientsApiRef api); void handleCustomIngredientConfirmationCQ(CustomIngredientConfirmation& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, - IngredientsApiRef api); + api::ApiClientRef api); } // namespace cookcookhnya::handlers::personal_account::ingredients diff --git a/src/handlers/personal_account/ingredients_list/delete.cpp b/src/handlers/personal_account/ingredients_list/delete.cpp new file mode 100644 index 00000000..0b5e0a60 --- /dev/null +++ b/src/handlers/personal_account/ingredients_list/delete.cpp @@ -0,0 +1,29 @@ +#include "delete.hpp" + +#include "backend/id_types.hpp" +#include "handlers/common.hpp" +#include "render/personal_account/ingredients_list/view.hpp" +#include "states.hpp" +#include "utils/parsing.hpp" + +namespace cookcookhnya::handlers::personal_account::ingredients { + +using namespace render::personal_account::ingredients; + +void handleCustomIngredientDeletionCQ(CustomIngredientDeletion& /*unused*/, + CallbackQueryRef cq, + BotRef& bot, + SMRef stateManager, + api::IngredientsApiRef api) { + bot.answerCallbackQuery(cq.id); + auto userId = cq.from->id; + auto chatId = cq.message->chat->id; + + auto ingredientId = utils::parseSafe(cq.data); + if (ingredientId) { + api.deleteCustom(userId, *ingredientId); + } + renderCustomIngredientsList(true, userId, chatId, bot, api); + stateManager.put(CustomIngredientsList{}); +} +} // namespace cookcookhnya::handlers::personal_account::ingredients diff --git a/src/handlers/personal_account/ingredients_list/delete.hpp b/src/handlers/personal_account/ingredients_list/delete.hpp new file mode 100644 index 00000000..f2338f26 --- /dev/null +++ b/src/handlers/personal_account/ingredients_list/delete.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "backend/api/ingredients.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::personal_account::ingredients { + +void handleCustomIngredientDeletionCQ(CustomIngredientDeletion& /*unused*/, + CallbackQueryRef cq, + BotRef& bot, + SMRef stateManager, + api::IngredientsApiRef api); + +} // namespace cookcookhnya::handlers::personal_account::ingredients diff --git a/src/handlers/personal_account/ingredients_list/publish.cpp b/src/handlers/personal_account/ingredients_list/publish.cpp index 05aa2f98..3622a9fd 100644 --- a/src/handlers/personal_account/ingredients_list/publish.cpp +++ b/src/handlers/personal_account/ingredients_list/publish.cpp @@ -1,5 +1,6 @@ #include "publish.hpp" +#include "backend/api/ingredients.hpp" #include "backend/id_types.hpp" #include "handlers/common.hpp" #include "render/personal_account/ingredients_list/view.hpp" @@ -10,8 +11,11 @@ namespace cookcookhnya::handlers::personal_account::ingredients { using namespace render::personal_account::ingredients; -void handleCustomIngredientPublishCQ( - CustomIngredientPublish& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, IngredientsApiRef api) { +void handleCustomIngredientPublishCQ(CustomIngredientPublish& /*unused*/, + CallbackQueryRef cq, + BotRef& bot, + SMRef stateManager, + api::IngredientsApiRef api) { bot.answerCallbackQuery(cq.id); auto userId = cq.from->id; auto chatId = cq.message->chat->id; diff --git a/src/handlers/personal_account/ingredients_list/publish.hpp b/src/handlers/personal_account/ingredients_list/publish.hpp index aaf2b74b..2c8c9956 100644 --- a/src/handlers/personal_account/ingredients_list/publish.hpp +++ b/src/handlers/personal_account/ingredients_list/publish.hpp @@ -1,10 +1,14 @@ #pragma once +#include "backend/api/ingredients.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::personal_account::ingredients { -void handleCustomIngredientPublishCQ( - CustomIngredientPublish& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, IngredientsApiRef api); +void handleCustomIngredientPublishCQ(CustomIngredientPublish& /*unused*/, + CallbackQueryRef cq, + BotRef& bot, + SMRef stateManager, + api::IngredientsApiRef api); } // namespace cookcookhnya::handlers::personal_account::ingredients diff --git a/src/handlers/personal_account/ingredients_list/view.cpp b/src/handlers/personal_account/ingredients_list/view.cpp index e7245bbf..a021aeb0 100644 --- a/src/handlers/personal_account/ingredients_list/view.cpp +++ b/src/handlers/personal_account/ingredients_list/view.cpp @@ -1,10 +1,11 @@ #include "view.hpp" +#include "backend/api/api.hpp" #include "handlers/common.hpp" #include "render/personal_account/ingredients_list/create.hpp" +#include "render/personal_account/ingredients_list/delete.hpp" #include "render/personal_account/ingredients_list/publish.hpp" #include "render/personal_account/view.hpp" - #include "states.hpp" namespace cookcookhnya::handlers::personal_account::ingredients { @@ -13,7 +14,7 @@ using namespace render::personal_account::ingredients; using namespace render::personal_account; void handleCustomIngredientsListCQ( - CustomIngredientsList& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, ApiClientRef api) { + CustomIngredientsList& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, api::ApiClientRef api) { bot.answerCallbackQuery(cq.id); auto userId = cq.from->id; auto chatId = cq.message->chat->id; @@ -22,6 +23,11 @@ void handleCustomIngredientsListCQ( stateManager.put(CustomIngredientCreationEnterName{}); return; } + if (cq.data == "delete") { + renderCustomIngredientDeletion(userId, chatId, bot, api); + stateManager.put(CustomIngredientDeletion{}); + return; + } if (cq.data == "publish") { renderCustomIngredientPublication(userId, chatId, bot, api); stateManager.put(CustomIngredientPublish{}); diff --git a/src/handlers/personal_account/ingredients_list/view.hpp b/src/handlers/personal_account/ingredients_list/view.hpp index c28eb38a..ef1fd4bf 100644 --- a/src/handlers/personal_account/ingredients_list/view.hpp +++ b/src/handlers/personal_account/ingredients_list/view.hpp @@ -1,10 +1,11 @@ #pragma once +#include "backend/api/api.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::personal_account::ingredients { void handleCustomIngredientsListCQ( - CustomIngredientsList& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, ApiClientRef api); + CustomIngredientsList& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, api::ApiClientRef api); } // namespace cookcookhnya::handlers::personal_account::ingredients diff --git a/src/handlers/personal_account/publication_history.cpp b/src/handlers/personal_account/publication_history.cpp new file mode 100644 index 00000000..6fcf6979 --- /dev/null +++ b/src/handlers/personal_account/publication_history.cpp @@ -0,0 +1,23 @@ +#include "publication_history.hpp" + +#include "render/personal_account/view.hpp" + +namespace cookcookhnya::handlers::personal_account { + +using namespace render::personal_account; + +void handleTotalPublicationHistoryCQ( + TotalPublicationHistory& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, api::ApiClientRef /**/) { + const std::string data = cq.data; + bot.answerCallbackQuery(cq.id); + auto chatId = cq.message->chat->id; + auto userId = cq.from->id; + + if (data == "back") { + renderPersonalAccountMenu(userId, chatId, bot); + stateManager.put(PersonalAccountMenu{}); + return; + } +} + +} // namespace cookcookhnya::handlers::personal_account diff --git a/src/handlers/personal_account/publication_history.hpp b/src/handlers/personal_account/publication_history.hpp new file mode 100644 index 00000000..376f9619 --- /dev/null +++ b/src/handlers/personal_account/publication_history.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::personal_account { + +void handleTotalPublicationHistoryCQ( + TotalPublicationHistory& state, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, api::ApiClientRef api); + +} // namespace cookcookhnya::handlers::personal_account diff --git a/src/handlers/personal_account/recipe/moderation_history.cpp b/src/handlers/personal_account/recipe/moderation_history.cpp new file mode 100644 index 00000000..4085d515 --- /dev/null +++ b/src/handlers/personal_account/recipe/moderation_history.cpp @@ -0,0 +1,63 @@ +#include "moderation_history.hpp" + +#include "backend/models/publication_request_status.hpp" +#include "handlers/common.hpp" +#include "render/personal_account/recipe/moderation_history.hpp" +#include "render/personal_account/recipe/view.hpp" + +namespace cookcookhnya::handlers::personal_account::recipe { + +using namespace render::personal_account::recipe; + +void handleCustomRecipePublicationHistoryCQ( + CustomRecipePublicationHistory& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + const std::string data = cq.data; + auto chatId = cq.message->chat->id; + auto userId = cq.from->id; + + if (data == "backFromRules") { + auto history = api.getRecipesApi().getRecipeRequestHistory(userId, state.recipeId); + renderPublicationHistory(userId, chatId, state.recipeName, state.errorReport, history, bot); + bot.answerCallbackQuery(cq.id); + return; + } + if (data == "rules") { + renderPublicationRules(userId, chatId, bot); + bot.answerCallbackQuery(cq.id); + return; + } + if (data == "back") { + auto ingredientsAndRecipeName = renderCustomRecipe(true, userId, chatId, state.recipeId, bot, api); + stateManager.put(CustomRecipeView{.recipeId = state.recipeId, + .pageNo = state.pageNo, + .ingredients = ingredientsAndRecipeName.first, + .recipeName = ingredientsAndRecipeName.second}); + return; + } + + if (data == "confirm") { + + auto history = api.getRecipesApi().getRecipeRequestHistory(userId, state.recipeId); + // Here check for emptiness first, thanks to lazy compilator + const bool shouldPublish = history.empty() || (history.back().status.status == + api::models::moderation::PublicationRequestStatus::REJECTED); + + if (shouldPublish) { + try { + api.getRecipesApi().publishCustom(userId, state.recipeId); + state.errorReport = ""; + } catch (...) { + state.errorReport = + utils::utf8str(u8"⚠️Что-то пошло не так, вероятно ваш рецепт содержит неопубликованные ингредиенты"); + bot.answerCallbackQuery(cq.id); + } + // Get updated history + history = api.getRecipesApi().getRecipeRequestHistory(userId, state.recipeId); + } + renderPublicationHistory(userId, chatId, state.recipeName, state.errorReport, history, bot); + bot.answerCallbackQuery(cq.id); + return; + } +} + +} // namespace cookcookhnya::handlers::personal_account::recipe diff --git a/src/handlers/personal_account/recipe/moderation_history.hpp b/src/handlers/personal_account/recipe/moderation_history.hpp new file mode 100644 index 00000000..285a4d22 --- /dev/null +++ b/src/handlers/personal_account/recipe/moderation_history.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::personal_account::recipe { + +void handleCustomRecipePublicationHistoryCQ( + CustomRecipePublicationHistory& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); + +} // namespace cookcookhnya::handlers::personal_account::recipe diff --git a/src/handlers/personal_account/recipe/search_ingredients.cpp b/src/handlers/personal_account/recipe/search_ingredients.cpp new file mode 100644 index 00000000..6422962a --- /dev/null +++ b/src/handlers/personal_account/recipe/search_ingredients.cpp @@ -0,0 +1,143 @@ +#include "search_ingredients.hpp" + +#include "backend/api/api.hpp" +#include "backend/api/ingredients.hpp" +#include "backend/id_types.hpp" +#include "backend/models/ingredient.hpp" +#include "handlers/common.hpp" +#include "message_tracker.hpp" +#include "render/personal_account/ingredients_list/create.hpp" +#include "render/personal_account/recipe/search_ingredients.hpp" +#include "render/personal_account/recipe/view.hpp" +#include "tg_types.hpp" +#include "utils/parsing.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cookcookhnya::handlers::personal_account::recipe { + +using namespace api::models::ingredient; +using namespace render::personal_account::ingredients; +using namespace render::personal_account::recipe; +using namespace std::literals; +using namespace std::views; +using std::ranges::to; + +namespace { + +const std::size_t numOfIngredientsOnPage = 5; +const std::size_t threshhold = 70; + +void updateSearch(CustomRecipeIngredientsSearch& state, + bool isQueryChanged, + BotRef bot, + tg_types::UserId userId, + api::IngredientsApiRef api) { + state.pageNo = isQueryChanged ? 0 : state.pageNo; + + auto response = api.searchForRecipe( + userId, state.recipeId, state.query, numOfIngredientsOnPage, state.pageNo * numOfIngredientsOnPage, threshhold); + + state.totalFound = response.found; + if (state.totalFound == 0) { + renderSuggestIngredientCustomisation(state, userId, userId, bot); + return; + } + + const auto idGetter = &IngredientSearchForRecipeItem::id; + if (std::ranges::equal(response.page, state.searchItems, {}, idGetter, idGetter)) + return; + + state.searchItems = std::move(response.page); + + if (auto mMessageId = message::getMessageId(userId)) { + if (state.totalFound != 0) { + renderRecipeIngredientsSearch(state, numOfIngredientsOnPage, userId, userId, bot); + return; + } + } +} + +} // namespace + +void handleCustomRecipeIngredientsSearchCQ( + CustomRecipeIngredientsSearch& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + bot.answerCallbackQuery(cq.id); + const auto userId = cq.from->id; + const auto chatId = cq.message->chat->id; + + if (cq.data == "back") { + auto ingredientsAndName = renderCustomRecipe(true, userId, chatId, state.recipeId, bot, api); + auto ingredients = state.recipeIngredients.getValues() | as_rvalue | to(); + stateManager.put(CustomRecipeView{.recipeId = state.recipeId, + .pageNo = 0, + .ingredients = std::move(ingredients), + .recipeName = ingredientsAndName.second}); + return; + } + + if (cq.data == "prev") { + state.pageNo -= 1; + updateSearch(state, false, bot, userId, api); + return; + } + + if (cq.data == "next") { + state.pageNo += 1; + updateSearch(state, false, bot, userId, api); + return; + } + + if (cq.data.starts_with("ingredient_")) { + const std::string ingredientName{std::string_view{cq.data}.substr("ingredient_"sv.size())}; + renderCustomIngredientConfirmation(true, ingredientName, userId, chatId, bot, api); + auto ingredients = state.recipeIngredients.getValues() | as_rvalue | to(); + stateManager.put(CustomIngredientConfirmation{ingredientName, state.recipeId, ingredients, std::nullopt}); + } + + if (cq.data != "dont_handle") { + auto mIngredient = utils::parseSafe(cq.data); + if (!mIngredient) + return; + auto it = std::ranges::find(state.searchItems, *mIngredient, &IngredientSearchForRecipeItem::id); + if (it == state.searchItems.end()) + return; + + if (it->isInRecipe) { + api.getIngredientsApi().deleteFromRecipe(userId, state.recipeId, *mIngredient); + state.recipeIngredients.remove(*mIngredient); + } else { + api.getIngredientsApi().putToRecipe(userId, state.recipeId, *mIngredient); + state.recipeIngredients.put({.id = it->id, .name = it->name}); + } + it->isInRecipe = !it->isInRecipe; + renderRecipeIngredientsSearch(state, numOfIngredientsOnPage, userId, chatId, bot); + } +} + +void handleCustomRecipeIngredientsSearchIQ(CustomRecipeIngredientsSearch& state, + InlineQueryRef iq, + BotRef bot, + api::IngredientsApiRef api) { + state.query = iq.query; + const auto userId = iq.from->id; + if (iq.query.empty()) { + // When query is empty then search shouldn't happen + state.searchItems.clear(); + state.totalFound = 0; + renderRecipeIngredientsSearch(state, numOfIngredientsOnPage, userId, userId, bot); + } else { + updateSearch(state, true, bot, userId, api); + } + // Cache is not disabled on Windows and Linux desktops. Works on Android and Web + // bot.answerInlineQuery(iq.id, {}, 0); +} + +} // namespace cookcookhnya::handlers::personal_account::recipe diff --git a/src/handlers/personal_account/recipes_list/recipe/search_ingredients.hpp b/src/handlers/personal_account/recipe/search_ingredients.hpp similarity index 52% rename from src/handlers/personal_account/recipes_list/recipe/search_ingredients.hpp rename to src/handlers/personal_account/recipe/search_ingredients.hpp index 065e154b..a176c414 100644 --- a/src/handlers/personal_account/recipes_list/recipe/search_ingredients.hpp +++ b/src/handlers/personal_account/recipe/search_ingredients.hpp @@ -1,15 +1,17 @@ #pragma once +#include "backend/api/api.hpp" +#include "backend/api/ingredients.hpp" #include "handlers/common.hpp" -namespace cookcookhnya::handlers::personal_account::recipes { +namespace cookcookhnya::handlers::personal_account::recipe { void handleCustomRecipeIngredientsSearchCQ( - CustomRecipeIngredientsSearch& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api); + CustomRecipeIngredientsSearch& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); void handleCustomRecipeIngredientsSearchIQ(CustomRecipeIngredientsSearch& state, InlineQueryRef iq, BotRef bot, - IngredientsApiRef api); + api::IngredientsApiRef api); -} // namespace cookcookhnya::handlers::personal_account::recipes +} // namespace cookcookhnya::handlers::personal_account::recipe diff --git a/src/handlers/personal_account/recipe/view.cpp b/src/handlers/personal_account/recipe/view.cpp new file mode 100644 index 00000000..e732c116 --- /dev/null +++ b/src/handlers/personal_account/recipe/view.cpp @@ -0,0 +1,63 @@ +#include "view.hpp" + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" +#include "render/personal_account/recipe/moderation_history.hpp" +#include "render/personal_account/recipe/search_ingredients.hpp" +#include "render/personal_account/recipes_list/view.hpp" +#include "states.hpp" + +#include +#include +#include +#include + +namespace cookcookhnya::handlers::personal_account::recipe { + +using namespace render::personal_account::recipes_list; +using namespace render::personal_account::recipe; +using namespace std::views; + +const std::size_t numOfIngredientsOnPage = 5; + +void handleRecipeCustomViewCQ( + CustomRecipeView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + const std::string data = cq.data; + auto chatId = cq.message->chat->id; + auto userId = cq.from->id; + + if (data == "back") { + renderCustomRecipesList(state.pageNo, userId, chatId, bot, api); + stateManager.put(CustomRecipesList{.pageNo = state.pageNo}); + return; + } + + if (data == "change") { + auto newState = CustomRecipeIngredientsSearch{state.recipeId, state.ingredients | as_rvalue, ""}; + renderRecipeIngredientsSearch(newState, numOfIngredientsOnPage, userId, chatId, bot); + stateManager.put(std::move(newState)); + return; + } + + if (data == "delete") { + api.getRecipesApi().delete_(userId, state.recipeId); + // If some recipe was deleted then return to first page + // Made to avoid bug when delete last recipe on page -> will return to the non-existent page + renderCustomRecipesList(0, userId, chatId, bot, api); + stateManager.put(CustomRecipesList{.pageNo = 0}); + bot.answerCallbackQuery(cq.id); + return; + } + + if (data == "publish") { + auto history = api.getRecipesApi().getRecipeRequestHistory(userId, state.recipeId); + std::string temp; + renderPublicationHistory(userId, chatId, state.recipeName, temp, history, bot); + stateManager.put(CustomRecipePublicationHistory{ + .recipeId = state.recipeId, .pageNo = state.pageNo, .recipeName = state.recipeName, .errorReport = ""}); + bot.answerCallbackQuery(cq.id); + return; + } +} + +} // namespace cookcookhnya::handlers::personal_account::recipe diff --git a/src/handlers/personal_account/recipe/view.hpp b/src/handlers/personal_account/recipe/view.hpp new file mode 100644 index 00000000..d150d4e8 --- /dev/null +++ b/src/handlers/personal_account/recipe/view.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::personal_account::recipe { + +void handleRecipeCustomViewCQ( + CustomRecipeView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); + +} // namespace cookcookhnya::handlers::personal_account::recipe diff --git a/src/handlers/personal_account/recipes_list/create.cpp b/src/handlers/personal_account/recipes_list/create.cpp index e65770ef..5c92cc0d 100644 --- a/src/handlers/personal_account/recipes_list/create.cpp +++ b/src/handlers/personal_account/recipes_list/create.cpp @@ -1,37 +1,38 @@ #include "create.hpp" +#include "backend/api/recipes.hpp" #include "backend/models/recipe.hpp" #include "handlers/common.hpp" -#include "render/personal_account/recipes_list/recipe/view.hpp" +#include "render/personal_account/recipe/view.hpp" #include "render/personal_account/recipes_list/view.hpp" -namespace cookcookhnya::handlers::personal_account::recipes { +namespace cookcookhnya::handlers::personal_account::recipes_list { -using namespace render::personal_account::recipes; +using namespace api::models::recipe; +using namespace render::personal_account::recipes_list; +using namespace render::personal_account::recipe; void handleCreateCustomRecipeMsg( - CreateCustomRecipe& state, MessageRef m, BotRef bot, SMRef stateManager, RecipesApiRef recipeApi) { - + CreateCustomRecipe& /*unused*/, MessageRef m, BotRef bot, SMRef stateManager, api::RecipesApiRef recipeApi) { // Init with no ingredients and link. My suggestion: to use link as author's alias - state.recipeId = recipeApi.create( - m.from->id, api::models::recipe::RecipeCreateBody{.name = m.text, .ingredients = {}, .link = ""}); // - - renderCustomRecipe(false, m.from->id, m.chat->id, state.recipeId, bot, recipeApi); - stateManager.put(RecipeCustomView{.recipeId = state.recipeId, - .pageNo = 0, - .ingredients = {}}); // If it went from creation then as user will return - // from RecipeView to RecipesList on 1st page + auto recipeId = recipeApi.create(m.from->id, RecipeCreateBody{.name = m.text, .ingredients = {}, .link = ""}); + + renderCustomRecipe(false, m.from->id, m.chat->id, recipeId, bot, recipeApi); + // If it went from creation then as user will return + // from RecipeView to RecipesList on 1st page + stateManager.put(CustomRecipeView{.recipeId = recipeId, .pageNo = 0, .ingredients = {}, .recipeName = m.text}); }; void handleCreateCustomRecipeCQ( - CreateCustomRecipe& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, RecipesApiRef recipeApi) { + CreateCustomRecipe& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::RecipesApiRef recipeApi) { bot.answerCallbackQuery(cq.id); + if (cq.data == "cancel_recipe_creation") { renderCustomRecipesList(state.pageNo, cq.from->id, cq.message->chat->id, bot, recipeApi); - stateManager.put(CustomRecipesList{ - .pageNo = state.pageNo}); // If it went from creation then as user will return from - // RecipeView to RecipesList on page from which they entered in this state; + // If it went from creation then as user will return from + // RecipeView to RecipesList on page from which they entered in this state; + stateManager.put(CustomRecipesList{.pageNo = state.pageNo}); } }; -} // namespace cookcookhnya::handlers::personal_account::recipes +} // namespace cookcookhnya::handlers::personal_account::recipes_list diff --git a/src/handlers/personal_account/recipes_list/create.hpp b/src/handlers/personal_account/recipes_list/create.hpp index e96edc7b..81721fde 100644 --- a/src/handlers/personal_account/recipes_list/create.hpp +++ b/src/handlers/personal_account/recipes_list/create.hpp @@ -1,13 +1,14 @@ #pragma once +#include "backend/api/recipes.hpp" #include "handlers/common.hpp" -namespace cookcookhnya::handlers::personal_account::recipes { +namespace cookcookhnya::handlers::personal_account::recipes_list { void handleCreateCustomRecipeMsg( - CreateCustomRecipe&, MessageRef m, BotRef bot, SMRef stateManager, RecipesApiRef recipeApi); + CreateCustomRecipe&, MessageRef m, BotRef bot, SMRef stateManager, api::RecipesApiRef recipeApi); void handleCreateCustomRecipeCQ( - CreateCustomRecipe&, CallbackQueryRef cq, BotRef bot, SMRef stateManager, RecipesApiRef recipeApi); + CreateCustomRecipe&, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::RecipesApiRef recipeApi); -} // namespace cookcookhnya::handlers::personal_account::recipes +} // namespace cookcookhnya::handlers::personal_account::recipes_list diff --git a/src/handlers/personal_account/recipes_list/recipe/search_ingredients.cpp b/src/handlers/personal_account/recipes_list/recipe/search_ingredients.cpp deleted file mode 100644 index 1d58b7e6..00000000 --- a/src/handlers/personal_account/recipes_list/recipe/search_ingredients.cpp +++ /dev/null @@ -1,115 +0,0 @@ -#include "search_ingredients.hpp" - -#include "backend/id_types.hpp" -#include "backend/models/ingredient.hpp" -#include "handlers/common.hpp" -#include "message_tracker.hpp" -#include "render/personal_account/recipes_list/recipe/search_ingredients.hpp" -#include "render/personal_account/recipes_list/recipe/view.hpp" -#include "tg_types.hpp" -#include "utils/parsing.hpp" - -#include -#include -#include -#include -#include - -namespace cookcookhnya::handlers::personal_account::recipes { - -using namespace api::models::ingredient; -using namespace render::recipe::ingredients; -using namespace render::personal_account::recipes; - -// Global vars -const size_t numOfIngredientsOnPage = 5; -const size_t threshhold = 70; - -namespace { -void updateSearch(CustomRecipeIngredientsSearch& state, BotRef bot, tg_types::UserId userId, IngredientsApiRef api) { - - auto response = api.searchForRecipe(userId, - state.recipeId, - state.inlineQuery, - threshhold, - numOfIngredientsOnPage, - state.pageNo * numOfIngredientsOnPage); - if (response.found != state.totalFound || !std::ranges::equal(response.page, - state.searchItems, - std::ranges::equal_to{}, - &IngredientSearchForRecipeItem::id, - &IngredientSearchForRecipeItem::id)) { - state.searchItems = std::move(response.page); - state.totalFound = response.found; - if (auto mMessageId = message::getMessageId(userId)) - renderRecipeIngredientsSearch(state, numOfIngredientsOnPage, userId, userId, bot); - } -} -} // namespace - -void handleCustomRecipeIngredientsSearchCQ( - CustomRecipeIngredientsSearch& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api) { - bot.answerCallbackQuery(cq.id); - const auto userId = cq.from->id; - const auto chatId = cq.message->chat->id; - - if (cq.data == "back") { - renderCustomRecipe(true, userId, chatId, state.recipeId, bot, api); - std::vector ingredient; - ingredient.assign(state.recipeIngredients.getAll().begin(), state.recipeIngredients.getAll().end()); - stateManager.put(RecipeCustomView{.recipeId = state.recipeId, .pageNo = 0, .ingredients = ingredient}); - return; - } - if (cq.data == "prev") { - state.pageNo -= 1; - updateSearch(state, bot, userId, api); - return; - } - - if (cq.data == "next") { - state.pageNo += 1; - updateSearch(state, bot, userId, api); - return; - } - - if (cq.data != "dont_handle") { - - auto mIngredient = utils::parseSafe(cq.data); - if (!mIngredient) - return; - auto it = std::ranges::find(state.searchItems, *mIngredient, &IngredientSearchForRecipeItem::id); - if (it == state.searchItems.end()) - return; - - if (it->isInRecipe) { - api.getIngredientsApi().deleteFromRecipe(userId, state.recipeId, *mIngredient); - state.recipeIngredients.remove(*mIngredient); - } else { - api.getIngredientsApi().putToRecipe(userId, state.recipeId, *mIngredient); - state.recipeIngredients.put({.id = it->id, .name = it->name}); - } - it->isInRecipe = !it->isInRecipe; - renderRecipeIngredientsSearch(state, numOfIngredientsOnPage, userId, chatId, bot); - } -} - -void handleCustomRecipeIngredientsSearchIQ(CustomRecipeIngredientsSearch& state, - InlineQueryRef iq, - BotRef bot, - IngredientsApiRef api) { - const size_t numOfIngredientsOnPage = 5; - state.inlineQuery = iq.query; - const auto userId = iq.from->id; - if (iq.query.empty()) { - state.searchItems.clear(); - // When query is empty then search shouldn't happen - state.totalFound = 0; - renderRecipeIngredientsSearch(state, numOfIngredientsOnPage, userId, userId, bot); - } else { - updateSearch(state, bot, userId, api); - } - // Cache is not disabled on Windows and Linux desktops. Works on Android and Web - bot.answerInlineQuery(iq.id, {}, 0); -} - -} // namespace cookcookhnya::handlers::personal_account::recipes diff --git a/src/handlers/personal_account/recipes_list/recipe/view.cpp b/src/handlers/personal_account/recipes_list/recipe/view.cpp deleted file mode 100644 index 3d14c577..00000000 --- a/src/handlers/personal_account/recipes_list/recipe/view.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include "view.hpp" - -#include "handlers/common.hpp" -#include "render/personal_account/recipes_list/recipe/search_ingredients.hpp" -#include "render/personal_account/recipes_list/view.hpp" -#include "states.hpp" - -#include -#include -#include - -namespace cookcookhnya::handlers::personal_account::recipes { - -using namespace render::personal_account::recipes; -using namespace render::recipe::ingredients; - -void handleRecipeCustomViewCQ( - RecipeCustomView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api) { - const std::string data = cq.data; - auto chatId = cq.message->chat->id; - auto userId = cq.from->id; - const size_t numOfIngredientsOnPage = 5; - if (data == "back") { - renderCustomRecipesList(state.pageNo, userId, chatId, bot, api); - stateManager.put(CustomRecipesList{.pageNo = state.pageNo}); - return; - } - - if (data == "change") { - stateManager.put(CustomRecipeIngredientsSearch{ - state.recipeId, - {std::make_move_iterator(state.ingredients.begin()), std::make_move_iterator(state.ingredients.end())}, - ""}); - renderRecipeIngredientsSearch( - std::get(*stateManager.get()), numOfIngredientsOnPage, userId, chatId, bot); - return; - } - - if (data == "delete") { - api.getRecipesApi().delete_(userId, state.recipeId); - // If some recipe was deleted then return to first page - // Made to avoid bug when delete last recipe on page -> will return to the non-existent page - renderCustomRecipesList(0, userId, chatId, bot, api); - stateManager.put(CustomRecipesList{.pageNo = 0}); - return; - } - - if (data == "publish") { // Should also create backend endpoint to track status of publish - api.getRecipesApi().publishCustom(userId, state.recipeId); - return; - } -} - -} // namespace cookcookhnya::handlers::personal_account::recipes diff --git a/src/handlers/personal_account/recipes_list/recipe/view.hpp b/src/handlers/personal_account/recipes_list/recipe/view.hpp deleted file mode 100644 index aac53df8..00000000 --- a/src/handlers/personal_account/recipes_list/recipe/view.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include "handlers/common.hpp" - -namespace cookcookhnya::handlers::personal_account::recipes { - -void handleRecipeCustomViewCQ( - RecipeCustomView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api); - -} // namespace cookcookhnya::handlers::personal_account::recipes diff --git a/src/handlers/personal_account/recipes_list/view.cpp b/src/handlers/personal_account/recipes_list/view.cpp index d8c39006..2e447c7a 100644 --- a/src/handlers/personal_account/recipes_list/view.cpp +++ b/src/handlers/personal_account/recipes_list/view.cpp @@ -1,22 +1,27 @@ #include "view.hpp" +#include "backend/api/api.hpp" #include "backend/id_types.hpp" #include "handlers/common.hpp" +#include "render/personal_account/recipe/view.hpp" #include "render/personal_account/recipes_list/create.hpp" -#include "render/personal_account/recipes_list/recipe/view.hpp" #include "render/personal_account/recipes_list/view.hpp" #include "render/personal_account/view.hpp" #include "states.hpp" #include "utils/parsing.hpp" #include +#include -namespace cookcookhnya::handlers::personal_account::recipes { +namespace cookcookhnya::handlers::personal_account::recipes_list { + +using namespace render::personal_account; +using namespace render::personal_account::recipe; +using namespace render::personal_account::recipes_list; +using namespace std::literals; void handleCustomRecipesListCQ( - CustomRecipesList& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api) { - using namespace render::personal_account; - using namespace render::personal_account::recipes; + CustomRecipesList& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { bot.answerCallbackQuery(cq.id); @@ -30,32 +35,35 @@ void handleCustomRecipesListCQ( bot.answerCallbackQuery(cq.id); return; } + if (data == "custom_recipe_create") { renderRecipeCreation(chatId, userId, bot); - stateManager.put(CreateCustomRecipe{.recipeId = {}, .pageNo = state.pageNo}); + stateManager.put(CreateCustomRecipe{.pageNo = state.pageNo}); bot.answerCallbackQuery(cq.id); return; } - if (data[0] == 'r') { - auto recipeId = utils::parseSafe( - data.substr(data.find(' ', 0) + 1, data.size())); // +1 is to move from space and get pure number + if (data.starts_with("recipe_")) { + auto recipeId = utils::parseSafe(std::string_view{data}.substr("recipe_"sv.size())); if (recipeId) { - auto ingredients = renderCustomRecipe(true, userId, chatId, recipeId.value(), bot, api); - stateManager.put( - RecipeCustomView{.recipeId = recipeId.value(), .pageNo = state.pageNo, .ingredients = ingredients}); + auto ingredientsAndName = renderCustomRecipe(true, userId, chatId, recipeId.value(), bot, api); + stateManager.put(CustomRecipeView{.recipeId = recipeId.value(), + .pageNo = state.pageNo, + .ingredients = ingredientsAndName.first, + .recipeName = ingredientsAndName.second}); } + bot.answerCallbackQuery(cq.id); return; } if (data != "dont_handle") { - auto pageNo = utils::parseSafe(data); - if (pageNo) { - state.pageNo = *pageNo; - } - renderCustomRecipesList(*pageNo, userId, chatId, bot, api); + if (data == "page_left") + state.pageNo--; + else if (data == "page_right") + state.pageNo++; + renderCustomRecipesList(state.pageNo, userId, chatId, bot, api); return; } } -} // namespace cookcookhnya::handlers::personal_account::recipes +} // namespace cookcookhnya::handlers::personal_account::recipes_list diff --git a/src/handlers/personal_account/recipes_list/view.hpp b/src/handlers/personal_account/recipes_list/view.hpp index c9194ccb..9f1f9cac 100644 --- a/src/handlers/personal_account/recipes_list/view.hpp +++ b/src/handlers/personal_account/recipes_list/view.hpp @@ -1,10 +1,11 @@ #pragma once +#include "backend/api/api.hpp" #include "handlers/common.hpp" -namespace cookcookhnya::handlers::personal_account::recipes { +namespace cookcookhnya::handlers::personal_account::recipes_list { void handleCustomRecipesListCQ( - CustomRecipesList& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api); + CustomRecipesList& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); -} // namespace cookcookhnya::handlers::personal_account::recipes +} // namespace cookcookhnya::handlers::personal_account::recipes_list diff --git a/src/handlers/personal_account/view.cpp b/src/handlers/personal_account/view.cpp index 36f2af94..69320e30 100644 --- a/src/handlers/personal_account/view.cpp +++ b/src/handlers/personal_account/view.cpp @@ -1,20 +1,27 @@ #include "handlers/personal_account/view.hpp" +#include "backend/api/api.hpp" #include "handlers/common.hpp" #include "render/main_menu/view.hpp" #include "render/personal_account/ingredients_list/view.hpp" +#include "render/personal_account/publication_history.hpp" #include "render/personal_account/recipes_list/view.hpp" +#include +#include #include namespace cookcookhnya::handlers::personal_account { -using namespace render::personal_account::recipes; using namespace render::main_menu; +using namespace render::personal_account::recipes_list; using namespace render::personal_account::ingredients; +using namespace render::personal_account; + +const std::size_t numOfHistoryInstances = 10; void handlePersonalAccountMenuCQ( - PersonalAccountMenu& /**/, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api) { + PersonalAccountMenu& /**/, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { const std::string data = cq.data; bot.answerCallbackQuery(cq.id); auto chatId = cq.message->chat->id; @@ -23,9 +30,10 @@ void handlePersonalAccountMenuCQ( if (data == "recipes") { renderCustomRecipesList(0, userId, chatId, bot, api); stateManager.put(CustomRecipesList{.pageNo = 0}); + return; } if (data == "back") { - renderMainMenu(true, userId, chatId, bot, api); + renderMainMenu(true, std::nullopt, userId, chatId, bot, api); stateManager.put(MainMenu{}); return; } @@ -34,6 +42,11 @@ void handlePersonalAccountMenuCQ( stateManager.put(CustomIngredientsList{}); return; } + if (data == "history") { + renderRequestHistory(userId, 0, numOfHistoryInstances, chatId, bot, api); + stateManager.put(TotalPublicationHistory{.pageNo = 0}); + return; + } } } // namespace cookcookhnya::handlers::personal_account diff --git a/src/handlers/personal_account/view.hpp b/src/handlers/personal_account/view.hpp index 3d31fdcd..ce94f2a0 100644 --- a/src/handlers/personal_account/view.hpp +++ b/src/handlers/personal_account/view.hpp @@ -1,10 +1,11 @@ #pragma once +#include "backend/api/api.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::personal_account { void handlePersonalAccountMenuCQ( - PersonalAccountMenu& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, ApiClientRef api); + PersonalAccountMenu& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, api::ApiClientRef api); } // namespace cookcookhnya::handlers::personal_account diff --git a/src/handlers/recipe/add_storage.cpp b/src/handlers/recipe/add_storage.cpp deleted file mode 100644 index 0e47bbce..00000000 --- a/src/handlers/recipe/add_storage.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "add_storage.hpp" - -#include "backend/id_types.hpp" -#include "handlers/common.hpp" -#include "render/recipe/add_storage.hpp" -#include "render/recipe/view.hpp" -#include "states.hpp" -#include "utils/parsing.hpp" - -#include - -namespace cookcookhnya::handlers::recipe { - -using namespace render::recipe; - -void handleRecipeStorageAdditionCQ( - RecipeStorageAddition& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api) { - std::string data = cq.data; - auto chatId = cq.message->chat->id; - auto userId = cq.from->id; - - if (data[0] == '+') { - auto newStorageIdStr = - data.substr(1, data.size()); // Here we got all selected storages and new one as last in string - auto newStorageId = utils::parseSafe(newStorageIdStr); - if (newStorageId) { - state.storageIds.push_back(*newStorageId); - renderStoragesSuggestion(state.storageIds, state.recipeId, userId, chatId, bot, api); - } - bot.answerCallbackQuery(cq.id); - } - - if (data == "back_from_adding_storages") { - renderRecipeView(state.storageIds, state.recipeId, userId, chatId, bot, api); - stateManager.put(RecipeView{.storageIds = state.storageIds, - .recipeId = state.recipeId, - .fromStorage = state.fromStorage, - .pageNo = state.pageNo}); - bot.answerCallbackQuery(cq.id); - return; - } -} - -} // namespace cookcookhnya::handlers::recipe diff --git a/src/handlers/recipe/add_storage.hpp b/src/handlers/recipe/add_storage.hpp deleted file mode 100644 index 3267f272..00000000 --- a/src/handlers/recipe/add_storage.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include "handlers/common.hpp" - -namespace cookcookhnya::handlers::recipe { - -void handleRecipeStorageAdditionCQ( - RecipeStorageAddition& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api); - -} // namespace cookcookhnya::handlers::recipe diff --git a/src/handlers/recipe/view.cpp b/src/handlers/recipe/view.cpp index b05e06c0..a57e4e87 100644 --- a/src/handlers/recipe/view.cpp +++ b/src/handlers/recipe/view.cpp @@ -1,77 +1,63 @@ #include "view.hpp" +#include "backend/api/api.hpp" #include "backend/id_types.hpp" #include "handlers/common.hpp" -#include "render/recipe/add_storage.hpp" -#include "render/recipes_suggestions/view.hpp" -#include "render/shopping_list/create.hpp" +#include "render/cooking_planning/view.hpp" +#include "render/main_menu/view.hpp" +#include "render/recipes_search/view.hpp" +#include "render/storages_selection/view.hpp" +#include "states.hpp" +#include "utils/ingredients_availability.hpp" -#include -#include +#include +#include #include namespace cookcookhnya::handlers::recipe { -using namespace render::recipes_suggestions; -using namespace render::shopping_list; -using namespace render::recipe; +using namespace render::recipes_search; +using namespace render::cooking_planning; +using namespace render::select_storages; +using namespace render::main_menu; -void handleRecipeViewCQ(RecipeView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api) { - std::string data = cq.data; - auto chatId = cq.message->chat->id; +void handleRecipeViewCQ(RecipeView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + bot.answerCallbackQuery(cq.id); auto userId = cq.from->id; + auto chatId = cq.message->chat->id; - if (data == "start_cooking") { - // TODO: add state of begginig of cooking + if (cq.data == "back") { + if (auto& prevState = state.prevState) { + renderRecipesSearch(prevState->pagination, prevState->page, userId, chatId, bot); + stateManager.put(auto{std::move(*prevState)}); + } else { + renderMainMenu(true, std::nullopt, userId, chatId, bot, api); + stateManager.put(MainMenu{}); + } return; } - if (data == "make_product_list") { - // Next lines of code is necessary preparation for making product list. Need to re-verify products which are - // certanly not in storage - // Besides we exactly need ingredient Ids, as then it will be sent on backend - const std::unordered_set storageIdsSet(state.storageIds.begin(), state.storageIds.end()); - auto recipesApi = api.getRecipesApi(); - auto ingredients = recipesApi.getIngredientsInRecipe(userId, state.recipeId).ingredients; - std::vector ingredientIds; - bool isHavingIngredient = false; + if (cq.data == "cook") { + auto storages = api.getStoragesApi().getStoragesList(userId); + if (storages.size() <= 1) { + std::vector storagesIds; + if (!storages.empty()) + storagesIds.push_back(storages[0].id); - for (auto& ingredient : ingredients) { // Iterate through each ingredient - isHavingIngredient = false; - for (const api::StorageId storage : ingredient.inStorages) { - // Then for this ingredient one of possible storages already selected - if (storageIdsSet.contains(storage)) { - isHavingIngredient = true; - break; // No need to iterate further - } - } - if (!isHavingIngredient) { - ingredientIds.push_back(ingredient.id); - } - } - renderShoppingListCreation(ingredientIds, userId, chatId, bot, api); - stateManager.put(ShoppingListCreation{.storageIdsFrom = state.storageIds, - .recipeIdFrom = state.recipeId, - .ingredientIdsInList = ingredientIds, - .fromStorage = state.fromStorage, - .pageNo = state.pageNo}); - bot.answerCallbackQuery(cq.id); - return; - } - if (data == "back_from_recipe_view") { - renderRecipesSuggestion(state.storageIds, state.pageNo, userId, chatId, bot, api); - stateManager.put(SuggestedRecipeList{ - .pageNo = state.pageNo, .storageIds = state.storageIds, .fromStorage = state.fromStorage}); - bot.answerCallbackQuery(cq.id); - return; - } + const api::RecipeId recipeId = state.recipeId; + auto availability = utils::inStoragesAvailability(storagesIds, recipeId, userId, api); - if (data[0] == '?') { - renderStoragesSuggestion(state.storageIds, state.recipeId, userId, chatId, bot, api); - stateManager.put(RecipeStorageAddition{.storageIds = state.storageIds, - .recipeId = state.recipeId, - .fromStorage = state.fromStorage, - .pageNo = state.pageNo}); + renderCookingPlanning(availability, recipeId, userId, chatId, bot, api); + stateManager.put( + CookingPlanning{.prevState = CookingPlanning::FromRecipeViewData{std::move(state), std::move(storages)}, + .addedStorages = {}, + .availability = std::move(availability), + .recipeId = recipeId}); + return; + } + StoragesSelection newState{.prevState = std::move(state), .selectedStorages = {}}; + renderStorageSelection(newState, userId, chatId, bot, api); + stateManager.put(std::move(newState)); return; } } diff --git a/src/handlers/recipe/view.hpp b/src/handlers/recipe/view.hpp index a425acfc..6cefa542 100644 --- a/src/handlers/recipe/view.hpp +++ b/src/handlers/recipe/view.hpp @@ -1,9 +1,10 @@ #pragma once +#include "backend/api/api.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::recipe { -void handleRecipeViewCQ(RecipeView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api); +void handleRecipeViewCQ(RecipeView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); } // namespace cookcookhnya::handlers::recipe diff --git a/src/handlers/recipes_search/view.cpp b/src/handlers/recipes_search/view.cpp new file mode 100644 index 00000000..a5e99a13 --- /dev/null +++ b/src/handlers/recipes_search/view.cpp @@ -0,0 +1,89 @@ +#include "view.hpp" + +#include "backend/api/api.hpp" +#include "backend/api/publicity_filter.hpp" +#include "backend/id_types.hpp" +#include "handlers/common.hpp" +#include "render/main_menu/view.hpp" +#include "render/recipe/view.hpp" +#include "render/recipes_search/view.hpp" +#include "utils/parsing.hpp" + +#include +#include +#include +#include + +namespace cookcookhnya::handlers::recipes_search { + +using namespace render::main_menu; +using namespace render::recipes_search; +using namespace render::recipe; +using namespace std::literals; + +const std::size_t pageSize = 5; +const unsigned threshold = 70; + +void handleRecipesSearchCQ( + RecipesSearch& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + bot.answerCallbackQuery(cq.id); + auto userId = cq.from->id; + auto chatId = cq.message->chat->id; + + if (cq.data == "back") { + renderMainMenu(true, std::nullopt, userId, chatId, bot, api); + stateManager.put(MainMenu{}); + return; + } + + if (cq.data == "page_left" || cq.data == "page_right") { + if (cq.data == "page_left") { + if (state.pagination.pageNo == 0) + return; + state.pagination.pageNo--; + } else { + if (state.pagination.totalItems <= state.pagination.pageNo * pageSize) + return; + state.pagination.pageNo++; + } + auto result = api.getRecipesApi().search( + userId, PublicityFilterType::All, state.query, pageSize, pageSize * state.pagination.pageNo, threshold); + state.page = result.page; + renderRecipesSearch(state.pagination, state.page, userId, userId, bot); + return; + } + + if (cq.data.starts_with("recipe_")) { + auto mRecipeId = utils::parseSafe(std::string_view{cq.data}.substr("recipe_"sv.size())); + if (!mRecipeId) + return; + auto recipe = api.getRecipesApi().get(userId, *mRecipeId); + renderRecipeView(recipe, *mRecipeId, userId, chatId, bot); + stateManager.put( + RecipeView{.prevState = {std::move(state)}, .recipe = std::move(recipe), .recipeId = *mRecipeId}); + return; + } +} + +void handleRecipesSearchIQ(RecipesSearch& state, InlineQueryRef iq, BotRef bot, api::RecipesApiRef api) { + auto userId = iq.from->id; + if (iq.query.empty()) { + if (state.query.empty()) + return; + state.query = ""; + state.pagination = {}; + state.page = {}; + } else { + auto result = api.search(userId, PublicityFilterType::All, iq.query, pageSize, 0, threshold); + const auto idGetter = &api::models::recipe::RecipeSummary::id; + if (std::ranges::equal(result.page, state.page, {}, idGetter, idGetter)) + return; + + state.query = iq.query; + state.pagination = {.pageNo = 0, .totalItems = result.found}; + state.page = result.page; + } + renderRecipesSearch(state.pagination, state.page, userId, userId, bot); +} + +} // namespace cookcookhnya::handlers::recipes_search diff --git a/src/handlers/recipes_search/view.hpp b/src/handlers/recipes_search/view.hpp new file mode 100644 index 00000000..c7660036 --- /dev/null +++ b/src/handlers/recipes_search/view.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "backend/api/recipes.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::recipes_search { + +void handleRecipesSearchCQ( + RecipesSearch& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); + +void handleRecipesSearchIQ(RecipesSearch& state, InlineQueryRef iq, BotRef bot, api::RecipesApiRef api); + +} // namespace cookcookhnya::handlers::recipes_search diff --git a/src/handlers/recipes_suggestions/view.cpp b/src/handlers/recipes_suggestions/view.cpp index fdef7c22..ed737970 100644 --- a/src/handlers/recipes_suggestions/view.cpp +++ b/src/handlers/recipes_suggestions/view.cpp @@ -1,73 +1,80 @@ #include "view.hpp" +#include "backend/api/api.hpp" #include "backend/id_types.hpp" +#include "backend/models/storage.hpp" #include "handlers/common.hpp" +#include "render/cooking_planning/view.hpp" #include "render/main_menu/view.hpp" -#include "render/recipe/view.hpp" #include "render/recipes_suggestions/view.hpp" #include "render/storage/view.hpp" #include "render/storages_selection/view.hpp" -#include "states.hpp" +#include "utils/ingredients_availability.hpp" #include "utils/parsing.hpp" +#include +#include #include #include +#include +#include namespace cookcookhnya::handlers::recipes_suggestions { using namespace render::recipes_suggestions; using namespace render::select_storages; using namespace render::storage; -using namespace render::recipe; +using namespace render::cooking_planning; using namespace render::main_menu; +using namespace api::models::storage; +using namespace std::views; -void handleSuggestedRecipeListCQ( - SuggestedRecipeList& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api) { +void handleSuggestedRecipesListCQ( + SuggestedRecipesList& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { bot.answerCallbackQuery(cq.id); auto chatId = cq.message->chat->id; auto userId = cq.from->id; - - auto data = cq.data; + const std::string& data = cq.data; if (data == "back") { - if (state.fromStorage) { - renderStorageView(state.storageIds[0], cq.from->id, chatId, bot, api); - stateManager.put(StorageView{state.storageIds[0]}); // Go to the only one storage - } else { - if (api.getStoragesApi().getStoragesList(userId).size() == 1) { - renderMainMenu(true, userId, chatId, bot, api); - stateManager.put(MainMenu{}); - } else { - renderStorageSelection(state.storageIds, userId, chatId, bot, api); - stateManager.put(StoragesSelection{.storageIds = std::move(state.storageIds)}); + if (auto* prevState = std::get_if(&state.prevState)) { + renderStorageView(prevState->storageId, userId, chatId, bot, api); + std::string storageName = api.getStoragesApi().get(userId, prevState->storageId).name; + stateManager.put(StorageView{prevState->storageId, std::move(storageName)}); + } else if (auto* prevState = std::get_if(&state.prevState)) { + renderMainMenu(true, std::nullopt, userId, chatId, bot, api); + stateManager.put(prevState->first); + } else if (auto* prevState = std::get_if(&state.prevState)) { + if (auto* prevPrevState = std::get_if(&prevState->prevState)) { + renderMainMenu(true, std::nullopt, userId, chatId, bot, api); + stateManager.put(auto{*prevPrevState}); } + throw std::runtime_error{"Unreachable path reached"}; } bot.answerCallbackQuery(cq.id); return; } - if (data[0] == 'r') { // Same naive implementation: if first char is r then it's recipe + if (data.starts_with("recipe_")) { + auto recipeId = utils::parseSafe(data.substr(sizeof("recipe_") - 1)); + if (!recipeId) + return; - auto recipeId = utils::parseSafe( - data.substr(data.find(' ', 0) + 1, data.size())); // +1 is to move from space and get pure number - if (recipeId) { - renderRecipeView(state.storageIds, *recipeId, userId, chatId, bot, api); - stateManager.put(RecipeView{.storageIds = state.storageIds, - .recipeId = *recipeId, - .fromStorage = state.fromStorage, - .pageNo = state.pageNo}); - } + std::vector inStorage = + utils::inStoragesAvailability(state.getStorageIds(), *recipeId, userId, api); + renderCookingPlanning(inStorage, *recipeId, userId, chatId, bot, api); + stateManager.put(CookingPlanning{ + .prevState = std::move(state), .addedStorages = {}, .availability = inStorage, .recipeId = *recipeId}); return; } if (data != "dont_handle") { - auto pageNo = utils::parseSafe(data); - if (pageNo) { - state.pageNo = *pageNo; - } - // Message is 100% exists as it was rendered by some another method - renderRecipesSuggestion(state.storageIds, *pageNo, userId, chatId, bot, api); + if (data == "page_left") + state.pageNo--; + else if (data == "page_right") + state.pageNo++; + renderRecipesSuggestion(state.getStorageIds(), state.pageNo, userId, chatId, bot, api); return; } } diff --git a/src/handlers/recipes_suggestions/view.hpp b/src/handlers/recipes_suggestions/view.hpp index 79cbc54f..be415b79 100644 --- a/src/handlers/recipes_suggestions/view.hpp +++ b/src/handlers/recipes_suggestions/view.hpp @@ -1,10 +1,11 @@ #pragma once +#include "backend/api/api.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::recipes_suggestions { -void handleSuggestedRecipeListCQ( - SuggestedRecipeList& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api); +void handleSuggestedRecipesListCQ( + SuggestedRecipesList& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); } // namespace cookcookhnya::handlers::recipes_suggestions diff --git a/src/handlers/shopping_list/create.cpp b/src/handlers/shopping_list/create.cpp index 96262d1f..6f18d3da 100644 --- a/src/handlers/shopping_list/create.cpp +++ b/src/handlers/shopping_list/create.cpp @@ -1,62 +1,70 @@ #include "create.hpp" +#include "backend/api/api.hpp" #include "backend/id_types.hpp" +#include "backend/models/ingredient.hpp" #include "handlers/common.hpp" -#include "render/recipe/view.hpp" +#include "render/cooking_planning/view.hpp" #include "render/shopping_list/create.hpp" #include "utils/parsing.hpp" +#include #include +#include +#include namespace cookcookhnya::handlers::shopping_list { using namespace render::shopping_list; -using namespace render::recipe; +using namespace render::cooking_planning; void handleShoppingListCreationCQ( - ShoppingListCreation& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api) { - std::string data = cq.data; + ShoppingListCreation& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + const std::string& data = cq.data; auto chatId = cq.message->chat->id; auto userId = cq.from->id; if (data == "back") { - renderRecipeView(state.storageIdsFrom, state.recipeIdFrom, userId, chatId, bot, api); - stateManager.put(RecipeView{.storageIds = state.storageIdsFrom, - .recipeId = state.recipeIdFrom, - .fromStorage = state.fromStorage, - .pageNo = state.pageNo}); + renderCookingPlanning(state.prevState.availability, state.prevState.recipeId, userId, chatId, bot, api); + stateManager.put(auto{std::move(state.prevState)}); bot.answerCallbackQuery(cq.id); return; } + if (data == "confirm") { - // Put ingredients in list auto shoppingApi = api.getShoppingListApi(); - shoppingApi.put(userId, state.ingredientIdsInList); - - // Return to previous state - renderRecipeView(state.storageIdsFrom, state.recipeIdFrom, userId, chatId, bot, api); - stateManager.put(RecipeView{.storageIds = state.storageIdsFrom, - .recipeId = state.recipeIdFrom, - .fromStorage = state.fromStorage, - .pageNo = state.pageNo}); + std::vector putIds; + putIds.reserve(state.selectedIngredients.size()); + for (const auto& ingredient : state.selectedIngredients) { + putIds.push_back(ingredient.id); + } + shoppingApi.put(userId, putIds); + renderCookingPlanning(state.prevState.availability, state.prevState.recipeId, userId, chatId, bot, api); + stateManager.put(auto{std::move(state.prevState)}); bot.answerCallbackQuery(cq.id); return; } - if (data[0] == 'i') { - auto newIngredientIdStr = - data.substr(1, data.size()); // Here we got all selected storages and new one as last in string - auto ingredientIdToRemove = utils::parseSafe(newIngredientIdStr); - // Remove ingredient which was chosen - if (ingredientIdToRemove) { - for (auto ingredientId = state.ingredientIdsInList.begin(); ingredientId < state.ingredientIdsInList.end(); - ingredientId++) { - if (*ingredientId == *ingredientIdToRemove) { - state.ingredientIdsInList.erase(ingredientId); - } - } + + if (data[0] == '+') { + auto newIngredientIdStr = data.substr(1, data.size()); + auto newIngredientId = utils::parseSafe(newIngredientIdStr); + if (newIngredientId) { + auto ingredient = api.getIngredientsApi().get(userId, *newIngredientId); + state.selectedIngredients.push_back(ingredient); } + renderShoppingListCreation(state.selectedIngredients, state.allIngredients, userId, chatId, bot); + return; + } - renderShoppingListCreation(state.ingredientIdsInList, userId, chatId, bot, api); + if (data[0] == '-') { + auto newIngredientIdStr = data.substr(1, data.size()); + auto newIngredientId = utils::parseSafe(newIngredientIdStr); + if (newIngredientId) { + state.selectedIngredients.erase(std::ranges::find( + state.selectedIngredients, *newIngredientId, &api::models::ingredient::Ingredient::id)); + } + renderShoppingListCreation(state.selectedIngredients, state.allIngredients, userId, chatId, bot); + return; } } } // namespace cookcookhnya::handlers::shopping_list diff --git a/src/handlers/shopping_list/create.hpp b/src/handlers/shopping_list/create.hpp index 2feaac37..bf8d6b62 100644 --- a/src/handlers/shopping_list/create.hpp +++ b/src/handlers/shopping_list/create.hpp @@ -1,10 +1,11 @@ #pragma once +#include "backend/api/api.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::shopping_list { void handleShoppingListCreationCQ( - ShoppingListCreation& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api); + ShoppingListCreation& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); } // namespace cookcookhnya::handlers::shopping_list diff --git a/src/handlers/shopping_list/search.cpp b/src/handlers/shopping_list/search.cpp new file mode 100644 index 00000000..e2740fec --- /dev/null +++ b/src/handlers/shopping_list/search.cpp @@ -0,0 +1,89 @@ +#include "search.hpp" + +#include "backend/api/api.hpp" +#include "backend/api/publicity_filter.hpp" +#include "backend/id_types.hpp" +#include "backend/models/shopping_list.hpp" +#include "handlers/common.hpp" +#include "render/shopping_list/search.hpp" +#include "render/shopping_list/view.hpp" +#include "utils/parsing.hpp" + +#include +#include +#include + +namespace cookcookhnya::handlers::shopping_list { + +using namespace render::shopping_list; +using namespace api; +using namespace api::models::shopping_list; + +const std::size_t searchThreshold = 70; + +void handleShoppingListIngredientSearchCQ( + ShoppingListIngredientSearch& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { + bot.answerCallbackQuery(cq.id); + auto userId = cq.from->id; + auto chatId = cq.message->chat->id; + + if (cq.data == "back") { + renderShoppingList(state.prevState, userId, chatId, bot); + stateManager.put(auto{std::move(state.prevState)}); + return; + } + + if (cq.data == "page_left" || cq.data == "page_right") { + if (cq.data == "page_left") { + if (state.pagination.pageNo == 0) + return; + state.pagination.pageNo--; + } else { + if (state.pagination.totalItems <= state.pagination.pageNo * searchPageSize) + return; + state.pagination.pageNo++; + } + auto result = api.getIngredientsApi().search(userId, + PublicityFilterType::All, + state.query, + searchPageSize, + searchPageSize * state.pagination.pageNo, + searchThreshold); + state.page = result.page; + renderShoppingListIngredientSearch(state, searchPageSize, userId, chatId, bot); + return; + } + + if (cq.data.starts_with("ingredient_")) { + auto mIngredient = utils::parseSafe(std::string_view{cq.data}.substr(sizeof("ingredient_") - 1)); + if (!mIngredient) + return; + api.getShoppingListApi().put(userId, {*mIngredient}); + auto ingredientInfo = api.getIngredientsApi().get(userId, *mIngredient); + state.prevState.items.put({ShoppingListItem{.ingredientId = ingredientInfo.id, .name = ingredientInfo.name}}); + return; + } +} + +void handleShoppingListIngredientSearchIQ(ShoppingListIngredientSearch& state, + InlineQueryRef iq, + BotRef bot, + api::IngredientsApiRef api) { + auto userId = iq.from->id; + if (iq.query.empty()) { + state.query = ""; + state.pagination = {}; + state.page = {}; + renderShoppingListIngredientSearch(state, searchPageSize, userId, userId, bot); + return; + } + + auto result = api.search(userId, PublicityFilterType::All, iq.query, searchPageSize, 0, searchThreshold); + state.query = iq.query; + state.pagination.pageNo = 0; + state.pagination.totalItems = result.found; + state.page = result.page; + renderShoppingListIngredientSearch(state, searchPageSize, userId, userId, bot); +} + +} // namespace cookcookhnya::handlers::shopping_list diff --git a/src/handlers/shopping_list/search.hpp b/src/handlers/shopping_list/search.hpp new file mode 100644 index 00000000..20c5f72e --- /dev/null +++ b/src/handlers/shopping_list/search.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" + +#include + +namespace cookcookhnya::handlers::shopping_list { + +const std::size_t searchPageSize = 10; + +void handleShoppingListIngredientSearchCQ( + ShoppingListIngredientSearch&, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); + +void handleShoppingListIngredientSearchIQ(ShoppingListIngredientSearch& state, + InlineQueryRef iq, + BotRef bot, + api::IngredientsApiRef api); + +} // namespace cookcookhnya::handlers::shopping_list diff --git a/src/handlers/shopping_list/storage_selection_to_buy.cpp b/src/handlers/shopping_list/storage_selection_to_buy.cpp new file mode 100644 index 00000000..41454864 --- /dev/null +++ b/src/handlers/shopping_list/storage_selection_to_buy.cpp @@ -0,0 +1,46 @@ +#include "storage_selection_to_buy.hpp" + +#include "backend/api/shopping_lists.hpp" +#include "backend/id_types.hpp" +#include "handlers/common.hpp" +#include "render/shopping_list/view.hpp" +#include "utils/parsing.hpp" + +#include +#include +#include + +namespace cookcookhnya::handlers::shopping_list { + +using render::shopping_list::renderShoppingList; + +void handleShoppingListStorageSelectionToBuyCQ(ShoppingListStorageSelectionToBuy& state, + CallbackQueryRef cq, + BotRef bot, + SMRef stateManager, + api::ShoppingListApiRef api) { + bot.answerCallbackQuery(cq.id); + auto userId = cq.from->id; + auto chatId = cq.message->chat->id; + + if (cq.data == "back") { + renderShoppingList(state.prevState, userId, chatId, bot); + stateManager.put(auto{std::move(state.prevState)}); + return; + } + + const std::size_t idStrStart = sizeof("storage_") - 1; + if (cq.data.size() <= idStrStart) + return; + auto mStorageId = utils::parseSafe(std::string_view{cq.data}.substr(idStrStart)); + if (!mStorageId) + return; + + api.buy(userId, *mStorageId, state.selectedIngredients); + for (auto& id : state.selectedIngredients) + state.prevState.items.remove(id); + renderShoppingList(state.prevState, userId, chatId, bot); + stateManager.put(auto{std::move(state.prevState)}); +} + +} // namespace cookcookhnya::handlers::shopping_list diff --git a/src/handlers/shopping_list/storage_selection_to_buy.hpp b/src/handlers/shopping_list/storage_selection_to_buy.hpp new file mode 100644 index 00000000..3bb7ea03 --- /dev/null +++ b/src/handlers/shopping_list/storage_selection_to_buy.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "backend/api/shopping_lists.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::shopping_list { + +void handleShoppingListStorageSelectionToBuyCQ(ShoppingListStorageSelectionToBuy& state, + CallbackQueryRef cq, + BotRef bot, + SMRef stateManager, + api::ShoppingListApiRef api); + +} // namespace cookcookhnya::handlers::shopping_list diff --git a/src/handlers/shopping_list/view.cpp b/src/handlers/shopping_list/view.cpp index e4a8f4e0..051243f4 100644 --- a/src/handlers/shopping_list/view.cpp +++ b/src/handlers/shopping_list/view.cpp @@ -1,34 +1,87 @@ #include "view.hpp" +#include "backend/api/api.hpp" #include "backend/id_types.hpp" #include "handlers/common.hpp" +#include "handlers/shopping_list/search.hpp" #include "render/main_menu/view.hpp" +#include "render/shopping_list/search.hpp" +#include "render/shopping_list/storage_selection_to_buy.hpp" #include "render/shopping_list/view.hpp" +#include "states.hpp" #include "utils/parsing.hpp" +#include +#include +#include +#include + namespace cookcookhnya::handlers::shopping_list { using namespace render::main_menu; using namespace render::shopping_list; +using namespace std::views; +using namespace std::ranges; void handleShoppingListViewCQ( - ShoppingListView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api) { + ShoppingListView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { bot.answerCallbackQuery(cq.id); auto userId = cq.from->id; auto chatId = cq.message->chat->id; + auto storages = api.getStoragesApi().getStoragesList(userId); + if (cq.data == "back") { - renderMainMenu(true, userId, cq.message->chat->id, bot, api); + renderMainMenu(true, std::nullopt, userId, cq.message->chat->id, bot, api); stateManager.put(MainMenu{}); return; } + if (cq.data == "search") { + ShoppingListIngredientSearch newState{.prevState = std::move(state), .query = "", .pagination = {}, .page = {}}; + renderShoppingListIngredientSearch(newState, searchPageSize, userId, chatId, bot); + stateManager.put(std::move(newState)); + return; + } + + if (cq.data == "remove") { + using SelectableItem = states::helpers::SelectableShoppingListItem; + auto toDelete = state.items.getValues() | filter(&SelectableItem::selected) | + views::transform(&SelectableItem::ingredientId) | to(); + + api.getShoppingListApi().remove(userId, toDelete); + for (auto& id : toDelete) + state.items.remove(id); + + renderShoppingList(state, userId, chatId, bot); + return; + } + + if (cq.data == "buy") { + using SelectableItem = states::helpers::SelectableShoppingListItem; + auto toBuy = state.items.getValues() | filter(&SelectableItem::selected) | + views::transform(&SelectableItem::ingredientId) | to(); + if (storages.size() == 1) { + api.getShoppingListApi().buy(userId, storages[0].id, toBuy); + for (auto& id : toBuy) + state.items.remove(id); + renderShoppingList(state, userId, chatId, bot); + } else { + auto newState = ShoppingListStorageSelectionToBuy{ + .prevState = std::move(state), .selectedIngredients = std::move(toBuy), .storages = storages}; + renderShoppingListStorageSelectionToBuy(newState, userId, chatId, bot); + stateManager.put(std::move(newState)); + } + return; + } + auto mIngredientId = utils::parseSafe(cq.data); if (!mIngredientId) return; - api.getShoppingListApi().remove(userId, {*mIngredientId}); - state.items.remove(*mIngredientId); - renderShoppingList(state.items.getAll(), userId, chatId, bot); + if (auto* mItem = state.items[*mIngredientId]) { + mItem->selected = !mItem->selected; + renderShoppingList(state, userId, chatId, bot); + } } } // namespace cookcookhnya::handlers::shopping_list diff --git a/src/handlers/shopping_list/view.hpp b/src/handlers/shopping_list/view.hpp index cf9d5d60..095f139e 100644 --- a/src/handlers/shopping_list/view.hpp +++ b/src/handlers/shopping_list/view.hpp @@ -1,9 +1,11 @@ #pragma once +#include "backend/api/api.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::shopping_list { -void handleShoppingListViewCQ(ShoppingListView&, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api); +void handleShoppingListViewCQ( + ShoppingListView&, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); } // namespace cookcookhnya::handlers::shopping_list diff --git a/src/handlers/storage/delete.cpp b/src/handlers/storage/delete.cpp new file mode 100644 index 00000000..9e6dfc71 --- /dev/null +++ b/src/handlers/storage/delete.cpp @@ -0,0 +1,35 @@ +#include "delete.hpp" + +#include "backend/api/storages.hpp" +#include "backend/id_types.hpp" +#include "handlers/common.hpp" +#include "render/storage/view.hpp" +#include "render/storages_list/view.hpp" +#include "states.hpp" +#include + +namespace cookcookhnya::handlers::storage { + +using namespace render::storages_list; +using namespace render::storage; + +void handleStorageDeletionCQ( + StorageDeletion& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::StorageApiRef storageApi) { + bot.answerCallbackQuery(cq.id); + auto userId = cq.from->id; + auto chatId = cq.message->chat->id; + + if (cq.data == "confirm") { + storageApi.delete_(userId, state.storageId); + renderStorageList(true, userId, chatId, bot, storageApi); + stateManager.put(StorageList{}); + } + + if (cq.data == "back") { + renderStorageView(state.storageId, userId, chatId, bot, storageApi); + std::string storageName = storageApi.get(userId, state.storageId).name; + stateManager.put(StorageView{state.storageId, std::move(storageName)}); + } +}; + +} // namespace cookcookhnya::handlers::storage diff --git a/src/handlers/storage/delete.hpp b/src/handlers/storage/delete.hpp new file mode 100644 index 00000000..a3e26683 --- /dev/null +++ b/src/handlers/storage/delete.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "backend/api/storages.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::storage { + +void handleStorageDeletionCQ( + StorageDeletion&, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::StorageApiRef storageApi); + +} // namespace cookcookhnya::handlers::storage diff --git a/src/handlers/storage/ingredients/delete.cpp b/src/handlers/storage/ingredients/delete.cpp new file mode 100644 index 00000000..068f5a18 --- /dev/null +++ b/src/handlers/storage/ingredients/delete.cpp @@ -0,0 +1,84 @@ +#include "delete.hpp" + +#include "backend/id_types.hpp" +#include "backend/models/ingredient.hpp" +#include "render/storage/ingredients/delete.hpp" +#include "render/storage/ingredients/view.hpp" +#include "states.hpp" +#include "utils/parsing.hpp" + +#include + +namespace cookcookhnya::handlers::storage::ingredients { + +using namespace render::storage::ingredients; +using namespace std::views; + +void handleStorageIngredientsDeletionCQ( + StorageIngredientsDeletion& state, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, api::ApiClientRef api) { + bot.answerCallbackQuery(cq.id); + auto userId = cq.from->id; + auto chatId = cq.message->chat->id; + + if (cq.data == "delete") { + for (const auto& ing : state.selectedIngredients) { + api.getIngredientsApi().deleteFromStorage(userId, state.storageId, ing.id); + state.selectedIngredients.erase( + std::ranges::find(state.selectedIngredients, ing.id, &api::models::ingredient::Ingredient::id)); + state.storageIngredients.erase( + std::ranges::find(state.storageIngredients, ing.id, &api::models::ingredient::Ingredient::id)); + } + auto ingredients = api.getIngredientsApi().getStorageIngredients(userId, state.storageId); + auto newState = StorageIngredientsList{state.storageId, ingredients | as_rvalue, ""}; + renderIngredientsListSearch(newState, userId, chatId, bot); + stateManager.put(newState); + return; + } + if (cq.data == "put_to_shop") { + api.getShoppingListApi().put( + userId, + state.selectedIngredients | std::views::transform([](const api::models::ingredient::Ingredient& obj) { + return obj.id; + }) | std::ranges::to()); + state.addedToShopList = true; + renderStorageIngredientsDeletion(state, userId, chatId, bot); + stateManager.put(StorageIngredientsDeletion{ + state.storageId, state.selectedIngredients, state.storageIngredients, state.addedToShopList, state.pageNo}); + return; + } + if (cq.data == "back") { + auto ingredients = api.getIngredientsApi().getStorageIngredients(userId, state.storageId); + auto newState = StorageIngredientsList{state.storageId, ingredients | as_rvalue, ""}; + renderIngredientsListSearch(newState, userId, chatId, bot); + stateManager.put(newState); + return; + } + if (cq.data == "prev") { + state.pageNo -= 1; + renderStorageIngredientsDeletion(state, userId, chatId, bot); + stateManager.put(StorageIngredientsDeletion{ + state.storageId, state.selectedIngredients, state.storageIngredients, state.addedToShopList, state.pageNo}); + return; + } + if (cq.data == "next") { + state.pageNo += 1; + renderStorageIngredientsDeletion(state, userId, chatId, bot); + stateManager.put(StorageIngredientsDeletion{ + state.storageId, state.selectedIngredients, state.storageIngredients, state.addedToShopList, state.pageNo}); + return; + } + if (cq.data != "dont_handle") { + if (auto id = utils::parseSafe(cq.data.substr(1))) { + if (cq.data[0] == '+') { + auto it = std::ranges::find(state.selectedIngredients, *id, &api::models::ingredient::Ingredient::id); + state.selectedIngredients.erase(it); + } else if (cq.data[0] == '-') { + auto ingredientDetails = api.getIngredientsApi().get(userId, *id); + state.selectedIngredients.push_back({.id = *id, .name = ingredientDetails.name}); + } + renderStorageIngredientsDeletion(state, userId, chatId, bot); + return; + } + } +} +} // namespace cookcookhnya::handlers::storage::ingredients diff --git a/src/handlers/storage/ingredients/delete.hpp b/src/handlers/storage/ingredients/delete.hpp new file mode 100644 index 00000000..09ddd6a3 --- /dev/null +++ b/src/handlers/storage/ingredients/delete.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "handlers/common.hpp" + +namespace cookcookhnya::handlers::storage::ingredients { + +void handleStorageIngredientsDeletionCQ( + StorageIngredientsDeletion& state, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, api::ApiClientRef api); + +} // namespace cookcookhnya::handlers::storage::ingredients diff --git a/src/handlers/storage/ingredients/view.cpp b/src/handlers/storage/ingredients/view.cpp index f8fa9494..1efd88ac 100644 --- a/src/handlers/storage/ingredients/view.cpp +++ b/src/handlers/storage/ingredients/view.cpp @@ -1,77 +1,116 @@ #include "view.hpp" +#include "backend/api/api.hpp" #include "backend/id_types.hpp" #include "backend/models/ingredient.hpp" #include "handlers/common.hpp" #include "message_tracker.hpp" +#include "render/storage/ingredients/delete.hpp" #include "render/storage/ingredients/view.hpp" + +#include "render/personal_account/ingredients_list/create.hpp" + #include "render/storage/view.hpp" +#include "states.hpp" #include "tg_types.hpp" #include "utils/parsing.hpp" #include #include -#include +#include +#include +#include #include +#include namespace cookcookhnya::handlers::storage::ingredients { using namespace render::storage; using namespace render::storage::ingredients; +using namespace render::suggest_custom_ingredient; +using namespace render::personal_account::ingredients; using namespace api::models::ingredient; +using namespace std::literals; + +namespace { -// Global vars const size_t numOfIngredientsOnPage = 5; const size_t threshhold = 70; -namespace { -void updateSearch(StorageIngredientsList& state, BotRef bot, tg_types::UserId userId, IngredientsApiRef api) { - +void updateSearch(StorageIngredientsList& state, + bool isQueryChanged, + BotRef bot, + tg_types::UserId userId, + api::IngredientsApiRef api) { + state.pageNo = isQueryChanged ? 0 : state.pageNo; auto response = api.searchForStorage(userId, state.storageId, state.inlineQuery, - threshhold, numOfIngredientsOnPage, - state.pageNo * numOfIngredientsOnPage); - if (response.found != state.totalFound || !std::ranges::equal(response.page, - state.searchItems, - std::ranges::equal_to{}, - &IngredientSearchForStorageItem::id, - &IngredientSearchForStorageItem::id)) { - state.searchItems = std::move(response.page); - state.totalFound = response.found; - if (auto mMessageId = message::getMessageId(userId)) - renderIngredientsListSearch(state, numOfIngredientsOnPage, userId, userId, bot); + state.pageNo * numOfIngredientsOnPage, + threshhold); + state.totalFound = response.found; + if (state.totalFound == 0) { + renderSuggestIngredientCustomisation(state, userId, userId, bot); + return; } + const auto idGetter = &IngredientSearchForStorageItem::id; + if (std::ranges::equal(response.page, state.searchItems, {}, idGetter, idGetter)) + return; + + state.searchItems = std::move(response.page); + state.totalFound = response.found; + if (auto mMessageId = message::getMessageId(userId)) + renderIngredientsListSearch(state, userId, userId, bot); } + } // namespace void handleStorageIngredientsListCQ( - StorageIngredientsList& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api) { + StorageIngredientsList& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { bot.answerCallbackQuery(cq.id); const auto userId = cq.from->id; const auto chatId = cq.message->chat->id; if (cq.data == "back") { renderStorageView(state.storageId, userId, chatId, bot, api); - stateManager.put(StorageView{state.storageId}); + std::string storageName = api.getStoragesApi().get(userId, state.storageId).name; + stateManager.put(StorageView{state.storageId, std::move(storageName)}); + return; + } + + if (cq.data == "delete") { + std::vector ingredients; + for (auto& ing : state.storageIngredients.getValues()) { + ingredients.push_back(ing); + } + auto newState = StorageIngredientsDeletion{state.storageId, {}, ingredients, false, 0}; + renderStorageIngredientsDeletion(newState, userId, chatId, bot); + stateManager.put(newState); return; } - if (cq.data == "prev") { + + if (cq.data == "page_left") { state.pageNo -= 1; - updateSearch(state, bot, userId, api); + updateSearch(state, false, bot, userId, api); return; } - if (cq.data == "next") { + if (cq.data == "page_right") { state.pageNo += 1; - updateSearch(state, bot, userId, api); + updateSearch(state, false, bot, userId, api); return; } - if (cq.data != "dont_handle") { + if (cq.data.starts_with("create_ingredient")) { + const std::string ingredientName = state.inlineQuery; + renderCustomIngredientConfirmation(true, ingredientName, userId, chatId, bot, api); + stateManager.put(CustomIngredientConfirmation{ingredientName, std::nullopt, std::nullopt, state.storageId}); + } - auto mIngredient = utils::parseSafe(cq.data); + if (cq.data.starts_with("ingredient_")) { + auto mIngredient = + utils::parseSafe(std::string_view{cq.data}.substr("ingredient_"sv.size())); if (!mIngredient) return; auto it = std::ranges::find(state.searchItems, *mIngredient, &IngredientSearchForStorageItem::id); @@ -86,27 +125,27 @@ void handleStorageIngredientsListCQ( state.storageIngredients.put({.id = it->id, .name = it->name}); } it->isInStorage = !it->isInStorage; - renderIngredientsListSearch(state, numOfIngredientsOnPage, userId, chatId, bot); + renderIngredientsListSearch(state, userId, chatId, bot); } } void handleStorageIngredientsListIQ(StorageIngredientsList& state, InlineQueryRef iq, BotRef bot, - IngredientsApiRef api) { - const size_t numOfIngredientsOnPage = 5; + api::IngredientsApiRef api) { const auto userId = iq.from->id; state.inlineQuery = iq.query; if (iq.query.empty()) { state.searchItems.clear(); // When query is empty then search shouldn't happen state.totalFound = 0; - renderIngredientsListSearch(state, numOfIngredientsOnPage, userId, userId, bot); + renderIngredientsListSearch(state, userId, userId, bot); } else { - updateSearch(state, bot, userId, api); + updateSearch(state, true, bot, userId, api); } // Cache is not disabled on Windows and Linux desktops. Works on Android and Web - bot.answerInlineQuery(iq.id, {}, 0); + // Not answer to disable cache + // bot.answerInlineQuery(iq.id, {}, 0); } } // namespace cookcookhnya::handlers::storage::ingredients diff --git a/src/handlers/storage/ingredients/view.hpp b/src/handlers/storage/ingredients/view.hpp index ca6ab044..bdcaa212 100644 --- a/src/handlers/storage/ingredients/view.hpp +++ b/src/handlers/storage/ingredients/view.hpp @@ -1,15 +1,16 @@ #pragma once +#include "backend/api/api.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::storage::ingredients { void handleStorageIngredientsListCQ( - StorageIngredientsList& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api); + StorageIngredientsList& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); void handleStorageIngredientsListIQ(StorageIngredientsList& state, InlineQueryRef iq, BotRef bot, - IngredientsApiRef api); + api::IngredientsApiRef api); } // namespace cookcookhnya::handlers::storage::ingredients diff --git a/src/handlers/storage/members/add.cpp b/src/handlers/storage/members/add.cpp index 317c4725..43a80c1d 100644 --- a/src/handlers/storage/members/add.cpp +++ b/src/handlers/storage/members/add.cpp @@ -1,5 +1,6 @@ #include "add.hpp" +#include "backend/api/storages.hpp" #include "handlers/common.hpp" #include "message_tracker.hpp" #include "render/storage/members/add.hpp" @@ -17,7 +18,7 @@ namespace cookcookhnya::handlers::storage::members { using namespace render::storage::members; void handleStorageMemberAdditionMsg( - StorageMemberAddition& state, MessageRef m, BotRef bot, SMRef stateManager, StorageApiRef storageApi) { + StorageMemberAddition& state, MessageRef m, BotRef bot, SMRef stateManager, api::StorageApiRef storageApi) { auto chatId = m.chat->id; auto userId = m.from->id; @@ -50,7 +51,7 @@ void handleStorageMemberAdditionMsg( }; void handleStorageMemberAdditionCQ( - StorageMemberAddition& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, StorageApiRef storageApi) { + StorageMemberAddition& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::StorageApiRef storageApi) { bot.answerCallbackQuery(cq.id); auto chatId = cq.message->chat->id; auto userId = cq.from->id; diff --git a/src/handlers/storage/members/add.hpp b/src/handlers/storage/members/add.hpp index a767890f..7aa24d70 100644 --- a/src/handlers/storage/members/add.hpp +++ b/src/handlers/storage/members/add.hpp @@ -1,13 +1,14 @@ #pragma once +#include "backend/api/storages.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::storage::members { void handleStorageMemberAdditionMsg( - StorageMemberAddition& state, MessageRef m, BotRef bot, SMRef stateManager, StorageApiRef storageApi); + StorageMemberAddition& state, MessageRef m, BotRef bot, SMRef stateManager, api::StorageApiRef storageApi); void handleStorageMemberAdditionCQ( - StorageMemberAddition& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, StorageApiRef storageApi); + StorageMemberAddition& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::StorageApiRef storageApi); } // namespace cookcookhnya::handlers::storage::members diff --git a/src/handlers/storage/members/delete.cpp b/src/handlers/storage/members/delete.cpp index 5ffabb33..f5595225 100644 --- a/src/handlers/storage/members/delete.cpp +++ b/src/handlers/storage/members/delete.cpp @@ -1,5 +1,6 @@ #include "delete.hpp" +#include "backend/api/storages.hpp" #include "handlers/common.hpp" #include "render/storage/members/view.hpp" #include "tg_types.hpp" @@ -10,7 +11,7 @@ namespace cookcookhnya::handlers::storage::members { using namespace render::storage::members; void handleStorageMemberDeletionCQ( - StorageMemberDeletion& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, StorageApiRef storageApi) { + StorageMemberDeletion& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::StorageApiRef storageApi) { bot.answerCallbackQuery(cq.id); auto chatId = cq.message->chat->id; diff --git a/src/handlers/storage/members/delete.hpp b/src/handlers/storage/members/delete.hpp index 562321dc..de4464fd 100644 --- a/src/handlers/storage/members/delete.hpp +++ b/src/handlers/storage/members/delete.hpp @@ -1,10 +1,11 @@ #pragma once +#include "backend/api/storages.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::storage::members { void handleStorageMemberDeletionCQ( - StorageMemberDeletion& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, StorageApiRef storageApi); + StorageMemberDeletion& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::StorageApiRef storageApi); } // namespace cookcookhnya::handlers::storage::members diff --git a/src/handlers/storage/members/view.cpp b/src/handlers/storage/members/view.cpp index 411100cb..76e61803 100644 --- a/src/handlers/storage/members/view.cpp +++ b/src/handlers/storage/members/view.cpp @@ -1,9 +1,11 @@ #include "view.hpp" +#include "backend/api/storages.hpp" #include "handlers/common.hpp" #include "render/storage/members/add.hpp" #include "render/storage/members/delete.hpp" #include "render/storage/view.hpp" +#include namespace cookcookhnya::handlers::storage::members { @@ -11,7 +13,7 @@ using namespace render::storage::members; using namespace render::storage; void handleStorageMemberViewCQ( - StorageMemberView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, StorageApiRef storageApi) { + StorageMemberView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::StorageApiRef storageApi) { bot.answerCallbackQuery(cq.id); auto chatId = cq.message->chat->id; auto userId = cq.from->id; @@ -23,7 +25,8 @@ void handleStorageMemberViewCQ( stateManager.put(StorageMemberDeletion{state.storageId}); } else if (cq.data == "back") { renderStorageView(state.storageId, userId, chatId, bot, storageApi); - stateManager.put(StorageView{state.storageId}); + std::string storageName = storageApi.get(userId, state.storageId).name; + stateManager.put(StorageView{state.storageId, std::move(storageName)}); } }; diff --git a/src/handlers/storage/members/view.hpp b/src/handlers/storage/members/view.hpp index 4fbe3ec4..2ad670ef 100644 --- a/src/handlers/storage/members/view.hpp +++ b/src/handlers/storage/members/view.hpp @@ -1,10 +1,11 @@ #pragma once +#include "backend/api/storages.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::storage::members { void handleStorageMemberViewCQ( - StorageMemberView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, StorageApiRef storageApi); + StorageMemberView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::StorageApiRef storageApi); } // namespace cookcookhnya::handlers::storage::members diff --git a/src/handlers/storage/view.cpp b/src/handlers/storage/view.cpp index 8ebe9d5b..ece4ea12 100644 --- a/src/handlers/storage/view.cpp +++ b/src/handlers/storage/view.cpp @@ -1,14 +1,17 @@ #include "view.hpp" +#include "backend/api/api.hpp" +#include "backend/models/storage.hpp" #include "handlers/common.hpp" #include "render/recipes_suggestions/view.hpp" +#include "render/storage/delete.hpp" #include "render/storage/ingredients/view.hpp" #include "render/storage/members/view.hpp" #include "render/storages_list/view.hpp" #include "states.hpp" -#include -#include +#include +#include #include namespace cookcookhnya::handlers::storage { @@ -17,30 +20,45 @@ using namespace render::storage::ingredients; using namespace render::storage::members; using namespace render::storages_list; using namespace render::recipes_suggestions; +using namespace render::delete_storage; +using namespace api::models::storage; +using namespace std::views; -void handleStorageViewCQ(StorageView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api) { +void handleStorageViewCQ( + StorageView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { bot.answerCallbackQuery(cq.id); auto chatId = cq.message->chat->id; auto userId = cq.from->id; - const size_t numOfIngredientsOnPage = 5; + if (cq.data == "ingredients") { auto ingredients = api.getIngredientsApi().getStorageIngredients(userId, state.storageId); - stateManager.put(StorageIngredientsList{ - state.storageId, - {std::make_move_iterator(ingredients.begin()), std::make_move_iterator(ingredients.end())}, - ""}); - renderIngredientsListSearch( - std::get(*stateManager.get()), numOfIngredientsOnPage, userId, chatId, bot); - } else if (cq.data == "members") { + auto newState = StorageIngredientsList{state.storageId, ingredients | as_rvalue, ""}; + renderIngredientsListSearch(newState, userId, chatId, bot); + stateManager.put(std::move(newState)); + return; + } + + if (cq.data == "members") { renderMemberList(true, state.storageId, userId, chatId, bot, api); stateManager.put(StorageMemberView{state.storageId}); - } else if (cq.data == "back") { + return; + } + + if (cq.data == "back") { renderStorageList(true, userId, chatId, bot, api); stateManager.put(StorageList{}); - } else if (cq.data == "wanna_eat") { + return; + } + + if (cq.data == "wanna_eat") { renderRecipesSuggestion({state.storageId}, 0, userId, chatId, bot, api); - stateManager.put( - SuggestedRecipeList{.pageNo = 0, .storageIds = std::vector{state.storageId}, .fromStorage = true}); + stateManager.put(SuggestedRecipesList{.prevState = state, .pageNo = 0}); + return; + } + + if (cq.data == "delete") { + renderStorageDeletion(state.storageId, chatId, bot, cq.from->id, api); + stateManager.put(StorageDeletion{state.storageId}); return; } } diff --git a/src/handlers/storage/view.hpp b/src/handlers/storage/view.hpp index 8d537a92..d1143a61 100644 --- a/src/handlers/storage/view.hpp +++ b/src/handlers/storage/view.hpp @@ -1,9 +1,11 @@ #pragma once +#include "backend/api/api.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::storage { -void handleStorageViewCQ(StorageView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api); +void handleStorageViewCQ( + StorageView& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); } // namespace cookcookhnya::handlers::storage diff --git a/src/handlers/storages_list/create.cpp b/src/handlers/storages_list/create.cpp index 42188ec2..d2798bc7 100644 --- a/src/handlers/storages_list/create.cpp +++ b/src/handlers/storages_list/create.cpp @@ -1,5 +1,6 @@ #include "create.hpp" +#include "backend/api/storages.hpp" #include "backend/models/storage.hpp" #include "handlers/common.hpp" #include "message_tracker.hpp" @@ -13,7 +14,7 @@ namespace cookcookhnya::handlers::storages_list { using namespace render::storages_list; void handleStorageCreationEnterNameMsg( - StorageCreationEnterName& /*unused*/, MessageRef m, BotRef bot, SMRef stateManager, StorageApiRef storageApi) { + StorageCreationEnterName& /*unused*/, MessageRef m, BotRef bot, SMRef stateManager, api::StorageApiRef storageApi) { storageApi.create(m.from->id, api::models::storage::StorageCreateBody{m.text}); // Create storage body with new name auto text = utils::utf8str(u8"🏷 Введите новое имя хранилища"); auto messageId = message::getMessageId(m.from->id); @@ -28,7 +29,7 @@ void handleStorageCreationEnterNameCQ(StorageCreationEnterName& /*unused*/, CallbackQueryRef cq, BotRef bot, SMRef stateManager, - StorageApiRef storageApi) { + api::StorageApiRef storageApi) { bot.answerCallbackQuery(cq.id); if (cq.data == "back") { renderStorageList(true, cq.from->id, cq.message->chat->id, bot, storageApi); diff --git a/src/handlers/storages_list/create.hpp b/src/handlers/storages_list/create.hpp index 32b834a3..29b45b63 100644 --- a/src/handlers/storages_list/create.hpp +++ b/src/handlers/storages_list/create.hpp @@ -1,13 +1,14 @@ #pragma once +#include "backend/api/storages.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::storages_list { void handleStorageCreationEnterNameMsg( - StorageCreationEnterName&, MessageRef m, BotRef bot, SMRef stateManager, StorageApiRef storageApi); + StorageCreationEnterName&, MessageRef m, BotRef bot, SMRef stateManager, api::StorageApiRef storageApi); void handleStorageCreationEnterNameCQ( - StorageCreationEnterName&, CallbackQueryRef cq, BotRef bot, SMRef stateManager, StorageApiRef storageApi); + StorageCreationEnterName&, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::StorageApiRef storageApi); } // namespace cookcookhnya::handlers::storages_list diff --git a/src/handlers/storages_list/delete.cpp b/src/handlers/storages_list/delete.cpp deleted file mode 100644 index d623a17b..00000000 --- a/src/handlers/storages_list/delete.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "delete.hpp" - -#include "backend/id_types.hpp" -#include "handlers/common.hpp" -#include "render/storages_list/view.hpp" -#include "utils/parsing.hpp" - -namespace cookcookhnya::handlers::storages_list { - -using namespace render::storages_list; - -void handleStorageDeletionCQ( - StorageDeletion& /*unused*/, CallbackQueryRef cq, BotRef bot, SMRef stateManager, StorageApiRef storageApi) { - bot.answerCallbackQuery(cq.id); - if (cq.data.starts_with("st")) { - auto storageId = utils::parseSafe(cq.data.substr(4)); - if (storageId) { - storageApi.delete_(cq.from->id, *storageId); - } - } - renderStorageList(true, cq.from->id, cq.message->chat->id, bot, storageApi); - stateManager.put(StorageList{}); -}; - -} // namespace cookcookhnya::handlers::storages_list diff --git a/src/handlers/storages_list/delete.hpp b/src/handlers/storages_list/delete.hpp deleted file mode 100644 index 28e25044..00000000 --- a/src/handlers/storages_list/delete.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include "handlers/common.hpp" - -namespace cookcookhnya::handlers::storages_list { - -void handleStorageDeletionCQ( - StorageDeletion&, CallbackQueryRef cq, BotRef bot, SMRef stateManager, StorageApiRef storageApi); - -} // namespace cookcookhnya::handlers::storages_list diff --git a/src/handlers/storages_list/view.cpp b/src/handlers/storages_list/view.cpp index 779a1601..190c7618 100644 --- a/src/handlers/storages_list/view.cpp +++ b/src/handlers/storages_list/view.cpp @@ -1,48 +1,46 @@ #include "view.hpp" -#include "backend/api/storages.hpp" +#include "backend/api/api.hpp" #include "backend/id_types.hpp" #include "handlers/common.hpp" #include "render/main_menu/view.hpp" #include "render/storage/view.hpp" #include "render/storages_list/create.hpp" -#include "render/storages_list/delete.hpp" #include "utils/parsing.hpp" #include +#include namespace cookcookhnya::handlers::storages_list { using namespace render::main_menu; -using namespace render::create_storage; -using namespace render::delete_storage; +using namespace render::storages_list; using namespace render::storage; void handleStorageListCQ( - StorageList& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, ApiClientRef api) { + StorageList& /*unused*/, CallbackQueryRef cq, BotRef& bot, SMRef stateManager, api::ApiClientRef api) { bot.answerCallbackQuery(cq.id); auto userId = cq.from->id; auto chatId = cq.message->chat->id; auto storages = api.getStoragesApi().getStoragesList(userId); + if (cq.data == "create") { renderStorageCreation(chatId, userId, bot); stateManager.put(StorageCreationEnterName{}); return; } - if (cq.data == "delete") { - renderStorageDeletion(chatId, bot, cq.from->id, api); - stateManager.put(StorageDeletion{}); - return; - } + if (cq.data == "back") { - renderMainMenu(true, userId, chatId, bot, api); + renderMainMenu(true, std::nullopt, userId, chatId, bot, api); stateManager.put(MainMenu{}); return; } + auto storageId = utils::parseSafe(cq.data); if (storageId) { renderStorageView(*storageId, cq.from->id, chatId, bot, api); - stateManager.put(StorageView{*storageId}); + std::string storageName = api.getStoragesApi().get(userId, *storageId).name; + stateManager.put(StorageView{*storageId, std::move(storageName)}); } } diff --git a/src/handlers/storages_list/view.hpp b/src/handlers/storages_list/view.hpp index 73594b68..37fcfb38 100644 --- a/src/handlers/storages_list/view.hpp +++ b/src/handlers/storages_list/view.hpp @@ -1,9 +1,10 @@ #pragma once +#include "backend/api/api.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::storages_list { -void handleStorageListCQ(StorageList&, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api); +void handleStorageListCQ(StorageList&, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); } // namespace cookcookhnya::handlers::storages_list diff --git a/src/handlers/storages_selection/view.cpp b/src/handlers/storages_selection/view.cpp index 918c5325..d89360a0 100644 --- a/src/handlers/storages_selection/view.cpp +++ b/src/handlers/storages_selection/view.cpp @@ -1,56 +1,82 @@ #include "view.hpp" +#include "backend/api/api.hpp" #include "backend/id_types.hpp" +#include "backend/models/storage.hpp" #include "handlers/common.hpp" +#include "render/cooking_planning/view.hpp" #include "render/main_menu/view.hpp" +#include "render/recipe/view.hpp" #include "render/recipes_suggestions/view.hpp" #include "render/storages_selection/view.hpp" +#include "utils/ingredients_availability.hpp" #include "utils/parsing.hpp" #include +#include +#include #include +#include #include namespace cookcookhnya::handlers::storages_selection { +using api::models::storage::StorageSummary; using namespace render::recipes_suggestions; using namespace render::select_storages; using namespace render::main_menu; +using namespace render::cooking_planning; +using namespace render::recipe; +using namespace std::views; +using std::ranges::to; void handleStoragesSelectionCQ( - StoragesSelection& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api) { + StoragesSelection& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api) { bot.answerCallbackQuery(cq.id); auto chatId = cq.message->chat->id; auto userId = cq.from->id; - auto selectedStorages = state.storageIds; if (cq.data == "confirm") { - renderRecipesSuggestion(selectedStorages, 0, userId, chatId, bot, api); - stateManager.put( - SuggestedRecipeList{.pageNo = 0, .storageIds = std::move(selectedStorages), .fromStorage = false}); + auto storagesIds = state.selectedStorages | transform(&StorageSummary::id) | to(); + if (auto* prevState = std::get_if(&state.prevState)) { + const api::RecipeId recipeId = prevState->recipeId; + auto availability = utils::inStoragesAvailability(storagesIds, recipeId, userId, api); + + renderCookingPlanning(availability, recipeId, userId, chatId, bot, api); + stateManager.put( + CookingPlanning{.prevState = CookingPlanning::FromRecipeViewData{std::move(*prevState), + std::move(state.selectedStorages)}, + .addedStorages = {}, + .availability = std::move(availability), + .recipeId = recipeId}); + } else if (std::holds_alternative(state.prevState)) { + renderRecipesSuggestion(storagesIds, 0, userId, chatId, bot, api); + stateManager.put(SuggestedRecipesList{.prevState = std::move(state), .pageNo = 0}); + } return; } + if (cq.data == "cancel") { - renderMainMenu(true, userId, chatId, bot, api); - stateManager.put(MainMenu{}); + if (auto* prevState = std::get_if(&state.prevState)) { + renderMainMenu(true, std::nullopt, userId, chatId, bot, api); + stateManager.put(auto{*prevState}); + } else if (auto* prevState = std::get_if(&state.prevState)) { + renderRecipeView(prevState->recipe, prevState->recipeId, userId, chatId, bot); + stateManager.put(auto{std::move(*prevState)}); + } return; } - auto storageId = utils::parseSafe(cq.data.substr(4)); - if (storageId) { - if (cq.data.starts_with("in")) { - auto it = std::ranges::find(selectedStorages, *storageId); - selectedStorages.erase(it); - renderStorageSelection(selectedStorages, userId, chatId, bot, api); - stateManager.put(StoragesSelection{.storageIds = selectedStorages}); - return; - } - if (cq.data.starts_with("out")) { - selectedStorages.push_back(*storageId); - renderStorageSelection(selectedStorages, userId, chatId, bot, api); - stateManager.put(StoragesSelection{.storageIds = selectedStorages}); - return; + if (auto mSelectedStorageId = utils::parseSafe(cq.data.substr(1))) { + if (cq.data[0] == '+') { + auto it = std::ranges::find(state.selectedStorages, *mSelectedStorageId, &StorageSummary::id); + state.selectedStorages.erase(it); + } else if (cq.data[0] == '-') { + auto storageDetails = api.getStoragesApi().get(userId, *mSelectedStorageId); + state.selectedStorages.push_back({.id = *mSelectedStorageId, .name = storageDetails.name}); } + renderStorageSelection(state, userId, chatId, bot, api); + return; } } } // namespace cookcookhnya::handlers::storages_selection diff --git a/src/handlers/storages_selection/view.hpp b/src/handlers/storages_selection/view.hpp index f8638f12..654cd486 100644 --- a/src/handlers/storages_selection/view.hpp +++ b/src/handlers/storages_selection/view.hpp @@ -1,10 +1,11 @@ #pragma once +#include "backend/api/api.hpp" #include "handlers/common.hpp" namespace cookcookhnya::handlers::storages_selection { void handleStoragesSelectionCQ( - StoragesSelection& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, ApiClientRef api); + StoragesSelection& state, CallbackQueryRef cq, BotRef bot, SMRef stateManager, api::ApiClientRef api); } // namespace cookcookhnya::handlers::storages_selection diff --git a/src/main.cpp b/src/main.cpp index 87eb9151..11b6668a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -33,14 +33,19 @@ int main(int argc, char* argv[]) { } try { - Setup>::Stater>::Stater + customRecipeIngredientsSearchIQHandler, + shoppingListStorageSelectionToBuyCQHandler, + customRecipePublicationHistoryCQHandler, + totalPublicationHistoryCQHandler, + shoppingListIngredientSearchCQHandler, + shoppingListIngredientSearchIQHandler, + recipesSearchCQHandler, + recipesSearchIQHandler, + recipeViewCQHandler, + cookingIngredientsSpendingCQHandler> bot{{}, {ApiClient{utils::getenvWithError("API_URL")}}}; - TgBot::Bot tgBot{utils::getenvWithError("BOT_TOKEN")}; + TgBot::Bot tgBot{utils::getenvWithError("BOT_TOKEN")}; // sdf if (useWebhook) { const std::string path = "/"s + utils::getenvWithError("WEBHOOK_SECRET"); // NOLINT(*include*) bot.startWebhook(std::move(tgBot), @@ -76,7 +91,7 @@ int main(int argc, char* argv[]) { path); } else { bot.start(std::move(tgBot)); - } + } // pipidastr } catch (std::exception& e) { std::cout << e.what() << '\n'; return 1; diff --git a/src/message_tracker.cpp b/src/message_tracker.cpp index 7a52eef4..98b389b4 100644 --- a/src/message_tracker.cpp +++ b/src/message_tracker.cpp @@ -24,4 +24,8 @@ void addMessageId(UserId userId, MessageId messageId) { map.insert_or_assign(userId, messageId); } +void deleteMessageId(UserId userId) { + map.erase(userId); +} + } // namespace cookcookhnya::message diff --git a/src/message_tracker.hpp b/src/message_tracker.hpp index 7f64c2be..ac41fd9f 100644 --- a/src/message_tracker.hpp +++ b/src/message_tracker.hpp @@ -10,4 +10,6 @@ std::optional getMessageId(tg_types::UserId userId); void addMessageId(tg_types::UserId userId, tg_types::MessageId messageId); +void deleteMessageId(tg_types::UserId userId); + } // namespace cookcookhnya::message diff --git a/src/patched_bot.cpp b/src/patched_bot.cpp index a969e7db..4343c77c 100644 --- a/src/patched_bot.cpp +++ b/src/patched_bot.cpp @@ -79,13 +79,17 @@ void appendToJson(std::string& json, std::string_view varName, std::string_view TgBot::Message::Ptr // NOLINT(*nodiscard) PatchedBot::sendMessage(tg_types::ChatId chatId, std::string_view text, - const TgBot::InlineKeyboardMarkup::Ptr& replyMarkup) const { + const TgBot::InlineKeyboardMarkup::Ptr& replyMarkup, + std::string_view parseMode) const { std::vector args; args.reserve(3); args.emplace_back("chat_id", chatId); args.emplace_back("text", text); if (replyMarkup) args.emplace_back("reply_markup", parseInlineKeyboardMarkup(replyMarkup)); + if (!parseMode.empty()) { + args.emplace_back("parse_mode", parseMode); + } return _tgTypeParser.parseJsonAndGetMessage(sendRequest("sendMessage", args)); } @@ -93,7 +97,8 @@ TgBot::Message::Ptr // NOLINT(*nodiscard) PatchedBot::editMessageText(std::string_view text, tg_types::ChatId chatId, tg_types::MessageId messageId, - const TgBot::InlineKeyboardMarkup::Ptr& replyMarkup) const { + const TgBot::InlineKeyboardMarkup::Ptr& replyMarkup, + std::string_view parseMode) const { std::vector args; args.reserve(4); args.emplace_back("text", text); @@ -102,6 +107,9 @@ PatchedBot::editMessageText(std::string_view text, if (replyMarkup) { args.emplace_back("reply_markup", parseInlineKeyboardMarkup(replyMarkup)); } + if (!parseMode.empty()) { + args.emplace_back("parse_mode", parseMode); + } return _tgTypeParser.parseJsonAndGetMessage(sendRequest("editMessageText", args)); } diff --git a/src/patched_bot.hpp b/src/patched_bot.hpp index 805b7f23..cc30cb8f 100644 --- a/src/patched_bot.hpp +++ b/src/patched_bot.hpp @@ -30,31 +30,34 @@ class PatchedBot : TgBot::Api { TgBot::Message::Ptr sendMessage(tg_types::ChatId chatId, // NOLINT(*nodiscard) std::string_view text, - const TgBot::InlineKeyboardMarkup::Ptr& replyMarkup) const; + const TgBot::InlineKeyboardMarkup::Ptr& replyMarkup, + std::string_view parseMode = "") const; // For code compatibility TgBot::Message::Ptr sendMessage(tg_types::ChatId chatId, // NOLINT(*nodiscard) std::string_view text, std::nullptr_t /*unused*/, std::nullptr_t /*unused*/, - const TgBot::InlineKeyboardMarkup::Ptr& replyMarkup) const { - return sendMessage(chatId, text, replyMarkup); + const TgBot::InlineKeyboardMarkup::Ptr& replyMarkup, + std::string_view parseMode = "") const { + return sendMessage(chatId, text, replyMarkup, parseMode); } TgBot::Message::Ptr editMessageText(std::string_view text, // NOLINT(*nodiscard) tg_types::ChatId chatId, tg_types::MessageId messageId, - const TgBot::InlineKeyboardMarkup::Ptr& replyMarkup) const; + const TgBot::InlineKeyboardMarkup::Ptr& replyMarkup, + std::string_view parseMode = "") const; // For code compatibility TgBot::Message::Ptr editMessageText(std::string_view text, // NOLINT(*nodiscard) tg_types::ChatId chatId, tg_types::MessageId messageId, const char* /*unused*/, - const char* /*unused*/, + const char* parseMode, std::nullptr_t, const TgBot::InlineKeyboardMarkup::Ptr& replyMarkup) const { - return editMessageText(text, chatId, messageId, replyMarkup); + return editMessageText(text, chatId, messageId, replyMarkup, parseMode); } TgBot::Message::Ptr editMessageReplyMarkup(tg_types::ChatId chatId, // NOLINT(*nodiscard*) diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt index 277c63d2..9faecd9e 100644 --- a/src/render/CMakeLists.txt +++ b/src/render/CMakeLists.txt @@ -4,33 +4,46 @@ target_sources(main PRIVATE src/render/personal_account/ingredients_list/create.cpp src/render/personal_account/ingredients_list/publish.cpp src/render/personal_account/ingredients_list/view.cpp + src/render/personal_account/ingredients_list/delete.cpp src/render/personal_account/recipes_list/create.cpp - src/render/personal_account/recipes_list/recipe/search_ingredients.cpp - src/render/personal_account/recipes_list/recipe/view.cpp src/render/personal_account/recipes_list/view.cpp + src/render/personal_account/recipe/search_ingredients.cpp + src/render/personal_account/recipe/view.cpp + src/render/personal_account/recipe/moderation_history.cpp + src/render/personal_account/view.cpp + src/render/personal_account/publication_history.cpp - src/render/recipe/add_storage.cpp - src/render/recipe/view.cpp + src/render/cooking_planning/add_storage.cpp + src/render/cooking_planning/view.cpp src/render/recipes_suggestions/view.cpp src/render/shopping_list/create.cpp + src/render/shopping_list/search.cpp + src/render/shopping_list/storage_selection_to_buy.cpp src/render/shopping_list/view.cpp src/render/storage/ingredients/view.cpp + src/render/storage/ingredients/delete.cpp src/render/storage/members/add.cpp src/render/storage/members/delete.cpp src/render/storage/members/view.cpp src/render/storage/view.cpp + src/render/storage/delete.cpp src/render/storages_list/create.cpp - src/render/storages_list/delete.cpp src/render/storages_list/view.cpp src/render/storages_selection/view.cpp + + src/render/recipes_search/view.cpp + + src/render/recipe/view.cpp + + src/render/cooking/ingredients_spending.cpp ) diff --git a/src/render/common.hpp b/src/render/common.hpp index 30140e88..4866cdba 100644 --- a/src/render/common.hpp +++ b/src/render/common.hpp @@ -1,15 +1,10 @@ #pragma once -#include "backend/api/api.hpp" -#include "backend/api/ingredients.hpp" -#include "backend/api/recipes.hpp" -#include "backend/api/shopping_lists.hpp" -#include "backend/api/storages.hpp" -#include "backend/api/users.hpp" #include "patched_bot.hpp" #include "tg_types.hpp" #include "utils/utils.hpp" +#include #include #include #include @@ -25,14 +20,6 @@ class InlineKeyboardMarkup; namespace cookcookhnya::render { -// API -using ApiClient = const api::ApiClient&; -using StorageApiRef = const api::StoragesApi&; -using IngredientsApiRef = const api::IngredientsApi&; -using UserApiRef = const api::UsersApi&; -using RecipesApiRef = const api::RecipesApi&; -using ShoppingListApiRef = const api::ShoppingListApi&; - using UserId = tg_types::UserId; using ChatId = tg_types::ChatId; using MessageId = tg_types::MessageId; @@ -71,4 +58,47 @@ inline std::shared_ptr makeKeyboardMarkup(InlineKey return markup; } +struct NewRow {}; + +class InlineKeyboardBuilder { + InlineKeyboard keyboard; + + public: + explicit InlineKeyboardBuilder(std::size_t reserve = 0) { + keyboard.reserve(reserve); + keyboard.emplace_back(); + } + + InlineKeyboardBuilder& operator<<(TgBot::InlineKeyboardButton::Ptr&& button) { + keyboard.back().push_back(std::move(button)); + return *this; + } + + void reserveInRow(std::size_t capacity) { + keyboard.back().reserve(capacity); + } + + InlineKeyboardBuilder& operator<<(NewRow /*tag*/) { + keyboard.emplace_back(); + return *this; + } + + void operator++(int) { + keyboard.emplace_back(); + } + + void removeRowIfEmpty() { + if (keyboard.back().empty()) + keyboard.pop_back(); + } + + TgBot::InlineKeyboardMarkup::Ptr build() && { + return makeKeyboardMarkup(std::move(keyboard)); + } + + operator TgBot::InlineKeyboardMarkup::Ptr() && { // NOLINT(*explicit*) + return makeKeyboardMarkup(std::move(keyboard)); + } +}; + } // namespace cookcookhnya::render diff --git a/src/render/cooking/ingredients_spending.cpp b/src/render/cooking/ingredients_spending.cpp new file mode 100644 index 00000000..251b8907 --- /dev/null +++ b/src/render/cooking/ingredients_spending.cpp @@ -0,0 +1,55 @@ +#include "ingredients_spending.hpp" + +#include "message_tracker.hpp" +#include "render/common.hpp" +#include "states.hpp" +#include "utils/utils.hpp" + +#include +#include + +namespace cookcookhnya::render::cooking { + +using namespace std::views; +using SelectableIngedient = states::helpers::SelectableShoppingListItem; + +void renderIngredientsSpending(const std::vector& ingredients, + bool canRemove, + UserId userId, + ChatId chatId, + BotRef bot) { + const std::string text = utils::utf8str( + canRemove ? u8"🧾 Вы можете убрать из хранилища продукты, если они закончились после готовки, а также сразу " + u8"добавить их в список покупок" + : u8"🧾 Вы можете добавить закончившиеся после готовки продукты в список покупок"); + + const bool anySelected = std::ranges::any_of(ingredients, &states::helpers::SelectableIngredient::selected); + + InlineKeyboardBuilder keyboard{2 + ((ingredients.size() / 2) + 1)}; // remove and/or buy, list (n/2), back + + if (anySelected) { + if (canRemove) + keyboard << makeCallbackButton(u8"🗑 Убрать", "remove"); + keyboard << makeCallbackButton(u8"🛒 В список покупок", "to_shopping_list") << NewRow{}; + } + + for (auto row : ingredients | chunk(2)) { + for (const auto& ing : row) { + const char8_t* const selectedMark = ing.selected ? u8"[ + ] " : u8"[ᅠ] "; // not empty! + keyboard << makeCallbackButton(utils::utf8str(selectedMark) + ing.name, + "ingredient_" + utils::to_string(ing.id)); + } + keyboard << NewRow{}; + } + + keyboard << makeCallbackButton(u8"↩️ Назад", "back"); + + if (auto messageId = message::getMessageId(userId)) { + bot.editMessageText(text, chatId, *messageId, std::move(keyboard)); + } else { + auto message = bot.sendMessage(chatId, text, std::move(keyboard)); + message::addMessageId(userId, message->messageId); + } +} + +} // namespace cookcookhnya::render::cooking diff --git a/src/render/cooking/ingredients_spending.hpp b/src/render/cooking/ingredients_spending.hpp new file mode 100644 index 00000000..e4aca33c --- /dev/null +++ b/src/render/cooking/ingredients_spending.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "render/common.hpp" +#include "states.hpp" + +#include + +namespace cookcookhnya::render::cooking { + +void renderIngredientsSpending(const std::vector& ingredients, + bool canRemove, + UserId userId, + ChatId chatId, + BotRef bot); + +} // namespace cookcookhnya::render::cooking diff --git a/src/render/cooking_planning/add_storage.cpp b/src/render/cooking_planning/add_storage.cpp new file mode 100644 index 00000000..0f4c4c0b --- /dev/null +++ b/src/render/cooking_planning/add_storage.cpp @@ -0,0 +1,123 @@ +#include "add_storage.hpp" + +#include "backend/api/api.hpp" +#include "backend/id_types.hpp" +#include "backend/models/recipe.hpp" +#include "backend/models/storage.hpp" +#include "message_tracker.hpp" +#include "render/common.hpp" +#include "states.hpp" +#include "utils/utils.hpp" + +#include +#include +#include +#include +#include +#include + +namespace cookcookhnya::render::cooking_planning { + +using namespace api::models::recipe; +using namespace api::models::storage; +using IngredientAvailability = states::CookingPlanning::IngredientAvailability; +using AvailabilityType = states::CookingPlanning::AvailabilityType; + +namespace { + +struct CookingInfo { + std::string renderText; + bool isIngredientNotAvailable; + bool isIngredientInOtherStorages; +}; + +CookingInfo storageAdditionView(const std::vector& inStoragesAvailability, + const std::vector& selectedStorages, + api::RecipeId recipeId, + UserId userId, + api::ApiClientRef api) { + auto recipe = api.getRecipesApi().getSuggested(userId, recipeId); + + bool isIngredientNotAvailable = false; + bool isIngredientIsOtherStorages = false; + const std::string recipeName = recipe.name; + auto text = std::format("{} Выбранные хранилища: ", utils::utf8str(u8"🍱")); + for (std::size_t i = 0; i != selectedStorages.size(); ++i) { + text += selectedStorages[i].name; + text += i != selectedStorages.size() - 1 ? ", " : "\n"; + } + text += std::format("{} Ингредиенты для *{}* \n\n", utils::utf8str(u8"📖"), recipeName); + + for (const auto& infoPair : inStoragesAvailability) { + if (infoPair.available == AvailabilityType::AVAILABLE) { + text += "`[+]` " + infoPair.ingredient.name + "\n"; + } else if (infoPair.available == AvailabilityType::OTHER_STORAGES) { + text += "`[?]` " + infoPair.ingredient.name + "\n"; + isIngredientIsOtherStorages = true; + text += "Доступно в: "; + auto storages = infoPair.storages; + for (std::size_t i = 0; i != storages.size(); ++i) { + text += storages[i].name; + text += i != storages.size() - 1 ? ", " : "\n"; + } + } else { + text += "`[ ]` " + infoPair.ingredient.name + "\n"; + isIngredientNotAvailable = true; + } + } + if (recipe.link) + text += utils::utf8str(u8"\n🌐 Источник: ") + *recipe.link; + + return {.renderText = text, + .isIngredientNotAvailable = isIngredientNotAvailable, + .isIngredientInOtherStorages = isIngredientIsOtherStorages}; +} + +} // namespace + +void renderStoragesSuggestion(const std::vector& inStoragesAvailability, + const std::vector& selectedStorages, + const std::vector& addedStorages, + api::RecipeId recipeId, + UserId userId, + ChatId chatId, + BotRef bot, + api::ApiClientRef api) { + auto textGen = storageAdditionView(inStoragesAvailability, selectedStorages, recipeId, userId, api); + std::vector storages; + for (const auto& infoPair : inStoragesAvailability) { + if (infoPair.available == AvailabilityType::OTHER_STORAGES) { + for (const auto& storage : infoPair.storages) { + if (std::ranges::find(storages, storage.id, &StorageSummary::id) == storages.end()) { + storages.push_back(storage); + } + } + } + } + storages.insert(storages.end(), addedStorages.begin(), addedStorages.end()); + const size_t buttonRows = ((storages.size() + 1) / 2) + 1; + InlineKeyboard keyboard(buttonRows); + + for (std::size_t i = 0; i < storages.size(); ++i) { + if (i % 2 == 0) + keyboard[i / 2].reserve(2); + const bool isSelected = + std::ranges::find(addedStorages, storages[i].id, &StorageSummary::id) != addedStorages.end(); + + std::string emoji = utils::utf8str(isSelected ? u8"[ + ]" : u8"[ᅠ]"); + const char* actionPrefix = isSelected ? "+" : "-"; + const std::string text = std::format("{} {}", emoji, storages[i].name); + const std::string data = actionPrefix + utils::to_string(storages[i].id); + keyboard[i / 2].push_back(makeCallbackButton(text, data)); + } + if (!storages.empty()) { + keyboard[buttonRows - 1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); + } + + auto messageId = message::getMessageId(userId); + if (messageId) { + bot.editMessageText( + textGen.renderText, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard)), "Markdown"); + } +} +} // namespace cookcookhnya::render::cooking_planning diff --git a/src/render/cooking_planning/add_storage.hpp b/src/render/cooking_planning/add_storage.hpp new file mode 100644 index 00000000..b34595bd --- /dev/null +++ b/src/render/cooking_planning/add_storage.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "backend/id_types.hpp" +#include "backend/models/storage.hpp" +#include "render/common.hpp" +#include "states.hpp" + +#include + +namespace cookcookhnya::render::cooking_planning { + +void renderStoragesSuggestion( + const std::vector& inStoragesAvailability, + const std::vector& selectedStorages, + const std::vector& addedStorages, + api::RecipeId recipeId, + UserId userId, + ChatId chatId, + BotRef bot, + api::ApiClientRef api); + +} // namespace cookcookhnya::render::cooking_planning diff --git a/src/render/cooking_planning/view.cpp b/src/render/cooking_planning/view.cpp new file mode 100644 index 00000000..87ac6bd1 --- /dev/null +++ b/src/render/cooking_planning/view.cpp @@ -0,0 +1,105 @@ +#include "view.hpp" + +#include "backend/api/api.hpp" +#include "backend/id_types.hpp" +#include "backend/models/recipe.hpp" +#include "message_tracker.hpp" +#include "render/common.hpp" +#include "states.hpp" +#include "utils/u8format.hpp" +#include "utils/utils.hpp" + +#include + +#include +#include +#include +#include + +namespace cookcookhnya::render::cooking_planning { + +using namespace api::models::recipe; +using IngredientAvailability = states::CookingPlanning::IngredientAvailability; +using AvailabilityType = states::CookingPlanning::AvailabilityType; + +namespace { + +struct CookingInfo { + std::string renderText; + std::string recipeName; + bool isIngredientNotAvailable; + bool isIngredientInOtherStorages; +}; + +CookingInfo recipeView(const std::vector& inStoragesAvailability, + api::RecipeId recipeId, + UserId userId, + api::ApiClientRef api) { + auto recipeIngredients = api.getRecipesApi().getSuggested(userId, recipeId); + + bool isIngredientNotAvailable = false; + bool isIngredientIsOtherStorages = false; + std::string& recipeName = recipeIngredients.name; + auto text = std::format("{} *{}* \n\n", utils::utf8str(u8"📖 Ингредиенты для"), recipeName); + + for (const auto& availability : inStoragesAvailability) { + if (availability.available == AvailabilityType::AVAILABLE) { + text += "`[+]` " + availability.ingredient.name + "\n"; + } else if (availability.available == AvailabilityType::OTHER_STORAGES) { + text += "`[?]` " + availability.ingredient.name + "\n"; + isIngredientIsOtherStorages = true; + } else { + text += "`[ ]` " + availability.ingredient.name + "\n"; + isIngredientNotAvailable = true; + } + } + if (recipeIngredients.link) + text += utils::utf8str(u8"\n🌐 Источник: ") + *recipeIngredients.link; + + return {.renderText = text, + .recipeName = recipeName, + .isIngredientNotAvailable = isIngredientNotAvailable, + .isIngredientInOtherStorages = isIngredientIsOtherStorages}; +} + +} // namespace + +void renderCookingPlanning(const std::vector& inStoragesAvailability, + api::RecipeId recipeId, + UserId userId, + ChatId chatId, + BotRef bot, + api::ApiClientRef api) { + auto cookingInfo = recipeView(inStoragesAvailability, recipeId, userId, api); + InlineKeyboardBuilder keyboard{4}; // Cook + add storages, shopping list, share, back + + keyboard << makeCallbackButton(u8"🧑‍🍳 Готовить", "start_cooking"); + if (cookingInfo.isIngredientInOtherStorages) + keyboard << makeCallbackButton(u8"?", "add_storages"); + keyboard << NewRow{}; + + if (cookingInfo.isIngredientNotAvailable) + keyboard << makeCallbackButton(u8"📝 Составить список продуктов", "shopping_list") << NewRow{}; + + auto shareButton = std::make_shared(); + shareButton->text = utils::utf8str(u8"📤 Поделиться"); + const std::string botAlias = bot.getUnderlying().getMe()->username; + const std::string recipeUrl = std::format("https://t.me/{}?start=recipe_{}", botAlias, recipeId); + const std::string shareText = + utils::u8format("{} **{}**", u8"Хочу поделиться с тобой рецептом", cookingInfo.recipeName); + + boost::urls::url url{"https://t.me/share/url"}; + url.params().append({"url", recipeUrl}); + url.params().append({"text", shareText}); + shareButton->url = url.buffer(); + + keyboard << std::move(shareButton) << NewRow{} << makeCallbackButton(u8"↩️ Назад", "back"); + + auto messageId = message::getMessageId(userId); + if (messageId) { + // Only on difference between function above + bot.editMessageText(cookingInfo.renderText, chatId, *messageId, std::move(keyboard), "Markdown"); + } +} + +} // namespace cookcookhnya::render::cooking_planning diff --git a/src/render/cooking_planning/view.hpp b/src/render/cooking_planning/view.hpp new file mode 100644 index 00000000..1c08b3b4 --- /dev/null +++ b/src/render/cooking_planning/view.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "backend/id_types.hpp" +#include "render/common.hpp" +#include "states.hpp" + +#include + +namespace cookcookhnya::render::cooking_planning { + +void renderCookingPlanning(const std::vector& inStoragesAvailability, + api::RecipeId recipeId, + UserId userId, + ChatId chatId, + BotRef bot, + api::ApiClientRef api); + +} // namespace cookcookhnya::render::cooking_planning diff --git a/src/render/main_menu/view.cpp b/src/render/main_menu/view.cpp index 0add0f9d..f7bc8a64 100644 --- a/src/render/main_menu/view.cpp +++ b/src/render/main_menu/view.cpp @@ -1,40 +1,49 @@ #include "view.hpp" +#include "backend/api/storages.hpp" #include "message_tracker.hpp" #include "render/common.hpp" #include "utils/utils.hpp" -#include +#include +#include #include namespace cookcookhnya::render::main_menu { using namespace tg_types; -void renderMainMenu(bool toBeEdited, UserId userId, ChatId chatId, BotRef bot, StorageApiRef storageApi) { +void renderMainMenu(bool toBeEdited, + std::optional> inviteStorage, + UserId userId, + ChatId chatId, + BotRef bot, + api::StorageApiRef storageApi) { auto storages = storageApi.getStoragesList(userId); - const std::size_t buttonRows = storages.empty() ? 3 : 4; - InlineKeyboard keyboard(buttonRows); - - if (!storages.empty()) { - keyboard[0].push_back(makeCallbackButton(u8"🍱 Хранилища", "storage_list")); - keyboard[1].push_back(makeCallbackButton(u8"😋 Хочу кушать!", "wanna_eat")); - keyboard[2].push_back(makeCallbackButton(u8"🗒 Список покупок", "shopping_list")); - keyboard[3].push_back(makeCallbackButton(u8"👤 Личный кабинет", "personal_account")); - } else { - keyboard[0].push_back(makeCallbackButton(u8"🍱 Хранилища", "storage_list")); - keyboard[1].push_back(makeCallbackButton(u8"🗒 Список покупок", "shopping_list")); - keyboard[2].push_back(makeCallbackButton(u8"👤 Личный кабинет", "personal_account")); + auto text = utils::utf8str( + u8"🍳 Добро пожаловать в CookCookNya — ваш личный бот для быстрого подбора рецептов и многого другого!"); + if (inviteStorage) { + if (*inviteStorage) + text += utils::utf8str(u8"\n\nВы были успешно добавлены в хранилище 🍱") + **inviteStorage; + else + text += utils::utf8str(u8"\n\nК сожалению, данное приглашение уже было использовано 🥲"); } - auto text = utils::utf8str( - u8"🍳 Добро пожаловать в CookCookNya — ваш личный бот для быстро подбора рецептов и многого другого!"); + const std::size_t rowsCount = 5; + InlineKeyboardBuilder keyboard{rowsCount}; + keyboard << makeCallbackButton(u8"🍱 Хранилища", "storage_list") << NewRow{}; + if (!storages.empty()) + keyboard << makeCallbackButton(u8"😋 Хочу кушать!", "wanna_eat") << NewRow{}; + keyboard << makeCallbackButton(u8"🧾 Список покупок", "shopping_list") << NewRow{} + << makeCallbackButton(u8"👨‍🍳 Рецепты", "recipes_search") << NewRow{} + << makeCallbackButton(u8"👤 Личный кабинет", "personal_account"); + if (toBeEdited) { if (auto messageId = message::getMessageId(userId)) - bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); + bot.editMessageText(text, chatId, *messageId, std::move(keyboard)); } else { - auto message = bot.sendMessage(chatId, text, makeKeyboardMarkup(std::move(keyboard))); + auto message = bot.sendMessage(chatId, text, std::move(keyboard)); message::addMessageId(userId, message->messageId); } } diff --git a/src/render/main_menu/view.hpp b/src/render/main_menu/view.hpp index 93bb21e1..f2e4b772 100644 --- a/src/render/main_menu/view.hpp +++ b/src/render/main_menu/view.hpp @@ -1,9 +1,18 @@ #pragma once +#include "backend/api/storages.hpp" #include "render/common.hpp" +#include +#include + namespace cookcookhnya::render::main_menu { -void renderMainMenu(bool toBeEdited, UserId userId, ChatId chatId, BotRef bot, StorageApiRef storageApi); +void renderMainMenu(bool toBeEdited, + std::optional> inviteStorage, + UserId userId, + ChatId chatId, + BotRef bot, + api::StorageApiRef storageApi); } // namespace cookcookhnya::render::main_menu diff --git a/src/render/pagination.hpp b/src/render/pagination.hpp new file mode 100644 index 00000000..7f287ecd --- /dev/null +++ b/src/render/pagination.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include "render/common.hpp" +#include "utils/utils.hpp" + +#include +#include +#include +#include +#include +#include +#include + +// forward declaration +namespace TgBot { +class InlineKeyboardButton; +} // namespace TgBot + +namespace cookcookhnya::render { + +template > ItemButtons> +struct PaginationMarkup { + ItemButtons itemButtons; + std::shared_ptr leftButton; + std::shared_ptr pageButton; + std::shared_ptr rightButton; + + friend InlineKeyboardBuilder& operator<<(InlineKeyboardBuilder& keyboard, PaginationMarkup&& markup) { + for (auto&& b : std::move(markup).itemButtons) + keyboard << std::move(b) << NewRow{}; + if (markup.leftButton) + keyboard << std::move(markup).leftButton << std::move(markup).pageButton << std::move(markup).rightButton + << NewRow{}; + return keyboard; + } +}; + +/** + * Single column list with an extra row of page switching if more than one page. + */ +template + requires std::is_invocable_r_v, + ItemButtonMaker, + std::ranges::range_reference_t> +auto constructPagination( + std::size_t pageNo, std::size_t pageSize, std::size_t totalItems, R&& page, ItemButtonMaker&& makeItemButton) { + using namespace std::views; + const std::size_t pagesCount = pageSize != 0 ? (totalItems + pageSize - 1) / pageSize : 0; // ceiling + const bool lastPage = pageNo + 1 >= pagesCount; + + auto itemButtons = std::forward(page) | transform(std::forward(makeItemButton)); + std::shared_ptr leftButton = nullptr; + std::shared_ptr pageButton = nullptr; + std::shared_ptr rightButton = nullptr; + + if (pagesCount > 1) { + enum PageArrows : std::uint8_t { + NOTHING = 0b00U, + LEFT = 0b01U, + RIGHT = 0b10U, + BOTH = 0b11U, + }; + + const char8_t* const emptyText = u8"ㅤ"; // not empty! invisible symbol + PageArrows b = NOTHING; + if (pageNo != 0) + b = static_cast(b | LEFT); + if (!lastPage) + b = static_cast(b | RIGHT); + + leftButton = + (b & LEFT) != 0 ? makeCallbackButton(u8"◀️", "page_left") : makeCallbackButton(emptyText, "do_not_handle"); + rightButton = + (b & RIGHT) != 0 ? makeCallbackButton(u8"▶️", "page_right") : makeCallbackButton(emptyText, "do_not_handle"); + pageButton = + makeCallbackButton(std::format("{} {} {}", pageNo + 1, utils::utf8str(u8"из"), pagesCount), "dont_handle"); + } + + return PaginationMarkup{ + std::move(itemButtons), std::move(leftButton), std::move(pageButton), std::move(rightButton)}; +} + +} // namespace cookcookhnya::render diff --git a/src/render/personal_account/ingredients_list/create.cpp b/src/render/personal_account/ingredients_list/create.cpp index 470b058c..e847ee3c 100644 --- a/src/render/personal_account/ingredients_list/create.cpp +++ b/src/render/personal_account/ingredients_list/create.cpp @@ -1,5 +1,7 @@ #include "create.hpp" +#include "backend/api/ingredients.hpp" +#include "backend/api/publicity_filter.hpp" #include "backend/models/ingredient.hpp" #include "message_tracker.hpp" #include "render/common.hpp" @@ -12,23 +14,27 @@ namespace cookcookhnya::render::personal_account::ingredients { +const std::size_t pageSize = 5; +const unsigned threshold = 70; + void renderCustomIngredientCreation(UserId userId, ChatId chatId, BotRef bot) { InlineKeyboard keyboard(1); keyboard[0].push_back(makeCallbackButton(u8"↩️ Назад", "back")); auto text = utils::utf8str(u8"🥦 Введите новое имя ингредиента"); auto messageId = message::getMessageId(userId); if (messageId) { - bot.editMessageText(text, chatId, *messageId, "", "", nullptr, makeKeyboardMarkup(std::move(keyboard))); + bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); } } void renderCustomIngredientConfirmation( - std::string ingredientName, UserId userId, ChatId chatId, BotRef bot, IngredientsApiRef api) { - InlineKeyboard keyboard(2); + bool toBeEdited, std::string ingredientName, UserId userId, ChatId chatId, BotRef bot, api::IngredientsApiRef api) { + InlineKeyboard keyboard{2}; keyboard[0].push_back(makeCallbackButton(u8"▶️ Подтвердить", "confirm")); keyboard[1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - auto similarIngredients = api.search(userId, std::move(ingredientName), 70, 5, 0).page; // NOLINT(*magic-numbers*) + auto similarIngredients = + api.search(userId, PublicityFilterType::All, std::move(ingredientName), pageSize, 0, threshold).page; std::string text; if (!similarIngredients.empty()) { @@ -44,8 +50,15 @@ void renderCustomIngredientConfirmation( } else { text = utils::utf8str(u8"Вы уверены, что хотите добавить новый ингредиент?"); } - auto message = bot.sendMessage(chatId, text, nullptr, nullptr, makeKeyboardMarkup(std::move(keyboard))); - message::addMessageId(userId, message->messageId); + + if (toBeEdited) { + if (auto messageId = message::getMessageId(userId)) + bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); + + } else { + auto message = bot.sendMessage(chatId, text, makeKeyboardMarkup(std::move(keyboard))); + message::addMessageId(userId, message->messageId); + } } } // namespace cookcookhnya::render::personal_account::ingredients diff --git a/src/render/personal_account/ingredients_list/create.hpp b/src/render/personal_account/ingredients_list/create.hpp index 06d4e87e..1a709038 100644 --- a/src/render/personal_account/ingredients_list/create.hpp +++ b/src/render/personal_account/ingredients_list/create.hpp @@ -1,5 +1,6 @@ #pragma once +#include "backend/api/ingredients.hpp" #include "render/common.hpp" #include @@ -9,6 +10,6 @@ namespace cookcookhnya::render::personal_account::ingredients { void renderCustomIngredientCreation(UserId userId, ChatId chatId, BotRef bot); void renderCustomIngredientConfirmation( - std::string ingredientName, UserId userId, ChatId chatId, BotRef bot, IngredientsApiRef api); + bool toBeEdited, std::string ingredientName, UserId userId, ChatId chatId, BotRef bot, api::IngredientsApiRef api); } // namespace cookcookhnya::render::personal_account::ingredients diff --git a/src/render/personal_account/ingredients_list/delete.cpp b/src/render/personal_account/ingredients_list/delete.cpp new file mode 100644 index 00000000..4796ddf7 --- /dev/null +++ b/src/render/personal_account/ingredients_list/delete.cpp @@ -0,0 +1,48 @@ +#include "delete.hpp" + +#include "backend/models/ingredient.hpp" +#include "backend/models/publication_request_status.hpp" +#include "message_tracker.hpp" +#include "render/common.hpp" +#include "utils/to_string.hpp" +#include "utils/utils.hpp" + +#include +#include +#include +#include + +namespace cookcookhnya::render::personal_account::ingredients { + +using namespace std::views; + +void renderCustomIngredientDeletion(UserId userId, ChatId chatId, BotRef bot, api::IngredientsApiRef api) { + auto ingredientsResp = api.customIngredientsSearch(userId, "", 0); + + // TODO: make pagination for ingredients + std::vector ingredients; + for (const auto& ing : ingredientsResp.page) { + if (ing.moderationStatus == api::models::moderation::PublicationRequestStatus::NO_REQUEST) { + ingredients.push_back(ing); + } + } + + InlineKeyboardBuilder keyboard{ingredients.size() + 1}; + for (auto chunk : ingredients | chunk(2)) { + keyboard.reserveInRow(2); + for (const auto& i : chunk) { + keyboard << makeCallbackButton("• " + i.name, utils::to_string(i.id)); + } + keyboard << NewRow{}; + } + + keyboard << makeCallbackButton(u8"↩️ Назад", "back"); + + auto text = std::format("{} Какой ингредиент вы хотите удалить?\n", utils::utf8str(u8"🚮")); + auto messageId = message::getMessageId(userId); + if (messageId) { + bot.editMessageText(text, chatId, *messageId, std::move(keyboard)); + } +} + +} // namespace cookcookhnya::render::personal_account::ingredients diff --git a/src/render/personal_account/ingredients_list/delete.hpp b/src/render/personal_account/ingredients_list/delete.hpp new file mode 100644 index 00000000..4a6fbd9b --- /dev/null +++ b/src/render/personal_account/ingredients_list/delete.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "backend/api/ingredients.hpp" +#include "render/common.hpp" + +namespace cookcookhnya::render::personal_account::ingredients { + +void renderCustomIngredientDeletion(UserId userId, ChatId chatId, BotRef bot, api::IngredientsApiRef api); + +} // namespace cookcookhnya::render::personal_account::ingredients diff --git a/src/render/personal_account/ingredients_list/publish.cpp b/src/render/personal_account/ingredients_list/publish.cpp index c2a58c6d..c0551e01 100644 --- a/src/render/personal_account/ingredients_list/publish.cpp +++ b/src/render/personal_account/ingredients_list/publish.cpp @@ -1,41 +1,51 @@ #include "publish.hpp" -#include "backend/api/common.hpp" +#include "backend/models/ingredient.hpp" +#include "backend/models/publication_request_status.hpp" + +#include "backend/api/ingredients.hpp" #include "message_tracker.hpp" #include "render/common.hpp" -#include "utils/to_string.hpp" #include "utils/utils.hpp" -#include #include -#include +#include #include +#include namespace cookcookhnya::render::personal_account::ingredients { -using namespace tg_types; +using namespace std::views; + +void renderCustomIngredientPublication(UserId userId, ChatId chatId, BotRef bot, api::IngredientsApiRef api) { + const std::size_t numOfIng = 500; + auto ingredientsResp = api.customIngredientsSearch(userId, "", 0, numOfIng); -void renderCustomIngredientPublication(UserId userId, ChatId chatId, BotRef bot, IngredientsApiRef api) { - auto ingredientsResp = api.search(userId, "", 0, 1000, 0, filterType::Custom); // NOLINT (*magic*) // TODO: make pagination for ingredients - auto ingredients = ingredientsResp.page; - const std::size_t buttonRows = ((ingredients.size() + 1) / 2) + 1; - InlineKeyboard keyboard(buttonRows); - for (std::size_t i = 0; i < ingredients.size(); i++) { - if (i % 2 == 0) - keyboard[(i / 2)].reserve(2); - keyboard[(i / 2)].push_back( - makeCallbackButton("• " + ingredients[i].name, utils::to_string(ingredients[i].id))); + std::vector ingredients; + for (const auto& ing : ingredientsResp.page) { + if (ing.moderationStatus == api::models::moderation::PublicationRequestStatus::NO_REQUEST) { + ingredients.push_back(ing); + } + } + + InlineKeyboardBuilder keyboard{ingredients.size() + 1}; + for (auto chunk : ingredients | chunk(2)) { + keyboard.reserveInRow(2); + for (const auto& i : chunk) { + keyboard << makeCallbackButton("• " + i.name, utils::to_string(i.id)); + } + keyboard << NewRow{}; } - keyboard[buttonRows - 1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); + keyboard << makeCallbackButton(u8"↩️ Назад", "back"); - auto text = std::format( - "{} Какой ингредиент вы хотите предложить для добавления в CookCookNya? (Все предложения проходят проверку)\n ", - utils::utf8str(u8"📥")); + auto text = std::format("{} Какой ингредиент вы хотите предложить для добавления в CookCookhNya? (Все предложения " + "проходят проверку)\n ", + utils::utf8str(u8"📥")); auto messageId = message::getMessageId(userId); if (messageId) { - bot.editMessageText(text, chatId, *messageId, "", "", nullptr, makeKeyboardMarkup(std::move(keyboard))); + bot.editMessageText(text, chatId, *messageId, std::move(keyboard)); } } diff --git a/src/render/personal_account/ingredients_list/publish.hpp b/src/render/personal_account/ingredients_list/publish.hpp index e8f28a86..66e8b9c9 100644 --- a/src/render/personal_account/ingredients_list/publish.hpp +++ b/src/render/personal_account/ingredients_list/publish.hpp @@ -1,9 +1,10 @@ #pragma once +#include "backend/api/ingredients.hpp" #include "render/common.hpp" namespace cookcookhnya::render::personal_account::ingredients { -void renderCustomIngredientPublication(UserId userId, ChatId chatId, BotRef bot, IngredientsApiRef api); +void renderCustomIngredientPublication(UserId userId, ChatId chatId, BotRef bot, api::IngredientsApiRef api); } // namespace cookcookhnya::render::personal_account::ingredients diff --git a/src/render/personal_account/ingredients_list/view.cpp b/src/render/personal_account/ingredients_list/view.cpp index 9980f692..2e957c63 100644 --- a/src/render/personal_account/ingredients_list/view.cpp +++ b/src/render/personal_account/ingredients_list/view.cpp @@ -1,52 +1,117 @@ #include "view.hpp" -#include "backend/api/common.hpp" +#include "backend/api/ingredients.hpp" #include "backend/models/ingredient.hpp" +#include "backend/models/publication_request_status.hpp" #include "message_tracker.hpp" #include "render/common.hpp" #include "utils/utils.hpp" -#include +#include + +#include #include #include #include +#include #include +#include namespace cookcookhnya::render::personal_account::ingredients { +using namespace api::models::ingredient; using namespace tg_types; +using namespace cookcookhnya::api::models::moderation; + +namespace { -void renderCustomIngredientsList(bool toBeEdited, UserId userId, ChatId chatId, BotRef bot, IngredientsApiRef api) { - auto ingredientsResp = api.search(userId, "", 0, 100, 0, filterType::Custom); // NOLINT(*magic*) - auto ingredients = ingredientsResp.page; - const std::size_t buttonRows = ingredients.empty() ? 2 : 3; - InlineKeyboard keyboard(buttonRows); +std::pair +constructMessage(api::models::ingredient::CustomIngredientList& ingredientsList) { // NOLINT(*complexity*) + std::size_t numOfRows = 0; + if (ingredientsList.found == 0) + numOfRows = 2; + else + numOfRows = 4; + std::string text; + InlineKeyboard keyboard(numOfRows); - if (ingredients.empty()) { + std::vector noReq; + std::vector pending; + std::vector accepted; + std::vector rejected; + for (const auto& ing : ingredientsList.page) { + switch (ing.moderationStatus) { + case PublicationRequestStatus::NO_REQUEST: + noReq.push_back(ing); + break; + case PublicationRequestStatus::PENDING: + pending.push_back(ing); + break; + case PublicationRequestStatus::ACCEPTED: + accepted.push_back(ing); + break; + case PublicationRequestStatus::REJECTED: + rejected.push_back(ing); + break; + } + } + + if (ingredientsList.found == 0) { + text = + utils::utf8str(u8"📋 Вы находитесь в Мои ингредиенты\\. Создавайте и делитесь новыми ингредиентами\\.\n\n"); keyboard[0].push_back(makeCallbackButton(u8"🆕 Создать", "create")); keyboard[1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); } else { + text = utils::utf8str(u8"📋 Вы находитесь в Мои ингредиенты\\. \nВами созданные ингредиенты:\n\n"); + if (!noReq.empty()) { + text += "⚪️ Без запроса на публикацию:\n"; + for (const auto& ing : noReq) { + text += std::format("• {}\n", ing.name); + } + } + if (!pending.empty()) { + text += "\n🟡 На рассмотрении:\n"; + for (const auto& ing : pending) { + text += std::format("• {}\n", ing.name); + } + } + if (!rejected.empty()) { + text += "\n🔴 Отклонены:\n"; + for (const auto& ing : rejected) { + text += std::format("• {}\n", ing.name); + } + } + if (!accepted.empty()) { + text += "\n🟢 Приняты:\n"; + for (const auto& ing : accepted) { + text += std::format("• {}\n", ing.name); + } + } keyboard[0].push_back(makeCallbackButton(u8"🆕 Создать", "create")); - keyboard[1].push_back(makeCallbackButton(u8"📢 Опубликовать", "publish")); - keyboard[2].push_back(makeCallbackButton(u8"↩️ Назад", "back")); + keyboard[1].push_back(makeCallbackButton(u8"🚮 Удалить", "delete")); + keyboard[2].push_back(makeCallbackButton(u8"📢 Опубликовать", "publish")); + keyboard[3].push_back(makeCallbackButton(u8"↩️ Назад", "back")); } + return std::make_pair(text, keyboard); +} - std::string formatedIngredients; - std::ranges::for_each(ingredients, [&formatedIngredients](const api::models::ingredient::Ingredient& item) { - formatedIngredients += "• " + item.name + "\n"; - }); +} // namespace - auto text = std::format("{} Вы находитесь в Мои ингредиенты. \nВами созданные ингредиенты:\n{}", - utils::utf8str(u8"📋"), - formatedIngredients); +void renderCustomIngredientsList( + bool toBeEdited, UserId userId, ChatId chatId, BotRef bot, api::IngredientsApiRef api) { + const std::size_t numOfIngredientsOnPage = 500; + auto ingredientsList = api.customIngredientsSearch(userId, "", 0, numOfIngredientsOnPage); + + auto res = constructMessage(ingredientsList); + auto text = res.first; + auto keyboard = res.second; if (toBeEdited) { - auto messageId = message::getMessageId(userId); - if (messageId) { - bot.editMessageText(text, chatId, *messageId, "", "", nullptr, makeKeyboardMarkup(std::move(keyboard))); - } + if (auto messageId = message::getMessageId(userId)) + bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard)), "MarkdownV2"); + } else { - auto message = bot.sendMessage(chatId, text, nullptr, nullptr, makeKeyboardMarkup(std::move(keyboard))); + auto message = bot.sendMessage(chatId, text, makeKeyboardMarkup(std::move(keyboard)), "MarkdownV2"); message::addMessageId(userId, message->messageId); } } diff --git a/src/render/personal_account/ingredients_list/view.hpp b/src/render/personal_account/ingredients_list/view.hpp index a277d5cd..c3501e80 100644 --- a/src/render/personal_account/ingredients_list/view.hpp +++ b/src/render/personal_account/ingredients_list/view.hpp @@ -1,9 +1,10 @@ #pragma once +#include "backend/api/ingredients.hpp" #include "render/common.hpp" namespace cookcookhnya::render::personal_account::ingredients { -void renderCustomIngredientsList(bool toBeEdited, UserId userId, ChatId chatId, BotRef bot, IngredientsApiRef api); +void renderCustomIngredientsList(bool toBeEdited, UserId userId, ChatId chatId, BotRef bot, api::IngredientsApiRef api); } // namespace cookcookhnya::render::personal_account::ingredients diff --git a/src/render/personal_account/publication_history.cpp b/src/render/personal_account/publication_history.cpp new file mode 100644 index 00000000..16c578f9 --- /dev/null +++ b/src/render/personal_account/publication_history.cpp @@ -0,0 +1,44 @@ +#include "publication_history.hpp" + +#include "backend/models/moderation.hpp" +#include "message_tracker.hpp" +#include "render/common.hpp" + +#include +#include +#include + +namespace cookcookhnya::render::personal_account { + +using namespace std::views; + +void renderRequestHistory(UserId userId, + size_t pageNo, + size_t numOfInstances, + ChatId chatId, + BotRef bot, + api::ModerationApiRef moderationApi) { + InlineKeyboardBuilder keyboard{1}; + + const std::size_t maxShownItems = 20; + auto history = moderationApi.getAllPublicationRequests(userId, maxShownItems, pageNo * numOfInstances); + + std::string toPrint = utils::utf8str(u8"ℹ️История запросов на публикацию ваших рецептов и ингредиентов\n\n"); + for (auto& req : history | reverse) { + std::string emoji = utils::utf8str(req.requestType == "recipe" ? u8"📖" : u8"🥬"); + toPrint += std::format("{} *{}*\nСтатус: {}\n", emoji, req.name, utils::to_string(req.status.status)); + if (req.status.reason.has_value()) + toPrint += std::format("По причине: {}\n", req.status.reason.value()); + toPrint += std::format("Запрос создан: {}\n", utils::to_string(req.created)); + if (req.updated.has_value()) { + toPrint += std::format("Последенее обновление: {}\n", utils::to_string(req.updated.value())); + } + toPrint += "\n\n"; + } + + keyboard << makeCallbackButton(u8"↩️ Назад", "back"); + if (auto messageId = message::getMessageId(userId)) + bot.editMessageText(toPrint, chatId, *messageId, std::move(keyboard), "Markdown"); +} + +} // namespace cookcookhnya::render::personal_account diff --git a/src/render/personal_account/publication_history.hpp b/src/render/personal_account/publication_history.hpp new file mode 100644 index 00000000..8e5ab749 --- /dev/null +++ b/src/render/personal_account/publication_history.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "backend/api/moderation.hpp" +#include "render/common.hpp" + +namespace cookcookhnya::render::personal_account { + +void renderRequestHistory(UserId userId, + std::size_t pageNo, + std::size_t numOfInstances, + ChatId chatId, + BotRef bot, + api::ModerationApiRef moderationApi); + +} // namespace cookcookhnya::render::personal_account diff --git a/src/render/personal_account/recipe/moderation_history.cpp b/src/render/personal_account/recipe/moderation_history.cpp new file mode 100644 index 00000000..4f05e7ca --- /dev/null +++ b/src/render/personal_account/recipe/moderation_history.cpp @@ -0,0 +1,93 @@ +#include "moderation_history.hpp" + +#include "backend/models/recipe.hpp" +#include "message_tracker.hpp" +#include "render/common.hpp" +#include "utils/to_string.hpp" +#include "utils/utils.hpp" + +#include +#include +#include +#include +#include + +namespace cookcookhnya::render::personal_account::recipe { + +using namespace std::views; + +void renderPublicationHistory(UserId userId, + ChatId chatId, + std::string& recipeName, + std::string& errorReport, + std::vector history, + BotRef bot) { + + bool isConfirm = true; + InlineKeyboardBuilder keyboard{3}; // confirm, back and rules + + std::string toPrint; + toPrint = (utils::utf8str(u8"История запросов на публикацию *") + recipeName + "*\n\n"); + if (!history.empty()) { + // Show confirm only if in those states + isConfirm = + history[history.size() - 1].status.status == api::models::moderation::PublicationRequestStatus::REJECTED; + const size_t lastUpdatedInstance = history.size() - 1; + // Construct current status string + toPrint += + utils::utf8str(u8"ℹ️ Текущий статус: ") + utils::to_string(history[lastUpdatedInstance].status.status); + if (auto reason = history[lastUpdatedInstance].status.reason) { + toPrint += std::format(" по причине {}", reason.value()); + } + toPrint += " " + utils::to_string(history[lastUpdatedInstance].created) + "\n\n"; + + // Remove the lastest history instance as it's showed differently + history.erase(history.end()); + + for (auto& req : history | reverse) { + toPrint += std::format("Статус: {} ", utils::to_string(req.status.status)); + if (req.status.reason.has_value()) + toPrint += std::format("по причине: {} ", req.status.reason.value()); + toPrint += std::format("запрос создан: {} ", utils::to_string(req.created)); + if (req.updated.has_value()) { + toPrint += std::format("последенее обновление: {}", utils::to_string(req.updated.value())); + } + toPrint += "\n\n"; + } + } + + toPrint += errorReport; + + if (isConfirm) { + keyboard << makeCallbackButton(u8"▶️Подтвердить", "confirm") << NewRow{}; + } + keyboard << makeCallbackButton(u8"❗️Правила", "rules") << NewRow{}; + keyboard << makeCallbackButton(u8"↩️ Назад", "back"); + + if (auto messageId = message::getMessageId(userId)) { + bot.editMessageText(toPrint, chatId, *messageId, std::move(keyboard), "Markdown"); + } +} + +void renderPublicationRules(UserId userId, ChatId chatId, BotRef bot) { + // Rules + const std::string toPrint = + utils::utf8str(u8"❗️ *Правила публикации рецептов:*") + + "\n1. *Статус рецепта*\nНельзя отправить запрос на публикацию, если рецепт:\n - уже принят " + "(опубликован);\n - находится на рассмотрении модерации. \n2. *Ингредиенты* \n Запрещено публиковать " + "рецепт, если в нём есть:\n - ингредиенты, не прошедшие модерацию (_ожидают проверки_);\n - " + "ингредиенты, отклонённые администрацией.\n3. *Название рецепта*\n - Не должно содержать " + "нецензурную лексику, оскорбления или спам;\n - Должно точно отражать содержание рецепта (например: " + "\"Спагетти карбонара\", а не \"Вкуснятина\"). \n4. *Дополнительно*\n - Запрещено размещать контактные " + "данные или рекламу;\n - Вы не сможете редактировать рецепт с статусом " + + utils::utf8str(u8"\"🟢 Принят\" и \"🟡 На рассмотрении\""); + utils::utf8str(u8"\n⚠️ Нарушение правил приведёт к отклонению рецепта "); + + InlineKeyboardBuilder keyboard{1}; // back + keyboard << makeCallbackButton(u8"↩️ Назад", "backFromRules"); + + if (auto messageId = message::getMessageId(userId)) { + bot.editMessageText(toPrint, chatId, *messageId, std::move(keyboard), "Markdown"); + } +} +} // namespace cookcookhnya::render::personal_account::recipe diff --git a/src/render/personal_account/recipe/moderation_history.hpp b/src/render/personal_account/recipe/moderation_history.hpp new file mode 100644 index 00000000..9913d1a2 --- /dev/null +++ b/src/render/personal_account/recipe/moderation_history.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "backend/models/recipe.hpp" +#include "render/common.hpp" + +namespace cookcookhnya::render::personal_account::recipe { + +void renderPublicationHistory(UserId userId, + ChatId chatId, + std::string& recipeName, + std::string& errorReport, + std::vector history, + BotRef bot); +void renderPublicationRules(UserId userId, ChatId chatId, BotRef bot); +} // namespace cookcookhnya::render::personal_account::recipe diff --git a/src/render/personal_account/recipe/search_ingredients.cpp b/src/render/personal_account/recipe/search_ingredients.cpp new file mode 100644 index 00000000..23cb4dee --- /dev/null +++ b/src/render/personal_account/recipe/search_ingredients.cpp @@ -0,0 +1,87 @@ +#include "search_ingredients.hpp" + +#include "backend/models/ingredient.hpp" +#include "message_tracker.hpp" +#include "patched_bot.hpp" +#include "render/common.hpp" +#include "render/pagination.hpp" +#include "states.hpp" +#include "utils/utils.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace TgBot { +class InlineKeyboardMarkup; +} // namespace TgBot + +namespace cookcookhnya::render::personal_account::recipe { + +using namespace api::models::ingredient; +using namespace tg_types; +using namespace std::views; +using std::ranges::to; + +namespace { + +std::shared_ptr +constructKeyboard(std::size_t pageNo, std::size_t pageSize, const states::CustomRecipeIngredientsSearch& state) { + InlineKeyboardBuilder keyboard; + + auto searchButton = std::make_shared(); + searchButton->text = utils::utf8str(u8"✏️ Редактировать"); + searchButton->switchInlineQueryCurrentChat = ""; + keyboard << std::move(searchButton) << NewRow{}; + + auto makeRecipeButton = [](const IngredientSearchForRecipeItem& ing) { + return makeCallbackButton((ing.isInRecipe ? "[ + ] " : "[ㅤ] ") + ing.name, utils::to_string(ing.id)); + }; + keyboard << constructPagination(pageNo, pageSize, state.totalFound, state.searchItems, makeRecipeButton) + << makeCallbackButton(u8"↩️ Назад", "back"); + + return keyboard; +} + +} // namespace + +void renderRecipeIngredientsSearch(const states::CustomRecipeIngredientsSearch& state, + std::size_t numOfIngredientsOnPage, + UserId userId, + ChatId chatId, + BotRef bot) { + std::string list = state.recipeIngredients.getValues() | + transform([](auto& i) { return std::format("• {}\n", i.name); }) | join | to(); + auto text = utils::utf8str(u8"📝Нажмите на кнопку ✏️ Редактировать и начните вводить названия продуктов:\n\n") + + std::move(list); + + if (auto messageId = message::getMessageId(userId)) + bot.editMessageText(text, chatId, *messageId, constructKeyboard(state.pageNo, numOfIngredientsOnPage, state)); +} + +void renderSuggestIngredientCustomisation(const states::CustomRecipeIngredientsSearch& state, + UserId userId, + ChatId chatId, + BotRef bot) { + InlineKeyboard keyboard(3); + const std::string text = utils::utf8str(u8"📝 Продолжите редактирование запроса или объявите личный ингредиент"); + + auto searchButton = std::make_shared(); + searchButton->text = utils::utf8str(u8"✏️ Редактировать"); + searchButton->switchInlineQueryCurrentChat = ""; + keyboard[0].push_back(std::move(searchButton)); + // Mark as ingredient + keyboard[1].push_back( + makeCallbackButton(std::format("Создать личный ингредиент: {}", state.query), "ingredient_" + state.query)); + keyboard[2].push_back(makeCallbackButton(u8"↩️ Назад", "back")); + + if (auto messageId = message::getMessageId(userId)) + bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); +} + +} // namespace cookcookhnya::render::personal_account::recipe diff --git a/src/render/personal_account/recipes_list/recipe/search_ingredients.hpp b/src/render/personal_account/recipe/search_ingredients.hpp similarity index 50% rename from src/render/personal_account/recipes_list/recipe/search_ingredients.hpp rename to src/render/personal_account/recipe/search_ingredients.hpp index b07672cb..050d877d 100644 --- a/src/render/personal_account/recipes_list/recipe/search_ingredients.hpp +++ b/src/render/personal_account/recipe/search_ingredients.hpp @@ -2,8 +2,10 @@ #include "render/common.hpp" #include "states.hpp" + #include -namespace cookcookhnya::render::recipe::ingredients { + +namespace cookcookhnya::render::personal_account::recipe { void renderRecipeIngredientsSearch(const states::CustomRecipeIngredientsSearch& state, size_t numOfIngredientsOnPage, @@ -11,4 +13,9 @@ void renderRecipeIngredientsSearch(const states::CustomRecipeIngredientsSearch& ChatId chatId, BotRef bot); -} // namespace cookcookhnya::render::recipe::ingredients +void renderSuggestIngredientCustomisation(const states::CustomRecipeIngredientsSearch& state, + UserId userId, + ChatId chatId, + BotRef bot); + +} // namespace cookcookhnya::render::personal_account::recipe diff --git a/src/render/personal_account/recipe/view.cpp b/src/render/personal_account/recipe/view.cpp new file mode 100644 index 00000000..4dbd5654 --- /dev/null +++ b/src/render/personal_account/recipe/view.cpp @@ -0,0 +1,68 @@ +#include "view.hpp" + +#include "backend/api/recipes.hpp" +#include "backend/id_types.hpp" +#include "backend/models/ingredient.hpp" +#include "backend/models/publication_request_status.hpp" +#include "message_tracker.hpp" +#include "render/common.hpp" +#include "utils/utils.hpp" + +#include +#include +#include +#include +#include + +namespace cookcookhnya::render::personal_account::recipe { + +using namespace api::models::ingredient; +using namespace api::models::moderation; + +std::pair, std::string> renderCustomRecipe( + bool toBeEdited, UserId userId, ChatId chatId, api::RecipeId recipeId, BotRef bot, api::RecipesApiRef recipesApi) { + auto recipeDetails = recipesApi.getSuggested(userId, recipeId); + + std::vector ingredients; + + const std::size_t rows = 4; // 1 for publish, 1 for delete, 1 for back, 1 for change + InlineKeyboardBuilder keyboard{rows}; + std::string toPrint; + toPrint += std::format("{} Ингредиенты для *{}* \n\n", utils::utf8str(u8"📖"), recipeDetails.name); + + for (auto& it : recipeDetails.ingredients) { + toPrint += std::format("• {}\n", it.name); + ingredients.push_back({.id = it.id, .name = it.name}); + } + + toPrint += "\n🌐 [Статус проверки] " + utils::to_string(recipeDetails.moderationStatus.status); + + keyboard << makeCallbackButton(u8"🚮 Удалить", "delete") << NewRow{}; + // Allow to edit recipe only if no request was made or it was rejected + if (recipeDetails.moderationStatus.status == PublicationRequestStatus::NO_REQUEST || + recipeDetails.moderationStatus.status == PublicationRequestStatus::REJECTED) { + keyboard << makeCallbackButton(u8"✏️ Редактировать", "change") << NewRow{}; + } + + // Show publish button only iff the status is not emty AND not rejected + if (recipeDetails.moderationStatus.status == PublicationRequestStatus::NO_REQUEST || + recipeDetails.moderationStatus.status == PublicationRequestStatus::REJECTED) { + keyboard << makeCallbackButton(u8"📢 Опубликовать", "publish") << NewRow{}; + } else { + keyboard << makeCallbackButton(u8"📢 История публикаций", "publish") << NewRow{}; + } + + keyboard << makeCallbackButton(u8"↩️ Назад", "back"); + + if (toBeEdited) { + auto messageId = message::getMessageId(userId); + if (messageId) + bot.editMessageText(toPrint, chatId, *messageId, std::move(keyboard), "Markdown"); + } else { + auto message = bot.sendMessage(chatId, toPrint, std::move(keyboard), "Markdown"); + message::addMessageId(userId, message->messageId); + } + return {std::move(ingredients), std::move(recipeDetails.name)}; +} + +} // namespace cookcookhnya::render::personal_account::recipe diff --git a/src/render/personal_account/recipe/view.hpp b/src/render/personal_account/recipe/view.hpp new file mode 100644 index 00000000..73a66d7f --- /dev/null +++ b/src/render/personal_account/recipe/view.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "backend/api/recipes.hpp" +#include "backend/id_types.hpp" +#include "backend/models/ingredient.hpp" +#include "render/common.hpp" + +#include +#include +#include + +namespace cookcookhnya::render::personal_account::recipe { + +std::pair, std::string> renderCustomRecipe( + bool toBeEdited, UserId userId, ChatId chatId, api::RecipeId recipeId, BotRef bot, api::RecipesApiRef recipesApi); + +} // namespace cookcookhnya::render::personal_account::recipe diff --git a/src/render/personal_account/recipes_list/create.cpp b/src/render/personal_account/recipes_list/create.cpp index 68b08dc0..cb5d2598 100644 --- a/src/render/personal_account/recipes_list/create.cpp +++ b/src/render/personal_account/recipes_list/create.cpp @@ -6,7 +6,7 @@ #include -namespace cookcookhnya::render::personal_account::recipes { +namespace cookcookhnya::render::personal_account::recipes_list { void renderRecipeCreation(ChatId chatId, UserId userId, BotRef bot) { // BackendProvider bkn InlineKeyboard keyboard(1); @@ -18,4 +18,4 @@ void renderRecipeCreation(ChatId chatId, UserId userId, BotRef bot) { // Backend } }; -} // namespace cookcookhnya::render::personal_account::recipes +} // namespace cookcookhnya::render::personal_account::recipes_list diff --git a/src/render/personal_account/recipes_list/create.hpp b/src/render/personal_account/recipes_list/create.hpp index d44f541a..e48ff0a4 100644 --- a/src/render/personal_account/recipes_list/create.hpp +++ b/src/render/personal_account/recipes_list/create.hpp @@ -2,8 +2,8 @@ #include "render/common.hpp" -namespace cookcookhnya::render::personal_account::recipes { +namespace cookcookhnya::render::personal_account::recipes_list { void renderRecipeCreation(ChatId chatId, UserId userId, BotRef bot); -} // namespace cookcookhnya::render::personal_account::recipes +} // namespace cookcookhnya::render::personal_account::recipes_list diff --git a/src/render/personal_account/recipes_list/recipe/search_ingredients.cpp b/src/render/personal_account/recipes_list/recipe/search_ingredients.cpp deleted file mode 100644 index d3fe76f2..00000000 --- a/src/render/personal_account/recipes_list/recipe/search_ingredients.cpp +++ /dev/null @@ -1,148 +0,0 @@ -#include "search_ingredients.hpp" - -#include "backend/models/ingredient.hpp" -#include "message_tracker.hpp" -#include "patched_bot.hpp" -#include "render/common.hpp" -#include "states.hpp" -#include "utils/to_string.hpp" -#include "utils/utils.hpp" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cookcookhnya::render::recipe::ingredients { - -using namespace api::models::ingredient; -using namespace tg_types; - -namespace { - -InlineKeyboard constructNavigationsMarkup(size_t offset, - size_t fullKeyBoardSize, - size_t numOfRecipesOnPage, - const states::CustomRecipeIngredientsSearch& state) { - using namespace std::views; - const size_t amountOfRecipes = state.totalFound; - int maxPageNum = - static_cast(std::ceil(static_cast(amountOfRecipes) / static_cast(numOfRecipesOnPage))); - - const size_t recipesToShow = std::min(numOfRecipesOnPage, state.searchItems.size()); - // + 1 because of the 0-indexing, as comparisson is between num of recipes gotten and that - // will be actually shown - const bool ifMaxPage = static_cast(amountOfRecipes) - - static_cast(numOfRecipesOnPage) * (static_cast(state.pageNo) + 1) <= - 0; - - if (offset + recipesToShow >= fullKeyBoardSize) { - InlineKeyboard error(0); - return error; - } - const size_t arrowsRow = offset + recipesToShow; - // Don't reserve for arrows if it's first page is max(im) - InlineKeyboard keyboard(state.pageNo == 0 && ifMaxPage ? fullKeyBoardSize - 1 : fullKeyBoardSize); - - auto searchButton = std::make_shared(); - searchButton->text = utils::utf8str(u8"✏️ Редактировать"); - searchButton->switchInlineQueryCurrentChat = ""; - keyboard[0].push_back(std::move(searchButton)); - - for (auto [row, ing] : zip(drop(keyboard, 1), state.searchItems)) - row.push_back(makeCallbackButton((ing.isInRecipe ? "[ + ] " : "[ㅤ] ") + ing.name, utils::to_string(ing.id))); - - if (state.pageNo == 0 && ifMaxPage) { - // instead of arrows row - keyboard[arrowsRow].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - return keyboard; - } - keyboard[arrowsRow].reserve(3); - - // Helps to reduce code. Power of C++ YEAH BABE! - uint8_t b = 0; - - // Simply enamurate every case - if (state.pageNo == 0) { - if (!ifMaxPage) { - b |= uint8_t{0b01}; - } - } else if (ifMaxPage) { - b |= uint8_t{0b10}; - } else { - b |= uint8_t{0b11}; - } - - // Check from left to right due to buttons being displayed like that - for (int i = 1; i >= 0; i--) { - // Compare two bits under b mask. If 1 was on b mask then we need to place arrow somewhere - if ((b & static_cast((uint8_t{0b1} << static_cast(i)))) == - (uint8_t{0b1} << static_cast(i))) { - // if we need to place arrow then check the i, which represents bit which we are checking right now - if (i == 1) { - keyboard[arrowsRow].push_back(makeCallbackButton(u8"◀️", "prev")); // left - } else { - keyboard[arrowsRow].push_back(makeCallbackButton(u8"▶️", "next")); // right - } - } else { - keyboard[arrowsRow].push_back(makeCallbackButton(u8"ㅤ", "dont_handle")); - } - } - // Put state.pageNo as button - keyboard[arrowsRow].insert( - keyboard[arrowsRow].begin() + 1, - makeCallbackButton(std::format("{} из {}", state.pageNo + 1, maxPageNum), "dont_handle")); - keyboard[arrowsRow + 1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - return keyboard; -} - -InlineKeyboard constructMarkup(size_t numOfRecipesOnPage, const states::CustomRecipeIngredientsSearch& state) { - // 1 for back button return, 1 for arrows (ALWAYS ACCOUNT ARROWS), 1 - // for editing - other buttons are ingredients - const size_t numOfRows = 3; - const size_t offset = 1; // Number of rows before list - - const size_t recipesToShow = std::min(numOfRecipesOnPage, state.searchItems.size()); - - InlineKeyboard keyboard = constructNavigationsMarkup(offset, numOfRows + recipesToShow, numOfRecipesOnPage, state); - if (keyboard.empty()) { // If error happened - return keyboard; - } - - return keyboard; -} - -} // namespace - -void renderRecipeIngredientsSearch(const states::CustomRecipeIngredientsSearch& state, - size_t numOfIngredientsOnPage, - UserId userId, - ChatId chatId, - BotRef bot) { - using namespace std::views; - using std::ranges::to; - - std::string list = state.recipeIngredients.getAll() | - transform([](auto& i) { return std::format("• {}\n", i.name); }) | join | to(); - - auto text = utils::utf8str(u8"🍗 Ваши ингредиенты:\n\n") + std::move(list); - if (auto messageId = message::getMessageId(userId)) { - bot.editMessageText(text, - chatId, - *messageId, - "", - "", - nullptr, - makeKeyboardMarkup(constructMarkup(numOfIngredientsOnPage, state))); - } -} - -} // namespace cookcookhnya::render::recipe::ingredients diff --git a/src/render/personal_account/recipes_list/recipe/view.cpp b/src/render/personal_account/recipes_list/recipe/view.cpp deleted file mode 100644 index 76aa2d90..00000000 --- a/src/render/personal_account/recipes_list/recipe/view.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include "view.hpp" - -#include "backend/id_types.hpp" -#include "backend/models/ingredient.hpp" -#include "backend/models/recipe.hpp" -#include "message_tracker.hpp" -#include "render/common.hpp" -#include "utils/utils.hpp" - -#include -#include -#include -#include -#include - -namespace cookcookhnya::render::personal_account::recipes { - -std::vector renderCustomRecipe( - bool toBeEdited, UserId userId, ChatId chatId, api::RecipeId recipeId, BotRef bot, RecipesApiRef recipesApi) { - auto recipeDetails = recipesApi.get(userId, recipeId); - std::vector ingredients; - - const std::size_t rows = 4; // 1 for publish, 1 for delete, 1 for back, 1 for change - InlineKeyboard keyboard(rows); - std::string toPrint; - toPrint += (utils::utf8str(u8"Рецепт: ") + recipeDetails.name + "\n"); - for (auto& it : recipeDetails.ingredients) { - toPrint += std::format("• {}\n", it.name); - ingredients.push_back({ - .id = it.id, - .name = it.name, - }); - } - - toPrint += recipeDetails.link; - keyboard[0].push_back(makeCallbackButton(u8"Удалить", "delete")); - keyboard[1].push_back(makeCallbackButton(u8"Редактировать", "change")); - keyboard[2].push_back(makeCallbackButton(u8"Опубликовать", "publish")); - keyboard[3].push_back(makeCallbackButton(u8"Назад", "back")); - - if (toBeEdited) { - auto messageId = message::getMessageId(userId); - if (messageId) - bot.editMessageText(toPrint, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); - } else { - auto message = bot.sendMessage(chatId, toPrint, makeKeyboardMarkup(std::move(keyboard))); - message::addMessageId(userId, message->messageId); - } - return ingredients; -} -} // namespace cookcookhnya::render::personal_account::recipes diff --git a/src/render/personal_account/recipes_list/recipe/view.hpp b/src/render/personal_account/recipes_list/recipe/view.hpp deleted file mode 100644 index 52c4386a..00000000 --- a/src/render/personal_account/recipes_list/recipe/view.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include "backend/id_types.hpp" -#include "backend/models/ingredient.hpp" -#include "render/common.hpp" -#include - -namespace cookcookhnya::render::personal_account::recipes { - -std::vector renderCustomRecipe( - bool toBeEdited, UserId userId, ChatId chatId, api::RecipeId recipeId, BotRef bot, RecipesApiRef recipesApi); - -} // namespace cookcookhnya::render::personal_account::recipes diff --git a/src/render/personal_account/recipes_list/view.cpp b/src/render/personal_account/recipes_list/view.cpp index 5c7bf452..6775c758 100644 --- a/src/render/personal_account/recipes_list/view.cpp +++ b/src/render/personal_account/recipes_list/view.cpp @@ -1,138 +1,56 @@ #include "view.hpp" -#include "backend/api/common.hpp" +#include "backend/api/publicity_filter.hpp" #include "backend/api/recipes.hpp" #include "backend/models/recipe.hpp" #include "message_tracker.hpp" #include "render/common.hpp" +#include "render/pagination.hpp" #include "render/personal_account/recipes_list/view.hpp" -#include "utils/to_string.hpp" #include "utils/utils.hpp" -#include -#include #include -#include #include +#include #include -#include -namespace cookcookhnya::render::personal_account::recipes { +namespace TgBot { +class InlineKeyboardMarkup; +} // namespace TgBot -// offset is variable which defines amout of rows before beggining of paging -// fullKeyBoardSize is self explanatory -InlineKeyboard constructNavigationsMarkup(size_t offset, - size_t fullKeyBoardSize, - size_t pageNo, - size_t numOfRecipesOnPage, - api::models::recipe::RecipeSearchResponse& recipesList) { - const size_t amountOfRecipes = recipesList.found; - int maxPageNum = - static_cast(std::ceil(static_cast(amountOfRecipes) / static_cast(numOfRecipesOnPage))); +namespace cookcookhnya::render::personal_account::recipes_list { - const size_t recipesToShow = std::min(numOfRecipesOnPage, recipesList.page.size()); +using namespace api::models::recipe; - const bool ifMaxPage = - static_cast(amountOfRecipes) - static_cast(numOfRecipesOnPage) * (static_cast(pageNo) + 1) <= - 0; // + 1 because of the 0-indexing, as comparisson is between num of recipes gotten and that - // will be actually shown +namespace { - if (offset + recipesToShow >= fullKeyBoardSize) { - InlineKeyboard error(0); - return error; - } - const size_t arrowsRow = offset + recipesToShow; - // Don't reserve for arrows if it's first page is max(im) - InlineKeyboard keyboard(pageNo == 0 && ifMaxPage ? fullKeyBoardSize - 1 : fullKeyBoardSize); - int counter = 0; - for (std::size_t i = 0; i < recipesToShow; i++) { - // Print on button in form "1. {Recipe}" - keyboard[i + offset].push_back(makeCallbackButton( - std::format("{}. {}", 1 + counter + ((pageNo)*numOfRecipesOnPage), recipesList.page[counter].name), - std::format("recipe: {}", recipesList.page[counter].id))); // RECIPE ID - counter++; - } - if (pageNo == 0 && ifMaxPage) { - // instead of arrows row - keyboard[arrowsRow].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - return keyboard; - } - keyboard[arrowsRow].reserve(3); - - // Helps to reduce code. Power of C++ YEAH BABE! - uint8_t b = 0; - - // Simply enamurate every case - if (pageNo == 0) { - if (!ifMaxPage) { - b |= uint8_t{0b01}; - } - } else if (ifMaxPage) { - b |= uint8_t{0b10}; - } else { - b |= uint8_t{0b11}; - } - - // Check from left to right due to buttons being displayed like that - for (int i = 1; i >= 0; i--) { - // Compare two bits under b mask. If 1 was on b mask then we need to place arrow somewhere - if ((b & static_cast((uint8_t{0b1} << static_cast(i)))) == - (uint8_t{0b1} << static_cast(i))) { - // if we need to place arrow then check the i, which represents bit which we are checking right now - if (i == 1) { - keyboard[arrowsRow].push_back(makeCallbackButton(u8"◀️", utils::to_string(pageNo - 1))); // left - } else { - keyboard[arrowsRow].push_back(makeCallbackButton(u8"▶️", utils::to_string(pageNo + 1))); // right - } - } else { - keyboard[arrowsRow].push_back(makeCallbackButton(u8"ㅤ", "dont_handle")); - } - } - // Put pageNo as button - keyboard[arrowsRow].insert(keyboard[arrowsRow].begin() + 1, - makeCallbackButton(std::format("{} из {}", pageNo + 1, maxPageNum), "dont_handle")); - keyboard[arrowsRow + 1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); +std::shared_ptr +constructKeyboard(std::size_t pageNo, std::size_t pageSize, RecipesList& recipesList) { + InlineKeyboardBuilder keyboard; + keyboard << makeCallbackButton(u8"🆕 Создать", "custom_recipe_create") << NewRow{}; + auto makeRecipeButton = [i = (pageNo * pageSize) + 1](RecipeSummary& r) mutable { + return makeCallbackButton(std::format("{}. {}", i++, r.name), "recipe_" + utils::to_string(r.id)); + }; + keyboard << constructPagination(pageNo, pageSize, recipesList.found, recipesList.page, makeRecipeButton) + << makeCallbackButton(u8"↩️ Назад", "back"); return keyboard; } -InlineKeyboard constructOnlyCreate() { - InlineKeyboard keyboard(2); - keyboard[1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - return keyboard; -} +} // namespace -InlineKeyboard -constructMarkup(size_t pageNo, size_t numOfRecipesOnPage, api::models::recipe::RecipeSearchResponse& recipesList) { - // 1 for back button return, 1 for arrows (ALWAYS ACCOUNT ARROWS), 1 - // for adding new recipe - other buttons are recipes - const size_t numOfRows = 3; - const size_t offset = 1; // Number of rows before list +void renderCustomRecipesList( + std::size_t pageNo, UserId userId, ChatId chatId, BotRef bot, api::RecipesApiRef recipesApi) { + const std::size_t numOfRecipesOnPage = 5; + auto recipesList = + recipesApi.getList(userId, PublicityFilterType::Custom, numOfRecipesOnPage, pageNo * numOfRecipesOnPage); - const size_t recipesToShow = std::min(numOfRecipesOnPage, recipesList.page.size()); + const std::string pageInfo = utils::utf8str( + recipesList.found > 0 ? u8"🔪 Рецепты созданные вами:" + : u8"🔪 Вы находитесь в Мои рецепты. Создавайте и делитесь новыми рецептами.\n\n"); - InlineKeyboard keyboard = - recipesList.found == 0 - ? constructOnlyCreate() - : constructNavigationsMarkup(offset, numOfRows + recipesToShow, pageNo, numOfRecipesOnPage, recipesList); - if (keyboard.empty()) { // If error happened - return keyboard; + if (auto messageId = message::getMessageId(userId)) { + bot.editMessageText(pageInfo, chatId, *messageId, constructKeyboard(pageNo, numOfRecipesOnPage, recipesList)); } - keyboard[0].push_back(makeCallbackButton(u8"Создать", "custom_recipe_create")); - - return keyboard; } -void renderCustomRecipesList(size_t pageNo, UserId userId, ChatId chatId, BotRef bot, RecipesApiRef recipesApi) { - const std::string pageInfo = utils::utf8str(u8"🔪 Рецепты созданные вами"); - - auto messageId = message::getMessageId(userId); - - const std::size_t numOfRecipesOnPage = 5; - auto recipesList = - recipesApi.getRecipesList(userId, "", 0, numOfRecipesOnPage, pageNo * numOfRecipesOnPage, filterType::Custom); - if (messageId) { - bot.editMessageText( - pageInfo, chatId, *messageId, makeKeyboardMarkup(constructMarkup(pageNo, numOfRecipesOnPage, recipesList))); - } -} -} // namespace cookcookhnya::render::personal_account::recipes +} // namespace cookcookhnya::render::personal_account::recipes_list diff --git a/src/render/personal_account/recipes_list/view.hpp b/src/render/personal_account/recipes_list/view.hpp index a78884d3..72ad983e 100644 --- a/src/render/personal_account/recipes_list/view.hpp +++ b/src/render/personal_account/recipes_list/view.hpp @@ -1,23 +1,13 @@ #pragma once -#include "backend/models/recipe.hpp" +#include "backend/api/recipes.hpp" #include "render/common.hpp" #include -namespace cookcookhnya::render::personal_account::recipes { +namespace cookcookhnya::render::personal_account::recipes_list { -void renderCustomRecipesList(size_t pageNo, UserId userId, ChatId chatId, BotRef bot, RecipesApiRef recipesApi); +void renderCustomRecipesList( + std::size_t pageNo, UserId userId, ChatId chatId, BotRef bot, api::RecipesApiRef recipesApi); -InlineKeyboard -constructMarkup(size_t pageNo, size_t numOfRecipesOnPage, api::models::recipe::RecipeSearchResponse& recipesList); - -InlineKeyboard constructOnlyCreate(); - -InlineKeyboard constructNavigationsMarkup(size_t offset, - size_t fullKeyBoardSize, - size_t pageNo, - size_t numOfRecipesOnPage, - api::models::recipe::RecipeSearchResponse& recipesList); - -} // namespace cookcookhnya::render::personal_account::recipes +} // namespace cookcookhnya::render::personal_account::recipes_list diff --git a/src/render/personal_account/view.cpp b/src/render/personal_account/view.cpp index 2870dcfd..efbc9671 100644 --- a/src/render/personal_account/view.cpp +++ b/src/render/personal_account/view.cpp @@ -11,12 +11,13 @@ namespace cookcookhnya::render::personal_account { using namespace tg_types; void renderPersonalAccountMenu(UserId userId, ChatId chatId, BotRef bot) { - const std::size_t buttonRows = 3; + const std::size_t buttonRows = 4; InlineKeyboard keyboard(buttonRows); keyboard[0].push_back(makeCallbackButton(u8"📋 Мои ингредиенты", "ingredients")); keyboard[1].push_back(makeCallbackButton(u8"📒 Мои рецепты", "recipes")); // 🗃️ - keyboard[2].push_back(makeCallbackButton(u8"↩️ Назад", "back")); + keyboard[2].push_back(makeCallbackButton(u8"🗃️ История запросов на публикацию", "history")); + keyboard[3].push_back(makeCallbackButton(u8"↩️ Назад", "back")); auto text = utils::utf8str(u8"👤 Вы находитесь в Личном Кабинете. Здесь вы можете добавить личные ингредиенты и " u8"рецепты, а также делиться ими с другими пользователями."); @@ -24,6 +25,9 @@ void renderPersonalAccountMenu(UserId userId, ChatId chatId, BotRef bot) { auto messageId = message::getMessageId(userId); if (messageId) { bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); + } else { + auto message = bot.sendMessage(chatId, text, makeKeyboardMarkup(std::move(keyboard))); + message::addMessageId(userId, message->messageId); } } diff --git a/src/render/recipe/add_storage.cpp b/src/render/recipe/add_storage.cpp deleted file mode 100644 index 4929ff40..00000000 --- a/src/render/recipe/add_storage.cpp +++ /dev/null @@ -1,123 +0,0 @@ -#include "add_storage.hpp" - -#include "backend/id_types.hpp" -#include "backend/models/recipe.hpp" -#include "message_tracker.hpp" -#include "render/common.hpp" -#include "utils/to_string.hpp" -#include "utils/utils.hpp" -#include "view.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cookcookhnya::render::recipe { - -std::vector storagesToShow(const std::vector& ingredients, - const std::vector& storageIdsToAccount) { - std::vector storageIdsToShow; - - std::unordered_set toAdd; // If there will be only one element of storageId then remove - bool isFound = false; - for (const auto& ingredient : ingredients) { - isFound = false; // Iterate through each ingredient - for (const api::StorageId inStorage : - ingredient.inStorages) { // Iterate through each storage where ingredient is present - for (const api::StorageId stId : storageIdsToAccount) { - if (stId == inStorage) { - isFound = true; - break; - } - } - if (isFound) { - break; - } - } - if (!isFound) { - // Proof that ingredient doesn't have "toxic" storages. Toxic storage is a storage which has some - // ingredient so because of it other storages with that ingredient are not needed - // But storages may be redeemed if they are in set of storages of ingredient where there is no toxic one - for (const api::StorageId temp : ingredient.inStorages) { - toAdd.insert(temp); - } - } - } - - storageIdsToShow.reserve(toAdd.size()); - for (auto add : toAdd) { - storageIdsToShow.push_back(add); - } - - return storageIdsToShow; -} - -void renderStoragesSuggestion(const std::vector& storageIdsToAccount, // storages which are selected - api::RecipeId recipeId, - UserId userId, - ChatId chatId, - BotRef bot, - ApiClient api) { - - auto storageApi = api.getStoragesApi(); - - auto recipesApi = api.getRecipesApi(); - auto recipeIngredients = recipesApi.getIngredientsInRecipe(userId, recipeId); - auto ingredients = recipeIngredients.ingredients; - - const std::vector storageIdsToShow = storagesToShow(ingredients, storageIdsToAccount); - - const textGenInfo text = textGen(storageIdsToAccount, recipeIngredients, userId, api); - auto toPrint = text.text; - - auto suggestionStrings = text.foundInStoragesStrings; - size_t counterOfSuggestionsFound = 0; - bool ifSuggestionEcountered = false; - - // This for can be moved to distinct function - for (size_t i = 0; i < toPrint.size(); i++) { // Put suggestions here - if (toPrint[i] == '\n' && ifSuggestionEcountered) { - toPrint.insert(i + 1, suggestionStrings[counterOfSuggestionsFound]); - counterOfSuggestionsFound++; - ifSuggestionEcountered = false; - } - if (toPrint[i] == '?') { - ifSuggestionEcountered = true; - } - } - // This for is similar to suggested storages can be unionaized with this part of textGen (which will be incredibly - // difficult to keep consistency of textGen fenction) To print storages which were added - std::string storagesWhichAccount = utils::utf8str(u8"Выбранные хранилища: "); - for (size_t i = 0; i < storageIdsToAccount.size(); i++) { - auto storage = storageApi.get(userId, storageIdsToAccount[i]); - storagesWhichAccount += std::format("\"{}\" ", storage.name); - if (i == storageIdsToAccount.size() - 1) { - storagesWhichAccount += "\n"; - } - } - toPrint.insert(0, storagesWhichAccount); - const int buttonRows = std::floor(((storageIdsToShow.size() + 1) / 2) + 1); // +1 for back - InlineKeyboard keyboard(buttonRows); - - uint64_t i = 0; - for (auto storageId : storageIdsToShow) { - const std::string name = storageApi.get(userId, storageId).name; - if (i % 2 == 0) { - keyboard[std::floor(i / 2)].reserve(2); - } - keyboard[std::floor(i / 2)].push_back(makeCallbackButton(name, "+" + utils::to_string(storageId))); - i++; - } - keyboard[std::floor((storageIdsToShow.size() + 1) / 2)].push_back( - makeCallbackButton(u8"↩️ Назад", "back_from_adding_storages")); - auto messageId = message::getMessageId(userId); - if (messageId) { - bot.editMessageText(toPrint, chatId, *messageId, "", "", nullptr, makeKeyboardMarkup(std::move(keyboard))); - } -} -} // namespace cookcookhnya::render::recipe diff --git a/src/render/recipe/add_storage.hpp b/src/render/recipe/add_storage.hpp deleted file mode 100644 index a63d420b..00000000 --- a/src/render/recipe/add_storage.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "backend/id_types.hpp" -#include "backend/models/recipe.hpp" -#include "render/common.hpp" - -#include - -namespace cookcookhnya::render::recipe { - -void renderStoragesSuggestion(const std::vector& storageIdsToAccount, - api::RecipeId recipeId, - UserId userId, - ChatId chatId, - BotRef bot, - ApiClient api); - -std::vector storagesToShow(const std::vector& ingredients, - const std::vector& storageIdsToAccount); -} // namespace cookcookhnya::render::recipe diff --git a/src/render/recipe/view.cpp b/src/render/recipe/view.cpp index f706876f..4783a329 100644 --- a/src/render/recipe/view.cpp +++ b/src/render/recipe/view.cpp @@ -4,143 +4,50 @@ #include "backend/models/recipe.hpp" #include "message_tracker.hpp" #include "render/common.hpp" -#include "utils/utils.hpp" +#include "utils/u8format.hpp" + +#include -#include -#include #include #include -#include #include -#include namespace cookcookhnya::render::recipe { -textGenInfo textGen(const std::vector& storageIds, - const api::models::recipe::RecipeDetails& recipeIngredients, - UserId userId, - ApiClient api) { // will return needed text and some additional elements - - // Get two api's from apiClient - auto storageApi = api.getStoragesApi(); - const std::unordered_set storageIdSet(storageIds.begin(), storageIds.end()); - - std::unordered_set suggestedStorageIds; - std::vector foundInStoragesStrings; - - auto ingredients = recipeIngredients.ingredients; - - const std::string recipeName = recipeIngredients.name; - auto toPrint = std::format("{} Ингредиенты для *{}* \n\n", utils::utf8str(u8"📖"), recipeName); - std::vector variants = { - "\n", " и ", ", "}; // difference is 0 -> last, difference is 1 -> предпоследний. - - bool isContains = false; - bool isSuggestionMade = false; - bool isIngredientNotWritten = true; - bool isAtLeastOneIngredientLack = false; - size_t counterOfSuggestion = 0; - for (auto& ingredient : ingredients) { // Iterate through each ingredient - isIngredientNotWritten = true; - isContains = false; - if (ingredient.inStorages.empty()) { - toPrint += std::format("`[ ]` {}\n", ingredient.name); - isAtLeastOneIngredientLack = true; - continue; - } - - for (size_t j = 0; j < ingredient.inStorages.size(); - j++) { // Iterate through each storage where ingredient is present - if (storageIdSet.contains( - ingredient.inStorages[j])) { // If it contains then ingredient is in chosen storages - toPrint += std::format("`[+]` {}\n", ingredient.name); - isContains = true; - break; - } - } - - if (isContains) { - continue; - } - - for (size_t j = 0; j < ingredient.inStorages.size(); - j++) { // Iterate through each storage where ingredient is present - isSuggestionMade = true; - if (isIngredientNotWritten) { - toPrint += std::format("`[?]` {}\n", ingredient.name); - isIngredientNotWritten = false; - - foundInStoragesStrings.emplace_back(""); // New place for string for suggestion - - if (ingredient.inStorages.size() == 1) { - - foundInStoragesStrings[counterOfSuggestion] += utils::utf8str(u8" *Найдено в хранилище: "); - } else { - foundInStoragesStrings[counterOfSuggestion] += utils::utf8str(u8" *Найдено в хранилищах: "); - } - } - auto storage = storageApi.get(userId, ingredient.inStorages[j]); - - suggestedStorageIds.insert(ingredient.inStorages[j]); // Keep set of storages which will be suggested - foundInStoragesStrings[counterOfSuggestion] += // I felt myself genious after writing that line of code - // (here is one for, if-else and if technically) - std::format("\"{}\"{}", - storage.name, - variants[std::min(variants.size() - 1, ingredient.inStorages.size() - j - 1)]); - } - counterOfSuggestion++; // If here then suggesiton was made - } - toPrint += "\n🌐 [Источник](" + recipeIngredients.link + ")"; - return {.text = toPrint, - .isSuggestionMade = isSuggestionMade, - .suggestedStorageIds = suggestedStorageIds, - .foundInStoragesStrings = foundInStoragesStrings, - .isAtLeastOneIngredientLack = - isAtLeastOneIngredientLack}; // Many info may be needed from that function to make right markup -} - -void renderRecipeView(const std::vector& storageIds, - api::RecipeId recipeId, +void renderRecipeView(const api::models::recipe::RecipeDetails& recipe, + const api::RecipeId& recipeId, UserId userId, ChatId chatId, - BotRef bot, - ApiClient api) { - - auto recipesApi = api.getRecipesApi(); - auto recipeIngredients = recipesApi.getIngredientsInRecipe(userId, recipeId); - const textGenInfo text = textGen(storageIds, recipeIngredients, userId, api); - - const bool isSuggestionMade = text.isSuggestionMade; - auto suggestedStorageIds = text.suggestedStorageIds; - auto toPrint = text.text; - const bool isAtLeastOneIngredientLack = text.isAtLeastOneIngredientLack; - - // if there is no lacking ingredients then there is no need to show field of shopping list - const size_t buttonRows = isAtLeastOneIngredientLack ? 3 : 2; - InlineKeyboard keyboard(buttonRows); - - keyboard[0].push_back(makeCallbackButton(u8"🧑‍🍳 Готовить", - "start_cooking")); // Add needed info for next states! - - if (isSuggestionMade) { - std::string dataForSuggestion = "?"; - for (auto id : suggestedStorageIds) { - dataForSuggestion += std::format("{} ", id); - } - keyboard[0].push_back(makeCallbackButton(u8"?", dataForSuggestion)); - } - - if (isAtLeastOneIngredientLack) { - keyboard[1].push_back(makeCallbackButton(u8"📝 Составить список продуктов", - "make_product_list")); // Add needed info for next states! - } - - keyboard[buttonRows - 1].push_back(makeCallbackButton(u8"↩️ Назад", "back_from_recipe_view")); - - auto messageId = message::getMessageId(userId); - if (messageId) { - // Only on difference between function above - bot.editMessageText(toPrint, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); + BotRef bot) { + std::string text = utils::u8format("{} *{}*\n\n{}", u8"📖 Рецепт", recipe.name, u8"Ингредиенты:\n"); + for (const auto& ing : recipe.ingredients) + text += utils::u8format("{} {}\n", u8"•", ing.name); + if (recipe.link) + text += utils::u8format("\n{}: {}\n", u8"🌐 Источник", *recipe.link); + if (recipe.creator) + text += utils::u8format("\n{}: {}\n", u8"👤 Автор", recipe.creator->fullName); + + InlineKeyboardBuilder keyboard{3}; // cook, share, back + + auto shareButton = std::make_shared(); + shareButton->text = utils::utf8str(u8"📤 Поделиться"); + const std::string botAlias = bot.getUnderlying().getMe()->username; + const std::string recipeUrl = std::format("https://t.me/{}?start=recipe_{}", botAlias, recipeId); + const std::string shareText = utils::u8format("{} **{}**", u8"Хочу поделиться с тобой рецептом", recipe.name); + + boost::urls::url url{"https://t.me/share/url"}; + url.params().append({"url", recipeUrl}); + url.params().append({"text", shareText}); + shareButton->url = url.buffer(); + + keyboard << makeCallbackButton(u8"🧑‍🍳 Хочу приготовить", "cook") << NewRow{} + << std::move(shareButton) << NewRow{} << makeCallbackButton(u8"↩️ Назад", "back"); + + if (auto mMessageId = message::getMessageId(userId)) + bot.editMessageText(text, chatId, *mMessageId, std::move(keyboard), "Markdown"); + else { + auto messageId = bot.sendMessage(chatId, text, std::move(keyboard), "Markdown")->messageId; + message::addMessageId(userId, messageId); } } diff --git a/src/render/recipe/view.hpp b/src/render/recipe/view.hpp index 9e7a6344..6f308838 100644 --- a/src/render/recipe/view.hpp +++ b/src/render/recipe/view.hpp @@ -1,34 +1,14 @@ #pragma once -#include "backend/id_types.hpp" #include "backend/models/recipe.hpp" #include "render/common.hpp" -#include -#include -#include - namespace cookcookhnya::render::recipe { -struct textGenInfo { - std::string text; - bool isSuggestionMade{}; - std::unordered_set suggestedStorageIds; - std::vector foundInStoragesStrings; - bool isAtLeastOneIngredientLack; -}; - -void renderRecipeView(const std::vector& storageIds, - api::RecipeId recipeId, +void renderRecipeView(const api::models::recipe::RecipeDetails& recipe, + const api::RecipeId& recipeId, UserId userId, ChatId chatId, - BotRef bot, - ApiClient api); -// Helper functions - -textGenInfo textGen(const std::vector& storageIds, - const api::models::recipe::RecipeDetails& recipeIngredients, - UserId userId, - ApiClient api); + BotRef bot); } // namespace cookcookhnya::render::recipe diff --git a/src/render/recipes_search/view.cpp b/src/render/recipes_search/view.cpp new file mode 100644 index 00000000..8a9a3fbc --- /dev/null +++ b/src/render/recipes_search/view.cpp @@ -0,0 +1,47 @@ +#include "view.hpp" + +#include "backend/models/recipe.hpp" +#include "message_tracker.hpp" +#include "render/common.hpp" +#include "render/pagination.hpp" +#include "states.hpp" +#include "utils/utils.hpp" + +#include +#include + +namespace cookcookhnya::render::recipes_search { + +using namespace api::models::recipe; + +void renderRecipesSearch(const states::helpers::Pagination& pagination, + const decltype(states::RecipesSearch::page)& page, + UserId userId, + ChatId chatId, + BotRef bot) { + const std::string text = utils::utf8str(u8"Используйте кнопку ниже, чтобы искать рецепты"); + + InlineKeyboardBuilder keyboard{2 + page.size()}; + + auto searchButton = std::make_shared(); + searchButton->text = utils::utf8str(u8"🔎 Поиск"); + searchButton->switchInlineQueryCurrentChat = ""; + keyboard << std::move(searchButton) << NewRow{}; + + auto makeRecipeButton = [](const RecipeSummary& r) { + return makeCallbackButton(utils::utf8str(u8"🔖 ") + r.name, "recipe_" + utils::to_string(r.id)); + }; + const std::size_t pageSize = 5; + keyboard << constructPagination(pagination.pageNo, pageSize, pagination.totalItems, page, makeRecipeButton); + + keyboard << makeCallbackButton(u8"↩️ Назад", "back"); + + if (auto mMessageId = message::getMessageId(userId)) { + bot.editMessageText(text, chatId, *mMessageId, std::move(keyboard)); + } else { + auto message = bot.sendMessage(chatId, text, std::move(keyboard)); + message::addMessageId(userId, message->messageId); + } +} + +} // namespace cookcookhnya::render::recipes_search diff --git a/src/render/recipes_search/view.hpp b/src/render/recipes_search/view.hpp new file mode 100644 index 00000000..9d109014 --- /dev/null +++ b/src/render/recipes_search/view.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "render/common.hpp" +#include "states.hpp" + +namespace cookcookhnya::render::recipes_search { + +void renderRecipesSearch(const states::helpers::Pagination& pagination, + const decltype(states::RecipesSearch::page)& page, + UserId userId, + ChatId chatId, + BotRef bot); + +} // namespace cookcookhnya::render::recipes_search diff --git a/src/render/recipes_suggestions/view.cpp b/src/render/recipes_suggestions/view.cpp index a49ed27a..576ad5eb 100644 --- a/src/render/recipes_suggestions/view.cpp +++ b/src/render/recipes_suggestions/view.cpp @@ -1,148 +1,66 @@ #include "view.hpp" +#include "backend/api/recipes.hpp" #include "backend/id_types.hpp" #include "backend/models/recipe.hpp" #include "message_tracker.hpp" #include "render/common.hpp" -#include "utils/to_string.hpp" +#include "render/pagination.hpp" #include "utils/utils.hpp" -#include -#include #include -#include #include +#include #include #include -namespace cookcookhnya::render::recipes_suggestions { - -// offset is variable which defines amout of rows before beggining of paging -// fullKeyBoardSize is self explanatory -InlineKeyboard constructNavigationsMarkup(size_t offset, - size_t fullKeyBoardSize, - size_t pageNo, - size_t numOfRecipesOnPage, - api::models::recipe::RecipesList recipesList) { - const size_t amountOfRecipes = recipesList.found; - std::size_t maxPageNum = std::ceil(static_cast(amountOfRecipes) / static_cast(numOfRecipesOnPage)); +namespace TgBot { +class InlineKeyboardMarkup; +} // namespace TgBot - const size_t recipesToShow = std::min(numOfRecipesOnPage, recipesList.page.size()); - // + 1 because of the 0-indexing, as comparisson is between num of recipes gotten and that - // will be actually shown - const bool ifMaxPage = - static_cast(amountOfRecipes) - static_cast(numOfRecipesOnPage) * (static_cast(pageNo) + 1) <= 0; - - if (offset + recipesToShow > fullKeyBoardSize) { // IN ERROR HANDLING MAY USE ASSERT - InlineKeyboard error(0); - return error; - } - const size_t arrowsRow = offset + recipesToShow; - - InlineKeyboard keyboard(pageNo == 0 && ifMaxPage ? fullKeyBoardSize - 1 : fullKeyBoardSize); - int counter = 0; - for (std::size_t i = 0; i < recipesToShow; i++) { - // Print on button in form "1. {Recipe}" - keyboard[i + offset].push_back( - makeCallbackButton(std::format("{}. {} [{} из {}]", - 1 + counter + ((pageNo)*numOfRecipesOnPage), - recipesList.page[counter].name, - recipesList.page[counter].available, - recipesList.page[counter].total), - std::format("recipe: {}", recipesList.page[counter].id))); // RECIPE ID - counter++; - } - if (pageNo == 0 && ifMaxPage) { - // instead of arrows row - keyboard[arrowsRow].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - return keyboard; - } - keyboard[arrowsRow].reserve(3); - - // Helps to reduce code. Power of C++ YEAH BABE! - uint8_t b = 0; - - // Simply enamurate every case - if (pageNo == 0) { - if (!ifMaxPage) { - b |= uint8_t{0x1}; - } - } else if (ifMaxPage) { - b |= uint8_t{0x2}; - } else { - b |= uint8_t{0x3}; - } - - // Check from left to right due to buttons being displayed like that - for (int i = 1; i >= 0; i--) { - // Compare two bits under b mask. If 1 was on b mask then we need to place arrow somewhere - if ((b & static_cast((uint8_t{0b1} << static_cast(i)))) == - (uint8_t{0b1} << static_cast(i))) { - // if we need to place arrow then check the i, which represents bit which we are checking right now - if (i == 1) { - keyboard[arrowsRow].push_back(makeCallbackButton(u8"◀️", utils::to_string(pageNo - 1))); // left - } else { - keyboard[arrowsRow].push_back(makeCallbackButton(u8"▶️", utils::to_string(pageNo + 1))); // right - } - } else { - keyboard[arrowsRow].push_back(makeCallbackButton(u8"ㅤ", "dont_handle")); - } - } - // Put pageNo as button - keyboard[arrowsRow].insert(keyboard[arrowsRow].begin() + 1, - makeCallbackButton(std::format("{} из {}", pageNo + 1, maxPageNum), "dont_handle")); - keyboard[arrowsRow + 1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - return keyboard; -} +namespace cookcookhnya::render::recipes_suggestions { -InlineKeyboard constructOnlyBack() { - InlineKeyboard keyboard(1); - keyboard[0].push_back(makeCallbackButton(u8"↩️ Назад", "back")); +using namespace api::models::storage; +using namespace api::models::recipe; +using namespace std::views; + +namespace { + +std::shared_ptr +constructKeyboard(std::size_t pageNo, std::size_t pageSize, RecipesListWithIngredientsCount& recipesList) { + InlineKeyboardBuilder keyboard; + auto makeRecipeButton = [i = (pageNo * pageSize) + 1](RecipeSummaryWithIngredients& r) mutable { + return makeCallbackButton(std::format("{}. {} [{} из {}]", i++, r.name, r.available, r.total), + std::format("recipe_{}", r.id)); + }; + keyboard << constructPagination(pageNo, pageSize, recipesList.found, recipesList.page, makeRecipeButton) + << makeCallbackButton(u8"↩️ Назад", "back"); return keyboard; } -InlineKeyboard -constructMarkup(size_t pageNo, size_t numOfRecipesOnPage, api::models::recipe::RecipesList& recipesList) { - - const size_t numOfRows = 2; // 1 for back button return, 1 for arrows (ALWAYS ACCOUNT ARROWS) - const size_t offset = 0; // Number of rows before list - const size_t recipesToShow = std::min(numOfRecipesOnPage, recipesList.page.size()); +} // namespace - InlineKeyboard keyboard = - recipesList.found == 0 - ? constructOnlyBack() - : constructNavigationsMarkup(offset, numOfRows + recipesToShow, pageNo, numOfRecipesOnPage, recipesList); - if (keyboard.empty()) { // If error happened ADD PROPER ERROR HANDLING IF FUNCTION WILL BE REUSED - return keyboard; - } - - return keyboard; -} - -void renderRecipesSuggestion(const std::vector& storageIds, - size_t pageNo, +void renderRecipesSuggestion(const std::vector& storages, + std::size_t pageNo, UserId userId, ChatId chatId, BotRef bot, - RecipesApiRef recipesApi) { - const std::string pageInfo = utils::utf8str(u8"🔪 Рецепты подобранные специально для вас"); - - auto messageId = message::getMessageId(userId); - - const size_t numOfRecipesOnPage = 5; - const size_t numOfRecipes = 500; + api::RecipesApiRef recipesApi) { + const std::size_t numOfRecipesOnPage = 5; auto recipesList = - recipesApi.getSuggestedRecipesList(userId, storageIds, numOfRecipes, pageNo * numOfRecipesOnPage); + recipesApi.getSuggestedRecipes(userId, storages, numOfRecipesOnPage, pageNo * numOfRecipesOnPage); + + const std::string text = + utils::utf8str(recipesList.found > 0 ? u8"🔪 Рецепты подобранные специально для вас" + : u8"😔 К сожалению, нам не удалось найти подходящие рецепты для вас..."); + auto keyboardMarkup = constructKeyboard(pageNo, numOfRecipesOnPage, recipesList); - if (messageId) { - bot.editMessageText(pageInfo, - chatId, - *messageId, - "", - "", - nullptr, - makeKeyboardMarkup(constructMarkup(pageNo, numOfRecipesOnPage, recipesList))); + if (auto messageId = message::getMessageId(userId)) { + bot.editMessageText(text, chatId, *messageId, keyboardMarkup); + } else { + auto message = bot.sendMessage(chatId, text, keyboardMarkup); + message::addMessageId(userId, message->messageId); } } diff --git a/src/render/recipes_suggestions/view.hpp b/src/render/recipes_suggestions/view.hpp index 7b8f1899..6c91d193 100644 --- a/src/render/recipes_suggestions/view.hpp +++ b/src/render/recipes_suggestions/view.hpp @@ -1,7 +1,7 @@ #pragma once +#include "backend/api/recipes.hpp" #include "backend/id_types.hpp" -#include "backend/models/recipe.hpp" #include "render/common.hpp" #include @@ -11,19 +11,11 @@ namespace cookcookhnya::render::recipes_suggestions { using namespace tg_types; -InlineKeyboard constructMarkup(size_t pageNo, size_t numOfRecipesOnPage, api::models::recipe::RecipesList& recipesList); -InlineKeyboard constructOnlyBack(); -InlineKeyboard constructNavigationsMarkup(size_t offset, - size_t fullKeyBoardSize, - size_t pageNo, - size_t numOfRecipesOnPage, - api::models::recipe::RecipesList recipesList); - -void renderRecipesSuggestion(const std::vector& storageIds, - size_t pageNo, +void renderRecipesSuggestion(const std::vector& storages, + std::size_t pageNo, UserId userId, ChatId chatId, BotRef bot, - RecipesApiRef recipesApi); + api::RecipesApiRef recipesApi); } // namespace cookcookhnya::render::recipes_suggestions diff --git a/src/render/shopping_list/create.cpp b/src/render/shopping_list/create.cpp index 37445ac8..5284dc6a 100644 --- a/src/render/shopping_list/create.cpp +++ b/src/render/shopping_list/create.cpp @@ -1,63 +1,52 @@ #include "create.hpp" -#include "backend/api/ingredients.hpp" -#include "backend/id_types.hpp" +#include "backend/models/ingredient.hpp" #include "message_tracker.hpp" #include "render/common.hpp" -#include "utils/to_string.hpp" #include "utils/utils.hpp" +#include #include -#include #include +#include #include #include #include namespace cookcookhnya::render::shopping_list { -std::vector renderShoppingListCreation(const std::vector& ingredientIds, - UserId userId, - ChatId chatId, - BotRef bot, - api::IngredientsApi ingredientsApi) { - - std::string text = utils::utf8str(u8"Основываясь на недостающих ингредиентах, составили для вас продукты " - u8"которые можно добавить в список покупок:\n *В самом низу выберите " - u8"ингредиенты которые вы хотите исключить из списка покупок\n"); - std::vector ingredientsName; - for (const api::IngredientId ingredientId : ingredientIds) { - // IMPORTANT!: Probably can be optimized because this data is available at the recipe page - // by Maxim Fomin - // (1) I believe that both ways are expensive: or it's run through string of textGen or it's several small - // queries to backend. While i understand that working with string is faster then sending such queries i think - // that it's better not to overengineering frontend in this aspect. - // - // (2) Besides this also lays on frontend additional work on maintaining ingredientsName vector (in this case - // deletion from it). - // by Ilia Kliantsevich - std::string name = ingredientsApi.get(userId, ingredientId).name; - ingredientsName.push_back(name); - text += std::format("- {}\n", name); - } - const std::size_t buttonRows = ((ingredientIds.size() + 1) / 2) + 2; // +1 for back, +1 for approve - - InlineKeyboard keyboard(buttonRows); - uint64_t i = 0; - for (auto ingredientId : ingredientIds) { - const std::string& name = ingredientsName[i]; - if (i % 2 == 0) { - keyboard[(i / 2)].reserve(2); +using namespace api::models::ingredient; +using namespace std::views; + +void renderShoppingListCreation(const std::vector& selectedIngredients, + const std::vector& allIngredients, + UserId userId, + ChatId chatId, + BotRef bot) { + const std::string text = utils::utf8str(u8"🧾 Выберите продукты, которые хотели бы добавить в список покупок\n\n"); + + const std::size_t buttonRows = ((selectedIngredients.size() + 1) / 2) + 1; // ceil(ingredientsCount / 2), back + InlineKeyboardBuilder keyboard{buttonRows}; + + for (auto chunk : allIngredients | chunk(2)) { + keyboard.reserveInRow(2); + for (const Ingredient& ing : chunk) { + const bool isSelected = std::ranges::contains(selectedIngredients, ing.id, &Ingredient::id); + std::string emoji = utils::utf8str(isSelected ? u8"[ + ]" : u8"[ᅠ]"); // second is not empty but invisible! + // button data is onclick action for bot: "+" is "add", "-" is "remove" + const char* actionPrefix = isSelected ? "-" : "+"; + const std::string text = std::format("{} {}", emoji, ing.name); + const std::string data = actionPrefix + utils::to_string(ing.id); + keyboard << makeCallbackButton(text, data); } - keyboard[i / 2].push_back( - makeCallbackButton(name, "i" + utils::to_string(ingredientId))); // i stands for ingredient - i++; + keyboard << NewRow{}; } - keyboard[((ingredientIds.size() + 1) / 2)].push_back(makeCallbackButton(u8"▶️ Подтвердить", "confirm")); - keyboard[(((ingredientIds.size() + 1) / 2) + 1)].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - auto messageId = message::getMessageId(userId); - if (messageId) - bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); - return ingredientIds; + + keyboard << makeCallbackButton(u8"↩️ Назад", "back"); + if (!selectedIngredients.empty()) + keyboard << makeCallbackButton(u8"▶️ Подтвердить", "confirm"); + + if (auto messageId = message::getMessageId(userId)) + bot.editMessageText(text, chatId, *messageId, std::move(keyboard), "MarkdownV2"); } } // namespace cookcookhnya::render::shopping_list diff --git a/src/render/shopping_list/create.hpp b/src/render/shopping_list/create.hpp index c58d6494..aaf0feb1 100644 --- a/src/render/shopping_list/create.hpp +++ b/src/render/shopping_list/create.hpp @@ -1,18 +1,16 @@ #pragma once -#include "backend/api/ingredients.hpp" - -#include "backend/id_types.hpp" +#include "backend/models/ingredient.hpp" #include "render/common.hpp" #include namespace cookcookhnya::render::shopping_list { -std::vector renderShoppingListCreation(const std::vector& ingredientIds, - UserId userId, - ChatId chatId, - BotRef bot, - api::IngredientsApi ingredientsApi); +void renderShoppingListCreation(const std::vector& selectedIngredients, + const std::vector& allIngredients, + UserId userId, + ChatId chatId, + BotRef bot); } // namespace cookcookhnya::render::shopping_list diff --git a/src/render/shopping_list/search.cpp b/src/render/shopping_list/search.cpp new file mode 100644 index 00000000..a73831a6 --- /dev/null +++ b/src/render/shopping_list/search.cpp @@ -0,0 +1,49 @@ +#include "search.hpp" + +#include "backend/models/ingredient.hpp" +#include "message_tracker.hpp" +#include "render/common.hpp" +#include "render/pagination.hpp" +#include "states.hpp" +#include "utils/utils.hpp" + +#include +#include +#include +#include + +namespace TgBot { +class InlineKeyboardButton; +} // namespace TgBot + +namespace cookcookhnya::render::shopping_list { + +using namespace api::models::ingredient; +using namespace tg_types; +using namespace std::views; +using namespace std::ranges; + +void renderShoppingListIngredientSearch( + const states::ShoppingListIngredientSearch& state, std::size_t pageSize, UserId userId, ChatId chatId, BotRef bot) { + const std::string text = utils::utf8str(u8"🍗 Используйте кнопку ниже, чтобы найти ингредиенты для добавления"); + + InlineKeyboardBuilder keyboard{state.page.size() + 2}; // search, items (n), back + + auto searchButton = std::make_shared(); + searchButton->text = utils::utf8str(u8"🔍 Искать"); + searchButton->switchInlineQueryCurrentChat = ""; + keyboard << std::move(searchButton) << NewRow{}; + + auto makeItemButton = [](const Ingredient& ing) { + // const auto* emptyBrackets = "[ㅤ] "; + return makeCallbackButton(ing.name, "ingredient_" + utils::to_string(ing.id)); + }; + keyboard << constructPagination( + state.pagination.pageNo, pageSize, state.pagination.totalItems, state.page, makeItemButton) + << makeCallbackButton(u8"↩️ Назад", "back"); + + if (auto messageId = message::getMessageId(userId)) + bot.editMessageText(text, chatId, *messageId, std::move(keyboard)); +} + +} // namespace cookcookhnya::render::shopping_list diff --git a/src/render/shopping_list/search.hpp b/src/render/shopping_list/search.hpp new file mode 100644 index 00000000..f6dfed70 --- /dev/null +++ b/src/render/shopping_list/search.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "render/common.hpp" +#include "states.hpp" + +#include + +namespace cookcookhnya::render::shopping_list { + +void renderShoppingListIngredientSearch( + const states::ShoppingListIngredientSearch& state, std::size_t pageSize, UserId userId, ChatId chatId, BotRef bot); + +} // namespace cookcookhnya::render::shopping_list diff --git a/src/render/shopping_list/storage_selection_to_buy.cpp b/src/render/shopping_list/storage_selection_to_buy.cpp new file mode 100644 index 00000000..2059fa55 --- /dev/null +++ b/src/render/shopping_list/storage_selection_to_buy.cpp @@ -0,0 +1,37 @@ +#include "storage_selection_to_buy.hpp" + +#include "message_tracker.hpp" +#include "render/common.hpp" +#include "states.hpp" +#include "utils/utils.hpp" + +#include +#include + +namespace cookcookhnya::render::shopping_list { + +using namespace std::views; + +void renderShoppingListStorageSelectionToBuy(const states::ShoppingListStorageSelectionToBuy& state, + UserId userId, + ChatId chatId, + BotRef bot) { + InlineKeyboardBuilder keyboard{((state.storages.size() + 1) / 2) + 1}; // ceil(storagesCount / 2) and back + + // in two columns + for (auto chunk : state.storages | chunk(2)) { + keyboard.reserveInRow(2); + for (const auto& s : chunk) + keyboard << makeCallbackButton(utils::utf8str(u8"🍱 ") + s.name, "storage_" + utils::to_string(s.id)); + keyboard << NewRow{}; + } + + keyboard << makeCallbackButton(u8"↩️ Назад", "back"); + + if (auto mMessageId = message::getMessageId(userId)) { + bot.editMessageText( + utils::utf8str(u8"Выберите хранилище, куда положить продукты"), chatId, *mMessageId, std::move(keyboard)); + } +} + +} // namespace cookcookhnya::render::shopping_list diff --git a/src/render/shopping_list/storage_selection_to_buy.hpp b/src/render/shopping_list/storage_selection_to_buy.hpp new file mode 100644 index 00000000..262839c3 --- /dev/null +++ b/src/render/shopping_list/storage_selection_to_buy.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "render/common.hpp" +#include "states.hpp" + +namespace cookcookhnya::render::shopping_list { + +void renderShoppingListStorageSelectionToBuy(const states::ShoppingListStorageSelectionToBuy& state, + UserId userId, + ChatId chatId, + BotRef bot); + +} // namespace cookcookhnya::render::shopping_list diff --git a/src/render/shopping_list/view.cpp b/src/render/shopping_list/view.cpp index 082f0515..49da6e35 100644 --- a/src/render/shopping_list/view.cpp +++ b/src/render/shopping_list/view.cpp @@ -3,23 +3,48 @@ #include "message_tracker.hpp" #include "render/common.hpp" #include "states.hpp" -#include "utils/to_string.hpp" #include "utils/utils.hpp" +#include #include #include namespace cookcookhnya::render::shopping_list { -void renderShoppingList(const states::ShoppingListView::ItemsDb::Set& items, UserId userId, ChatId chatId, BotRef bot) { - InlineKeyboard keyboard(1 + items.size()); - for (auto [i, item] : std::views::enumerate(items)) - keyboard[i].push_back(makeCallbackButton(item.name, utils::to_string(item.ingredientId))); - keyboard[items.size()].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - auto messageId = message::getMessageId(userId); - if (messageId) { - auto text = utils::utf8str(u8"🔖 Ваш список покупок. Нажмите на элемент, чтобы вычеркнуть из списка."); - bot.editMessageText(text, chatId, *messageId, "", "", nullptr, makeKeyboardMarkup(std::move(keyboard))); +using namespace std::views; + +void renderShoppingList(const states::ShoppingListView& state, UserId userId, ChatId chatId, BotRef bot) { + auto items = state.items.getValues(); + const bool anySelected = std::ranges::any_of(items, &states::helpers::SelectableShoppingListItem::selected); + + InlineKeyboardBuilder keyboard{3 + ((items.size() / 2) + 1)}; // add, remove and/or buy, list (n/2), back + + keyboard << makeCallbackButton(u8"🔍 Поиск", "search") << NewRow{}; + + if (anySelected) { + keyboard << makeCallbackButton(u8"🗑 Убрать", "remove"); + if (state.canBuy) + keyboard << makeCallbackButton(u8"🛒 Купить", "buy"); + keyboard << NewRow{}; + } + + for (auto row : items | chunk(2)) { + for (const auto& item : row) { + const char* const selectedMark = item.selected ? "[ + ] " : "[ᅠ] "; // not empty! + keyboard << makeCallbackButton(selectedMark + item.name, utils::to_string(item.ingredientId)); + } + keyboard << NewRow{}; + } + + keyboard << makeCallbackButton(u8"↩️ Назад", "back"); + + auto text = + utils::utf8str(u8"🧾 Ваш список покупок. Выбирайте, чтобы добавить в хранилище или вычеркнуть из списка."); + if (auto messageId = message::getMessageId(userId)) { + bot.editMessageText(text, chatId, *messageId, std::move(keyboard)); + } else { + auto message = bot.sendMessage(chatId, text, std::move(keyboard)); + message::addMessageId(userId, message->messageId); } } diff --git a/src/render/shopping_list/view.hpp b/src/render/shopping_list/view.hpp index 4da9193c..95f665ab 100644 --- a/src/render/shopping_list/view.hpp +++ b/src/render/shopping_list/view.hpp @@ -5,6 +5,6 @@ namespace cookcookhnya::render::shopping_list { -void renderShoppingList(const states::ShoppingListView::ItemsDb::Set& items, UserId userId, ChatId chatId, BotRef bot); +void renderShoppingList(const states::ShoppingListView& state, UserId userId, ChatId chatId, BotRef bot); } // namespace cookcookhnya::render::shopping_list diff --git a/src/render/storage/delete.cpp b/src/render/storage/delete.cpp new file mode 100644 index 00000000..9c9379cb --- /dev/null +++ b/src/render/storage/delete.cpp @@ -0,0 +1,25 @@ +#include "delete.hpp" + +#include "backend/api/storages.hpp" +#include "backend/id_types.hpp" +#include "message_tracker.hpp" +#include "render/common.hpp" +#include "utils/utils.hpp" + +#include + +namespace cookcookhnya::render::delete_storage { + +void renderStorageDeletion( + api::StorageId storageId, ChatId chatId, BotRef bot, UserId userId, api::StorageApiRef storageApi) { + auto storage = storageApi.get(userId, storageId); + InlineKeyboard keyboard(2); + keyboard[0].push_back(makeCallbackButton(u8"▶️ Подтвердить", "confirm")); + keyboard[1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); + auto text = utils::utf8str(u8"🚮 Вы уверены, что хотите удалить хранилище?"); + auto messageId = message::getMessageId(userId); + if (messageId) + bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); +}; + +} // namespace cookcookhnya::render::delete_storage diff --git a/src/render/storage/delete.hpp b/src/render/storage/delete.hpp new file mode 100644 index 00000000..00d2f27c --- /dev/null +++ b/src/render/storage/delete.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "backend/api/storages.hpp" +#include "backend/id_types.hpp" +#include "render/common.hpp" + +namespace cookcookhnya::render::delete_storage { + +void renderStorageDeletion( + api::StorageId storageId, ChatId chatId, BotRef bot, UserId userId, api::StorageApiRef storageApi); + +} // namespace cookcookhnya::render::delete_storage diff --git a/src/render/storage/ingredients/delete.cpp b/src/render/storage/ingredients/delete.cpp new file mode 100644 index 00000000..e467ee72 --- /dev/null +++ b/src/render/storage/ingredients/delete.cpp @@ -0,0 +1,154 @@ +#include "delete.hpp" + +#include "backend/models/ingredient.hpp" +#include "message_tracker.hpp" +#include "render/common.hpp" +#include "states.hpp" + +#include +#include +#include +#include + +namespace cookcookhnya::render::storage::ingredients { + +namespace { + +std::vector constructNavigationButtons(std::size_t pageNo, std::size_t maxPageNum) { + std::vector buttons; + auto forward = makeCallbackButton(u8"▶️", "next"); + auto backward = makeCallbackButton(u8"◀️", "prev"); + auto dont_handle = makeCallbackButton(u8"ㅤ", "dont_handle"); + auto page = makeCallbackButton(std::format("{} из {}", (pageNo + 1), maxPageNum), "dont_handle"); + if (pageNo == maxPageNum) { + buttons.push_back(backward); + buttons.push_back(page); + buttons.push_back(dont_handle); + } else if (pageNo == 0) { + buttons.push_back(dont_handle); + buttons.push_back(page); + buttons.push_back(forward); + } else { + buttons.push_back(backward); + buttons.push_back(page); + buttons.push_back(forward); + } + return buttons; +} + +std::vector +constructIngredientsButton(std::vector& selectedIngredients, + std::vector& storageIngredients, + std::size_t size, + std::size_t offset) { + size = std::min(offset + size, storageIngredients.size()) - offset; + std::vector buttons; + for (std::size_t i = offset; i != size + offset; ++i) { + const bool isSelected = std::ranges::contains( + selectedIngredients, storageIngredients[i].id, &api::models::ingredient::Ingredient::id); + const std::string emoji = utils::utf8str(isSelected ? u8"[ + ]" : u8"[ᅠ]"); + const char* actionPrefix = isSelected ? "+" : "-"; + const std::string text = std::format("{} {}", emoji, storageIngredients[i].name); + const std::string data = actionPrefix + utils::to_string(storageIngredients[i].id); + buttons.push_back(makeCallbackButton(text, data)); + } + return buttons; +} + +std::pair +constructMessage(std::vector& selectedIngredients, // NOLINT(*complexity*) + std::vector& storageIngredients, + std::size_t pageNo, + std::size_t numOfIngredientsOnPage, + bool withoutPutToShoppingListButton) { + const std::size_t ingSize = storageIngredients.size(); + const std::size_t maxPageNum = + std::ceil(static_cast(ingSize) / static_cast(numOfIngredientsOnPage)); + std::size_t buttonRows = std::min(ingSize, numOfIngredientsOnPage); + if (selectedIngredients.empty()) { + if (ingSize <= numOfIngredientsOnPage) + buttonRows += 1; // + back + else + buttonRows += 2; // + back + navig + } else { + if (ingSize <= numOfIngredientsOnPage && pageNo == 0) { + if (withoutPutToShoppingListButton) + buttonRows += 2; // + back + delete + else + buttonRows += 3; // + back + delete + shop + } else { + if (withoutPutToShoppingListButton) + buttonRows += 3; // + back + navig + delete + else + buttonRows += 4; + } + } + + const std::string text = utils::utf8str(u8"🍅 Выберите ингредиенты для удаления\\.\n\n"); + InlineKeyboardBuilder keyboard{buttonRows}; + + for (auto& b : constructIngredientsButton( + selectedIngredients, storageIngredients, numOfIngredientsOnPage, pageNo * numOfIngredientsOnPage)) { + keyboard << std::move(b) << NewRow{}; + } + + auto backButton = makeCallbackButton(u8"↩️ Назад", "back"); + auto deleteButton = makeCallbackButton(u8"🗑 Удалить", "delete"); + auto shopButton = makeCallbackButton(u8"🧾 Добавить в Список покупок", "put_to_shop"); + if (selectedIngredients.empty()) { + if (ingSize <= numOfIngredientsOnPage && pageNo == 0) { + keyboard << std::move(backButton); + } else { + for (auto& b : constructNavigationButtons(pageNo, maxPageNum)) { + keyboard << std::move(b); + } + keyboard << NewRow{}; + keyboard << std::move(backButton); + } + } else { + if (ingSize <= numOfIngredientsOnPage && pageNo == 0) { + if (!withoutPutToShoppingListButton) { + keyboard << std::move(shopButton); + keyboard << NewRow{}; + } + keyboard << std::move(deleteButton); + keyboard << NewRow{}; + keyboard << std::move(backButton); + } else { + for (auto& b : constructNavigationButtons(pageNo, maxPageNum)) { + keyboard << std::move(b); + } + keyboard << NewRow{}; + if (!withoutPutToShoppingListButton) { + keyboard << std::move(shopButton); + keyboard << NewRow{}; + } + keyboard << std::move(deleteButton); + keyboard << NewRow{}; + keyboard << std::move(backButton); + } + } + return std::make_pair(text, keyboard); +} + +} // namespace + +void renderStorageIngredientsDeletion(states::StorageIngredientsDeletion& state, + UserId userId, + ChatId chatId, + BotRef bot) { + + const std::size_t numOfIngredientsOnPage = 7; + + auto res = constructMessage(state.selectedIngredients, + state.storageIngredients, + state.pageNo, + numOfIngredientsOnPage, + state.addedToShopList); + auto text = res.first; + auto keyboard = res.second; + if (auto messageId = message::getMessageId(userId)) + bot.editMessageText(text, chatId, *messageId, std::move(keyboard), "MarkdownV2"); +} + +} // namespace cookcookhnya::render::storage::ingredients diff --git a/src/render/storage/ingredients/delete.hpp b/src/render/storage/ingredients/delete.hpp new file mode 100644 index 00000000..b4a64ebd --- /dev/null +++ b/src/render/storage/ingredients/delete.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "render/common.hpp" +#include "states.hpp" + +namespace cookcookhnya::render::storage::ingredients { + +void renderStorageIngredientsDeletion(states::StorageIngredientsDeletion& state, + UserId userId, + ChatId chatId, + BotRef bot); + +} // namespace cookcookhnya::render::storage::ingredients diff --git a/src/render/storage/ingredients/view.cpp b/src/render/storage/ingredients/view.cpp index 871263cd..53a18e60 100644 --- a/src/render/storage/ingredients/view.cpp +++ b/src/render/storage/ingredients/view.cpp @@ -2,120 +2,49 @@ #include "backend/models/ingredient.hpp" #include "message_tracker.hpp" -#include "patched_bot.hpp" #include "render/common.hpp" +#include "render/pagination.hpp" #include "states.hpp" -#include "utils/to_string.hpp" #include "utils/utils.hpp" -#include - -#include -#include #include -#include #include #include #include #include #include -#include + +namespace TgBot { +class InlineKeyboardMarkup; +} // namespace TgBot namespace cookcookhnya::render::storage::ingredients { using namespace api::models::ingredient; using namespace tg_types; +using namespace std::views; +using std::ranges::to; namespace { -InlineKeyboard constructNavigationsMarkup(size_t offset, - size_t fullKeyBoardSize, - size_t numOfRecipesOnPage, - const states::StorageIngredientsList& state) { - using namespace std::views; - const size_t amountOfRecipes = state.totalFound; - int maxPageNum = - static_cast(std::ceil(static_cast(amountOfRecipes) / static_cast(numOfRecipesOnPage))); - - const size_t recipesToShow = std::min(numOfRecipesOnPage, state.searchItems.size()); - // + 1 because of the 0-indexing, as comparisson is between num of recipes gotten and that - // will be actually shown - const bool ifMaxPage = static_cast(amountOfRecipes) - - static_cast(numOfRecipesOnPage) * (static_cast(state.pageNo) + 1) <= - 0; - - if (offset + recipesToShow >= fullKeyBoardSize) { - InlineKeyboard error(0); - return error; - } - const size_t arrowsRow = offset + recipesToShow; - // Don't reserve for arrows if it's first page is max(im) - InlineKeyboard keyboard(state.pageNo == 0 && ifMaxPage ? fullKeyBoardSize - 1 : fullKeyBoardSize); +std::shared_ptr +constructKeyboard(std::size_t pageNo, std::size_t pageSize, const states::StorageIngredientsList& state) { + InlineKeyboardBuilder keyboard; auto searchButton = std::make_shared(); - searchButton->text = utils::utf8str(u8"✏️ Редактировать"); + searchButton->text = utils::utf8str(u8"🛒 Добавить"); searchButton->switchInlineQueryCurrentChat = ""; - keyboard[0].push_back(std::move(searchButton)); + keyboard << std::move(searchButton) << NewRow{}; - for (auto [row, ing] : zip(drop(keyboard, 1), state.searchItems)) - row.push_back(makeCallbackButton((ing.isInStorage ? "[ + ] " : "[ㅤ] ") + ing.name, utils::to_string(ing.id))); + auto makeIngredientButton = [](const IngredientSearchForStorageItem& ing) { + return makeCallbackButton((ing.isInStorage ? "[ + ] " : "[ㅤ] ") + ing.name, + "ingredient_" + utils::to_string(ing.id)); + }; + keyboard << constructPagination(pageNo, pageSize, state.totalFound, state.searchItems, makeIngredientButton); - if (state.pageNo == 0 && ifMaxPage) { - // instead of arrows row - keyboard[arrowsRow].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - return keyboard; - } - keyboard[arrowsRow].reserve(3); - - // Helps to reduce code. Power of C++ YEAH BABE! - uint8_t b = 0; - - // Simply enamurate every case - if (state.pageNo == 0) { - if (!ifMaxPage) { - b |= uint8_t{0b01}; - } - } else if (ifMaxPage) { - b |= uint8_t{0b10}; - } else { - b |= uint8_t{0b11}; - } - - // Check from left to right due to buttons being displayed like that - for (int i = 1; i >= 0; i--) { - // Compare two bits under b mask. If 1 was on b mask then we need to place arrow somewhere - if ((b & static_cast((uint8_t{0b1} << static_cast(i)))) == - (uint8_t{0b1} << static_cast(i))) { - // if we need to place arrow then check the i, which represents bit which we are checking right now - if (i == 1) { - keyboard[arrowsRow].push_back(makeCallbackButton(u8"◀️", "prev")); // left - } else { - keyboard[arrowsRow].push_back(makeCallbackButton(u8"▶️", "next")); // right - } - } else { - keyboard[arrowsRow].push_back(makeCallbackButton(u8"ㅤ", "dont_handle")); - } - } - // Put state.pageNo as button - keyboard[arrowsRow].insert( - keyboard[arrowsRow].begin() + 1, - makeCallbackButton(std::format("{} из {}", state.pageNo + 1, maxPageNum), "dont_handle")); - keyboard[arrowsRow + 1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - return keyboard; -} - -InlineKeyboard constructMarkup(size_t numOfRecipesOnPage, const states::StorageIngredientsList& state) { - // 1 for back button return, 1 for arrows (ALWAYS ACCOUNT ARROWS), 1 - // for editing - other buttons are ingredients - const size_t numOfRows = 3; - const size_t offset = 1; // Number of rows before list - - const size_t recipesToShow = std::min(numOfRecipesOnPage, state.searchItems.size()); - - InlineKeyboard keyboard = constructNavigationsMarkup(offset, numOfRows + recipesToShow, numOfRecipesOnPage, state); - if (keyboard.empty()) { // If error happened - return keyboard; - } + if (!state.storageIngredients.getValues().empty()) + keyboard << makeCallbackButton(u8"🗑 Удалить", "delete") << NewRow{}; + keyboard << makeCallbackButton(u8"↩️ Назад", "back"); return keyboard; } @@ -123,26 +52,48 @@ InlineKeyboard constructMarkup(size_t numOfRecipesOnPage, const states::StorageI } // namespace void renderIngredientsListSearch(const states::StorageIngredientsList& state, - size_t numOfIngredientsOnPage, UserId userId, ChatId chatId, BotRef bot) { - using namespace std::views; - using std::ranges::to; - - std::string list = state.storageIngredients.getAll() | - transform([](auto& i) { return std::format("• {}\n", i.name); }) | join | to(); + const std::size_t numOfIngredientsOnPage = 5; + + const std::string list = state.storageIngredients.getValues() | + transform([](auto& i) { return std::format("• {}\n", i.name); }) | join | + to(); + auto text = + state.storageIngredients.getValues().empty() + ? utils::utf8str(u8"🍗 Кажется, в вашем хранилище пока нет никаких продуктов. Чтобы добавить новый, " + u8"нажмите на кнопку 🛒 Добавить и начните вводить название продукта...\n\n") + : utils::utf8str(u8"🍗 Ваши продукты:\n\n"); + text += list; - auto text = utils::utf8str(u8"🍗 Ваши ингредиенты:\n\n") + std::move(list); if (auto messageId = message::getMessageId(userId)) { - bot.editMessageText(text, - chatId, - *messageId, - "", - "", - nullptr, - makeKeyboardMarkup(constructMarkup(numOfIngredientsOnPage, state))); + bot.editMessageText(text, chatId, *messageId, constructKeyboard(state.pageNo, numOfIngredientsOnPage, state)); } } } // namespace cookcookhnya::render::storage::ingredients + +namespace cookcookhnya::render::suggest_custom_ingredient { + +void renderSuggestIngredientCustomisation(const states::StorageIngredientsList& state, + UserId userId, + ChatId chatId, + BotRef bot) { + InlineKeyboard keyboard(3); + const std::string text = utils::utf8str(u8"📝 Продолжите редактирование запроса или объявите личный ингредиент"); + + auto searchButton = std::make_shared(); + searchButton->text = utils::utf8str(u8"🛒 Редактировать"); + searchButton->switchInlineQueryCurrentChat = ""; + keyboard[0].push_back(std::move(searchButton)); + // Mark as ingredient + keyboard[1].push_back( + makeCallbackButton(std::format("Создать личный ингредиент: {}", state.inlineQuery), "create_ingredient")); + keyboard[2].push_back(makeCallbackButton(u8"↩️ Назад", "back")); + + if (auto messageId = message::getMessageId(userId)) { + bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); + } +} +} // namespace cookcookhnya::render::suggest_custom_ingredient diff --git a/src/render/storage/ingredients/view.hpp b/src/render/storage/ingredients/view.hpp index 6cfe491d..baedff76 100644 --- a/src/render/storage/ingredients/view.hpp +++ b/src/render/storage/ingredients/view.hpp @@ -1,14 +1,19 @@ #pragma once + #include "render/common.hpp" #include "states.hpp" -#include namespace cookcookhnya::render::storage::ingredients { -void renderIngredientsListSearch(const states::StorageIngredientsList& state, - size_t numOfIngredientsOnPage, - UserId userId, - ChatId chatId, - BotRef bot); +void renderIngredientsListSearch(const states::StorageIngredientsList& state, UserId userId, ChatId chatId, BotRef bot); } // namespace cookcookhnya::render::storage::ingredients + +namespace cookcookhnya::render::suggest_custom_ingredient { + +void renderSuggestIngredientCustomisation(const states::StorageIngredientsList& state, + UserId userId, + ChatId chatId, + BotRef bot); + +} // namespace cookcookhnya::render::suggest_custom_ingredient diff --git a/src/render/storage/members/add.cpp b/src/render/storage/members/add.cpp index 27a97565..662a7895 100644 --- a/src/render/storage/members/add.cpp +++ b/src/render/storage/members/add.cpp @@ -1,12 +1,16 @@ #include "add.hpp" +#include "backend/api/storages.hpp" #include "backend/id_types.hpp" #include "message_tracker.hpp" #include "render/common.hpp" +#include "utils/u8format.hpp" #include "utils/utils.hpp" +#include #include +#include #include #include #include @@ -14,35 +18,39 @@ namespace cookcookhnya::render::storage::members { void renderStorageMemberAddition( - const api::StorageId& storageId, UserId userId, ChatId chatId, BotRef bot, StorageApiRef storageApi) { + const api::StorageId& storageId, UserId userId, ChatId chatId, BotRef bot, api::StorageApiRef storageApi) { auto storage = storageApi.get(userId, storageId); - const int buttonRows = 2; + const std::size_t buttonRows = 2; InlineKeyboard keyboard(buttonRows); keyboard[0].push_back(makeCallbackButton(u8"🔗 Создать ссылку", "create_link")); keyboard[1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); auto text = utils::utf8str(u8"📩 Создайте ссылку или перешлите сообщение пользователя, чтобы добавить его в хранилище.\n"); auto messageId = message::getMessageId(userId); - if (messageId) { - bot.editMessageText(text, chatId, *messageId, "", "", nullptr, makeKeyboardMarkup(std::move(keyboard))); - } + if (messageId) + bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); }; void renderShareLinkMemberAddition( - const api::StorageId& storageId, UserId userId, ChatId chatId, BotRef bot, StorageApiRef storageApi) { + const api::StorageId& storageId, UserId userId, ChatId chatId, BotRef bot, api::StorageApiRef storageApi) { auto storage = storageApi.get(userId, storageId); - const int buttonRows = 2; - InlineKeyboard keyboard(buttonRows); + const std::size_t buttonRows = 2; + InlineKeyboard keyboard{buttonRows}; auto inviteButton = std::make_shared(); inviteButton->text = utils::utf8str(u8"📤 Поделиться"); - auto hash = storageApi.inviteMember(userId, storageId); - const auto telegramBotAlias = bot.getUnderlying().getMe()->username; - auto inviteText = "Нажми на ссылку, чтобы стать участником хранилища 🍱**" + storage.name + - "** в CookCookhNya!\nhttps://t.me/" + telegramBotAlias + "?start=" + hash; - inviteButton->url = "https://t.me/share/url?url=" + inviteText; + const std::string botAlias = bot.getUnderlying().getMe()->username; + const api::InvitationId hash = storageApi.inviteMember(userId, storageId); + const std::string storageUrl = std::format("https://t.me/{}?start=invite_{}", botAlias, hash); + const std::string shareText = utils::u8format( + "{} **{}** {}", u8"Нажми на ссылку, чтобы стать участником хранилища 🍱", storage.name, "в CookCookhNya!"); + + boost::urls::url url{"https://t.me/share/url"}; + url.params().append({"url", storageUrl}); + url.params().append({"text", shareText}); + inviteButton->url = url.buffer(); keyboard[0].push_back(std::move(inviteButton)); keyboard[1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); diff --git a/src/render/storage/members/add.hpp b/src/render/storage/members/add.hpp index 18462217..6bcabd1f 100644 --- a/src/render/storage/members/add.hpp +++ b/src/render/storage/members/add.hpp @@ -1,14 +1,15 @@ #pragma once +#include "backend/api/storages.hpp" #include "backend/id_types.hpp" #include "render/common.hpp" namespace cookcookhnya::render::storage::members { void renderStorageMemberAddition( - const api::StorageId& storageId, UserId userId, ChatId chatId, BotRef bot, StorageApiRef storageApi); + const api::StorageId& storageId, UserId userId, ChatId chatId, BotRef bot, api::StorageApiRef storageApi); void renderShareLinkMemberAddition( - const api::StorageId& storageId, UserId userId, ChatId chatId, BotRef bot, StorageApiRef storageApi); + const api::StorageId& storageId, UserId userId, ChatId chatId, BotRef bot, api::StorageApiRef storageApi); } // namespace cookcookhnya::render::storage::members diff --git a/src/render/storage/members/delete.cpp b/src/render/storage/members/delete.cpp index 2a5bff41..f557205f 100644 --- a/src/render/storage/members/delete.cpp +++ b/src/render/storage/members/delete.cpp @@ -1,5 +1,6 @@ #include "delete.hpp" +#include "backend/api/storages.hpp" #include "backend/id_types.hpp" #include "message_tracker.hpp" #include "render/common.hpp" @@ -14,11 +15,11 @@ namespace cookcookhnya::render::storage::members { void renderStorageMemberDeletion( - const api::StorageId& storageId, UserId userId, ChatId chatId, BotRef bot, StorageApiRef storageApi) { + const api::StorageId& storageId, UserId userId, ChatId chatId, BotRef bot, api::StorageApiRef storageApi) { auto storage = storageApi.get(userId, storageId); auto members = storageApi.getStorageMembers(userId, storageId); - const unsigned int buttonRows = members.size(); + const std::size_t buttonRows = members.size(); InlineKeyboard keyboard(buttonRows); keyboard[0].push_back(makeCallbackButton(u8"↩️ Назад", "cancel_member_deletion")); size_t k = 1; diff --git a/src/render/storage/members/delete.hpp b/src/render/storage/members/delete.hpp index 71dae8c0..ef17bc5b 100644 --- a/src/render/storage/members/delete.hpp +++ b/src/render/storage/members/delete.hpp @@ -1,11 +1,12 @@ #pragma once +#include "backend/api/storages.hpp" #include "backend/id_types.hpp" #include "render/common.hpp" namespace cookcookhnya::render::storage::members { void renderStorageMemberDeletion( - const api::StorageId& storageId, UserId userId, ChatId chatId, BotRef bot, StorageApiRef storageApi); + const api::StorageId& storageId, UserId userId, ChatId chatId, BotRef bot, api::StorageApiRef storageApi); } // namespace cookcookhnya::render::storage::members diff --git a/src/render/storage/members/view.cpp b/src/render/storage/members/view.cpp index ae2e32f1..f1fd4a2a 100644 --- a/src/render/storage/members/view.cpp +++ b/src/render/storage/members/view.cpp @@ -1,10 +1,12 @@ #include "view.hpp" +#include "backend/api/storages.hpp" #include "backend/id_types.hpp" #include "message_tracker.hpp" #include "render/common.hpp" #include "utils/utils.hpp" +#include #include #include #include @@ -19,18 +21,17 @@ void renderMemberList(bool toBeEdited, UserId userId, ChatId chatId, BotRef bot, - StorageApiRef storageApi) { + api::StorageApiRef storageApi) { auto storage = storageApi.get(userId, storageId); const bool isOwner = storage.ownerId == userId; - const int buttonRows = isOwner ? 2 : 1; + const std::size_t buttonRows = isOwner ? 2 : 1; InlineKeyboard keyboard(buttonRows); if (isOwner) { keyboard[0].push_back(makeCallbackButton(u8"🔐 Добавить", "add")); - if (storageApi.getStorageMembers(userId, storageId).size() > 1) { + if (storageApi.getStorageMembers(userId, storageId).size() > 1) keyboard[0].push_back(makeCallbackButton(u8"🔒 Удалить", "delete")); - } keyboard[1].push_back(makeCallbackButton(u8"↩️Назад", "back")); } else { keyboard[0].push_back(makeCallbackButton(u8"↩️Назад", "back")); @@ -44,9 +45,9 @@ void renderMemberList(bool toBeEdited, for (auto [i, name] : std::views::enumerate(memberNames)) std::format_to(std::back_inserter(list), " {}. {}\n", i + 1, name); auto text = utils::utf8str(u8"👥 Список участников\n") + list; + if (toBeEdited) { - auto messageId = message::getMessageId(userId); - if (messageId) + if (auto messageId = message::getMessageId(userId)) bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); } else { auto messageId = bot.sendMessage(chatId, text, makeKeyboardMarkup(std::move(keyboard))); diff --git a/src/render/storage/members/view.hpp b/src/render/storage/members/view.hpp index 8ab807a8..fd16b370 100644 --- a/src/render/storage/members/view.hpp +++ b/src/render/storage/members/view.hpp @@ -1,5 +1,6 @@ #pragma once +#include "backend/api/storages.hpp" #include "backend/id_types.hpp" #include "render/common.hpp" @@ -10,6 +11,6 @@ void renderMemberList(bool toBeEdited, UserId userId, ChatId chatId, BotRef bot, - StorageApiRef storageApi); + api::StorageApiRef storageApi); } // namespace cookcookhnya::render::storage::members diff --git a/src/render/storage/view.cpp b/src/render/storage/view.cpp index e1d9eb3c..e5b4b60f 100644 --- a/src/render/storage/view.cpp +++ b/src/render/storage/view.cpp @@ -1,5 +1,6 @@ #include "view.hpp" +#include "backend/api/storages.hpp" #include "backend/id_types.hpp" #include "message_tracker.hpp" #include "render/common.hpp" @@ -10,16 +11,28 @@ namespace cookcookhnya::render::storage { -void renderStorageView(api::StorageId storageId, UserId userId, ChatId chatId, BotRef bot, StorageApiRef storageApi) { +void renderStorageView( + api::StorageId storageId, UserId userId, ChatId chatId, BotRef bot, api::StorageApiRef storageApi) { auto storage = storageApi.get(userId, storageId); - const std::size_t buttonRows = 2; + const std::size_t buttonRows = storage.ownerId == userId ? 3 : 2; InlineKeyboard keyboard(buttonRows); - keyboard[0].reserve(2); - keyboard[0].push_back(makeCallbackButton(u8"🍗 Продукты", "ingredients")); - keyboard[0].push_back(makeCallbackButton(u8"👥 Участники", "members")); - keyboard[1].reserve(2); - keyboard[1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - keyboard[1].push_back(makeCallbackButton(u8"😋 Хочу кушать!", "wanna_eat")); + if (storage.ownerId == userId) { + keyboard[0].reserve(2); + keyboard[0].push_back(makeCallbackButton(u8"🍗 Продукты", "ingredients")); + keyboard[0].push_back(makeCallbackButton(u8"👥 Участники", "members")); + keyboard[1].push_back(makeCallbackButton(u8"😋 Хочу кушать!", "wanna_eat")); + keyboard[2].reserve(2); + keyboard[2].push_back(makeCallbackButton(u8"↩️ Назад", "back")); + keyboard[2].push_back(makeCallbackButton(u8"🚮 Удалить", "delete")); + } else { + keyboard[0].reserve(2); + keyboard[0].push_back(makeCallbackButton(u8"🍗 Продукты", "ingredients")); + keyboard[0].push_back(makeCallbackButton(u8"👥 Участники", "members")); + keyboard[1].reserve(2); + keyboard[1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); + keyboard[1].push_back(makeCallbackButton(u8"😋 Хочу кушать!", "wanna_eat")); + } + auto text = utils::utf8str(u8"Вы находитесь в хранилище 🍱 ") + storage.name + "\n"; auto messageId = message::getMessageId(userId); if (messageId) { diff --git a/src/render/storage/view.hpp b/src/render/storage/view.hpp index 15240b5e..b4d1245b 100644 --- a/src/render/storage/view.hpp +++ b/src/render/storage/view.hpp @@ -1,10 +1,12 @@ #pragma once +#include "backend/api/storages.hpp" #include "backend/id_types.hpp" #include "render/common.hpp" namespace cookcookhnya::render::storage { -void renderStorageView(api::StorageId storageId, UserId userId, ChatId chatId, BotRef bot, StorageApiRef storageApi); +void renderStorageView( + api::StorageId storageId, UserId userId, ChatId chatId, BotRef bot, api::StorageApiRef storageApi); } // namespace cookcookhnya::render::storage diff --git a/src/render/storages_list/create.cpp b/src/render/storages_list/create.cpp index 0d99ae7f..6617ea55 100644 --- a/src/render/storages_list/create.cpp +++ b/src/render/storages_list/create.cpp @@ -6,7 +6,7 @@ #include -namespace cookcookhnya::render::create_storage { +namespace cookcookhnya::render::storages_list { void renderStorageCreation(ChatId chatId, UserId userId, BotRef bot) { // BackendProvider bkn InlineKeyboard keyboard(1); @@ -18,4 +18,4 @@ void renderStorageCreation(ChatId chatId, UserId userId, BotRef bot) { // Backen } }; -} // namespace cookcookhnya::render::create_storage +} // namespace cookcookhnya::render::storages_list diff --git a/src/render/storages_list/create.hpp b/src/render/storages_list/create.hpp index c2e9671e..c7c39418 100644 --- a/src/render/storages_list/create.hpp +++ b/src/render/storages_list/create.hpp @@ -2,8 +2,8 @@ #include "render/common.hpp" -namespace cookcookhnya::render::create_storage { +namespace cookcookhnya::render::storages_list { void renderStorageCreation(ChatId chatId, UserId userId, BotRef bot); -} // namespace cookcookhnya::render::create_storage +} // namespace cookcookhnya::render::storages_list diff --git a/src/render/storages_list/delete.cpp b/src/render/storages_list/delete.cpp deleted file mode 100644 index 083b70ba..00000000 --- a/src/render/storages_list/delete.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "delete.hpp" - -#include "message_tracker.hpp" -#include "render/common.hpp" -#include "utils/to_string.hpp" -#include "utils/utils.hpp" - -#include -#include -#include - -namespace cookcookhnya::render::delete_storage { - -void renderStorageDeletion(ChatId chatId, BotRef bot, UserId userId, StorageApiRef storageApi) { - auto storages = storageApi.getStoragesList(userId); - size_t numStoragesOwner = 0; - for (auto& storage : storages) { - if (userId == storage.ownerId) { - numStoragesOwner++; - } - } - InlineKeyboard keyboard(numStoragesOwner + 1); - keyboard[0].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - size_t k = 1; - for (auto& storage : storages) { - if (userId == storage.ownerId) { - keyboard[k++].push_back(makeCallbackButton(std::format("{} {}", utils::utf8str(u8"🍱"), storage.name), - "st__" + utils::to_string(storage.id))); - } - } - auto text = utils::utf8str(u8"🚮 Выберите хранилище для удаления"); - auto messageId = message::getMessageId(userId); - if (messageId) - bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); -}; - -} // namespace cookcookhnya::render::delete_storage diff --git a/src/render/storages_list/delete.hpp b/src/render/storages_list/delete.hpp deleted file mode 100644 index 23f7eace..00000000 --- a/src/render/storages_list/delete.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "render/common.hpp" - -namespace cookcookhnya::render::delete_storage { - -void renderStorageDeletion(ChatId chatId, BotRef bot, UserId userId, StorageApiRef storageApi); - -} // namespace cookcookhnya::render::delete_storage diff --git a/src/render/storages_list/view.cpp b/src/render/storages_list/view.cpp index bb6c657e..02fe41ea 100644 --- a/src/render/storages_list/view.cpp +++ b/src/render/storages_list/view.cpp @@ -1,45 +1,43 @@ #include "view.hpp" +#include "backend/api/storages.hpp" #include "message_tracker.hpp" #include "render/common.hpp" -#include "utils/to_string.hpp" #include "utils/utils.hpp" #include +#include #include namespace cookcookhnya::render::storages_list { using namespace tg_types; +using namespace std::views; + +void renderStorageList(bool toBeEdited, UserId userId, ChatId chatId, BotRef bot, api::StorageApiRef storageApi) { + const auto text = utils::utf8str(u8"🍱 Ваши хранилища"); -void renderStorageList(bool toBeEdited, UserId userId, ChatId chatId, BotRef bot, StorageApiRef storageApi) { auto storages = storageApi.getStoragesList(userId); - const std::size_t buttonRows = storages.empty() ? 2 : ((storages.size() + 1) / 2) + 2; // ceiling - InlineKeyboard keyboard(buttonRows); + const std::size_t buttonRows = ((storages.size() + 1) / 2) + 1; // ceil(storagesCount / 2) and back + InlineKeyboardBuilder keyboard{buttonRows}; - if (!storages.empty()) { - keyboard[0].reserve(2); - keyboard[0].push_back(makeCallbackButton(u8"🆕 Создать", "create")); - keyboard[0].push_back(makeCallbackButton(u8"🚮 Удалить", "delete")); - } else { - keyboard[0].push_back(makeCallbackButton(u8"🆕 Создать", "create")); + for (auto chunk : storages | chunk(2)) { + keyboard.reserveInRow(2); + for (auto& s : chunk) + keyboard << makeCallbackButton(utils::utf8str(u8"🍱 ") + s.name, utils::to_string(s.id)); + keyboard << NewRow{}; } - for (std::size_t i = 0; i < storages.size(); i++) { - if (i % 2 == 0) - keyboard[1 + (i / 2)].reserve(2); - keyboard[1 + (i / 2)].push_back(makeCallbackButton("🍱 " + storages[i].name, utils::to_string(storages[i].id))); - } - keyboard[buttonRows - 1].push_back(makeCallbackButton(u8"↩️ Назад", "back")); - auto text = utils::utf8str(u8"🍱 Ваши хранилища"); + keyboard << makeCallbackButton(u8"↩️ Назад", "back") << makeCallbackButton(u8"🆕 Создать", "create"); + if (toBeEdited) { auto messageId = message::getMessageId(userId); if (messageId) { - bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); + bot.editMessageText(text, chatId, *messageId, std::move(keyboard)); } } else { - auto message = bot.sendMessage(chatId, text, makeKeyboardMarkup(std::move(keyboard))); + auto message = bot.sendMessage(chatId, text, std::move(keyboard)); message::addMessageId(userId, message->messageId); } } diff --git a/src/render/storages_list/view.hpp b/src/render/storages_list/view.hpp index 2e5ea72b..30204116 100644 --- a/src/render/storages_list/view.hpp +++ b/src/render/storages_list/view.hpp @@ -1,9 +1,10 @@ #pragma once +#include "backend/api/storages.hpp" #include "render/common.hpp" namespace cookcookhnya::render::storages_list { -void renderStorageList(bool toBeEdited, UserId userId, ChatId chatId, BotRef bot, StorageApiRef storageApi); +void renderStorageList(bool toBeEdited, UserId userId, ChatId chatId, BotRef bot, api::StorageApiRef storageApi); } // namespace cookcookhnya::render::storages_list diff --git a/src/render/storages_selection/view.cpp b/src/render/storages_selection/view.cpp index 01bf062c..48205181 100644 --- a/src/render/storages_selection/view.cpp +++ b/src/render/storages_selection/view.cpp @@ -1,51 +1,61 @@ #include "view.hpp" -#include "backend/id_types.hpp" +#include "backend/api/storages.hpp" +#include "backend/models/storage.hpp" #include "message_tracker.hpp" #include "render/common.hpp" -#include "utils/to_string.hpp" +#include "states.hpp" #include "utils/utils.hpp" #include #include #include +#include #include #include #include namespace cookcookhnya::render::select_storages { +using api::models::storage::StorageSummary; +using states::StoragesSelection; using namespace tg_types; - -void renderStorageSelection(const std::vector& selected_storages, - UserId userId, - ChatId chatId, - BotRef bot, - StorageApiRef storageApi) { - auto storages = storageApi.getStoragesList(userId); - const std::size_t buttonRows = ((storages.size() + 1) / 2) + 1; - InlineKeyboard keyboard(buttonRows); - - for (std::size_t i = 0; i < storages.size(); ++i) { - if (i % 2 == 0) - keyboard[i / 2].reserve(2); - const bool isSelected = std::ranges::find(selected_storages, storages[i].id) != selected_storages.end(); - - std::string emoji = utils::utf8str(isSelected ? u8"[ + ]" : u8"[ᅠ]"); - const char* actionPrefix = isSelected ? "in__" : "out_"; - const std::string text = std::format("{} {}", emoji, storages[i].name); - const std::string data = actionPrefix + utils::to_string(storages[i].id); - keyboard[i / 2].push_back(makeCallbackButton(text, data)); - } - keyboard[buttonRows - 1].reserve(2); - keyboard[buttonRows - 1].push_back(makeCallbackButton(u8"↩️ Назад", "cancel")); - if (!selected_storages.empty()) { - keyboard[buttonRows - 1].push_back(makeCallbackButton(u8"▶️ Подтвердить", "confirm")); +using namespace std::views; + +void renderStorageSelection( + const StoragesSelection& state, UserId userId, ChatId chatId, BotRef bot, api::StorageApiRef storageApi) { + const auto& selectedStorages = state.selectedStorages; + auto allStorages = storageApi.getStoragesList(userId); + + const std::size_t buttonRows = ((allStorages.size() + 1) / 2) + 1; // ceil(storagesCount / 2) and back + InlineKeyboardBuilder keyboard{buttonRows}; + + for (auto chunk : allStorages | chunk(2)) { + keyboard.reserveInRow(2); + for (auto& storage : chunk) { + const bool isSelected = std::ranges::contains(selectedStorages, storage.id, &StorageSummary::id); + std::string emoji = utils::utf8str(isSelected ? u8"[ + ]" : u8"[ᅠ]"); + const char* actionPrefix = isSelected ? "+" : "-"; + const std::string text = std::format("{} {}", emoji, storage.name); + const std::string data = actionPrefix + utils::to_string(storage.id); + keyboard << makeCallbackButton(text, data); + } + keyboard << NewRow{}; } + + keyboard.reserveInRow(2); + keyboard << makeCallbackButton(u8"↩️ Назад", "cancel"); + if (!selectedStorages.empty()) + keyboard << makeCallbackButton(u8"▶️ Подтвердить", "confirm"); + auto text = utils::utf8str(u8"🍱 Откуда брать продукты?"); auto messageId = message::getMessageId(userId); if (messageId) - bot.editMessageText(text, chatId, *messageId, makeKeyboardMarkup(std::move(keyboard))); + bot.editMessageText(text, chatId, *messageId, std::move(keyboard).build()); + else { + auto message = bot.sendMessage(chatId, text, std::move(keyboard).build()); + message::addMessageId(userId, message->messageId); + } } } // namespace cookcookhnya::render::select_storages diff --git a/src/render/storages_selection/view.hpp b/src/render/storages_selection/view.hpp index f490b55f..4fe026f2 100644 --- a/src/render/storages_selection/view.hpp +++ b/src/render/storages_selection/view.hpp @@ -1,16 +1,12 @@ #pragma once -#include "backend/id_types.hpp" +#include "backend/api/storages.hpp" #include "render/common.hpp" - -#include +#include "states.hpp" namespace cookcookhnya::render::select_storages { -void renderStorageSelection(const std::vector& selected_storages, - UserId userId, - ChatId chatId, - BotRef bot, - StorageApiRef storageApi); +void renderStorageSelection( + const states::StoragesSelection& state, UserId userId, ChatId chatId, BotRef bot, api::StorageApiRef storageApi); } // namespace cookcookhnya::render::select_storages diff --git a/src/states.hpp b/src/states.hpp index 0e247579..b7a75ffa 100644 --- a/src/states.hpp +++ b/src/states.hpp @@ -2,13 +2,20 @@ #include "backend/id_types.hpp" #include "backend/models/ingredient.hpp" +#include "backend/models/recipe.hpp" #include "backend/models/shopping_list.hpp" +#include "backend/models/storage.hpp" #include "utils/fast_sorted_db.hpp" +#include "utils/utils.hpp" +#include +#include #include #include #include +#include +#include #include #include #include @@ -16,36 +23,83 @@ namespace cookcookhnya::states { -namespace detail { +namespace helpers { struct StorageIdMixin { api::StorageId storageId; - StorageIdMixin(api::StorageId storageId) : storageId{storageId} {} // NOLINT(*-explicit-*) + StorageIdMixin(api::StorageId storageId) : storageId{storageId} {} // NOLINT(*explicit*) }; -} // namespace detail +struct Pagination { + std::size_t pageNo; + std::size_t totalItems; +}; + +struct SelectableShoppingListItem : api::models::shopping_list::ShoppingListItem { + bool selected = false; + SelectableShoppingListItem(api::models::shopping_list::ShoppingListItem item) // NOLINT(*explicit*) + : ShoppingListItem{std::move(item)} {} +}; + +struct SelectableIngredient : api::models::ingredient::Ingredient { + bool selected = false; + SelectableIngredient(api::models::ingredient::Ingredient item) // NOLINT(*explicit*) + : Ingredient{std::move(item)} {} +}; + +} // namespace helpers struct MainMenu {}; +struct RecipesSearch { + std::string query; + helpers::Pagination pagination; + std::vector page; +}; + +struct RecipeView { + std::optional prevState; + api::models::recipe::RecipeDetails recipe; + api::RecipeId recipeId; +}; + struct PersonalAccountMenu {}; +struct TotalPublicationHistory { + std::size_t pageNo; +}; struct CustomIngredientsList {}; struct CustomIngredientCreationEnterName {}; struct CustomIngredientConfirmation { std::string name; + + // All optionals are for "back" from this menu, so this state won't erase all info + std::optional recipeFrom; + std::optional> ingredients; + std::optional storageFrom; + + explicit CustomIngredientConfirmation( + std::string name, + std::optional recipeId = std::nullopt, + std::optional> ingredients = std::nullopt, + std::optional storageId = std::nullopt) + : name(std::move(name)), recipeFrom(recipeId), ingredients(std::move(ingredients)), storageFrom(storageId) {}; }; +struct CustomIngredientDeletion {}; struct CustomIngredientPublish {}; struct StorageList {}; -struct StorageDeletion {}; struct StorageCreationEnterName {}; -struct StorageView : detail::StorageIdMixin {}; -struct StorageMemberView : detail::StorageIdMixin {}; -struct StorageMemberAddition : detail::StorageIdMixin {}; -struct StorageMemberDeletion : detail::StorageIdMixin {}; +struct StorageView : helpers::StorageIdMixin { + std::string name; +}; +struct StorageDeletion : helpers::StorageIdMixin {}; +struct StorageMemberView : helpers::StorageIdMixin {}; +struct StorageMemberAddition : helpers::StorageIdMixin {}; +struct StorageMemberDeletion : helpers::StorageIdMixin {}; -struct StorageIngredientsList : detail::StorageIdMixin { +struct StorageIngredientsList : helpers::StorageIdMixin { using IngredientsDb = utils::FastSortedDb; IngredientsDb storageIngredients; @@ -53,78 +107,162 @@ struct StorageIngredientsList : detail::StorageIdMixin { std::size_t pageNo = 0; std::vector searchItems; std::string inlineQuery; - StorageIngredientsList(api::StorageId storageId, IngredientsDb::Set ingredients, std::string iq) - : StorageIdMixin{storageId}, storageIngredients{std::move(ingredients)}, inlineQuery(std::move(iq)) {} + + template R> + StorageIngredientsList(api::StorageId storageId, R&& ingredients, std::string iq) + : StorageIdMixin{storageId}, storageIngredients{std::forward(ingredients)}, inlineQuery(std::move(iq)) {} +}; + +struct StorageIngredientsDeletion : helpers::StorageIdMixin { + std::vector selectedIngredients; + std::vector storageIngredients; + bool addedToShopList; + std::size_t pageNo; }; struct StoragesSelection { - std::vector storageIds; + std::variant prevState; + std::vector selectedStorages; }; -struct SuggestedRecipeList { +struct SuggestedRecipesList { + private: + using StorageSummary = api::models::storage::StorageSummary; + + public: + using FromMainMenuData = std::pair; + std::variant prevState; std::size_t pageNo; - std::vector storageIds; - bool fromStorage; + + [[nodiscard]] std::vector getStorageIds() const { + if (const auto* prevState_ = std::get_if(&prevState)) + return {prevState_->storageId}; + + if (const auto* prevState_ = std::get_if(&prevState)) + return {prevState_->second.id}; + + if (const auto* prevState_ = std::get_if(&prevState)) + return prevState_->selectedStorages | std::views::transform(&StorageSummary::id) | + std::ranges::to(); + return {}; + } + + [[nodiscard]] std::variant>, std::vector> + getStorages() const { + if (const auto* prevState_ = std::get_if(&prevState)) + return std::vector{StorageSummary{.id = prevState_->storageId, .name = prevState_->name}}; + if (const auto* prevState_ = std::get_if(&prevState)) + return std::vector{prevState_->second}; + if (const auto* prevState_ = std::get_if(&prevState)) + return std::ref(prevState_->selectedStorages); + return std::vector{}; + } }; -struct RecipeView { - std::vector storageIds; +struct CookingPlanning { + private: + using StorageSummary = api::models::storage::StorageSummary; + + public: + enum struct AvailabilityType : std::uint8_t { NOT_AVAILABLE, AVAILABLE, OTHER_STORAGES }; + + struct IngredientAvailability { + cookcookhnya::api::models::recipe::IngredientInRecipe ingredient; + AvailabilityType available = AvailabilityType::NOT_AVAILABLE; + std::vector storages; + }; + + using FromRecipeViewData = std::pair>; + + std::variant prevState; + std::vector addedStorages; + std::vector availability; api::RecipeId recipeId; - bool fromStorage; - size_t pageNo; + + [[nodiscard]] std::variant>, std::vector> + getStorages() const { + if (const auto* prevState_ = std::get_if(&prevState)) + return prevState_->getStorages(); + if (const auto* prevState_ = std::get_if(&prevState)) { + return std::ref(prevState_->second); + } + return std::vector{}; + } }; -struct RecipeStorageAddition { - std::vector storageIds; - api::RecipeId recipeId; - bool fromStorage; - size_t pageNo; +struct CookingPlanningStorageAddition { + CookingPlanning prevState; +}; + +struct ShoppingListCreation { + CookingPlanning prevState; + std::vector selectedIngredients; + std::vector allIngredients; }; struct CustomRecipesList { - size_t pageNo; + std::size_t pageNo; }; struct CustomRecipeIngredientsSearch { using IngredientsDb = utils::FastSortedDb; + api::RecipeId recipeId; IngredientsDb recipeIngredients; + std::string query; std::size_t totalFound = 0; std::size_t pageNo = 0; std::vector searchItems; - std::string inlineQuery; - CustomRecipeIngredientsSearch(api::RecipeId recipeId, IngredientsDb::Set ingredients, std::string iq) - : recipeId(recipeId), recipeIngredients{std::move(ingredients)}, inlineQuery(std::move(iq)) {} + template R> + CustomRecipeIngredientsSearch(api::RecipeId recipeId, R&& ingredients, std::string inlineQuery) + : recipeId(recipeId), recipeIngredients{std::forward(ingredients)}, query(std::move(inlineQuery)) {} }; -struct RecipeCustomView { +struct CustomRecipeView { api::RecipeId recipeId; - size_t pageNo; + std::size_t pageNo; std::vector ingredients; + std::string recipeName; }; struct CreateCustomRecipe { - api::RecipeId recipeId; - size_t pageNo; -}; - -struct ShoppingListCreation { - std::vector storageIdsFrom; - api::RecipeId recipeIdFrom; - std::vector ingredientIdsInList; - bool fromStorage; std::size_t pageNo; }; struct RecipeIngredientsSearch { api::RecipeId recipeId; - size_t pageNo; + std::size_t pageNo; }; -struct ShoppingListView { - using ItemsDb = utils::FastSortedDb; +struct ShoppingListView { // NOLINT(*member-init) // Strange. Flags only this struct due to ItemsDb + using ItemsDb = + utils::FastSortedDb; ItemsDb items; + bool canBuy; +}; +struct ShoppingListStorageSelectionToBuy { + ShoppingListView prevState; + std::vector selectedIngredients; + std::vector storages; +}; +struct ShoppingListIngredientSearch { + ShoppingListView prevState; + std::string query; + helpers::Pagination pagination; + std::vector page; +}; + +struct CustomRecipePublicationHistory { + api::RecipeId recipeId; + std::size_t pageNo; + std::string recipeName; + std::string errorReport; +}; + +struct CookingIngredientsSpending { // NOLINT(*member-init) + CookingPlanning prevState; + std::optional storageId; + std::vector ingredients; }; using State = std::variant; + CookingPlanningStorageAddition, + RecipeIngredientsSearch, + CustomRecipePublicationHistory, + TotalPublicationHistory, + ShoppingListIngredientSearch, + RecipesSearch, + RecipeView, + CookingIngredientsSpending>; using StateManager = tg_stater::StateProxy>; diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index 77e53af3..3c20a6f7 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -3,4 +3,5 @@ target_sources(main PRIVATE src/utils/to_string.cpp src/utils/utils.cpp src/utils/uuid.cpp + src/utils/ingredients_availability.cpp ) diff --git a/src/utils/fast_sorted_db.hpp b/src/utils/fast_sorted_db.hpp index 6a4e9e10..0ec3cb26 100644 --- a/src/utils/fast_sorted_db.hpp +++ b/src/utils/fast_sorted_db.hpp @@ -1,7 +1,10 @@ #pragma once +#include "utils/utils.hpp" + #include -#include +#include +#include #include #include #include @@ -11,38 +14,37 @@ namespace cookcookhnya::utils { template class FastSortedDb { using Id = std::remove_cvref_t>; - - struct Comparator { - bool operator()(const T& l, const T& r) const { - return std::ranges::less{}(std::invoke(SortProjection, l), std::invoke(SortProjection, r)); - } - }; + using SortKey = std::remove_cvref_t>; public: - using Set = std::set; + using mapped_type = T; + using Map = std::map; private: - Set items; - std::unordered_map index; + Map items; + std::unordered_map index; public: FastSortedDb() = default; - FastSortedDb(Set items) : items{std::move(items)} { // NOLINT(*explicit*) - for (auto it = this->items.begin(); it != this->items.end(); ++it) - index.try_emplace(std::invoke(IdProjection, std::as_const(*it)), it); + template R> + FastSortedDb(R&& items) { // NOLINT(*explicit*) + for (auto&& item : std::ranges::views::all(std::forward(items))) + put(std::forward(item)); } void put(const T& item) { - const auto [it, inserted] = items.insert(item); + SortKey key = std::invoke(SortProjection, item); + auto [it, inserted] = this->items.emplace(std::move(key), item); if (inserted) - index.try_emplace(std::invoke(IdProjection, std::as_const(*it)), it); + index.try_emplace(std::invoke(IdProjection, std::as_const(it->second)), std::move(it)); } void put(T&& item) { - const auto [it, inserted] = items.insert(std::move(item)); + SortKey key = std::invoke(SortProjection, std::as_const(item)); + auto [it, inserted] = this->items.emplace(std::move(key), std::move(item)); if (inserted) - index.try_emplace(std::invoke(IdProjection, std::as_const(*it)), it); + index.try_emplace(std::invoke(IdProjection, std::as_const(it->second)), std::move(it)); } void remove(const Id& id) { @@ -53,9 +55,37 @@ class FastSortedDb { index.erase(it); } - const Set& getAll() const { + // as optional non-owning reference + [[nodiscard]] T* operator[](const Id& id) { + auto it = index.find(id); + if (it == index.end()) + return nullptr; + return &it->second->second; + } + + // as optional non-owning reference + [[nodiscard]] const T* operator[](const Id& id) const { + auto it = index.find(id); + if (it == index.end()) + return nullptr; + return &it->second->second; + } + + [[nodiscard]] Map& getAll() { + return items; + } + + [[nodiscard]] const Map& getAll() const { return items; } + + [[nodiscard]] auto getValues() { + return items | std::views::values; + } + + [[nodiscard]] auto getValues() const { + return items | std::views::values; + } }; } // namespace cookcookhnya::utils diff --git a/src/utils/ingredients_availability.cpp b/src/utils/ingredients_availability.cpp new file mode 100644 index 00000000..f677f024 --- /dev/null +++ b/src/utils/ingredients_availability.cpp @@ -0,0 +1,99 @@ +#include "ingredients_availability.hpp" + +#include "backend/api/api.hpp" +#include "backend/id_types.hpp" +#include "backend/models/storage.hpp" +#include "states.hpp" +#include "tg_types.hpp" + +#include +#include +#include +#include +#include +#include + +namespace cookcookhnya::utils { + +using namespace api; +using namespace api::models::storage; +using namespace tg_types; +using IngredientAvailability = states::CookingPlanning::IngredientAvailability; +using AvailabilityType = states::CookingPlanning::AvailabilityType; +using std::ranges::to; + +std::vector inStoragesAvailability(const std::vector& selectedStorages, + RecipeId recipeId, + UserId userId, + const api::ApiClient& api) { + auto allStorages = api.getStoragesApi().getStoragesList(userId); + auto recipe = api.getRecipesApi().getSuggested(userId, recipeId); + + auto selectedStoragesSet = selectedStorages | to(); + + std::unordered_map allStoragesMap; + for (const auto& storage : allStorages) { + allStoragesMap.emplace(storage.id, storage); + } + + std::vector result; + + for (auto& ingredient : recipe.ingredients) { + IngredientAvailability availability; + std::vector storages; + + bool hasInSelected = false; + bool hasInOther = false; + + for (auto& storage : ingredient.inStorages) { + auto it = allStoragesMap.find(storage.id); + if (it == allStoragesMap.end()) + continue; + if (selectedStoragesSet.contains(storage.id)) { + storages.push_back(it->second); + hasInSelected = true; + } else { + storages.push_back(it->second); + hasInOther = true; + } + } + + availability.ingredient = std::move(ingredient); + if (hasInSelected) { + availability.available = AvailabilityType::AVAILABLE; + availability.storages = std::move(storages); + } else if (hasInOther) { + availability.available = AvailabilityType::OTHER_STORAGES; + availability.storages = std::move(storages); + } else { + availability.available = AvailabilityType::NOT_AVAILABLE; + } + + result.push_back(std::move(availability)); + } + + return result; +} + +void addStorage(std::vector& availability, const StorageSummary& storage) { + for (auto& info : availability) { + auto it = std::ranges::find(info.storages, storage.id, &StorageSummary::id); + if (it != info.storages.end()) { + info.storages.erase(it); + info.available = AvailabilityType::AVAILABLE; + } + } +} + +void deleteStorage(std::vector& availability, const StorageSummary& storage) { + for (auto& infoPair : availability) { + for (auto& storage_ : infoPair.ingredient.inStorages) { + if (storage.id == storage_.id) { + infoPair.storages.push_back(storage); + infoPair.available = AvailabilityType::OTHER_STORAGES; + } + } + } +} + +} // namespace cookcookhnya::utils diff --git a/src/utils/ingredients_availability.hpp b/src/utils/ingredients_availability.hpp new file mode 100644 index 00000000..5b800a50 --- /dev/null +++ b/src/utils/ingredients_availability.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "backend/api/api.hpp" +#include "backend/id_types.hpp" +#include "backend/models/storage.hpp" +#include "states.hpp" +#include "tg_types.hpp" + +#include + +namespace cookcookhnya::utils { + +std::vector +inStoragesAvailability(const std::vector& selectedStorages, + api::RecipeId recipeId, + tg_types::UserId userId, + const api::ApiClient& api); + +void addStorage(std::vector& availability, + const api::models::storage::StorageSummary& storage); + +void deleteStorage(std::vector& availability, + const api::models::storage::StorageSummary& storage); + +} // namespace cookcookhnya::utils diff --git a/src/utils/parsing.cpp b/src/utils/parsing.cpp index 508aa4a4..2018f09a 100644 --- a/src/utils/parsing.cpp +++ b/src/utils/parsing.cpp @@ -5,7 +5,9 @@ #include #include +#include #include +#include #include namespace cookcookhnya::utils { @@ -23,4 +25,14 @@ Uuid parse(std::string_view s) noexcept(false) { return boost::lexical_cast(s); } +std::chrono::system_clock::time_point parseIsoTime(std::string s) { + std::chrono::system_clock::time_point tp; + std::istringstream ss(std::move(s)); + ss >> std::chrono::parse("%FT%TZ", tp); // Parse as UTC + if (ss.fail()) { + throw std::runtime_error("Could not parse datetime"); + } + return tp; // Still UTC, but debugger may show local time +} + } // namespace cookcookhnya::utils diff --git a/src/utils/parsing.hpp b/src/utils/parsing.hpp index 50f82e87..0885071b 100644 --- a/src/utils/parsing.hpp +++ b/src/utils/parsing.hpp @@ -3,8 +3,10 @@ #include "uuid.hpp" #include +#include #include #include +#include #include #include @@ -34,4 +36,6 @@ T parse(std::string_view s) noexcept(false) { template <> Uuid parse(std::string_view s) noexcept(false); +std::chrono::system_clock::time_point parseIsoTime(std::string s); + } // namespace cookcookhnya::utils diff --git a/src/utils/to_string.cpp b/src/utils/to_string.cpp index 38941db2..c8de6895 100644 --- a/src/utils/to_string.cpp +++ b/src/utils/to_string.cpp @@ -1,15 +1,18 @@ #include "to_string.hpp" -#include "uuid.hpp" - -#include - +#include +#include #include +#include namespace cookcookhnya::utils { -std::string to_string(const Uuid& u) { - return boost::lexical_cast(u); +std::string to_string(std::chrono::system_clock::time_point tp) { + const std::time_t time = std::chrono::system_clock::to_time_t(tp); + const std::tm tm = *std::localtime(&time); + std::ostringstream oss; + oss << std::put_time(&tm, "%Y-%m-%d %H:%M"); + return std::move(oss).str(); } } // namespace cookcookhnya::utils diff --git a/src/utils/to_string.hpp b/src/utils/to_string.hpp index 5179ec0a..2e8532aa 100644 --- a/src/utils/to_string.hpp +++ b/src/utils/to_string.hpp @@ -1,7 +1,6 @@ #pragma once -#include "uuid.hpp" - +#include #include namespace cookcookhnya::utils { @@ -14,6 +13,6 @@ std::string to_string(const T& t) { return std::to_string(t); } -std::string to_string(const Uuid& u); +std::string to_string(std::chrono::system_clock::time_point tp); } // namespace cookcookhnya::utils diff --git a/src/utils/u8format.hpp b/src/utils/u8format.hpp new file mode 100644 index 00000000..e171e8f0 --- /dev/null +++ b/src/utils/u8format.hpp @@ -0,0 +1,36 @@ +#include +#include +#include +#include + +namespace cookcookhnya::utils { + +namespace detail { + +template +struct TransformUtf8 { + using type = T; + static T&& transform(T&& t) { // NOLINT(*not-moved) + return std::forward(t); + } +}; + +template +struct TransformUtf8 { // NOLINT(*c-arrays) + using type = std::string; + static std::string transform(const char8_t (&literal)[N]) { // NOLINT(*not-moved,*c-arrays) + return {literal, literal + N - 1}; // NOLINT(*decay) + } +}; + +template +using TransformUtf8_t = TransformUtf8::type; + +} // namespace detail + +template +std::string u8format(std::format_string...> format, Args&&... args) { + return std::format(format, detail::TransformUtf8::transform(std::forward(args))...); +} + +} // namespace cookcookhnya::utils diff --git a/src/utils/utils.hpp b/src/utils/utils.hpp index 25157add..15233d3d 100644 --- a/src/utils/utils.hpp +++ b/src/utils/utils.hpp @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include #include #include @@ -17,4 +19,7 @@ std::shared_ptr make_shared(T&& t) { return std::make_shared>(std::forward(t)); } +template +concept range_of = std::ranges::range && std::convertible_to, ValueType>; + } // namespace cookcookhnya::utils diff --git a/src/utils/uuid.cpp b/src/utils/uuid.cpp index 94fa5072..daeae6d5 100644 --- a/src/utils/uuid.cpp +++ b/src/utils/uuid.cpp @@ -5,6 +5,8 @@ #include #include +#include + namespace boost::uuids { uuid tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { @@ -12,3 +14,11 @@ uuid tag_invoke(json::value_to_tag /*tag*/, const json::value& j) { } } // namespace boost::uuids + +namespace cookcookhnya::utils { + +std::string to_string(const Uuid& u) { + return boost::lexical_cast(u); +} + +} // namespace cookcookhnya::utils diff --git a/src/utils/uuid.hpp b/src/utils/uuid.hpp index 1343dd0f..eec3d1b8 100644 --- a/src/utils/uuid.hpp +++ b/src/utils/uuid.hpp @@ -22,6 +22,12 @@ using Uuid = boost::uuids::uuid; } // namespace cookcookhnya +namespace cookcookhnya::utils { + +std::string to_string(const Uuid& u); + +} // namespace cookcookhnya::utils + template <> struct std::formatter : formatter { template