diff --git a/CHANGELOG.md b/CHANGELOG.md index b68c22203..e706b0093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ [#366](https://github.com/nextcloud/cookbook/pull/366/) @christianlupus - Enfoce update of changelog through CI [#366](https://github.com/nextcloud/cookbook/pull/366/) @christianlupus +- Keyword cloud is displayed in recipe + [#373](https://github.com/nextcloud/cookbook/pull/373/) @seyfeb ### Changed - Switch of project ownership to neextcloud organization in GitHub diff --git a/appinfo/routes.php b/appinfo/routes.php index 0458a4441..42b9236c5 100755 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -26,6 +26,7 @@ ['name' => 'config#config', 'url' => '/config', 'verb' => 'POST'], /* API routes */ ['name' => 'main#category', 'url' => '/api/category/{category}', 'verb' => 'GET'], + ['name' => 'main#tags', 'url' => '/api/tags/{keywords}', 'verb' => 'GET'], ['name' => 'main#search', 'url' => '/api/search/{query}', 'verb' => 'GET'], ], diff --git a/lib/Controller/MainController.php b/lib/Controller/MainController.php index 4b4f98e80..424badadf 100755 --- a/lib/Controller/MainController.php +++ b/lib/Controller/MainController.php @@ -192,6 +192,35 @@ public function category($category) } } + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function tags($keywords) + { + $this->dbCacheService->triggerCheck(); + $keywords = urldecode($keywords); + + try { + $recipes = $this->service->getRecipesByKeywords($keywords); + foreach ($recipes as $i => $recipe) { + $recipes[$i]['imageUrl'] = $this->urlGenerator->linkToRoute( + 'cookbook.recipe.image', + [ + 'id' => $recipe['recipe_id'], + 'size' => 'thumb', + 't' => $this->service->getRecipeMTime($recipe['recipe_id']) + ] + ); + } + + return new DataResponse($recipes, Http::STATUS_OK, ['Content-Type' => 'application/json']); + } catch (\Exception $e) { + error_log($e->getMessage()); + return new DataResponse($e->getMessage(), 500); + } + } + /** * @NoAdminRequired * @NoCSRFRequired diff --git a/lib/Db/RecipeDb.php b/lib/Db/RecipeDb.php index 05208a389..16409dcc6 100755 --- a/lib/Db/RecipeDb.php +++ b/lib/Db/RecipeDb.php @@ -234,6 +234,34 @@ public function getRecipesByCategory(string $category, string $user_id) { return $this->unique($result); } + + /** + * @throws \OCP\AppFramework\Db\DoesNotExistException if not found + * + */ + public function getRecipesByKeywords(string $keywords, string $user_id) { + $keywords_arr = explode(',', $keywords); + + $qb = $this->db->getQueryBuilder(); + + $qb->select(['r.recipe_id', 'r.name']) + ->from(self::DB_TABLE_KEYWORDS, 'k') + ->where('k.name IN (:keywords)') + ->andWhere('k.user_id = :user') + ->having('COUNT(DISTINCT k.name) = :keywordsCount') + ->setParameter('user', $user_id, TYPE::INTEGER) + ->setParameter('keywords', $keywords_arr, IQueryBuilder::PARAM_STR_ARRAY) + ->setParameter('keywordsCount', sizeof($keywords_arr), TYPE::INTEGER); + $qb->join('k', self::DB_TABLE_RECIPES, 'r', 'k.recipe_id = r.recipe_id'); + $qb->groupBy(['r.name', 'r.recipe_id']); + $qb->orderBy('r.name'); + + $cursor = $qb->execute(); + $result = $cursor->fetchAll(); + $cursor->closeCursor(); + + return $this->unique($result); + } /** * @throws \OCP\AppFramework\Db\DoesNotExistException if not found diff --git a/lib/Service/RecipeService.php b/lib/Service/RecipeService.php index 90c73567e..4fa47f83d 100755 --- a/lib/Service/RecipeService.php +++ b/lib/Service/RecipeService.php @@ -917,6 +917,18 @@ public function getRecipesByCategory($category) return $this->db->getRecipesByCategory($category, $this->user_id); } + /** + * Get all recipes containing all of the keywords. + * + * @param string $keywords Keywords/tags as a comma-separated string. + * + * @return array + */ + public function getRecipesByKeywords($keywords) + { + return $this->db->getRecipesByKeywords($keywords, $this->user_id); + } + /** * Search for recipes by keywords * diff --git a/src/components/AppControls.vue b/src/components/AppControls.vue index 22a8ba2a3..70fd4b102 100644 --- a/src/components/AppControls.vue +++ b/src/components/AppControls.vue @@ -13,7 +13,7 @@ - + @@ -179,8 +179,8 @@ export default { return t('cookbook', 'Category') } else if (this.$route.name === 'search-name') { return t('cookbook', 'Recipe name') - } else if (this.$route.name === 'search-tag') { - return t('cookbook', 'Tag') + } else if (this.$route.name === 'search-tags') { + return t('cookbook', 'Tags') } else { return t('cookbook', 'Search for recipes') } diff --git a/src/components/RecipeKeyword.vue b/src/components/RecipeKeyword.vue new file mode 100644 index 000000000..e22015cb7 --- /dev/null +++ b/src/components/RecipeKeyword.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/src/components/RecipeView.vue b/src/components/RecipeView.vue index 68b146e44..883b06457 100644 --- a/src/components/RecipeView.vue +++ b/src/components/RecipeView.vue @@ -6,6 +6,11 @@

{{ $store.state.recipe.name }}

+

+

    + +
+

{{ $store.state.recipe.description }}

{{ t('cookbook', 'Source') }}: {{ $store.state.recipe.url }} @@ -50,6 +55,7 @@ import RecipeImages from './RecipeImages' import RecipeIngredient from './RecipeIngredient' import RecipeInstruction from './RecipeInstruction' +import RecipeKeyword from './RecipeKeyword' import RecipeTimer from './RecipeTimer' import RecipeTool from './RecipeTool' @@ -59,6 +65,7 @@ export default { RecipeImages, RecipeIngredient, RecipeInstruction, + RecipeKeyword, RecipeTimer, RecipeTool, }, @@ -67,6 +74,7 @@ export default { // Own properties ingredients: [], instructions: [], + keywords: [], timerCook: null, timerPrep: null, timerTotal: null, @@ -74,6 +82,14 @@ export default { } }, methods: { + /** + * Callback for click on keyword + */ + keywordClicked: function(keyword) { + if(keyword) { + this.$router.push('/tags/'+keyword); + } + }, setup: function() { // Make the control row show that a recipe is loading if (!this.$store.state.recipe) { @@ -109,6 +125,10 @@ export default { $this.instructions = Object.values($this.$store.state.recipe.recipeInstructions) } + if ($this.$store.state.recipe.keywords) { + $this.keywords = String($this.$store.state.recipe.keywords).split(','); + } + if ($this.$store.state.recipe.cookTime) { let cookT = $this.$store.state.recipe.cookTime.match(/PT(\d+?)H(\d+?)M/) $this.timerCook = { hours: parseInt(cookT[1]), minutes: parseInt(cookT[2]) } diff --git a/src/components/SearchResults.vue b/src/components/SearchResults.vue index 1b9552542..a4f937a8f 100644 --- a/src/components/SearchResults.vue +++ b/src/components/SearchResults.vue @@ -24,10 +24,21 @@ export default { if (this.query === 'name') { // Search by name } - if (this.query === 'tag') { - // Search by tag + else if (this.query === 'tags') { + // Search by tags + let $this = this + let tags = this.$route.params.value + $.get(this.$window.baseUrl + '/api/tags/'+tags).done(function(json) { + $this.results = json + }).fail(function (jqXHR, textStatus, errorThrown) { + $this.results = [] + alert(t('cookbook', 'Failed to load recipes with keywords: ' + tags)) + if (errorThrown && errorThrown instanceof Error) { + throw errorThrown + } + }) } - if (this.query === 'cat') { + else if (this.query === 'cat') { // Search by category let $this = this let cat = this.$route.params.value diff --git a/src/router/index.js b/src/router/index.js index d895fbbb8..695943934 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -28,7 +28,7 @@ const routes = [ { path: '/category/:value', name: 'search-category', component: Search, props: { query: 'cat' } }, { path: '/name/:value', name: 'search-name', component: Search, props: { query: 'name' } }, { path: '/search/:value', name: 'search-general', component: Search, props: { query: 'general' } }, - { path: '/tag/:value', name: 'search-tag', component: Search, props: { query: 'tag' } }, + { path: '/tags/:value', name: 'search-tags', component: Search, props: { query: 'tags' } }, // Recipe routes // Vue router has a strange way of determining when it renders a component again and when not.