Add recipe view page

This commit is contained in:
Chris 2018-05-22 00:35:52 +01:00
parent 2dcc4a6bf8
commit 0e2ff8160e
24 changed files with 466 additions and 31 deletions

232
blocks/ORB/View.pm Normal file
View File

@ -0,0 +1,232 @@
## @file
# This file contains the implementation of the view page.
#
# @author Chris Page <chris@starforge.co.uk>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see http://www.gnu.org/licenses/.
## @class
package ORB::View;
use strict;
use parent qw(ORB); # This class extends the ORB block class
use experimental qw(smartmatch);
use Regexp::Common qw(URI);
use v5.14;
## @method private $ _generate_ingredients($ingreds)
# Given an array of ingredients, convert them to a list of ingredients
# to show in the recipe.
#
# @param ingreds A reference to an array of ingredient hashes.
# @return A string containing the convered ingredient list.
sub _generate_ingredients {
my $self = shift;
my $ingreds = shift;
my @result;
foreach my $ingred (@{$ingreds}) {
if($ingred -> {"separator"}) {
push(@result, $self -> {"template"} -> load_template("view/separator.tem",
{
"%(separator)s" => $ingred -> {"separator"}
}));
} else {
my $units = $ingred -> {"units"} eq "None" ? "" : $ingred -> {"units"};
my $quantity = $ingred -> {"quantity"} ? $ingred -> {"quantity"} : "";
push(@result, $self -> {"template"} -> load_template("view/ingredient.tem",
{
"%(quantity)s" => $quantity,
"%(units)s" => $units,
"%(prepmethod)s" => $ingred -> {"prepmethod"},
"%(ingredient)s" => $ingred -> {"ingredient"},
"%(notes)s" => $ingred -> {"notes"} ? "(".$ingred -> {"notes"}.")" : "",
}));
}
}
return join("\n", @result);
}
## @method private $ _generate_tags($tags)
# Convert a list of tags into a string that can be shown in the recipe
# page.
#
# @param tags A reference to a list of tag names.
# @return A string containing the tag list.
sub _generate_tags {
my $self = shift;
my $tags = shift;
my @result;
foreach my $tag (@{$tags}) {
push(@result, $self -> {"template"} -> load_template("view/tag.tem",
{
"%(name)s" => $tag -> {"name"},
"%(color)s" => $tag -> {"color"},
"%(bgcol)s" => $tag -> {"background"},
"%(faicon)s" => $tag -> {"fa-icon"}
}));
}
return join("", @result);
}
## @method private $ _convert_source($source)
# Replace all URLs in the specified source string with clickable links.
#
# @param source The source string to replace URLs in.
# @return The processed source string.
sub _convert_source {
my $self = shift;
my $source = shift;
my $match = $RE{URI}{HTTP}{-scheme => qr(https?)};
$source =~ s|($match)|<a href="$1">$1</a>|gi;
return $source;
}
## @method private $ _generate_view($rid)
# Generate a page containing the recipe identified by the specified ID.
#
# @param rid The ID of the recipe to fetch the data for
# @return An array containing the page title, content, extra header data, and
# extra javascript content.
sub _generate_view {
my $self = shift;
my $rid = shift;
# Try to fetch the data.
my $recipe = $self -> {"system"} -> {"recipe"} -> get_recipe($rid);
# Stop here if there's no recipe data available...
return $self -> _fatal_error("{L_VIEW_ERROR_NORECIPE}")
unless($recipe && $recipe -> {"id"});
# convert various bits to blocks of HTML
my $ingreds = $self -> _generate_ingredients($recipe -> {"ingredients"});
my $tags = $self -> _generate_tags($recipe -> {"tags"});
my $source = $self -> _convert_source($recipe -> {"source"});
my $title = $recipe -> {"name"};
my $state = $self -> check_permission('recipe.edit') ? "enabled" : "disabled";
my $controls = $self -> {"template"} -> load_template("view/controls-$state.tem",
{
"%(url-edit)s" => $self -> build_url(block => "edit",
pathinfo => [ $recipe -> {"id"} ]),
"%(url-delete)s" => $self -> build_url(block => "delete",
pathinfo => [ $recipe -> {"id"} ]),
});
# and build the page itself
my $body = $self -> {"template"} -> load_template("view/content.tem",
{
"%(name)s" => $title,
"%(source)s" => $source,
"%(yield)s" => $recipe -> {"yield"},
"%(timereq)s" => $recipe -> {"timereq"},
"%(timemins)s" => $self -> {"template"} -> humanise_seconds($recipe -> {"timemins"} * 60),
"%(temp)s" => $recipe -> {"temp"} ? $recipe -> {"temp"} : "",
"%(temptype)s" => $recipe -> {"temptype"} // "",
"%(type)s" => $recipe -> {"type"},
"%(status)s" => $recipe -> {"status"},
"%(tags)s" => $tags,
"%(ingredients)s" => $ingreds,
"%(method)s" => $recipe -> {"method"},
"%(notes)s" => $recipe -> {"notes"},
"%(controls)s" => $controls
});
return ($title,
$body,
$self -> {"template"} -> load_template("view/extrahead.tem"),
"");
}
# ============================================================================
# UI handler/dispatcher functions
## @method private @ _fatal_error($error)
# Generate the tile and content for an error page.
#
# @param error A string containing the error message to display
# @return The title of the error page and an error message to place in the page.
sub _fatal_error {
my $self = shift;
my $error = shift;
return ("{L_VIEW_ERROR_FATAL}",
$self -> {"template"} -> load_template("error/page_error.tem",
{ "%(message)s" => $error,
"%(url-logout)s" => $self -> build_url(block => "login", pathinfo => ["signout"])
})
);
}
## @method private $ _dispatch_ui()
# Implements the core behaviour dispatcher for non-api functions. This will
# inspect the state of the pathinfo and invoke the appropriate handler
# function to generate content for the user.
#
# @return A string containing the page HTML.
sub _dispatch_ui {
my $self = shift;
# We need to determine what the page title should be, and the content to shove in it...
my @pathinfo = $self -> {"cgi"} -> multi_param("pathinfo");
my ($title, $body, $extrahead, $extrajs) = $self -> _generate_view($pathinfo[0]);
# Done generating the page content, return the filled in page template
return $self -> generate_orb_page(title => $title,
content => $body,
extrahead => $extrahead,
extrajs => $extrajs,
active => substr($title, 0, 1),
doclink => 'summary');
}
# ============================================================================
# Module interface functions
## @method $ page_display()
# Generate the page content for this module.
sub page_display {
my $self = shift;
# Is this an API call, or a normal page operation?
my $apiop = $self -> is_api_operation();
if(defined($apiop)) {
# API call - dispatch to appropriate handler.
given($apiop) {
default {
return $self -> api_response($self -> api_errorhash('bad_op',
$self -> {"template"} -> replace_langvar("API_BAD_OP")))
}
}
} else {
return $self -> _dispatch_ui();
}
}
1;

20
lang/en/view.lang Normal file
View File

@ -0,0 +1,20 @@
VIEW_NAME = Name
VIEW_SOURCE = Source
VIEW_YIELD = Yield
VIEW_PREPINFO = Prep info
VIEW_TIMEREQ = Time required
VIEW_OVENTEMP = Oven preheat
VIEW_TYPE = Type
VIEW_STATUS = Status
VIEW_TAGS = Tags
VIEW_INGREDIENTS = Ingredients
VIEW_METHOD = Method
VIEW_NOTES = Notes
VIEW_EDIT = Edit
VIEW_CLONE = Clone
VIEW_DELETE = Delete
VIEW_ERROR_FATAL = View error
VIEW_ERROR_NORECIPE = No matching recipe found

View File

@ -324,7 +324,7 @@ sub get_recipe_list {
my @wherefrag = ();
# Get the status IDs for excluded states
my $states = $slef -> _convert_states($exlstates);
my $states = $self -> _convert_states($exlstates);
# And add them to the query
if(scalar(@{$states})) {
@ -508,7 +508,7 @@ sub find {
$args -> {"tagids"} = $self -> _hashlist_to_list($tags, "id");
# Find should always exclude deleted and edited recipes
my $exclstates = $self -> _convert_state($self -> {"settings"} -> {"config"} -> {"Recipe:status:edited"} // "Edited",
my $exclstates = $self -> _convert_states($self -> {"settings"} -> {"config"} -> {"Recipe:status:edited"} // "Edited",
$self -> {"settings"} -> {"config"} -> {"Recipe:status:deleted"} // "Deleted");
# Fix up default matching modes
@ -750,11 +750,18 @@ sub _get_ingredients {
$self -> clear_error();
my $ingh = $self -> {"dbh"} -> prepare("SELECT `ri`.*, `i`.`name`
my $ingh = $self -> {"dbh"} -> prepare("SELECT `ri`.*,
`i`.`name` AS `ingredient`,
`p`.`name` AS `prepmethod`,
`u`.`name` AS `units`
FROM `".$self -> {"settings"} -> {"database"} -> {"recipeing"}."` AS `ri`
`".$self -> {"settings"} -> {"database"} -> {"ingredients"}."` AS `i`
WHERE `i`.`id` = `ri`.`ingred_id`
AND `ri`.`recipe_id` = ?
LEFT JOIN `".$self -> {"settings"} -> {"database"} -> {"ingredients"}."` AS `i`
ON `i`.`id` = `ri`.`ingred_id`
LEFT JOIN `".$self -> {"settings"} -> {"database"} -> {"prep"}."` AS `p`
ON `p`.`id` = `ri`.`prep_id`
LEFT JOIN `".$self -> {"settings"} -> {"database"} -> {"units"}."` AS `u`
ON `u`.`id` = `ri`.`unit_id`
WHERE `ri`.`recipe_id` = ?
ORDER BY `ri`.`position`");
$ingh -> execute($recipeid)
or return $self -> self_error("Ingredient lookup for '$recipeid' failed: ".$self -> {"dbh"} -> errstr());

View File

@ -0,0 +1,24 @@
.clear-holder{
position:relative;
float:left;
width: 100%;
}
.clear-helper{
margin-top: 4px;
margin-right: 4px;
text-align: center;
position: absolute;
right: 4px;
height: 2rem;
width: 2rem;
cursor: pointer;
display:none;
background:#e0e0e0;
border-radius:2px;
line-height: 2rem;
}
/* button will be misplaced if input is not inline */
.clear-holder > input {
display: inline-block;
}

View File

@ -25,6 +25,7 @@ div.top-bar-title {
/* fix menu button pad */
#menubtn {
margin-right: 0.5rem;
margin-top: 0.5rem;
}
/* fix padding for menu text with image */

View File

@ -2,3 +2,7 @@
div.contextlink {
font-size: 0.75rem;
}
div.topspace {
margin-top: 0.5rem;
}

View File

@ -1,5 +1,5 @@
li.recipe {
padding: 3px;
padding: 5px 3px 3px 3px;
border-bottom: 1px solid #e7e7e7;
}
@ -54,3 +54,7 @@ span.ljs-tag {
border-radius: 0;
white-space: nowrap;
}
#recipelist div.input-group-button {
margin-bottom: 0px;
}

View File

@ -0,0 +1,38 @@
div.recipe {
margin: 0px 1.5rem;
}
div.recipe h4 {
border-bottom: 1px solid #888;
}
div.title {
font-weight: bold;
}
div.ingredients ul {
margin: 0px;
padding: 0px;
}
div.ingredients ul li {
list-style: none;
}
div.ingredients ul li.ingredient {
margin-left: 1rem;
}
div.ingredients ul li.separator {
font-weight: bold;
border-bottom: 1px solid #ccc;
}
.tag {
font-size: 0.7rem;
display: inline-block;
padding: .33333rem .5rem;
margin-right: .3rem;
border-radius: 0;
white-space: nowrap;
}

View File

@ -1,4 +1,4 @@
<div class="small-8 small-centered columns">
<div class="cell small-8 small-offset-2">
<table class="notebox notebox-error">
<tr>
<td class="nbox-image show-for-medium"><img src="{V_[templatepath]}images/error.png" width="48" height="48" alt="error" /></td>

View File

@ -0,0 +1,25 @@
CKEDITOR.editorConfig = function( config ) {
config.toolbar = [
{ name: 'document', items: [ 'Source', 'Maximize', 'ShowBlocks' ] },
{ name: 'clipboard', items: [ 'Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo' ] },
{ name: 'editing', items: [ 'Find', 'Replace' ] },
{ name: 'paragraph', items: [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', 'CreateDiv', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock' ] },
{ name: 'links', items: [ 'Link', 'Unlink', 'Anchor', 'Image', 'Table', 'HorizontalRule', 'Smiley', 'SpecialChar' ] },
'/',
{ name: 'styles', items: [ 'Styles', 'Format', 'Font', 'FontSize' ] },
{ name: 'basicstyles', items: [ 'Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'CopyFormatting', 'RemoveFormat' ] },
{ name: 'colors', items: [ 'TextColor', 'BGColor' ] }
];
config.font_names = 'Arial;Book Antiqua;Courier New; Tahoma;Times New Roman;Trebuchet MS;Verdana';
};
CKEDITOR.config.font_names = 'Arial/Arial, Helvetica, sans-serif;' +
'Book Antiqua/Book Antiqua, cursive;' +
'Courier New/Courier New, Courier, monospace;' +
'Georgia/Georgia, serif;' +
'Lucida Sans Unicode/Lucida Sans Unicode, Lucida Grande, sans-serif;' +
'Tahoma/Tahoma, Geneva, sans-serif;' +
'Times New Roman/Times New Roman, Times, serif;' +
'Trebuchet MS/Trebuchet MS, Helvetica, sans-serif;' +
'Verdana/Verdana, Geneva, sans-serif';

View File

@ -0,0 +1,21 @@
/* Based on suggestions from Kroehre and Johannes in the following
* stackoverflow: http://stackoverflow.com/questions/5917776/clear-search-box-on-the-click-of-a-little-x-inside-of-it
*/
(function ($, undefined) {
$.fn.clearable = function () {
var $this = this;
$this.wrap('<div class="clear-holder input-group-field" />');
var helper = $('<span class="clear-helper"><i class="fas fa-times-circle"></i></span>');
$this.parent().append(helper);
$this.parent().on('keyup', function() {
if($this.val()) {
helper.css('display', 'inline-block');
} else helper.hide();
});
helper.click(function(){
$this.val("");
$this.trigger("keyup");
helper.hide();
});
};
})(jQuery);

View File

@ -7,4 +7,6 @@ $(function() {
var recipeList = new List('recipelist', options);
$('#listfilter').delaysearch(recipeList);
$('#listfilter').clearable();
});

View File

@ -1,11 +1,4 @@
%(pagemenu)s
<div id="recipelist" class="columns">
<nav aria-label="You are here:" role="navigation">
<ul class="breadcrumbs">
<li><a href="%(url-front)s">{L_LIST_SUMMARIES}</a></li>
<li>%(page)s</li>
</ul>
</nav>
<div id="recipelist" class="cell">
<div class="column row clearfix">
<div class="small input-group nomargin">
<input type="search" class="input-group-field" id="listfilter" placeholder="{L_LIST_FILTER}" />

View File

@ -7,7 +7,7 @@
<li class="time"><i class="fa fa-clock-o" aria-hidden="true"></i>&nbsp;<span class="ljs-time">%(time)s</span></li>
%(temp)s
</ul>
<div class="tags"><span class="intro"><i class="fa fa-tags" aria-hidden="true"></i>:</span>%(tags)s</div>
<div class="tags">%(tags)s</div>
</div>
%(controls)s
</li>

View File

@ -6,11 +6,11 @@
<title>%(title)s</title>
<link rel="stylesheet" href="{V_[templatepath]}3rdparty/jquery-ui/jquery-ui.min.css" />
<link rel="stylesheet" href="{V_[templatepath]}3rdparty/foundation/css/foundation.min.css" />
<link rel="stylesheet" href="{V_[templatepath]}3rdparty/fontawesome/css/font-awesome.min.css" />
<link rel="stylesheet" href="{V_[csspath]}notebox.css" />
<link rel="stylesheet" href="{V_[csspath]}messagebox.css" />
<link rel="stylesheet" href="{V_[csspath]}foundation.mods.css" />
<link rel="stylesheet" href="{V_[csspath]}clear-input.css" />
%(extrahead)s
</head>
<body>
@ -22,13 +22,14 @@
<!-- Start top bar -->
%(userbar)s
<!-- End top bar -->
<br />
<!-- Start content -->
<div class="row" id="content">
<div class="grid-container fluid">
%(pagemenu)s
<div class="grid-x grid-margin-x" id="content">
%(content)s
</div>
<!-- End content -->
</div>
</div><!-- content wrapper -->
</div><!-- canvas wrapper -->
@ -36,7 +37,9 @@
<script src="{V_[templatepath]}3rdparty/foundation/js/vendor/jquery.js"></script>
<script src="{V_[templatepath]}3rdparty/jquery-ui/jquery-ui.min.js"></script>
<script src="{V_[templatepath]}3rdparty/foundation/js/vendor/foundation.min.js"></script>
<script src="{V_[templatepath]}3rdparty/fontawesome/js/fontawesome-all.js"></script>
<script src="{V_[jspath]}foundation.js"></script>
<script src="{V_[jspath]}clear-input.js"></script>
<script>
%(extrajs)s
</script>

View File

@ -1,11 +1,10 @@
%(pagemenu)s
<div class="large-4 columns">
<div class="cell large-auto">
<h4>{L_SUMMARY_ADDED}</h4>
<table>
<thead>
<tr>
<td class="thead">{L_SUMMARY_NAME}</td>
<td class="thead">{L_SUMMARY_TYPE}</td>
<td class="thead">{L_SUMMARY_NAME}</td>
</tr>
</thead>
<tbody>
@ -14,13 +13,13 @@
</table>
</div>
<div class="large-4 columns">
<div class="cell large-auto">
<h4>{L_SUMMARY_VIEWED}</h4>
<table>
<thead>
<tr>
<td class="thead">{L_SUMMARY_NAME}</td>
<td class="thead">{L_SUMMARY_TYPE}</td>
<td class="thead">{L_SUMMARY_NAME}</td>
</tr>
</thead>
<tbody>
@ -29,13 +28,13 @@
</table>
</div>
<div class="large-4 columns">
<div class="cell large-auto">
<h4>{L_SUMMARY_UPDATED}</h4>
<table>
<thead>
<tr>
<td class="thead">{L_SUMMARY_NAME}</td>
<td class="thead">{L_SUMMARY_TYPE}</td>
<td class="thead">{L_SUMMARY_NAME}</td>
</tr>
</thead>
<tbody>

View File

@ -1,4 +1,4 @@
<tr>
<td><a href="%(url)s">%(name)s</a></td>
<td>%(type)s</td>
<td><a href="%(url)s">%(name)s</a></td>
</tr>

View File

@ -0,0 +1,52 @@
<div class="recipe">
<div>
<h3>%(name)s</h3>
<div class="grid-x grid-margin-x">
<div class="cell medium-2 medium-text-right title">{L_VIEW_SOURCE}</div>
<div class="cell medium-10 text-left">%(source)s</div>
</div>
<div class="grid-x grid-margin-x">
<div class="cell medium-2 medium-text-right title">{L_VIEW_YIELD}</div>
<div class="cell medium-10 text-left">%(yield)s</div>
</div>
<div class="grid-x grid-margin-x">
<div class="cell medium-2 medium-text-right title">{L_VIEW_PREPINFO}</div>
<div class="cell medium-10 text-left">%(timereq)s</div>
</div>
<div class="grid-x grid-margin-x">
<div class="cell medium-2 medium-text-right title">{L_VIEW_TIMEREQ}</div>
<div class="cell medium-10 text-left">%(timemins)s</div>
</div>
<div class="grid-x grid-margin-x">
<div class="cell medium-2 medium-text-right title">{L_VIEW_OVENTEMP}</div>
<div class="cell medium-10 text-left">%(temp)s%(temptype)s</div>
</div>
<div class="grid-x grid-margin-x">
<div class="cell medium-2 medium-text-right title">{L_VIEW_TYPE}</div>
<div class="cell medium-10 text-left">%(type)s</div>
</div>
<div class="grid-x grid-margin-x">
<div class="cell medium-2 medium-text-right title">{L_VIEW_STATUS}</div>
<div class="cell medium-10 text-left">%(status)s</div>
</div>
<div class="grid-x grid-margin-x">
<div class="cell medium-2 medium-text-right title">{L_VIEW_TAGS}</div>
<div class="cell medium-10 text-left">%(tags)s</div>
</div>
</div>
<div class="ingredients">
<h4>{L_VIEW_INGREDIENTS}</h4>
<ul>
%(ingredients)s
</ul>
</div>
<div class="method">
<h4>{L_VIEW_METHOD}</h4>
<div class="methodtext">%(method)s</div>
</div>
<div class="notes">
<h4>{L_VIEW_NOTES}</h4>
<div class="notetext">%(notes)s</div>
</div>
%(controls)s
</div>

View File

@ -0,0 +1 @@
<!-- no controls -->

View File

@ -0,0 +1,5 @@
<div class="cell button-group align-right">
<a href="%(url-edit)s" class="button" title="Edit"><i class="fa fa-edit" aria-hidden="true"></i> {L_VIEW_EDIT}</a>
<a href="%(url-clone)s" class="button" title="Clone"><i class="fa fa-copy" aria-hidden="true"></i> {L_VIEW_CLONE}</a>
<a href="%(url-delete)s" class="button alert" title="Delete"><i class="fa fa-trash" aria-hidden="true"></i> {L_VIEW_DELETE}</a>
</div>

View File

@ -0,0 +1 @@
<link rel="stylesheet" href="{V_[csspath]}view.css" />

View File

@ -0,0 +1 @@
<li class="ingredient">%(quantity)s %(units)s %(prepmethod)s %(ingredient)s %(notes)s</li>

View File

@ -0,0 +1 @@
<li class="separator">%(separator)s</li>

View File

@ -0,0 +1 @@
<span class="tag" style="color: %(color)s; background-color: %(bgcol)s" ><i class="fa %(faicon)s"></i> %(name)s</span>