Recipe ingredients, tools and instructions in Vue

This commit is contained in:
Sampsa Lohi 2020-04-18 19:07:22 +03:00
Родитель 6cce0cd853
Коммит 1c35246b4a
13 изменённых файлов: 1004 добавлений и 78 удалений

Просмотреть файл

@ -38,7 +38,7 @@ Cookbook.prototype = {
var url = action === '#' ? location.hash.substr(1) : action;
var data = $(form).serialize();
var deferred = $.Deferred();
$.ajax({
url: this._baseUrl + '/' + url,
method: form.getAttribute('method'),
@ -48,10 +48,10 @@ Cookbook.prototype = {
}).fail(function (jqXHR, textStatus, errorThrown) {
deferred.reject(new Error(jqXHR.responseText));
});
return deferred.promise();
},
/**
* Loads a recipe by id
*
@ -231,7 +231,7 @@ var Content = function (cookbook) {
*/
self.render = function () {
var route = location.hash.substr(1);
if(route.length === 0) {
route = 'home';
}
@ -241,39 +241,39 @@ var Content = function (cookbook) {
})
.done(function (html) {
$('#app-content-wrapper').html(html);
// Common
$('#print-recipe').click(self.onPrintRecipe);
$('#delete-recipe').click(self.onDeleteRecipe);
// Editor
$('#app-content-wrapper form').off('submit');
$('#app-content-wrapper form').submit(self.onUpdateRecipe);
$('#pick-image').off('click');
$('#pick-image').click(self.onPickImage);
$('#app-content-wrapper form ul + button.add-list-item').off('click');
$('#app-content-wrapper form ul + button.add-list-item').click(self.onAddListItem);
$('#app-content-wrapper form ul li input[type="text"]').off('keypress');
$('#app-content-wrapper form ul li input[type="text"]').on('keypress', self.onListInputKeyDown);
$('#app-settings [title]').tooltip('destroy');
$('#app-settings [title]').tooltip();
self.updateListItems();
// View
$('header img').click(self.onImageClick);
$('main .instruction').click(self.onInstructionClick);
$('.time button').click(self.onTimerToggle);
nav.highlightActive();
})
.fail(function(e) {
$('#app-content-wrapper').load(cookbook._baseUrl + '/error');
if(e && e instanceof Error) { throw e; }
});
};
@ -332,10 +332,12 @@ var Content = function (cookbook) {
/**
* Event: click on a recipe instruction
* NOTE: This functionality is handled by the Vue component
*
* self.onInstructionClick = function(e) {
* $(e.target).toggleClass('done');
* }
*/
self.onInstructionClick = function(e) {
$(e.target).toggleClass('done');
}
/**
* Event: click the recipe's image
@ -361,7 +363,7 @@ var Content = function (cookbook) {
self.minutes = minutes;
self.seconds = 0;
}
self.timer = window.setInterval(function() {
self.seconds--;
@ -379,13 +381,13 @@ var Content = function (cookbook) {
if(self.hours < 10) { text += '0'; }
text += self.hours + ':';
if(self.minutes < 10) { text += '0'; }
text += self.minutes + ':';
if(self.seconds < 10) { text += '0'; }
text += self.seconds;
$(e.target).closest('.time').find('p').text(text);
if(self.hours < 0 || self.minutes < 0) {
@ -414,25 +416,25 @@ var Content = function (cookbook) {
$('#app-content-wrapper form .remove-list-item')
.off('click')
.click(self.onDeleteListItem);
$('#app-content-wrapper form .move-list-item-up')
.off('click')
.click(self.onMoveListItemUp)
.prop('disabled', false);
$('#app-content-wrapper form li:first-of-type .move-list-item-up').prop('disabled', true);
$('#app-content-wrapper form .move-list-item-down')
.off('click')
.click(self.onMoveListItemDown)
.prop('disabled', false);
$('#app-content-wrapper form li:last-of-type .move-list-item-down').prop('disabled', true);
$('#app-content-wrapper form ul li input[type="text"]')
.off('keypress')
.on('keypress', self.onListInputKeyDown);
console.log('order');
$('#app-content-wrapper form ul').each(function() {
var stepNumber = 1;
@ -454,7 +456,7 @@ var Content = function (cookbook) {
var list = listItem.parentElement;
list.removeChild(listItem);
self.updateListItems();
};
@ -491,10 +493,10 @@ var Content = function (cookbook) {
$ul.append($item);
$item.find('input').focus();
self.updateListItems();
};
/**
* Event: Click move list item up
*/
@ -510,10 +512,10 @@ var Content = function (cookbook) {
}
$(listItem).insertBefore($(listItem.previousElementSibling));
self.updateListItems();
};
/**
* Event: Click move list item down
*/
@ -523,13 +525,13 @@ var Content = function (cookbook) {
var button = e.currentTarget;
var tools = button.parentElement;
var listItem = tools.parentElement;
if(!listItem.nextElementSibling) {
return;
}
$(listItem).insertAfter($(listItem.nextElementSibling));
self.updateListItems();
};
@ -538,7 +540,7 @@ var Content = function (cookbook) {
*/
self.onUpdateRecipe = function(e) {
e.preventDefault();
cookbook.update(e.currentTarget)
.then(function(id) {
location.hash = '/recipes/' + id;
@ -678,7 +680,7 @@ var Nav = function (cookbook) {
json = json || [];
var html = '<li class="icon-category-organization"><a href="#">' + t(appName, 'All recipes') + '</a></li>';
html += json.map(function(category) {
var entry = '<li class="icon-category-files">';
entry += '<a href="#category/' + encodeURIComponent(category.name) + '">';
@ -687,7 +689,7 @@ var Nav = function (cookbook) {
entry += '</a></li>';
return entry;
}).join("\n");
$('#app-navigation #categories').html(html);
self.highlightActive();
@ -705,7 +707,7 @@ var Nav = function (cookbook) {
// Change cache update interval
$('#recipe-update-interval').off('change');
$('#recipe-update-interval').change(self.onChangeRecipeUpdateInterval);
// Change print image setting
$('#recipe-print-image').off('change');
$('#recipe-print-image').change(self.onChangePrintImage);

748
js/vue.js

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -46,7 +46,7 @@ class MainController extends Controller
return new TemplateResponse($this->appName, 'index', $view_data); // templates/index.php
}
/**
* @NoAdminRequired
* @NoCSRFRequired
@ -75,7 +75,7 @@ class MainController extends Controller
{
try {
$recipes = $this->service->getAllRecipesInSearchIndex();
foreach ($recipes as $i => $recipe) {
$recipes[$i]['image_url'] = $this->urlGenerator->linkToRoute(
'cookbook.recipe.image',
@ -86,7 +86,7 @@ class MainController extends Controller
]
);
}
$response = new TemplateResponse($this->appName, 'content/search', ['recipes' => $recipes]);
$response->renderAs('blank');
@ -107,7 +107,7 @@ class MainController extends Controller
return $response;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
@ -117,7 +117,7 @@ class MainController extends Controller
$query = urldecode($query);
try {
$recipes = $this->service->findRecipesInSearchIndex($query);
foreach ($recipes as $i => $recipe) {
$recipes[$i]['image_url'] = $this->urlGenerator->linkToRoute(
'cookbook.recipe.image',
@ -128,7 +128,7 @@ class MainController extends Controller
]
);
}
$response = new TemplateResponse($this->appName, 'content/search', ['query' => $query, 'recipes' => $recipes]);
$response->renderAs('blank');
@ -137,7 +137,7 @@ class MainController extends Controller
return new DataResponse($e->getMessage(), 500);
}
}
/**
* @NoAdminRequired
* @NoCSRFRequired
@ -148,7 +148,7 @@ class MainController extends Controller
try {
$recipes = $this->service->getRecipesByCategory($category);
foreach ($recipes as $i => $recipe) {
$recipes[$i]['image_url'] = $this->urlGenerator->linkToRoute(
'cookbook.recipe.image',
@ -159,7 +159,7 @@ class MainController extends Controller
]
);
}
$response = new TemplateResponse($this->appName, 'content/search', ['tag' => $tag, 'recipes' => $recipes]);
$response->renderAs('blank');
@ -187,7 +187,7 @@ class MainController extends Controller
);
$recipe['id'] = $id;
$recipe['print_image'] = $this->service->getPrintImage();
$response = new TemplateResponse($this->appName, 'content/recipe', $recipe);
$response = new TemplateResponse($this->appName, 'content/recipe_vue', $recipe);
$response->renderAs('blank');
return $response;
@ -204,7 +204,7 @@ class MainController extends Controller
{
try {
$recipe = [];
$response = new TemplateResponse($this->appName, 'content/edit', $recipe);
$response->renderAs('blank');
@ -213,7 +213,7 @@ class MainController extends Controller
return new DataResponse($e->getMessage(), 500);
}
}
/**
* @NoAdminRequired
* @NoCSRFRequired
@ -243,7 +243,7 @@ class MainController extends Controller
try {
$recipe_data = $_POST;
$file = $this->service->addRecipe($recipe_data);
return new DataResponse($file->getParent()->getId());
} catch (\Exception $e) {
return new DataResponse($e->getMessage(), 500);
@ -266,7 +266,7 @@ class MainController extends Controller
$recipe['id'] = $id;
}
$response = new TemplateResponse($this->appName, 'content/edit', $recipe);
$response->renderAs('blank');
@ -287,7 +287,7 @@ class MainController extends Controller
parse_str(file_get_contents("php://input"), $recipeData);
$recipeData['id'] = $id;
$file = $this->service->addRecipe($recipeData);
return new DataResponse($id);
} catch (\Exception $e) {
return new DataResponse($e->getMessage(), 500);

1
nextcloud-cookbook Submodule

@ -0,0 +1 @@
Subproject commit 6cce0cd853f1704f18887d1fa192d5834609298f

Просмотреть файл

@ -35,7 +35,7 @@
<div class="recipe-content">
<h2><?php echo $_['name']; ?></h2>
<div class="recipe-details">
<p><?php echo $_['description']; ?></p>
@ -44,7 +44,7 @@
<?php } ?>
<p><strong><?php p($l->t('Servings')); ?>: </strong><?php echo $_['recipeYield']; ?></p>
<div class="times">
<?php if(isset($_['prepTime']) && $_['prepTime']) {
$prep_interval = new DateInterval($_['prepTime']);
@ -70,7 +70,7 @@
<p><?php echo $cook_hours . ':' . $cook_mins; ?></p>
</div>
<?php }} ?>
<?php
if(isset($_['totalTime']) && $_['totalTime']) {
$total_interval = new DateInterval($_['totalTime']);
@ -99,7 +99,7 @@
</ul>
<?php } ?>
</section>
<section>
<?php if(!empty($_['tool'])) { ?>
<h3><?php p($l->t('Tools')); ?></h3>

Просмотреть файл

@ -0,0 +1,92 @@
<div id="controls">
<div class="breadcrumb">
<div class="crumb svg crumbhome ui-droppable">
<a href="#" class="icon-category-organization"></a>
</div>
<div class="crumb svg">
<a href="#recipes/<?php echo $_['id']; ?>"><?php echo $_['name']; ?></a>
</div>
</div>
<div class="actions">
<a id="edit-recipe" href="#recipes/<?php echo $_['id']; ?>/edit" class="button svg action" title="<?php p($l->t('Edit recipe')); ?>">
<span class="icon icon-rename"></span>
<span class="hidden-visually"><?php p($l->t('Edit recipe')); ?></span>
</a>
</div>
<div class="actions">
<button id="print-recipe" class="button svg action" title="<?php p($l->t('Print recipe')); ?>">
<span class="icon icon-category-office"></span>
<span class="hidden-visually"><?php p($l->t('Print recipe')); ?></span>
</button>
</div>
<div class="actions">
<button id="delete-recipe" class="button svg action" data-id="<?php echo $_['id']; ?>" title="<?php p($l->t('Delete recipe')); ?>">
<span class="icon icon-delete"></span>
<span class="hidden-visually"><?php p($l->t('Delete recipe')); ?></span>
</button>
</div>
</div>
<?php if(isset($_['image']) && $_['image']) { ?>
<header class="collapsed<?php if($_['print_image']) echo ' printable'; ?>">
<img src="<?php echo $_['image_url']; ?>">
</header>
<?php } ?>
<div class="recipe-content">
<h2><?php echo $_['name']; ?></h2>
<div class="recipe-details">
<p><?php echo $_['description']; ?></p>
<?php if(isset($_['url']) && $_['url']) { ?>
<p><strong><?php p($l->t('Source')); ?>: </strong><a target="_blank" href="<?php echo $_['url']; ?>"><?php echo $_['url']; ?></a></p>
<?php } ?>
<p><strong><?php p($l->t('Servings')); ?>: </strong><?php echo $_['recipeYield']; ?></p>
<div class="times">
<?php if(isset($_['prepTime']) && $_['prepTime']) {
$prep_interval = new DateInterval($_['prepTime']);
$prep_mins = $prep_interval->format('%I');
$prep_hours = $prep_interval->format('%h');
if ($prep_hours > 0 || $prep_mins > 0) {
?>
<div class="time" data-raw="<?php echo $_['prepTime']; ?>">
<h4><?php p($l->t('Preparation time')); ?></h4>
<p><?php echo $prep_hours . ':' . $prep_mins; ?></p>
</div>
<?php }} ?>
<?php if(isset($_['cookTime']) && $_['cookTime']) {
$cook_interval = new DateInterval($_['cookTime']);
$cook_mins = $cook_interval->format('%I');
$cook_hours = $cook_interval->format('%h');
if ($cook_hours > 0 || $cook_mins > 0) {
?>
<div class="time" data-raw="<?php echo $_['cookTime']; ?>">
<button type="button" class="icon-play" data-hours="<?php echo $cook_hours ?>" data-minutes="<?php echo $cook_mins ?>"></button>
<h4><?php p($l->t('Cooking time')); ?></h4>
<p><?php echo $cook_hours . ':' . $cook_mins; ?></p>
</div>
<?php }} ?>
<?php
if(isset($_['totalTime']) && $_['totalTime']) {
$total_interval = new DateInterval($_['totalTime']);
$total_mins = $total_interval->format('%I');
$total_hours = $total_interval->format('%h');
if ($total_hours > 0 || $total_mins > 0) {
?>
<div class="time" data-raw="<?php echo $_['totalTime']; ?>">
<h4><?php p($l->t('Total time')); ?></h4>
<p><?php echo $total_hours . ':' . $total_mins; ?></p>
</div>
<?php }} ?>
</div>
</div>
<div id="app-recipe-content"></div>
<div id="app-recipe-data" style="display:none"><?php echo str_replace("'", "\\'", json_encode($_)); ?></div>
</div>

Просмотреть файл

@ -1,5 +1,6 @@
<?php
script('cookbook', 'script');
script('cookbook', 'vue');
style('cookbook', 'style');
?>
@ -17,4 +18,3 @@ style('cookbook', 'style');
</div>
</div>
</div>

Просмотреть файл

@ -1,10 +1,12 @@
<template>
<li>{{ ingredient }}</li>
</template>
<script>
export default {
props: ['ingredient'],
}
</script>

Просмотреть файл

@ -1,13 +1,30 @@
<template>
<li :class="{ 'instruction': true, 'done': isDone }" @click="toggleDone">{{ instruction }}</li>
</template>
<script>
export default {
props: ['instruction'],
data () {
return {
isDone: false
}
},
methods: {
toggleDone: function() {
this.isDone = !this.isDone
},
},
}
</script>
<style scoped>
.instruction {
/* I find this more convenient than a ln2br-function */
white-space: pre-line;
}
</style>

Просмотреть файл

@ -0,0 +1,15 @@
<template>
<li>{{ tool }}</li>
</template>
<script>
export default {
props: ['tool'],
}
</script>
<style scoped>
</style>

Просмотреть файл

@ -1,10 +1,67 @@
<template>
<section>
<aside>
<section>
<h3 v-if="ingredients.length">{{ $t('recipe.view.ingredients.header') }}</h3>
<ul v-if="ingredients.length">
<RecipeIngredient v-for="ingredient in ingredients" :key="ingredient" :ingredient="ingredient" />
</ul>
</section>
<section>
<h3 v-if="tools.length">{{ $t('recipe.view.tools.header') }}</h3>
<ul v-if="tools.length">
<RecipeTool v-for="tool in tools" :key="tool" :tool="tool" />
</ul>
</section>
</aside>
<main v-if="instructions.length">
<h3>{{ $t('recipe.view.instructions.header') }}</h3>
<ol class="instructions">
<RecipeInstruction v-for="instruction in instructions" :key="instruction" :instruction="instruction" />
</ol>
</main>
</section>
</template>
<script>
import RecipeIngredient from './RecipeIngredient'
import RecipeInstruction from './RecipeInstruction'
import RecipeTool from './RecipeTool'
export default {
components: {
RecipeIngredient,
RecipeInstruction,
RecipeTool,
},
props: ['recipe'],
data () {
return {
ingredients: [],
instructions: [],
tools: [],
}
},
mounted () {
// Have to use a gimmic to get the recipe data at this point
let recipeData = JSON.parse(document.getElementById("app-recipe-data").innerHTML)
if (recipeData.recipeIngredient) {
this.ingredients = recipeData.recipeIngredient
}
if (recipeData.recipeInstructions) {
this.instructions = recipeData.recipeInstructions
}
if (recipeData.tool) {
this.tools = recipeData.tool
}
},
}
</script>

Просмотреть файл

@ -6,12 +6,22 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
const messages = {
// Each localization has their own messages as an object
en: {
recipe: {
view: {
ingredients: {
header: "Ingredients",
},
instructions: {
header: "Instructions",
},
tools: {
header: "Tools",
},
},
},
},

Просмотреть файл

@ -70,6 +70,10 @@ import RecipeView from './components/RecipeView'
return value.toString().replace('.', ',')
}
}
// This will replace the PHP function nl2br in Vue components
window.nl2br = function(text) {
return text.replace(/\n/g, '<br />')
}
// The following functions may seem a bit redundant, but I have needed them
// in previous projects to check or process the inputs, so they can be
// useful in the future.
@ -90,11 +94,19 @@ import RecipeView from './components/RecipeView'
// Start the app once document is done loading
$(document).ready(function () {
// This is a shameful gimmick but needed at this point since
// the recipe view is loaded asyncronously.
// It has to be kept running, otherwise the app doesn't rerender
// for example after editing a recipe and returning to the view.
const App = Vue.extend(RecipeView)
new App({
store,
router,
i18n
}).$mount("#app-recipe-view")
let waitForElem = window.setInterval(function() {
if ($("#app-recipe-content").length) {
new App({
store,
router,
i18n
}).$mount("#app-recipe-content")
}
}, 250)
})
})(OC, window, jQuery)