Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
],

Expand Down
29 changes: 29 additions & 0 deletions lib/Controller/MainController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions lib/Db/RecipeDb.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions lib/Service/RecipeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
4 changes: 3 additions & 1 deletion src/components/AppControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</Breadcrumb>
<!-- SEARCH PAGE -->
<Breadcrumb v-if="isSearch" class="not-link" :title="searchTitle" :disableDrop="true" />
<Breadcrumb v-if="isSearch && $route.params.value" class="active" :title="$route.params.value=='_'?'None':$route.params.value" :disableDrop="true" />
<Breadcrumb v-if="isSearch && $route.params.value" class="active" :title="$route.params.value=='_'?'None':decodeURIComponent($route.params.value)" :disableDrop="true" />
<!-- RECIPE PAGES -->
<!-- Edit recipe -->
<Breadcrumb v-if="isEdit" class="not-link" :title="t('cookbook', 'Edit recipe')" :disableDrop="true" />
Expand Down Expand Up @@ -181,6 +181,8 @@ export default {
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')
}
Expand Down
49 changes: 49 additions & 0 deletions src/components/RecipeKeyword.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<template>
<a v-on:click="clicked"><li>{{ keyword }}</li></a>
</template>

<script>
export default {
name: 'RecipeKeyword',
props: ['keyword'],
data () {
return {
};
},
computed: {
},
methods: {
clicked() {
this.$emit('keyword-clicked')
}
}

}
</script>

<style scoped>

li {
display: inline-block;
margin-right: .3em;
margin-bottom: .3em;
padding: 0px .5em;
border: 1px solid var(--color-border-dark);
border-radius: 4px;

/* prevent text selection - doesn't look good */
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+/Edge */
user-select: none; /* Standard */
}

.active li {
background-color: var(--color-primary);
color: var(--color-primary-text);
}

li:hover, .active li:hover {
border: 1px solid var(--color-primary);
}
</style>
20 changes: 20 additions & 0 deletions src/components/RecipeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
<h2>{{ $store.state.recipe.name }}</h2>

<div class="details">
<p v-if="keywords.length">
<ul v-if="keywords.length">
<RecipeKeyword v-for="(keyword,idx) in keywords" :key="'keyw'+idx" :keyword="keyword" v-on:keyword-clicked="keywordClicked(keyword)" />
</ul>
</p>
<p class="description">{{ $store.state.recipe.description }}</p>
<p v-if="$store.state.recipe.url">
<strong>{{ t('cookbook', 'Source') }}: </strong><a target="_blank" :href="$store.state.recipe.url">{{ $store.state.recipe.url }}</a>
Expand Down Expand Up @@ -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'

Expand All @@ -59,6 +65,7 @@ export default {
RecipeImages,
RecipeIngredient,
RecipeInstruction,
RecipeKeyword,
RecipeTimer,
RecipeTool,
},
Expand All @@ -67,13 +74,22 @@ export default {
// Own properties
ingredients: [],
instructions: [],
keywords: [],
timerCook: null,
timerPrep: null,
timerTotal: null,
tools: [],
}
},
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) {
Expand Down Expand Up @@ -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]) }
Expand Down
17 changes: 14 additions & 3 deletions src/components/SearchResults.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const routes = [
{ 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.
Expand Down