310 lines
45 KiB
Go
310 lines
45 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"html/template"
|
||
|
"io/ioutil"
|
||
|
|
||
|
log "github.com/sirupsen/logrus"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
minicss = `/*MIT License
|
||
|
|
||
|
Copyright (c) 2016-2017 Angelos Chalaris
|
||
|
|
||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
of this software and associated documentation files (the "Software"), to deal
|
||
|
in the Software without restriction, including without limitation the rights
|
||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
copies of the Software, and to permit persons to whom the Software is
|
||
|
furnished to do so, subject to the following conditions:
|
||
|
|
||
|
The above copyright notice and this permission notice shall be included in all
|
||
|
copies or substantial portions of the Software.
|
||
|
|
||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
|
SOFTWARE.*/
|
||
|
/* https://github.com/Chalarangelo/mini.css v2.3.2*/
|
||
|
html{font-size:16px}html,*{font-family:-apple-system, BlinkMacSystemFont,"Segoe UI","Roboto", "Droid Sans","Helvetica Neue", Helvetica, Arial, sans-serif;line-height:1.5;-webkit-text-size-adjust:100%}*{font-size:1rem}body{margin:0;color:#212121;background:#f8f8f8}article,aside,section,figcaption,figure,main,details,menu{display:block}summary{display:list-item}abbr[title]{border-bottom:none;text-decoration:underline}audio,video{display:inline-block}svg:not(:root){overflow:hidden}input{overflow:visible}img{max-width:100%;height:auto}dfn{font-style:italic}h1,h2,h3,h4,h5,h6{line-height:1.2em;margin:0.75rem 0.5rem;font-weight:500}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{color:#424242;display:block;margin-top:-.25rem}h1{font-size:2rem}h2{font-size:1.6875rem}h3{font-size:1.4375rem}h4{font-size:1.1875rem}h5{font-size:1rem}h6{font-size:.8125rem}p{margin:.5rem}ol,ul{margin:.5rem;padding-left:1rem}b,strong{font-weight:700}hr{box-sizing:content-box;border:0;overflow:visible;line-height:1.25em;margin:.5rem;height:.0625rem;background:linear-gradient(to right, #bdbdbd, #8c8c8c, #bdbdbd)}blockquote{display:block;position:relative;font-style:italic;background:#eee;margin:.5rem;padding:0.5rem 0.5rem 1.5rem;border-left:.25rem solid #505050;border-radius:0 .125rem .125rem 0}blockquote:after{position:absolute;font-style:normal;font-size:.875rem;color:#505050;left:.625rem;bottom:0;content:"— " attr(cite)}code,kbd,pre,samp{font-family:monospace, monospace}code{border-radius:.125rem;background:#e6e6e6;padding:0.125rem 0.25rem}pre{overflow:auto;border-radius:0 .125rem .125rem 0;background:#e6e6e6;padding:.75rem;margin:.5rem;border-left:.25rem solid #1565c0}kbd{border-radius:.125rem;background:#0c0c0c;color:#fafafa;padding:0.125rem 0.25rem}small,sup,sub{font-size:.75em}sup{top:-.5em}sub{bottom:-.25em}sup,sub{line-height:0;position:relative;vertical-align:baseline}a{color:#0277bd;text-decoration:underline;opacity:1;transition:opacity 0.3s}a:visited{color:#01579b}a:hover,a:focus{opacity:0.75}figcaption{font-size:.8125rem;color:#424242}.container{margin:0 auto;padding:0 .75rem}.row{box-sizing:border-box;display:-webkit-box;-webkit-box-flex:0;-webkit-box-orient:horizontal;-webkit-box-direction:normal;display:-webkit-flex;display:flex;-webkit-flex:0 1 auto;flex:0 1 auto;-webkit-flex-flow:row wrap;flex-flow:row wrap}.col-sm,[class^='col-sm-'],[class^='col-sm-offset-'],.row[class*='cols-sm-']>*{box-sizing:border-box;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;padding:0 0.25rem}.col-sm,.row.cols-sm>*{-webkit-box-flex:1;max-width:100%;-webkit-flex-grow:1;flex-grow:1;-webkit-flex-basis:0;flex-basis:0}.col-sm-1,.row.cols-sm-1>*{max-width:8.33333%;-webkit-flex-basis:8.33333%;flex-basis:8.33333%}.col-sm-2,.row.cols-sm-2>*{max-width:16.66667%;-webkit-flex-basis:16.66667%;flex-basis:16.66667%}.col-sm-3,.row.cols-sm-3>*{max-width:25%;-webkit-flex-basis:25%;flex-basis:25%}.col-sm-4,.row.cols-sm-4>*{max-width:33.33333%;-webkit-flex-basis:33.33333%;flex-basis:33.33333%}.col-sm-5,.row.cols-sm-5>*{max-width:41.66667%;-webkit-flex-basis:41.66667%;flex-basis:41.66667%}.col-sm-6,.row.cols-sm-6>*{max-width:50%;-webkit-flex-basis:50%;flex-basis:50%}.col-sm-7,.row.cols-sm-7>*{max-width:58.33333%;-webkit-flex-basis:58.33333%;flex-basis:58.33333%}.col-sm-8,.row.cols-sm-8>*{max-width:66.66667%;-webkit-flex-basis:66.66667%;flex-basis:66.66667%}.col-sm-9,.row.cols-sm-9>*{max-width:75%;-webkit-flex-basis:75%;flex-basis:75%}.col-sm-10,.row.cols-sm-10>*{max-width:83.33333%;-webkit-flex-basis:83.33333%;flex-basis:83.33333%}.col-sm-11,.row.cols-sm-11>*{max-width:91.66667%;-webkit-flex-basis:91.66667%;flex-basis:91.66667%}.col-sm-12,.row.cols-sm-12>*{max-width:100%;-webkit-flex-basis:100%;flex-basis:100%}.col-sm-offset-0{margin-left:0}.col-sm-offset-1{margin-left:8.33333%}.col-sm-offset-2{margin-left:16.66667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.33333%}.col-sm-offset-5{margin-left:41.66667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.33333%}.col-sm-offset-8{margin-left:66.66667%}.col-sm-offset-9{margin-lef
|
||
|
transform 0.3s}.tabs>label:not(:first-of-type){border-left:0}.tabs>:checked+label{background:#eee;border-bottom-color:#0277bd}.tabs>:checked+label:hover,.tabs>:checked+label:focus{background:rgba(238,238,238,0.8)}.tabs>:checked+label+div{box-sizing:border-box;position:relative;height:400px;width:100%;overflow:auto;margin:0;-webkit-transform:scaleY(1);transform:scaleY(1);background:#fafafa;border:.0625rem solid #bdbdbd;border-top:0;padding:.5rem;clip:auto;-webkit-clip-path:inset(0%);clip-path:inset(0%)}.tabs.stacked{-webkit-box-orient:vertical;-webkit-flex-direction:column;flex-direction:column}.tabs.stacked>label{-webkit-order:initial;order:initial}.tabs.stacked>:checked+label{border:.0625rem solid #bdbdbd}.tabs.stacked>label+div{-webkit-order:initial;order:initial;-webkit-transform-origin:top;transform-origin:top}.tabs.stacked>label:not(:first-of-type){border:.0625rem solid #bdbdbd;border-top:0}.tabs.stacked>:checked+label+div{height:auto}@media screen and (max-width: 767px){.tabs{-webkit-box-orient:vertical;-webkit-flex-direction:column;flex-direction:column}.tabs>label{-webkit-order:initial;order:initial}.tabs>:checked+label{border:.0625rem solid #bdbdbd}.tabs>label+div{-webkit-order:initial;order:initial}.tabs>label:not(:first-of-type){border:.0625rem solid #bdbdbd;border-top:0}}mark{background:#0277bd;color:#fafafa;font-size:.9375em;line-height:1em;border-radius:.125rem;padding:0.125em 0.25em}mark.inline-block{display:inline-block}.toast{position:fixed;background:#424242;bottom:1.5rem;left:50%;transform:translate(-50%, -50%);color:#fafafa;border-radius:2rem;padding:0.75rem 1.5rem;z-index:1111}.tooltip{position:relative;display:inline-block}.tooltip:before,.tooltip:after{position:absolute;opacity:0;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%);transition:all 0.3s;z-index:1010;left:50%}.tooltip:not(.bottom):before,.tooltip:not(.bottom):after{bottom:75%}.tooltip.bottom:before,.tooltip.bottom:after{top:75%}.tooltip:hover:before,.tooltip:hover:after,.tooltip:focus:before,.tooltip:focus:after{opacity:1;clip:auto;-webkit-clip-path:inset(0%);clip-path:inset(0%)}.tooltip:before{content:'';background:transparent;border:.5rem solid transparent;left:50%;left:calc(50% - .5rem)}.tooltip:not(.bottom):before{border-top-color:#212121}.tooltip.bottom:before{border-bottom-color:#212121}.tooltip:after{content:attr(aria-label);background:#212121;border-radius:.125rem;color:#fafafa;padding:.5rem;white-space:nowrap;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.tooltip:not(.bottom):after{margin-bottom:1rem}.tooltip.bottom:after{margin-top:1rem}.modal{position:fixed;top:0;left:0;display:none;width:100vw;height:100vh;background:rgba(0,0,0,0.45)}.modal .card{margin:0 auto;max-height:50vh;overflow:auto}.modal .card .close{position:absolute;top:.75rem;right:.25rem;padding:0}:checked+.modal{display:-webkit-box;-webkit-box-flex:0;display:-webkit-flex;display:flex;-webkit-flex:0 1 auto;flex:0 1 auto;z-index:1200}:checked+.modal .card .close{z-index:1211}mark.secondary{background:#e53935}mark.tertiary{background:#689f38}mark.tag{border-radius:200px;padding:0.25em 0.5em}mark.inline-block{font-size:1em;line-height:1.375em;padding:.375em}.toast.small{border-radius:1.5rem;padding:0.5rem 1rem}.toast.large{border-radius:3rem;padding:1.125rem 2.25rem}progress{display:block;vertical-align:baseline;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:.625rem;width:90%;width:calc(100% - 1rem);margin:.5rem .5rem;border:0;border-radius:.125rem;background:#e0e0e0;color:#0277bd}progress::-webkit-progress-value{background:#0277bd;border-top-left-radius:.125rem;border-bottom-left-radius:.125rem}progress::-webkit-progress-bar{background:#e0e0e0}progress::-moz-progress-bar{background:#0277bd;border-top-left-radius:.125rem;border-bottom-left-radius:.125rem}progress[value="1000"]::-webkit-progress-value{border-radius:.125rem}progress[value="1000"]::-moz-progress-bar{border-radius:.125rem}@-webkit-keyframes spinner-donut-anim{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@keyframes sp
|
||
|
|
||
|
maincss = `h1, h2, h3, h4, h5, h6 {
|
||
|
text-transform: capitalize;
|
||
|
}
|
||
|
img {
|
||
|
image-orientation: from-image;
|
||
|
}
|
||
|
.recipeCardTitle {
|
||
|
text-decoration: none;
|
||
|
color: unset;
|
||
|
}
|
||
|
.recipeCardDesc {
|
||
|
max-height: 200px;
|
||
|
overflow: auto;
|
||
|
}
|
||
|
#recipeImages {
|
||
|
padding-top: 20px;
|
||
|
text-align: center;
|
||
|
}
|
||
|
.recipeImage {
|
||
|
cursor: pointer;
|
||
|
}
|
||
|
.imageModal {
|
||
|
margin-top: 15%;
|
||
|
max-width: 50%;
|
||
|
max-height: 50%;
|
||
|
text-align: right;
|
||
|
}
|
||
|
.recipeCardImageSection {
|
||
|
text-align: center;
|
||
|
}`
|
||
|
|
||
|
templateHeader = `{{ define "header" }}
|
||
|
<!doctype html>
|
||
|
<html lang="en">
|
||
|
<head>
|
||
|
<title>{{ .PageTitle }}</title>
|
||
|
<meta charset="utf-8">
|
||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
|
<link rel="stylesheet" href="/css/mini.css">
|
||
|
<link rel="stylesheet" href="/css/main.css">
|
||
|
</head>
|
||
|
<body>
|
||
|
<header class="sticky">
|
||
|
<a href="/" class="logo">Recipe Card</a>
|
||
|
<a role="button" href="/recipes">Recipes</a>
|
||
|
</header>
|
||
|
{{ end }}`
|
||
|
|
||
|
templateFooter = `{{ define "footer" }}
|
||
|
</body>
|
||
|
</html>
|
||
|
{{ end }}`
|
||
|
|
||
|
templateSearchForm = `{{ define "searchform" }}
|
||
|
<div class="row cols-sm-4">
|
||
|
<div>
|
||
|
</div>
|
||
|
<form action="/search/" method="post">
|
||
|
<div class="input-group vertical">
|
||
|
<input type="text" value="{{ .SearchValue }}" name="search" id="search" placeholder="search">
|
||
|
</div>
|
||
|
</form>
|
||
|
</div>
|
||
|
{{ end }}`
|
||
|
|
||
|
templateIndex = `{{ define "index" }}
|
||
|
{{ template "header" . }}
|
||
|
{{ template "searchform" . }}
|
||
|
{{ template "footer" . }}
|
||
|
{{ end }}`
|
||
|
|
||
|
templateRecipeCard = `{{ define "recipecard" }}
|
||
|
<div class="card">
|
||
|
{{ if .StockImage }}
|
||
|
<div class="section recipeCardImageSection">
|
||
|
<a href="{{ .URL }}">
|
||
|
<img class="media shadowed rounded" alt="Recipe Image" src="{{ index .StockImage }}">
|
||
|
</a>
|
||
|
</div>
|
||
|
{{ end }}
|
||
|
<div class="section">
|
||
|
<a href="{{ .URL }}" class="recipeCardTitle"><h2>{{ .ID }}</h2></a>
|
||
|
</div>
|
||
|
<div class="section recipeCardDesc">
|
||
|
{{ .Description }}
|
||
|
</div>
|
||
|
</div>
|
||
|
{{ end }}`
|
||
|
|
||
|
templateRecipeCards = `{{ define "recipecards" }}
|
||
|
<div class="row">
|
||
|
{{ range . }}
|
||
|
{{ template "recipecard" . }}
|
||
|
{{ end }}
|
||
|
</div>
|
||
|
{{ end }}`
|
||
|
|
||
|
templateSearch = `{{ define "search" }}
|
||
|
{{ template "header" . }}
|
||
|
{{ template "searchform" . }}
|
||
|
{{ if .Recipes }}
|
||
|
{{ template "recipecards" .Recipes }}
|
||
|
{{ else }}
|
||
|
<h4>Unable to find recipes. :(</h4>
|
||
|
{{ end }}
|
||
|
{{ template "footer" . }}
|
||
|
{{ end }}`
|
||
|
|
||
|
templateRecipes = `{{ define "recipes" }}
|
||
|
{{ template "header" . }}
|
||
|
{{ if .Recipes }}
|
||
|
{{ template "recipecards" .Recipes }}
|
||
|
{{ else }}
|
||
|
<h1>No recipes found :(</h2>
|
||
|
{{ end }}
|
||
|
{{ template "footer" . }}
|
||
|
{{ end }}`
|
||
|
|
||
|
templateRecipe = `{{ define "recipe" }}
|
||
|
{{ template "header" . }}
|
||
|
{{ with index .Recipes 0 }}
|
||
|
<div class="container">
|
||
|
<div class="row">
|
||
|
{{ if or .StockImage .Images }}
|
||
|
<div id="recipeImages" class="col-sm-3">
|
||
|
{{ end }}
|
||
|
{{ if .StockImage }}
|
||
|
<img class="shadowed rounded" src="{{ .StockImage }}" alt="Stock image">
|
||
|
{{ end }}
|
||
|
{{ range $index, $results := .Images }}
|
||
|
<label for="modal-{{ $index }}">
|
||
|
<img class="recipeImage shadowed rounded" src="{{ . }}" alt="Recipe card">
|
||
|
</label>
|
||
|
<input id="modal-{{ $index }}" type="checkbox">
|
||
|
<div class="modal">
|
||
|
<div class="container imageModal">
|
||
|
<label for="modal-{{ $index }}" class="close"></label>
|
||
|
<img src="{{ . }}" alt="Recipe card">
|
||
|
</div>
|
||
|
</div>
|
||
|
{{ end }}
|
||
|
{{ if or .StockImage .Images }}
|
||
|
</div>
|
||
|
{{ end }}
|
||
|
<div class="col-sm">
|
||
|
<a class="recipeCardTitle" href="{{ .DocxURL }}"><h1>{{ .ID }}</h1></a>
|
||
|
<hr>
|
||
|
{{ .Description }}
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
{{ end }}
|
||
|
{{ template "footer" . }}
|
||
|
{{ end }}`
|
||
|
)
|
||
|
|
||
|
// TemplateData defines default template data to pass to every template
|
||
|
type TemplateData struct {
|
||
|
// title of the page
|
||
|
PageTitle string
|
||
|
// previous value used for searching
|
||
|
SearchValue string
|
||
|
// list of recipes to display
|
||
|
Recipes []*TemplateRecipe
|
||
|
}
|
||
|
|
||
|
// TemplateRecipe used for all recipes whether it is an aggregate or a singular recipe
|
||
|
type TemplateRecipe struct {
|
||
|
// title of the recipe
|
||
|
ID string
|
||
|
// description of the recipe in HTML
|
||
|
Description template.HTML
|
||
|
// relative URL to the recipe page itself
|
||
|
URL string
|
||
|
// relative URL to the recipe docx file
|
||
|
DocxURL string
|
||
|
// stock image relative path for the recipe
|
||
|
StockImage string
|
||
|
// card scan images relative paths for the recipe
|
||
|
Images []string
|
||
|
}
|
||
|
|
||
|
// NewTemplate creates a new template instance with all recipe-card related templates parsed
|
||
|
func NewTemplate(logger *log.Logger) (tmpl *template.Template, err error) {
|
||
|
tmpl = template.New("recipe-card")
|
||
|
|
||
|
if logger == nil {
|
||
|
logger = log.New()
|
||
|
logger.Out = ioutil.Discard
|
||
|
}
|
||
|
|
||
|
logger.Debugln("Parsing header template")
|
||
|
_, err = tmpl.Parse(templateHeader)
|
||
|
if err != nil {
|
||
|
err = fmt.Errorf("templateHeader: %s", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
logger.Debugln("Finished parsing header template")
|
||
|
|
||
|
logger.Debugln("Parsing footer template")
|
||
|
_, err = tmpl.Parse(templateFooter)
|
||
|
if err != nil {
|
||
|
err = fmt.Errorf("templateFooter: %s", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
logger.Debugln("Finished parsing footer template")
|
||
|
|
||
|
logger.Debugln("Parsing recipe card template")
|
||
|
_, err = tmpl.Parse(templateRecipeCard)
|
||
|
if err != nil {
|
||
|
err = fmt.Errorf("templateRecipeCard: %s", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
logger.Debugln("Finished parsing recipe card template")
|
||
|
|
||
|
logger.Debugln("Parsing recipe cards template")
|
||
|
_, err = tmpl.Parse(templateRecipeCards)
|
||
|
if err != nil {
|
||
|
err = fmt.Errorf("templateRecipeCards: %s", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
logger.Debugln("Finished parsing recipe cards template")
|
||
|
|
||
|
logger.Debugln("Parsing search form template")
|
||
|
_, err = tmpl.Parse(templateSearchForm)
|
||
|
if err != nil {
|
||
|
err = fmt.Errorf("templateSearchForm: %s", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
logger.Debugln("Finished parsing search form template")
|
||
|
|
||
|
logger.Debugln("Parsing index template")
|
||
|
_, err = tmpl.Parse(templateIndex)
|
||
|
if err != nil {
|
||
|
err = fmt.Errorf("templateIndex: %s", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
logger.Debugln("Finished parsing index template")
|
||
|
|
||
|
logger.Debugln("Parsing recipes template")
|
||
|
_, err = tmpl.Parse(templateRecipes)
|
||
|
if err != nil {
|
||
|
err = fmt.Errorf("templateRecipes: %s", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
logger.Debugln("Finished parsing recipes template")
|
||
|
|
||
|
logger.Debugln("Parsing recipe template")
|
||
|
_, err = tmpl.Parse(templateRecipe)
|
||
|
if err != nil {
|
||
|
err = fmt.Errorf("templateRecipe: %s", err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
logger.Debugln("Finished parsing recipe template")
|
||
|
|
||
|
logger.Debugln("Parsing search template")
|
||
|
_, err = tmpl.Parse(templateSearch)
|
||
|
if err != nil {
|
||
|
err = fmt.Errorf("templateSearch: %s", err)
|
||
|
}
|
||
|
|
||
|
logger.Debugln("Finished parsing search template")
|
||
|
|
||
|
return
|
||
|
}
|