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 @@ -60,6 +60,8 @@
[#435](https://github.com/nextcloud/cookbook/pull/435) @christianlupus
- Images in recipe list are lazily loaded
[#413](https://github.com/nextcloud/cookbook/pull/413/) @seyfeb
- Improved keyword filtering in recipe lists
[#408](https://github.com/nextcloud/cookbook/pull/408/) @seyfeb

### Fixed
- Add a min PHP restriction in the metadata
Expand Down
2 changes: 1 addition & 1 deletion lib/Db/RecipeDb.php
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ public function findRecipes(array $keywords, string $user_id) {
$qb->setParameters($params, $types);
$qb->setParameter('user', $user_id, TYPE::STRING);

$qb->groupBy(['r.name', 'r.recipe_id']);
$qb->groupBy(['r.name', 'r.recipe_id', 'k.name']);
$qb->orderBy('r.name');

$cursor = $qb->execute();
Expand Down
93 changes: 76 additions & 17 deletions src/components/AppIndex.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
<template>
<div>
<ul v-if="keywords.length" class="keywords">
<RecipeKeyword v-for="(keyword,idx) in keywords" :key="'kw'+idx" :keyword="keyword" v-on:keyword-clicked="keywordClicked(keyword)" v-bind:class="{active : keywordFilter.includes(keyword), disabled : !keywordContainedInVisibleRecipes(keyword)}" />
</ul>
<div class="kw">
<transition-group v-if="keywords.length" class="keywords" name="keyword-list" tag="ul">
<RecipeKeyword v-for="keyword in keywords"
:key="keyword.name"
:name="keyword.name"
:count="keyword.count"
:title="keywordContainedInVisibleRecipes(keyword) ? t('cookbook','Toggle keyword') : t('cookbook','Keyword not contained in visible recipes')"
v-on:keyword-clicked="keywordClicked(keyword)"
:class="{keyword, active : keywordFilter.includes(keyword.name), disabled : !keywordContainedInVisibleRecipes(keyword)}"
/>
</transition-group>
</div>
<ul class="recipes">
<li v-for="(recipe, index) in filteredRecipes" :key="recipe.recipe_id" v-show="recipeVisible(index)">
<router-link :to="'/recipe/'+recipe.recipe_id">
Expand Down Expand Up @@ -51,28 +60,36 @@ export default {
}
},
},
watch: {
keywordFilter: {
handler: function() {
this.sortKeywords()
},
deep: true
}
},
methods: {
/**
* Callback for click on keyword
* Callback for click on keyword, add to or remove from list
*/
keywordClicked: function(keyword) {
const index = this.keywordFilter.indexOf(keyword)
const index = this.keywordFilter.indexOf(keyword.name)
if (index > -1) {
this.keywordFilter.splice(index, 1)
} else {
this.keywordFilter.push(keyword)
this.keywordFilter.push(keyword.name)
}
},
/**
* Check if a keyword exists in the currently visible recipes.
*/
keywordContainedInVisibleRecipes: function(keyword) {
for (let i=0; i<this.recipes.length; ++i) {
if (this.recipeVisible(i)
if (this.recipeVisible(i)
&& this.recipes[i].keywords
&& this.recipes[i].keywords.split(',').includes(keyword)) {
&& this.recipes[i].keywords.split(',').includes(keyword.name)) {
return true
}
}
}
return false
},
Expand All @@ -99,7 +116,7 @@ export default {
* Check if recipe should be displayed, depending on selected keyword filter.
* Returns true if recipe contains all selected keywords.
*/
recipeVisible: function(index) {
recipeVisible: function(index) {
if (this.keywordFilter.length == 0) {
return true
} else {
Expand All @@ -111,22 +128,48 @@ export default {
}
},
/**
* Extract and set list of keywords from the returned recipes
* Extract and set list of keywords from the returned recipes.
*/
setKeywords: function(recipes) {
this.keywords = []
if ((recipes.length) > 0) {
recipes.forEach(recipe => {
if(recipe['keywords']) {
if(recipe['keywords']) {
recipe['keywords'].split(',').forEach(kw => {
if(!this.keywords.includes(kw)) {
this.keywords.push(kw)
const idx = this.keywords.findIndex(el => el.name == kw)
if (idx > -1) {
this.keywords[idx].count++
} else {
this.keywords.push({name: kw, count: 1})
}
})
}
}
})
this.sortKeywords()
}
},
/**
* Sort keywords.
*/
sortKeywords: function() {
// Sort by number of recipes containing keyword
this.keywords = this.keywords.sort((k1, k2) => k2.count - k1.count)

// Move selected keywords to the front and unselectable to the end
let selected_kw = [], selectable_kw = [], unavailable_kw = []
this.keywords.forEach(kw => {
if (this.keywordFilter.includes(kw.name)) {
selected_kw.push(kw)
}
else if (this.keywordContainedInVisibleRecipes(kw)) {
selectable_kw.push(kw)
}
else {
unavailable_kw.push(kw)
}
})
this.keywords = selected_kw.concat(selectable_kw.concat(unavailable_kw))
},
},
mounted () {
this.$root.$off('applyRecipeFilter')
Expand All @@ -140,12 +183,28 @@ export default {

<style scoped>

div.kw {
width: 100%;
max-height: 6.7em;
overflow-x: hidden;
overflow-y: scroll;
margin-bottom: 1em;
padding: .1em;
}

ul.keywords {
display: flex;
flex-wrap: wrap;
flex-direction: row;
width: 100%;
margin: .5rem 1rem .5rem;
padding: .5rem 1rem .5rem;
}

.keyword {
display: inline-block;
}

.keyword-list-move {
transition: transform .5s;
}

ul.recipes {
Expand Down
38 changes: 36 additions & 2 deletions src/components/RecipeKeyword.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
<template>
<a v-on:click="clicked" ref="link"><li>{{ keyword }}</li></a>
<a v-on:click="clicked" ref="link">
<li>
<span>{{ name }}</span>
<span v-if="count != null" class="count">({{count}})</span>
</li>
</a>
</template>

<script>
export default {
name: 'RecipeKeyword',
props: ['keyword'],
props: {
name: {
type: String,
required: true
},
count: {
type: Number,
default: null
}
},
data () {
return {
}
Expand Down Expand Up @@ -39,23 +53,43 @@ li {
user-select: none; /* Standard */
}


li .count {
margin-left: .35em;
font-size: .8em;
color: var(--color-text-light);
}

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

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

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

.disabled li .count {
color: var(--color-border);
}

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

.disabled :hover {
cursor: default;
}

li:hover, .active li:hover {
border: 1px solid var(--color-primary);
}

</style>
33 changes: 17 additions & 16 deletions src/components/RecipeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,24 @@

<div class='meta'>
<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="dates">
<span v-if="showCreatedDate" class="date" :title="t('cookbook', 'Date created')">
<span class="icon-calendar-dark date-icon" />
<span class="date-text">{{ dateCreated }}</span>
</span>
<span v-if="showModifiedDate" class="date" :title="t('cookbook', 'Last modified')">
<span class="icon-rename date-icon" />
<span class="date-text">{{ dateModified }}</span>
</span>
</p>
<p v-if="keywords.length">
<ul v-if="keywords.length">
<RecipeKeyword v-for="(keyword,idx) in keywords" :key="'keyw'+idx" :name="keyword" :title="t('cookbook', 'Search recipes with this keyword')" v-on:keyword-clicked="keywordClicked(keyword)" />
</ul>
</p>

<p class="dates">
<span v-if="showCreatedDate" class="date" :title="t('cookbook', 'Date created')">
<span class="icon-calendar-dark date-icon" />
<span class="date-text">{{ dateCreated }}</span>
</span>
<span v-if="showModifiedDate" class="date" :title="t('cookbook', 'Last modified')">
<span class="icon-rename date-icon" />
<span class="date-text">{{ dateModified }}</span>
</span>
</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" class='source-url'>{{ $store.state.recipe.url }}</a>
Expand Down
Loading