Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions 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 @@ -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')
}
Expand Down
61 changes: 61 additions & 0 deletions src/components/RecipeKeyword.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<template>
<a v-on:click="clicked" ref="link"><li>{{ keyword }}</li></a>
</template>

<script>
export default {
name: 'RecipeKeyword',
props: ['keyword'],
data () {
return {
};
},
computed: {
},
methods: {
clicked() {
if(!this.$refs.link.classList.contains('disabled')) {
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: var(--border-radius-pill);

/* 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);
}

.disabled li {
background-color: #FFF;
border-color: var(--color-border);
color: var(--color-border);
}

.disabled li:hover {
border-color: var(--color-border);
cursor: default;
}

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
2 changes: 1 addition & 1 deletion src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down