This commit is contained in:
Martin Donnelly 2019-12-11 00:07:07 +00:00
commit 9e2c07ba38
42 changed files with 12428 additions and 0 deletions

5
.babelrc Normal file
View File

@ -0,0 +1,5 @@
{
"presets": [
"es2015"
]
}

55
.eslintrc Normal file
View File

@ -0,0 +1,55 @@
{
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"ecmaFeatures": {
"jsx": false
}
},
"env": {
"browser": false,
"node": true,
"es6": true
},
"rules": {
"arrow-spacing": "error",
"block-scoped-var": "error",
"block-spacing": "error",
"brace-style": ["error", "stroustrup", {}],
"camelcase": "error",
"comma-dangle": ["error", "never"],
"comma-spacing": ["error", { "before": false, "after": true }],
"comma-style": [1, "last"],
"consistent-this": [1, "_this"],
"curly": [1, "multi"],
"eol-last": 1,
"eqeqeq": 1,
"func-names": 1,
"indent": ["error", 2, { "SwitchCase": 1 }],
"lines-around-comment": ["error", { "beforeBlockComment": true, "allowArrayStart": true }],
"max-len": [1, 180, 2, { "ignoreTemplateLiterals": true, "ignoreStrings": true, "ignoreUrls": true, "ignoreTrailingComments": true, "ignoreComments": true }], // 2 spaces per tab, max 80 chars per line
"new-cap": 1,
"newline-before-return": "error",
"no-array-constructor": 1,
"no-inner-declarations": [1, "both"],
"no-mixed-spaces-and-tabs": 1,
"no-multi-spaces": 2,
"no-new-object": 1,
"no-shadow-restricted-names": 1,
"object-curly-spacing": ["error", "always"],
"padded-blocks": ["error", { "blocks": "never", "switches": "always" }],
"prefer-const": "error",
"prefer-template": "error",
"one-var": 0,
"quote-props": ["error", "always"],
"quotes": [1, "single"],
"radix": 1,
"semi": [1, "always"],
"space-before-blocks": [1, "always"],
"space-infix-ops": 1,
"vars-on-top": 1,
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 1 }],
"spaced-comment": ["error", "always", { "markers": ["/"] }]
}
}

163
.gitignore vendored Normal file
View File

@ -0,0 +1,163 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# artefacts/screenshots/*.png
artefacts/*.txt
artefacts/*.json
# artefacts/*.html
# artefacts/*
/tests/*.zip
/output/
/dist/
!/tests/data/
/tests/sink/
/debug/
/update.sh
/setup/web/
/backup/
/archive.tar.gz
/user/
/zip
!/artefacts/

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
node_modules/*
public

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 2,
"useTabs": false
}

12
.travis.yml Normal file
View File

@ -0,0 +1,12 @@
language: node_js
sudo: false
node_js:
- "8.11.1"
install:
- npm install
script:
- npm run test

33
gulp/backbone.js Normal file
View File

@ -0,0 +1,33 @@
'use strict';
const browserify = require('browserify');
const gulp = require('gulp');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const uglify = require('gulp-uglify-es').default;
const sourcemaps = require('gulp-sourcemaps');
const gutil = require('gulp-util');
const rename = require('gulp-rename');
const stripDebug = require('gulp-strip-debug');
gulp.task('bundleApp', function () {
// set up the browserify instance on a task basis
const b = browserify({
'debug': true,
'entries': './src/v1/js/app.js'
});
return b.bundle()
.pipe(source('app.js'))
.pipe(buffer())
// .pipe(stripDebug())
.pipe(rename('bundle.js'))
.pipe(sourcemaps.init({ 'loadMaps': true }))
// Add transformation tasks to the pipeline here.
// .pipe(uglify())
.on('error', gutil.log)
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('./live/js'));
});

5
gulpfile.js Normal file
View File

@ -0,0 +1,5 @@
const gulp = require('gulp');
const requireDir = require('require-dir');
requireDir('./gulp');

407
list.js Normal file
View File

@ -0,0 +1,407 @@
const mailer = new UltraSES({
'aws': {
'accessKeyId': 'AKIAJWJS75F7WNCGK64A',
'secretAccessKey': '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',
'region': 'eu-west-1'
}, 'defaults': {
'from': 'Martin Donnelly <martind2000@gmail.com>'
}
});
const menu = [
{
'name': 'butter chicken in silky sauce',
'url': 'http://www.marksdailyapple.com/butter-chicken-in-a-silky-sauce/#axzz29jTSubMo',
'hash': '5117109d69d399915c0959eabbc383f9'
}, {
'name': 'steak',
'url': 'https://www.farmison.com/community/recipe/how-to-cook-your-beef-fillet-steak',
'hash': '35e244830bde1d967298fb9a585854f7'
}, {
'name': 'hazelnut crusted chicken',
'url': 'http://www.marksdailyapple.com/hazelnut-crusted-chicken-with-stealth-coconut/#axzz29jTSubMo',
'hash': 'baaf075a2384ff84ef3d9ce4eebfafff'
}, { 'name': 'vegetable latkes', 'url': 'http://www.marksdailyapple.com/vegetable-latkes/#axzz29jTSubMo', 'hash': '11a043bfe06505f88b0bcfb59095ecab' },
{
'name': 'beef stew',
'url': 'http://www.marksdailyapple.com/beef-stew-and-chicken-soup-in-35-minutes-or-less/#axzz29jTSubMo',
'hash': '45f5a2176ff5547585a5e562e0914776'
}, {
'name': 'sausage patties',
'url': 'https://www.tasteslovely.com/homemade-paleo-breakfast-sausage-patties/',
'hash': 'c6152fba4df0de77cb139488eaeb4690'
}, {
'name': 'primal tex mex torillas',
'url': 'http://www.marksdailyapple.com/primal-tex-mex-tortillas-and-taco-seasoning/#axzz29jTSubMo',
'hash': '08d2e6f8a56619b833e412473ef31e06'
}, {
'name': 'primal texas chili',
'url': 'http://www.marksdailyapple.com/primal-texas-chili/#axzz29jTSubMo',
'hash': '319718f91f88fe96840cbdbf09bdb0b3'
}, { 'name': 'moroccan burgers', 'url': 'https://sarahfragoso.com/moroccan-burgers-and-beet-salad/', 'hash': '0a4e01068e349791d6e9f2a0c9a80d98' }, {
'name': 'paleo spaghetti',
'url': 'https://paleoleap.com/paleo-spaghetti/',
'hash': '59394666f0320c909c057fc535b99387'
}, {
'name': 'primal chicken tikka masala',
'url': 'http://www.marksdailyapple.com/primal-chicken-tikka-masala/#axzz29jTSubMo',
'hash': '1222c577eb67f601088f7649efd329a9'
}, {
'name': 'butter stuffed chicken kiev',
'url': 'http://www.marksdailyapple.com/butter-stuffed-chicken-kiev/#axzz29jTSubMo',
'hash': '1b657d8d5e88af4dba220b875e496155'
}, {
'name': 'tender lemon parsley brisket',
'url': 'http://www.marksdailyapple.com/tender-lemon-parsley-brisket/#axzz29jTSubMo',
'hash': '5256dd240e04a62c5b002e5dc4234618'
}
,{
'name': 'taco bowl crispy kale',
'url': 'http://www.marksdailyapple.com/taco-bowl-with-crispy-kale-chips/#axzz29jTSubMo',
'hash': 'fb3e4a7648a53e93490f6274f731e427'
}, {
'name': 'lime and basil kebabs',
'url': 'http://www.marksdailyapple.com/lime-and-basil-beef-kebabs/#axzz29jTSubMo',
'hash': '47d38404c7b5f4b50df1f39b6c26845d'
}, {
'name': 'sun dried tomato meatballs',
'url': 'https://sarahfragoso.com/sun-dried-tomato-meatballs-with-creamy-pesto/',
'hash': 'bb22a54c34eed937b17bb232aee5231c'
}, {
'name': 'slow cooked coconut ginger pork',
'url': 'http://www.marksdailyapple.com/slow-cooked-coconut-ginger-pork/#axzz29jTSubMo',
'hash': '810acff788fae541bf6f037becc88926'
}, {
'name': 'balsamic glazed drumsticks',
'url': 'http://www.marksdailyapple.com/balsamic-glazed-drumsticks/#axzz29jTSubMo',
'hash': 'b4ea4f7e46f8d435ac87e56f4069fb29'
}, {
'name': 'herb chicken cooked under a brick',
'url': 'http://www.marksdailyapple.com/herb-chicken-cooked-under-a-brick/#axzz29jTSubMo',
'hash': '296cf41b69560cb8f898044471cef370'
}, {
'name': 'pork stuffed jalapeno peppers',
'url': 'http://www.marksdailyapple.com/pork-stuffed-jalapeno-peppers/#axzz29jTSubMo',
'hash': 'd85f7f04d1efe18ca0342a95e6a94318'
}, {
'name': 'spiced pork butternut squash',
'url': 'http://www.marksdailyapple.com/spiced-pork-and-butternut-squash-with-sage/#axzz29jTSubMo',
'hash': 'd681bdb030c64156e9155d29533f945b'
}, {
'name': 'shakshuka',
'url': 'http://www.marksdailyapple.com/shakshuka-eggs-poached-in-spicy-tomato-sauce/#axzz29jTSubMo',
'hash': 'fdf0904c40e5c037b42ff657538a0670'
}, {
'name': 'grilled lime chicken',
'url': 'http://www.simplyrecipes.com/recipes/grilled_lime_chicken_with_black_bean_sauce/',
'hash': '9c7cf2b0f15378cbebfd4cc9c6d72091'
}, { 'name': 'eggs benadict burger', 'url': 'https://paleoleap.com/eggs-benedict-burgers/', 'hash': '5d71a32d58190c67ac0597289db0b472' }, {
'name': 'asian marinated steaks',
'url': 'https://web.archive.org/web/20120330045230/http://www.paleotable.com/2011/01/asian-marinated-steaks.html',
'hash': 'bd6a0fe62b93e93a988cdbc1ead53f8f'
}, {
'name': 'tango burgers',
'url': 'https://web.archive.org/web/20120127101048/http://www.paleotable.com/2011/01/tango-burgers.html',
'hash': '61f794329b2144d17ef55d1806b635f9'
}, {
'name': 'meatza',
'url': 'https://web.archive.org/web/20120118062740/http://www.paleotable.com/2011/02/meatza.html',
'hash': 'cf8306e8f197a1c66337141c87f0355c'
}, {
'name': 'baked chicken with roasted tomatos',
'url': 'https://web.archive.org/web/20120125102938/http://www.paleotable.com/2011/01/baked-chicken-with-roasted-tomatoes.html',
'hash': '065784446d4c1296409589bc9e864ac5'
}, {
'name': 'blackened chicken',
'url': 'https://web.archive.org/web/20120420232533/http://www.paleotable.com/2011/02/blackened-chicken.html',
'hash': '75bbaf2bac48017ed81a51a6675fe2ca'
}, {
'name': 'breaded baked chicken',
'url': 'https://www.tastesoflizzyt.com/paleo-baked-chicken/',
'hash': '801def843d96d8bd8c2bb5cca201a0e9'
}, {
'name': 'chicken and avocado tostadas',
'url': 'https://web.archive.org/web/20120707073826/http://www.paleotable.com/2011/03/chicken-and-avocado-tostadas.html',
'hash': '917337fb338a88ddd5d86191514d8b14'
}, {
'name': 'sausages and pepperonata',
'url': 'https://web.archive.org/web/20120707071653/http://www.paleotable.com/2011/01/sausages-and-pepperonata.html',
'hash': '600d87a5710f69079e8ca1103dc3a03d'
}, {
'name': 'Mediterranean Beef Stew with Green Olive Pesto',
'url': 'http://www.health-bent.com/soups/paleo-mediterranean-beef-stew',
'hash': 'acd935ee010ce4f06de7a3f593c140dc'
}, {
'name': 'Beanless Chili',
'url': 'https://web.archive.org/web/20121004005636/http://www.realfoodfreaks.com/2012/04/02/chili-beanless-cuz-i-dont-eat-em/',
'hash': 'e4db2a9a21f7ac494ca8a8b988d2ea2b'
}, {
'name': 'Grilled Steak with Roasted Jalapeño Chimichurri',
'url': 'https://www.seriouseats.com/recipes/2012/05/ted-allens-grilled-steak-with-roasted-jalepeno.html',
'hash': '52eb19e90993d5fbe2efa5451b291462'
}, {
'name': 'Lime and Coconut Chicken',
'url': 'http://chaosinthekitchen.com/2009/07/lime-and-coconut-chicken/',
'hash': '7f7bda5e4f91b3efb63c2982c9780d21'
}, {
'name': 'BEEF AND VEGETABLE CHILI',
'url': 'http://www.paleoplan.com/2011/10-12/steak-and-vegetable-chili/',
'hash': 'd9a16cfd6a533d820536ba231e68fe2b'
}, {
'name': 'PORK LOIN WITH PEPPERS, MUSHROOMS AND ONIONS',
'url': 'https://www.paleoplan.com/2010/02-23/pork-loin-with-peppers-mushrooms-n-onions/',
'hash': '7c3b3f53e7acc60b651f65cd9743f9bd'
}, {
'name': 'Melt In Your Mouth Slow Cooker Beef Brisket',
'url': 'https://paleogrubs.com/beef-brisket-recipe',
'hash': '92e81d0fc1d96b1669d0dcd98a0e355e'
}, {
'name': 'Yummy Sweet Potato Gnocchi',
'url': 'https://paleogrubs.com/sweet-potato-gnocchi-recipe',
'hash': '9c1c921687e157a5670c6746bbceeee4'
}, {
'name': 'Simple Beef and Broccoli Stir Fry',
'url': 'https://paleogrubs.com/beef-and-broccoli-recipe',
'hash': '13019ef536801ac1a7d702b1b856ce0b'
}, {
'name': 'Crockpot Balsamic Roast Beef',
'url': 'https://www.primallyinspired.com/crockpot-balsamic-roast-beef/',
'hash': 'a725b6277a8c56cc9eca3e3f66128278'
}, {
'name': 'Quick & Easy Crock Pot Chili',
'url': 'https://www.healthstartsinthekitchen.com/2014/01/04/quick-crock-pot-chili/',
'hash': '3d119153603010aaad3b921da7ddeed0'
}, {
'name': 'Roasted, Herbed Beef Tenderloin Recipe',
'url': 'https://www.chowhound.com/recipes/roasted-herbed-beef-tenderloin-30901',
'hash': '1162e3121bfa2e3acbf9b112ca917318'
}, {
'name': 'Chorizo Carbonara',
'url': 'https://www.jamieoliver.com/recipes/pasta-recipes/chorizo-carbonara-with-catalan-market-salad/#1EmLf9RiqepY4Z95.97',
'hash': '5a81acc6802404c16ccb595946638706'
}, {
'name': 'swedish meatballs',
'url': 'https://www.paleorunningmomma.com/paleo-swedish-meatballs-whole30/',
'hash': '77724ec628b02f8ce7914a03c1266162'
}, { 'name': 'Hungarian Beef Goulash', 'url': 'https://paleoleap.com/hungarian-beef-goulash/', 'hash': '45c162ef0c0bab221120df4236bdcbf3' }, {
'name': 'Beef Bourguignon',
'url': 'http://paleoleap.com/beef-bourguignon/',
'hash': '16fb7eb44aec586069f9e34251c5f49a'
}, {
'name': 'Minted pesto chicken stir-fry',
'url': 'http://paleoleap.com/quick-and-easy-paleo-stir-fries/',
'hash': 'e3a28f3e02eab851a46c8e5455172791'
}, {
'name': 'Coconut curry stir-fry',
'url': 'http://paleoleap.com/quick-and-easy-paleo-stir-fries/',
'hash': '4efbc7c204e2a003edf3027187f44d27'
}, {
'name': 'Citrus beef salad stir-fry',
'url': 'http://paleoleap.com/quick-and-easy-paleo-stir-fries/',
'hash': '14ac93f7b0c699cc0ed4ca91891029b0'
},
{ 'name': 'a normal chicken stir fry', 'url': '', 'hash': 'bbbef5bd2c023ebd904cc87d4c82c047' }, {
'name': 'hunters chicken ( chicken wrapped in bacon / pancetta )',
'url': '',
'hash': '68575933d8055d3d893ebcce9d4a5461'
}, {
'name': 'Caribbean Pork Chops with Mango Salsa | Paleo Grubs',
'url': 'https://paleogrubs.com/caribbean-pork-chops-recipe',
'hash': '40d87c777140a4d09e4354ad1931efdb'
}, {
'name': 'Paleo Pork Tenderloin with Fig and Balsamic Glaze',
'url': 'http://paleogrubs.com/balsamic-pork-tenderloin-recipe',
'hash': 'ff4a57645ef5da39e7f138fda943f720'
}, {
'name': 'Chicken Cordon-Bacon',
'url': 'https://fastpaleo.com/recipe/bacon-wrapped-chicken-cordon-bleu/',
'hash': '8752697521079bc508bf42a3de61c03e'
}, {
'name': 'Grilled Tandoori Chicken',
'url': 'http://cookingweekends.blogspot.com/2012/05/grilled-tandoori-chicken.html',
'hash': 'cf57581455fc17e6e9b657b0d2bbaa6f'
}, {
'name': 'Stuffed Chicken Breast (paleo and grain free)',
'url': 'http://www.cavegirlcuisine.com/recipes/stuffed-chicken-breast/',
'hash': '709e1d41d3d0999a2c8443e0a170783f'
}, {
'name': 'Lemon Chicken Breast Recipe',
'url': 'https://www.everydaymaven.com/lemon-chicken-breast/',
'hash': '370ca9c8e6a4bb5ff892f066a82c4071'
}, {
'name': 'Grilled Lemon Garlic Rosemary Chicken Breasts',
'url': 'https://www.phoenixhelix.com/2013/07/21/grilled-lemon-garlic-rosemary-chicken-breasts/',
'hash': '463ee932b56b617bde9812f6032a6974'
}, {
'name': 'Paleo Spicy Mustard Crockpot Chicken',
'url': 'https://www.plaidandpaleo.com/2013/10/paleo-spicy-mustard-crockpot-chicken.html',
'hash': 'a6c8493204d4a117f512f516e7ebbc38'
}, {
'name': 'Creamy Paleo Chicken Skillet',
'url': 'http://paleogrubs.com/skillet-chicken-recipe',
'hash': '9b5602e594a5bba813b06835b3acef78'
}, {
'name': 'Sweet and Savory Moroccan Chicken',
'url': 'http://paleogrubs.com/moroccan-chicken-recipe',
'hash': 'bff3e9ac8dc0b2e0c59ddbb0d3cce27a'
}, {
'name': 'Simple and Addictive Chicken Kabobs',
'url': 'http://paleogrubs.com/chicken-kabob-recipe',
'hash': 'ad4f16c87dc76df8f53f0c57e517d574'
}, {
'name': 'Indian Paleo Stew with Chicken',
'url': 'http://paleogrubs.com/indian-paleo-stew-recipe',
'hash': 'd9fad92471bf383f97b512d4b0a81fb2'
}, {
'name': 'Slow-Cooker Swedish Kalops Stew',
'url': 'https://web.archive.org/web/20130501032143/http://www.mylivingnutrition.com/2013/04/11/slow-cooker-swedish-kalops-stew/',
'hash': '866b670b9da5974e56b7a7759b406897'
}, {
'name': 'Beef, Bacon and Rum Stew',
'url': 'http://domesticsoul.com/2011/12/beef-bacon-rum-stew-recipe.html',
'hash': '7601dee9f0ab54604b1afca6978bda53'
}, {
'name': 'Paleo Orange Cinnamon Beef Stew',
'url': 'https://thehealthyfoodie.com/orange-cinnamon-beef-stew/',
'hash': '9c2190641bb066f638bf87f3147a5347'
}, { 'name': 'pepper steak', 'url': 'http://paleoleap.com/pepper-steak/', 'hash': 'e61d26358e9854fb6e661df024cc4aac' }, {
'name': 'GARLIC ROASTED COD',
'url': 'http://paleoleap.com/garlic-roasted-cod/',
'hash': '860ef03f3f862e46ee7a12468cee4a8f'
}, {
'name': 'Easy Bacon-Wrapped Paleo Meatloaf Muffins',
'url': 'https://paleogrubs.com/bacon-wrapped-meatloaf-muffins',
'hash': '41eeb396e1a113b34383a3ab286ac38f'
}, {
'name': 'Easy Ground Beef Stuffed Peppers',
'url': 'https://paleogrubs.com/ground-beef-stuffed-peppers',
'hash': '4349670e1f9c08d89491e83fea5f3041'
}, {
'name': 'balsamic grilled chicken with greek style salad',
'url': 'https://paleoleap.com/balsamic-grilled-chicken-greek-style-salad/',
'hash': '9b9ee3d83b88ed2c8b4f4ff50ca0b879'
}, {
'name': 'slow cooked hawaiian style kalua pork',
'url': 'https://paleoleap.com/slow-cooked-hawaiian-style-kalua-pork/',
'hash': 'b3411415c6ea78f4e4b73596b0b3aa0c'
}, {
'name': 'Sweet Thai Chili Chicken With Roasted Peppers',
'url': 'http://paleoleap.com/sweet-thai-chili-chicken-roasted-peppers/',
'hash': '4f524ce44423b2abe6ee5e92b379e69b'
}, {
'name': 'Steak Ratatouille',
'url': 'http://www.jamieoliver.com/recipes/beef-recipes/grilled-steak-ratatouille-saffron-rice/',
'hash': 'f21ac2e974c00ac45245aa68c83f477c'
}, {
'name': 'Slow Cooked Balsamic Pork Roast',
'url': 'https://paleoleap.com/slow-cooked-balsamic-pork-roast/',
'hash': 'c2c3c4b5c60fe6549c59d4dd1c6d55bf'
}, {
'name': 'Seared Cod with Fresh Herb Sauce',
'url': 'http://paleoleap.com/seared-cod-fresh-herb-sauce/',
'hash': '85e1f67407636b53958918142080635f'
}, {
'name': 'Honey Ginger Apple Shredded Pork',
'url': 'https://paleomg.com/honey-ginger-apple-shredded-pork/',
'hash': 'bd0485bf2f1e0019298e08c38011641b'
}, {
'name': 'Orange Pineapple Pulled Pork',
'url': 'http://www.smokedngrilled.com/orange-pineapple-pulled-pork/',
'hash': 'afb59b8879277bbeb2b5ec3b9b33ec52'
}, { 'name': 'Brazilian Curry Chicken', 'url': 'https://paleomg.com/brazilian-curry-chicken/', 'hash': 'd2c78d69054333cb52a9be6bbbf3a497' }, {
'name': 'Teriyaki Meatballs',
'url': 'https://paleogrubs.com/teriyaki-meatballs',
'hash': '4285b9472917c5c03402b973721d506f'
}, {
'name': 'Paleo Slow Cooker Bacon BBQ Chicken',
'url': 'https://paleogrubs.com/chicken-breast-recipes',
'hash': '3391a35433f97ed9c58918a41eb59fa7'
}, {
'name': 'Honey-Lime Grilled Chicken Skewers',
'url': 'https://paleogrubs.com/honey-lime-grilled-chicken-skewers',
'hash': '8a84beaad4169b3c845f0d65b2f33c29'
}, {
'name': 'Pork Chop With Applesauce And Roasted Tomatoes',
'url': 'https://paleoleap.com/pork-chop-applesauce-roasted-tomatoes/',
'hash': '88ec8f4a7e89cec6fe9687a38595b318'
}, {
'name': 'Slow Cooked Jerk Pork Tenderloin',
'url': 'https://paleoleap.com/slow-cooked-jerk-pork-tenderloin/',
'hash': '74dccaea2c0a83343720aee35ca91dc2'
}, { 'name': 'Slow cooked beef cheeks with pork & vanilla', 'url': 'https://irenamacri.com/recipes/slow-cooked-beef-cheeks/', 'hash': 'a39657743c78e7a1e5438243444af9b5' }];
const soups = [
{
'name': 'chicken soup',
'url':
'http://www.marksdailyapple.com/beef-stew-and-chicken-soup-in-35-minutes-or-less/#axzz29jTSubMo',
'hash': 'f5970ca52c37f67cd1fda14edad7fdce'
},
{
'name': 'Tom Kha Gai -- Thai Coconut Soup',
'url': 'https://www.angsarap.net/2012/03/08/tom-kha-gai/',
'hash': '670819e255ec45b344ec1cc30ad436fe'
},
{
'name': 'Roasted Tomato Soup with Fresh Basil',
'url': 'https://civilizedcaveman.com/recipes/soups/roasted-tomato-soup/',
'hash': 'a0f61259e62dfe6fdbf4feb42402f9cb'
},
{
'name': 'Tom Yum Gai - hot and sour soup',
'url': 'http://angsarap.net/2013/08/07/tom-yum-gai/',
'hash': 'f0edd41d24a04b2a4dcf72f685f2367b'
},
{
'name': 'Coconut Lime Chicken Soup',
'url': 'https://paleoleap.com/coconut-lime-chicken-soup/',
'hash': 'd2e316b3f58fe2191367d24d5e20deb9'
},
{
'name': 'Cream of Chicken Soup',
'url': 'https://paleogrubs.com/crockpot-cream-of-chicken-soup',
'hash': '46e33b03f670037de46d3425fdce4e78'
},
{
'name': 'Roasted Carrot Ginger Paleo Soup',
'url': 'https://paleogrubs.com/soup-recipes',
'hash': '26d8938367235d9c891ec1b87fd50cd8'
},
{
'name': 'Slow Cooker Taco Soup',
'url': 'https://paleogrubs.com/slow-cooker-taco-soup',
'hash': '926dcc730930547b3dabdc623613f8e9'
},
{
'name': 'Chicken Soup - MD',
'url':
'https://paleogrubs.com/wp-content/uploads/2019/03/Homemade-Chicken-No-Noodle-Soup-1.jpg',
'hash': 'dc7eacaaf9b2d2fe6d16273d20f5d052'
},
{
'name': 'Ham Soup - MD',
'url':
'https://paleogrubs.com/wp-content/uploads/2019/03/Homemade-Chicken-No-Noodle-Soup-1.jpg',
'hash': 'ec2bc0d8622baeab237b9057a828933c'
},
{
'name': 'Chicken & Chorizo - MD',
'url':
'https://paleogrubs.com/wp-content/uploads/2019/03/Homemade-Chicken-No-Noodle-Soup-1.jpg',
'hash': 'ab727cbfbc73cea127c61cc742b38d25'
},
{
'name': 'Chickpea Soup - Jess',
'url':
'https://paleogrubs.com/wp-content/uploads/2019/03/Homemade-Chicken-No-Noodle-Soup-1.jpg',
'hash': '85620ea6a3dc6692ee772adf2e4abb40'
},
{
'name': 'Crockpot Paleo Minestrone Soup',
'url': 'https://www.ourpaleolife.com/crockpot-paleo-minestrone-soup/',
'hash': 'b9d1e302487be049a6b1c573e007e751'
}
];

12
list.json Normal file
View File

@ -0,0 +1,12 @@
[
{
"name": "Asian pepper steak crock pot recipe",
"url": "https://www.marksdailyapple.com/asian-pepper-steak-crock-pot-recipe/#axzz29jTSubMo",
"hash": "bd96f4ac0089d553a93b8a37052568b6"
},
{
"name": "Beef burgandy",
"url": "http://www.marksdailyapple.com/beef-burgundy-recipe/#axzz29jTSubMo",
"hash": "1da0aafeccfe6a5b4beb853fdbec7aca"
}
]

428
live/css/App.css Normal file
View File

@ -0,0 +1,428 @@
@import url('https://fonts.googleapis.com/css?family=Roboto');
/* Global Styles */
:root {
--primary-color: #64B5F6;
--dark-color: #333333;
--light-color: #f4f4f4;
--danger-color: #dc3545;
--success-color: #28a745;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Roboto', sans-serif;
font-size: 1rem;
line-height: 1.6;
background-color: #fff;
color: #333;
}
a {
color: var(--primary-color);
text-decoration: none;
}
a:hover {
color: #666;
}
ul {
list-style: none;
}
img {
width: 100%;
}
.dataRow {
cursor: pointer;
}
/* Utilities */
.container {
max-width: 1100px;
margin: auto;
overflow: hidden;
padding: 0 2rem;
}
/* Text Styles*/
.x-large {
font-size: 4rem;
line-height: 1.2;
margin-bottom: 1rem;
}
.large {
font-size: 3rem;
line-height: 1.2;
margin-bottom: 1rem;
}
.lead {
font-size: 1.5rem;
margin-bottom: 1rem;
}
.text-center {
text-align: center;
}
.text-primary {
color: var(--primary-color);
}
.text-dark {
color: var(--dark-color);
}
.text-success {
color: var(--success-color);
}
.text-danger {
color: var(--danger-color);
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}
/* Center All */
.all-center {
display: flex;
flex-direction: column;
width: 100%;
margin: auto;
justify-content: center;
align-items: center;
text-align: center;
}
/* Cards */
.card {
padding: 1rem;
border: #ccc 1px dotted;
margin: 0.7rem 0;
}
/* List */
.list {
margin: 0.5rem 0;
}
.list li {
padding-bottom: 0.3rem;
}
/* Padding */
.p {
padding: 0.5rem;
}
.p-1 {
padding: 1rem;
}
.p-2 {
padding: 2rem;
}
.p-3 {
padding: 3rem;
}
.py {
padding: 0.5rem 0;
}
.py-1 {
padding: 1rem 0;
}
.py-2 {
padding: 2rem 0;
}
.py-3 {
padding: 3rem 0;
}
/* Margin */
.m {
margin: 0.5rem;
}
.m-1 {
margin: 1rem;
}
.m-2 {
margin: 2rem;
}
.m-3 {
margin: 3rem;
}
.my {
margin: 0.5rem 0;
}
.my-1 {
margin: 1rem 0;
}
.my-2 {
margin: 2rem 0;
}
.my-3 {
margin: 3rem 0;
}
/* Grid */
.grid-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 1rem;
}
.grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1rem;
}
.grid-4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 1rem;
}
.btn {
display: inline-block;
background: var(--light-color);
color: #333;
padding: 0.4rem 1.3rem;
font-size: 1rem;
border: none;
cursor: pointer;
margin-right: 0.5rem;
transition: opacity 0.2s ease-in;
outline: none;
}
.btn-link {
background: none;
padding: 0;
margin: 0;
}
.btn-block {
display: block;
width: 100%;
}
.btn-sm {
font-size: 0.8rem;
padding: 0.3rem 1rem;
margin-right: 0.2rem;
}
.badge {
display: inline-block;
font-size: 0.8rem;
padding: 0.2rem 0.7rem;
text-align: center;
margin: 0.3rem;
background: var(--light-color);
color: #333;
border-radius: 5px;
}
.alert {
padding: 0.7rem;
margin: 1rem 0;
opacity: 0.9;
background: var(--light-color);
color: #333;
}
.btn-primary,
.bg-primary,
.badge-primary,
.alert-primary {
background: var(--primary-color);
color: #fff;
}
.btn-light,
.bg-light,
.badge-light,
.alert-light {
background: var(--light-color);
color: #333;
}
.btn-dark,
.bg-dark,
.badge-dark,
.alert-dark {
background: var(--dark-color);
color: #fff;
}
.btn-danger,
.bg-danger,
.badge-danger,
.alert-danger {
background: var(--danger-color);
color: #fff;
}
.btn-success,
.bg-success,
.badge-success,
.alert-success {
background: var(--success-color);
color: #fff;
}
.btn-white,
.bg-white,
.badge-white,
.alert-white {
background: #fff;
color: #333;
border: #ccc solid 1px;
}
.btn:disabled {
cursor: not-allowed;
pointer-events: none;
opacity: 0.60;
-webkit-box-shadow: none;
box-shadow: none;
}
.btn:enabled:hover {
opacity: 0.8;
}
.bg-light,
.badge-light {
border: #ccc solid 1px;
}
.round-img {
border-radius: 50%;
}
/* Forms */
input {
margin: 1.2rem 0;
}
.form-text {
display: block;
margin-top: 0.3rem;
color: #888;
}
input[type='text'],
input[type='email'],
input[type='password'],
input[type='date'],
select,
textarea {
display: block;
width: 100%;
padding: 0.4rem;
font-size: 1.2rem;
border: 1px solid #ccc;
}
input[type='submit'],
button {
font: inherit;
}
table th,
table td {
padding: 1rem;
text-align: left;
}
table th {
background: var(--light-color);
}
/* Navbar */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.7rem 2rem;
z-index: 1;
width: 100%;
opacity: 0.9;
margin-bottom: 1rem;
}
.navbar ul {
display: flex;
}
.navbar a {
color: #fff;
padding: 0.45rem;
margin: 0 0.25rem;
}
.navbar a:hover {
color: var(--light-color);
}
.navbar .welcome span {
margin-right: 0.6rem;
}
/* Mobile Styles */
@media (max-width: 700px) {
.hide-sm {
display: none;
}
.grid-2,
.grid-3,
.grid-4 {
grid-template-columns: 1fr;
}
/* Text Styles */
.x-large {
font-size: 3rem;
}
.large {
font-size: 2rem;
}
.lead {
font-size: 1rem;
}
/* Navbar */
.navbar {
display: block;
text-align: center;
}
.navbar ul {
text-align: center;
justify-content: center;
}
}

57
live/css/login.css Normal file
View File

@ -0,0 +1,57 @@
@import url(https://fonts.googleapis.com/css?family=Open+Sans);
.btn { display: inline-block; *display: inline; *zoom: 1; padding: 4px 10px 4px; margin-bottom: 0; font-size: 13px; line-height: 18px; color: #333333; text-align: center;text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); vertical-align: middle; background-color: #f5f5f5; background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); background-image: linear-gradient(top, #ffffff, #e6e6e6); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr=#ffffff, endColorstr=#e6e6e6, GradientType=0); border-color: #e6e6e6 #e6e6e6 #e6e6e6; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); border: 1px solid #e6e6e6; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); cursor: pointer; *margin-left: .3em; }
.btn:hover, .btn:active, .btn.active, .btn.disabled, .btn[disabled] { background-color: #e6e6e6; }
.btn-large { padding: 9px 14px; font-size: 15px; line-height: normal; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; }
.btn:hover { color: #333333; text-decoration: none; background-color: #e6e6e6; background-position: 0 -15px; -webkit-transition: background-position 0.1s linear; -moz-transition: background-position 0.1s linear; -ms-transition: background-position 0.1s linear; -o-transition: background-position 0.1s linear; transition: background-position 0.1s linear; }
.btn-primary, .btn-primary:hover { text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); color: #ffffff; }
.btn-primary.active { color: rgba(255, 255, 255, 0.75); }
.btn-primary { background-color: #4a77d4; background-image: -moz-linear-gradient(top, #6eb6de, #4a77d4); background-image: -ms-linear-gradient(top, #6eb6de, #4a77d4); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#6eb6de), to(#4a77d4)); background-image: -webkit-linear-gradient(top, #6eb6de, #4a77d4); background-image: -o-linear-gradient(top, #6eb6de, #4a77d4); background-image: linear-gradient(top, #6eb6de, #4a77d4); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr=#6eb6de, endColorstr=#4a77d4, GradientType=0); border: 1px solid #3762bc; text-shadow: 1px 1px 1px rgba(0,0,0,0.4); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.5); }
.btn-primary:hover, .btn-primary:active, .btn-primary.active, .btn-primary.disabled, .btn-primary[disabled] { filter: none; background-color: #4a77d4; }
.btn-block { width: 100%; display:block; }
* { -webkit-box-sizing:border-box; -moz-box-sizing:border-box; -ms-box-sizing:border-box; -o-box-sizing:border-box; box-sizing:border-box; }
html { width: 100%; height:100%; overflow:hidden; }
body {
width: 100%;
height:100%;
font-family: 'Open Sans', sans-serif;
background: #092756;
background: -moz-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%),-moz-linear-gradient(top, rgba(57,173,219,.25) 0%, rgba(42,60,87,.4) 100%), -moz-linear-gradient(-45deg, #670d10 0%, #092756 100%);
background: -webkit-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), -webkit-linear-gradient(top, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), -webkit-linear-gradient(-45deg, #670d10 0%,#092756 100%);
background: -o-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), -o-linear-gradient(top, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), -o-linear-gradient(-45deg, #670d10 0%,#092756 100%);
background: -ms-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), -ms-linear-gradient(top, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), -ms-linear-gradient(-45deg, #670d10 0%,#092756 100%);
background: -webkit-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), linear-gradient(to bottom, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), linear-gradient(135deg, #670d10 0%,#092756 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#3E1D6D', endColorstr='#092756',GradientType=1 );
}
.login {
position: absolute;
top: 50%;
left: 50%;
margin: -150px 0 0 -150px;
width:300px;
height:300px;
}
.login h1 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; }
input {
width: 100%;
margin-bottom: 10px;
background: rgba(0,0,0,0.3);
border: none;
outline: none;
padding: 10px;
font-size: 13px;
color: #fff;
text-shadow: 1px 1px 1px rgba(0,0,0,0.3);
border: 1px solid rgba(0,0,0,0.3);
border-radius: 4px;
box-shadow: inset 0 -5px 45px rgba(100,100,100,0.2), 0 1px 1px rgba(255,255,255,0.2);
-webkit-transition: box-shadow .5s ease;
-moz-transition: box-shadow .5s ease;
-o-transition: box-shadow .5s ease;
-ms-transition: box-shadow .5s ease;
transition: box-shadow .5s ease;
}
input:focus { box-shadow: inset 0 -5px 45px rgba(100,100,100,0.4), 0 1px 1px rgba(255,255,255,0.2); }

BIN
live/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

BIN
live/gfx/recipes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

3659
live/js/bundle.js Normal file

File diff suppressed because it is too large Load Diff

1
live/js/bundle.js.map Normal file

File diff suppressed because one or more lines are too long

18
live/login.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
<link href="/css/login.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<div class="login">
<h1>Login</h1>
<form action="auth" method="post">
<input type="text" name="u" placeholder="Username" required="required" />
<input type="password" name="p" placeholder="Password" required="required" />
<button type="submit" class="btn btn-primary btn-block btn-large">Let me in.</button>
</form>
</div>
</body>
</html>

83
live/recipes.html Normal file
View File

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Aida Recipes</title>
<link href="/css/app.css" rel="stylesheet" type="text/css"/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div class="app">
<div id="header" class="navbar bg-primary">
<h2>
Recipes
</h2>
<ul>
<li>
<button class="btn btn-sm" id="new" type="button">New Recipe</button>
</li>
</ul>
</div>
<div id="editor" class="container" style="display:none ;">
<form autocomplete="off">
<label for="name">Name:</label>
<input type="text" id="name" name="name" value="" required/>
<label for="url">Url:</label>
<input type="text" id="url" name="url" value="" required/>
<label for="md">Markdown:</label>
<textarea id="md" name="md" cols="50" rows="10"></textarea>
<label for="meat">Meat</label>
<select id="meat" name="meat" required>
<option></option>
<option value="1">Chicken</option>
<option value="2">Beef</option>
<option value="3">Pork</option>
<option value="4">Fish</option>
<option value="5">Egg</option>
<option value="6">Vegetable</option>
</select>
<label for="mealtype">Meal type</label>
<select id="mealtype" name="mealtype" required>
<option></option>
<option value="1">Main</option>
<option value="2">Soup</option>
</select>
<input id="_id" name="id" type="hidden" value="" disabled/>
<input type="hidden" id="short" name="short" value="" disabled/>
<input type="hidden" id="hash" name="hash" value="" disabled/>
<input type="hidden" id="lastused" name="lastused" value="" disabled/>
<div class="my text-right">
<button class="btn btn-danger btn-sm" id="delete" type="button">Delete</button>
<button class="btn btn-sm" id="close" type="button">Close</button>
<button class="btn btn-primary btn-sm" id="save" type="button">Save</button>
</div>
</form>
</div>
<div id="content" class="container">
</div>
</div>
<script type='module' src="js/bundle.js" async></script>
</body>
</html>

1
live/tpl/header.html Normal file
View File

@ -0,0 +1 @@
<button class="new">New Recipe</button>

View File

@ -0,0 +1,12 @@
<div class="form-left-col">
<label>Id:</label>
<input id="wineId" name="_id" type="text" value="<%= _id %>" disabled />
<label>Name:</label>
<input type="text" id="name" name="name" value="<%= name %>" required/>
<button class="save">Save</button>
<button class="delete">Delete</button>
</div>

View File

@ -0,0 +1 @@
<a href='#recipe/<%= _id %>'><%= name %></a>

BIN
menu.db Normal file

Binary file not shown.

6006
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "menuserver",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.19.0",
"backbone": "^1.4.0",
"body-parser": "^1.19.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-session": "^1.17.0",
"helmet": "^3.21.2",
"jquery": "^3.4.1",
"lodash": "^4.17.15",
"markdown": "^0.5.0",
"minibus": "^3.1.0",
"nosql": "^6.1.0",
"schema-check": "0.0.7",
"short-hash": "^1.0.0",
"sqlite3": "^4.1.1"
},
"devDependencies": {
"browserify": "^16.5.0",
"eslint": "^6.6.0",
"gulp": "^3.9.1",
"gulp-rename": "^2.0.0",
"gulp-sourcemaps": "^2.6.5",
"gulp-strip-debug": "^3.0.0",
"gulp-uglify-es": "^2.0.0",
"gulp-util": "^3.0.8",
"require-dir": "^1.2.0",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0"
}
}

311
preload.js Normal file
View File

@ -0,0 +1,311 @@
var sqlite3 = require('sqlite3').verbose();
var db = new sqlite3.Database('./menu.db');
const _ = require('lodash');
const shortHash = require('short-hash');
const SchemaCheck = require('schema-check');
const { promisify } = require('util');
const eachAsync = promisify(db.each);
const meats = ['Chicken', 'Beef', 'Pork'];
const mealTypes = ['Meal', 'Soup'];
const runSetup = false;
const obj = {
'name': 'garlic pulled pork',
'url': 'https://www.marksdailyapple.com/garlic-pulled-pork',
'md': `Garlic Pulled Pork
===
[Source](https://www.marksdailyapple.com/garlic-pulled-pork)
Ingredients:
---
- 1 pork shoulder cut (butt or picnic), weighing 3-4 pounds
- 1-2 tablespoons kosher salt
- 1/2 teaspoon cumin
- 1 teaspoon of black pepper
- 1-2 tablespoons of granulated garlic or garlic powder
- optional: 6 fresh garlic cloves, peeled
- The juice of one lime (or sour orange, if you can get one)
- 1 onion
- 1 bay leaf
Instructions:
---
1. Mix together salt, cumin, black pepper and granulated garlic.
1. Juice the lime over the seasonings and rub the mixture all over the pork.
1. If you love garlic as much as Pat does, you might want to use fresh garlic, too. Use a knife to slice six thin cuts in the pork and push each clove securely inside each cut. You dont want the fresh garlic to fall out and touch the cooking vessel or it will burn and affect the flavor of the meat.
1. The meat should sit out of refrigeration a half hour before you put it in the oven. This ensures that it will cook evenly throughout. If you want to let the meat marinate in the rub longer than this, put it in the fridge for an hour or even overnight.
1. When youre ready to cook, preheat the oven to 250 degrees Fahrenheit. Place the roast in a pan with one sliced onion and a bay leaf. Cover and roast for three to four hours, or until the middle of the roast reads about 190 degrees and falls apart easily when pulled with a fork.
1. Let the roast rest for twenty minutes or so, then uncover. Youll notice a lot of liquid at the bottom. Use it as a sauce for the meat, which you will now viciously attack with two forks. Itll fall apart pretty readily, and youll get the idea of the shredding method after a couple of pulls. Enjoy!
`,
'meat':1,
'mealtype':1
};
const objB = {
'name': 'Tom Kha Gai -- Thai Coconut Soup',
'url': 'https://www.angsarap.net/2012/03/08/tom-kha-gai/',
'md' :`# Tom Kha Gai
[Source](https://www.angsarap.net/2012/03/08/tom-kha-gai/)
## Ingredients
- 600g chicken breast, sliced
- 6 cups chicken stock
- 1 cup coconut milk
- 4 pcs long tamarind, wrapped in muslin cloth
- 3 stalks lemongrass, white part
- 4 pcs kaffir limes leaves
- 1 1/2 cup straw mushrooms, cut in half
- 1 thumb size galangal, cut into 4 pieces
- 3 pcs dried chili, chopped
- 1 tbsp brown sugar
- handful fresh coriander leaves
- handful fresh basil leaves
- fish sauce or sea salt, according to taste
## Method
1. Pour in chicken stock in a pot and bring it to a boil.
2. Once boiling add chicken, galangal, mushrooms, lemon grass, kaffir, chilli and tamarind.
3. Simmer for 8 minutes.
4. Press muslin cloth with tamarind on top of the pot to extract the juices then discard.
5. Add coconut milk, fish sauce, sugar and simmer for another 2 minutes.
6. Place in bowls then top with basil and coriander.
`,
'meat':1,
'mealtype':2
};
const newRaw = {
'name': 'Asian pepper steak crock pot recipe',
'url': 'https://www.marksdailyapple.com/asian-pepper-steak-crock-pot-recipe',
'md': 'Asian Pepper Steak Crock Pot Recipe\n ===\n [Source](https://www.marksdailyapple.com/asian-pepper-steak-crock-pot-recipe/)\n \n Ingredients:\n ---\n * 2 lbs. steak (sirloin is preferable, but any other good cut will do)\n * 2 tbsp coconut oil\n * 1-2 cloves of garlic, minced\n * 1/4 cup wheat-free tamari (similar to soy sauce)\n * 1 16 oz can bean sprouts, drained\n * 1 16 oz can diced tomatoes\n * 1 large green pepper, sliced in thin strips\n * 1 small onion, sliced\n * Salt and pepper to taste\n \n Method:\n ---\n 1. On a chopping board, cut the steak on an angle to make strips about a &frac12; inch thick. \n \n 2. In a large frying pan, add the oil and heat. Saute the steak until it lightly browns. \n \n 3. Drain excess fat, liberally coat the meat with ground pepper and put the meat in the crock pot. \n \n 4. Add garlic and tamari, and mix so that the steak is thoroughly coated.\n \n 5. Cook in a crock pot on low for 6 hours. \n \n 6. One hour before serving, add sprouts, tomatoes, green peppers and onions and turn crock pot to high. \n \n 7. Cook for one hour and then serve piping hot.',
'meat': 2,
'mealtype': 1
};
const lorem = {
'_id': 6,
'name': 'Lorem ipsum dolor sit amet',
'url': 'https://www.marksdailyapple.com/asian-pepper-steak-crock-pot-recipe',
'md': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
'short': 'asian-pepper-steak-crock-pot-recipe',
'hash': 'eac12bae',
'meat': 2,
'mealtype': 1,
'lastused': 0
};
function prepareData(_obj) {
const newObj = Object.assign({}, _obj);
newObj.short = _.kebabCase(newObj.name);
newObj.hash = shortHash(newObj.name);
return newObj;
}
function insertRecord(newObj) {
const workObj = prepareData(newObj);
db.serialize(() => {
const stmt = db.prepare('INSERT INTO menu VALUES (?,?,?,?,?,?,?,?,?)');
stmt.run(null, workObj.name, workObj.url, workObj.md, workObj.short, workObj.hash, workObj.meat, workObj.mealtype, 0);
stmt.finalize();
});
}
if (runSetup) {
// Create table
db.serialize(() => {
db.run(`CREATE TABLE "menu" (
\t"_id"\tINTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
\t"name"\tTEXT,
\t"url"\tTEXT,
\t"md"\tTEXT,
\t"short"\tTEXT UNIQUE,
\t"hash"\tTEXT UNIQUE,
\t"meat"\tINTEGER NOT NULL DEFAULT 0,
\t"mealtype"\tINTEGER NOT NULL DEFAULT 0,
\t"lastused"\tINTEGER NOT NULL DEFAULT 0
);`);
});
// Insert record
insertRecord(obj);
insertRecord(objB);
}
function getAll() {
const outgoing = [];
const sql = 'SELECT _id, short, hash, name FROM menu';
return new Promise((resolve, reject) => {
db.all(sql, [], (err, rows) => {
if (err)
reject(err);
rows.forEach((row) => {
outgoing.push(row);
});
resolve(outgoing) ;
});
});
}
function getOne(hash) {
const sql = 'SELECT * FROM menu WHERE hash = ?';
return new Promise((resolve, reject) => {
db.get(sql, [hash], (err, row) => {
if (err)
reject(err);
if (!err) resolve(row);
});
});
}
function insertOne(data) {
const sql = 'INSERT INTO menu VALUES (?,?,?,?,?,?,?,?,?)';
const workObj = prepareData(data);
return new Promise((resolve, reject) => {
db.run(sql, [null, workObj.name, workObj.url, workObj.md, workObj.short, workObj.hash, workObj.meat, workObj.mealtype, 0], function(err) {
if (err)
reject(err);
resolve({ 'msg':'Row inserted', '_id': this.lastID });
});
});
}
function deleteOne(hash) {
const sql = 'DELETE FROM menu WHERE hash=?';
return new Promise((resolve, reject) => {
db.run('DELETE FROM menu WHERE hash=?', hash, function(err) {
if (err)
reject(err);
resolve({ 'msg':'Row deleted', 'changes': this.changes });
});
});
}
function updateOne(data) {
const oldHash = data.hash;
const workObj = prepareData(data);
const sql = `UPDATE menu
SET name = ?, url = ?, md = ?, short = ?, hash = ?, meat = ?, mealtype = ?, lastused = ?
WHERE hash = ?`;
const newData = [workObj.name, workObj.url, workObj.md, workObj.short, workObj.hash, workObj.meat, workObj.mealtype, workObj.lastused, oldHash];
return new Promise((resolve, reject) => {
db.run(sql, newData, function(err) {
if (err)
reject(err);
resolve({ 'msg':'Row updated', 'changes': this.changes });
});
});
}
/* deleteOne('eac12bae')
.then((d) => {
console.log('deleteOne', d);
})
.catch((err) => {
console.error(err);
});*/
/* deleteOne('eac12bae')
.then((d) => {
console.log('deleteOne', d);
})
.catch((err) => {
console.error(err);
});*/
insertOne(newRaw)
.then((v) => {
console.log('inserted', v);
})
.catch((err) => {
console.error(err);
});
getAll().then((d) => {
console.log('then', d);
});
let oldRec;
getOne('eac12bae')
.then((d) => {
console.log('One', d);
oldRec = d;
lorem['_id'] = oldRec['_id'];
updateOne(lorem)
.then((v) => {
console.log('updated', v);
})
.catch((err) => {
console.error(err);
});
})
.catch((err) => {
console.error(err);
});
getOne('eac12bae')
.then((d) => {
console.log('One', d);
oldRec = d;
})
.catch((err) => {
console.error(err);
});
// console.log(obj);
// db.insert(obj);
/*
db.insert({ name: 'Peter ' });
db.insert({ name: 'Lucia' });
db.insert({ name: 'Igor' });
db.insert({ name: 'Stano' });
db.insert({ name: 'Jozef' });
*/
// console.log(r);
// db.update({ name: 'Jozef', age: 33 }).where('name', 'Jozef');

79
server.js Normal file
View File

@ -0,0 +1,79 @@
const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const path = require('path');
const helmet = require('helmet');
const db = require('./server/lib/loginmanager');
// create express app
const app = express();
require('dotenv').config();
const serverPort = process.env.PORT || 3000;
const sitePath = 'live';
app.use(helmet());
app.use(session({
'secret': 'rBLH5#Q89Z4',
'resave': true,
'saveUninitialized': true
}));
app.get('/', function(request, response) {
if (request.session.loggedin !== true)
response.sendFile(path.join(`${__dirname }/server/static/login.html`));
else
response.redirect('/recipes.html');
});
app.use(express.static(path.join(__dirname, sitePath)));
// parse requests of content-type - application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ 'extended': true }));
// parse requests of content-type - application/json
app.use(bodyParser.json());
app.post('/auth', function(request, response) {
const username = request.body.u;
const password = request.body.p;
console.log(username, password);
if (username && password)
db.getOne(username, password)
.then((data) => {
console.log('data', data);
if (!data)
response.send('Incorrect Username and/or Password!');
else {
request.session.loggedin = true;
request.session.username = username;
response.redirect('/recipes.html');
}
})
.catch((err) => {
console.log(err);
response.status(500).send({
'message': err.message || 'Some error occurred while querying the database.'
});
});
else {
response.send('Please enter Username and Password!');
response.end();
}
});
require('./server/routes/recipe.routes')(app);
require('./server/routes/view.routes')(app);
// listen for requests
app.listen(serverPort, () => {
console.log('Server is listening on port 3000');
});

View File

@ -0,0 +1,101 @@
// Create and Save a new Note
const dbmanager = require('../lib/dbmanager');
exports.create = (req, res) => {
// Validate request
console.log('>create req', req.body);
if(!req.body.name)
return res.status(400).send({
'message': 'Recipe content can not be empty'
});
dbmanager.insertOne(req.body)
.then((data) => {
res.send(data);
})
.catch((err) => {
res.status(500).send({
'message': err.message || 'Some error occurred while querying the database.'
});
});
};
// Retrieve and return all notes from the database.
exports.findAll = (req, res) => {
console.log('>findAll req');
dbmanager.getAll()
.then((data) => {
res.send(data);
})
.catch((err) => {
res.status(500).send({
'message': err.message || 'Some error occurred while querying the database.'
});
});
};
// Find a single note with a noteId
exports.findOne = (req, res) => {
console.log('>findOne req', req.params);
if(!req.params.recipeId)
return res.status(400).send({
'message': 'Recipe id missing'
});
const hash = req.params.recipeId;
dbmanager.getOne(hash)
.then((data) => {
res.send(data);
})
.catch((err) => {
res.status(500).send({
'message': err.message || 'Some error occurred while querying the database.'
});
});
};
// Update a note identified by the noteId in the request
exports.update = (req, res) => {
console.log('>update req', req.body.content);
if(!req.params.recipeId)
return res.status(400).send({
'message': 'Recipe id missing'
});
dbmanager.updateOne(req.body)
.then((data) => {
res.send(data);
})
.catch((err) => {
res.status(500).send({
'message': err.message || 'Some error occurred while querying the database.'
});
});
};
// Delete a note with the specified noteId in the request
exports.delete = (req, res) => {
console.log('>delete req', req.params.recipeId);
if(!req.params.recipeId)
return res.status(400).send({
'message': 'Recipe id missing'
});
const hash = req.params.recipeId;
dbmanager.deleteOne(hash)
.then((data) => {
res.send(data);
})
.catch((err) => {
res.status(500).send({
'message': err.message || 'Some error occurred while querying the database.'
});
});
};

10
server/controllers/t.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,48 @@
// Create and Save a new Note
const dbmanager = require('../lib/dbmanager');
const markdown = require( 'markdown' ).markdown;
function makeHtml(data) {
const md = data.md;
const html = markdown.toHTML( md );
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${data.name}</title>
<style>
@import url('https://fonts.googleapis.com/css?family=Roboto');:root {--primary-color: #64B5F6;--dark-color: #333333;--light-color: #f4f4f4;--danger-color: #dc3545;--success-color: #28a745;}* {box-sizing: border-box;margin: 0;padding: 0;}body {font-family: 'Roboto', sans-serif;font-size: 1rem;line-height: 1.6;background-color: #fff;color: #333;}a {color: var(--primary-color);text-decoration: none;}a:hover {color: #666;}ul {list-style: none;}img {width: 100%;}.container {max-width: 1100px;margin: auto;overflow: hidden;padding: 0 2rem;}@media (max-width: 700px) {.hide-sm {display: none;}.grid-2, .grid-3, .grid-4 {grid-template-columns: 1fr;}.x-large {font-size: 3rem;}.large {font-size: 2rem;}.lead {font-size: 1rem;}.navbar {display: block;text-align: center;}.navbar ul {text-align: center;justify-content: center;}}
</style>
</head>
<body>
<div class="container">
${html}
</div>
</body>
</html>`;
}
exports.findOne = (req, res) => {
console.log('>findOne req', req.params);
if(!req.params.recipeId)
return res.status(400).send({
'message': 'Recipe id missing'
});
const short = req.params.recipeId;
dbmanager.getOneShort(short)
.then((data) => {
res.send(makeHtml(data));
})
.catch((err) => {
res.status(500).send({
'message': err.message || 'Some error occurred while querying the database.'
});
});
};

6
server/lib/connect.js Normal file
View File

@ -0,0 +1,6 @@
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database(`${__dirname}/../../menu.db`);
console.log(`${__dirname}/../../menu.db`);
module.exports = db;

108
server/lib/dbmanager.js Normal file
View File

@ -0,0 +1,108 @@
const db = require('../lib/connect');
const _ = require('lodash');
const shortHash = require('short-hash');
// exports.create = (req, res) => {
function prepareData(_obj) {
const newObj = Object.assign({}, _obj);
newObj.short = _.kebabCase(newObj.name);
newObj.hash = shortHash(newObj.name);
return newObj;
}
exports.getAll = () => {
const outgoing = [];
const sql = 'SELECT _id, short, hash, name, meat, mealtype FROM menu';
return new Promise((resolve, reject) => {
db.all(sql, [], (err, rows) => {
if (err)
reject(err);
rows.forEach((row) => {
outgoing.push(row);
});
resolve(outgoing) ;
});
});
};
exports.getOne = (hash) => {
const sql = 'SELECT * FROM menu WHERE hash = ?';
return new Promise((resolve, reject) => {
db.get(sql, [hash], (err, row) => {
if (err)
reject(err);
if (!err) resolve(row);
});
});
};
exports.getOneShort = (short) => {
const sql = 'SELECT * FROM menu WHERE short = ?';
return new Promise((resolve, reject) => {
db.get(sql, [short], (err, row) => {
if (err)
reject(err);
if (!err) resolve(row);
});
});
};
exports.insertOne = (data) => {
const sql = 'INSERT INTO menu VALUES (?,?,?,?,?,?,?,?,?)';
const workObj = prepareData(data);
return new Promise((resolve, reject) => {
db.run(sql, [null, workObj.name, workObj.url, workObj.md, workObj.short, workObj.hash, workObj.meat, workObj.mealtype, 0], function(err) {
if (err)
reject(err);
resolve({ 'msg':'Row inserted', '_id': this.lastID });
});
});
};
exports.deleteOne = (hash) => {
const sql = 'DELETE FROM menu WHERE hash=?';
return new Promise((resolve, reject) => {
db.run('DELETE FROM menu WHERE hash=?', hash, function(err) {
if (err)
reject(err);
resolve({ 'msg':'Row deleted', 'changes': this.changes });
});
});
};
exports.updateOne = (data) => {
const oldHash = data.hash;
const workObj = prepareData(data);
const sql = `UPDATE menu
SET name = ?, url = ?, md = ?, short = ?, hash = ?, meat = ?, mealtype = ?, lastused = ?
WHERE hash = ?`;
const newData = [workObj.name, workObj.url, workObj.md, workObj.short, workObj.hash, workObj.meat, workObj.mealtype, workObj.lastused, oldHash];
return new Promise((resolve, reject) => {
db.run(sql, newData, function(err) {
if (err)
reject(err);
resolve({ 'msg':'Row updated', 'changes': this.changes });
});
});
};

View File

@ -0,0 +1,14 @@
const db = require('../lib/connect');
exports.getOne = (username, password) => {
const sql = 'SELECT * FROM accounts WHERE username = ? and password = ?';
return new Promise((resolve, reject) => {
db.get(sql, [username, password], (err, row) => {
if (err)
reject(err);
if (!err) resolve(row);
});
});
};

View File

@ -0,0 +1,6 @@
module.exports = (app) => {
const view = require('../controllers/view.controller');
app.route('/view/:recipeId')
.get(view.findOne);
};

View File

@ -0,0 +1,12 @@
module.exports = (app) => {
const recipes = require('../controllers/recipe.controller');
app.route('/recipes')
.get(recipes.findAll)
.post(recipes.create);
app.route('/recipes/:recipeId')
.get(recipes.findOne)
.put(recipes.update)
.delete(recipes.delete);
};

83
server/static/index.html Normal file
View File

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Aida Recipes</title>
<link href="/css/app.css" rel="stylesheet" type="text/css"/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div class="app">
<div id="header" class="navbar bg-primary">
<h2>
Recipes
</h2>
<ul>
<li>
<button class="btn btn-sm" id="new" type="button">New Recipe</button>
</li>
</ul>
</div>
<div id="editor" class="container" style="display:none ;">
<form autocomplete="off">
<label for="name">Name:</label>
<input type="text" id="name" name="name" value="" required/>
<label for="url">Url:</label>
<input type="text" id="url" name="url" value="" required/>
<label for="md">Markdown:</label>
<textarea id="md" name="md" cols="50" rows="10"></textarea>
<label for="meat">Meat</label>
<select id="meat" name="meat" required>
<option></option>
<option value="1">Chicken</option>
<option value="2">Beef</option>
<option value="3">Pork</option>
<option value="4">Fish</option>
<option value="5">Egg</option>
<option value="6">Vegetable</option>
</select>
<label for="mealtype">Meal type</label>
<select id="mealtype" name="mealtype" required>
<option></option>
<option value="1">Main</option>
<option value="2">Soup</option>
</select>
<input id="_id" name="id" type="hidden" value="" disabled/>
<input type="hidden" id="short" name="short" value="" disabled/>
<input type="hidden" id="hash" name="hash" value="" disabled/>
<input type="hidden" id="lastused" name="lastused" value="" disabled/>
<div class="my text-right">
<button class="btn btn-danger btn-sm" id="delete" type="button">Delete</button>
<button class="btn btn-sm" id="close" type="button">Close</button>
<button class="btn btn-primary btn-sm" id="save" type="button">Save</button>
</div>
</form>
</div>
<div id="content" class="container">
</div>
</div>
<script type='module' src="js/bundle.js" async></script>
</body>
</html>

78
server/static/login.html Normal file
View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
<style>
@import url(https://fonts.googleapis.com/css?family=Open+Sans);
.btn { display: inline-block; *display: inline; *zoom: 1; padding: 4px 10px 4px; margin-bottom: 0; font-size: 13px; line-height: 18px; color: #333333; text-align: center;text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); vertical-align: middle; background-color: #f5f5f5; background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); background-image: linear-gradient(top, #ffffff, #e6e6e6); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr=#ffffff, endColorstr=#e6e6e6, GradientType=0); border-color: #e6e6e6 #e6e6e6 #e6e6e6; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); border: 1px solid #e6e6e6; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); cursor: pointer; *margin-left: .3em; }
.btn:hover, .btn:active, .btn.active, .btn.disabled, .btn[disabled] { background-color: #e6e6e6; }
.btn-large { padding: 9px 14px; font-size: 15px; line-height: normal; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; }
.btn:hover { color: #333333; text-decoration: none; background-color: #e6e6e6; background-position: 0 -15px; -webkit-transition: background-position 0.1s linear; -moz-transition: background-position 0.1s linear; -ms-transition: background-position 0.1s linear; -o-transition: background-position 0.1s linear; transition: background-position 0.1s linear; }
.btn-primary, .btn-primary:hover { text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); color: #ffffff; }
.btn-primary.active { color: rgba(255, 255, 255, 0.75); }
.btn-primary { background-color: #4a77d4; background-image: -moz-linear-gradient(top, #6eb6de, #4a77d4); background-image: -ms-linear-gradient(top, #6eb6de, #4a77d4); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#6eb6de), to(#4a77d4)); background-image: -webkit-linear-gradient(top, #6eb6de, #4a77d4); background-image: -o-linear-gradient(top, #6eb6de, #4a77d4); background-image: linear-gradient(top, #6eb6de, #4a77d4); background-repeat: repeat-x; filter: progid:dximagetransform.microsoft.gradient(startColorstr=#6eb6de, endColorstr=#4a77d4, GradientType=0); border: 1px solid #3762bc; text-shadow: 1px 1px 1px rgba(0,0,0,0.4); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.5); }
.btn-primary:hover, .btn-primary:active, .btn-primary.active, .btn-primary.disabled, .btn-primary[disabled] { filter: none; background-color: #4a77d4; }
.btn-block { width: 100%; display:block; }
* { -webkit-box-sizing:border-box; -moz-box-sizing:border-box; -ms-box-sizing:border-box; -o-box-sizing:border-box; box-sizing:border-box; }
html { width: 100%; height:100%; overflow:hidden; }
body {
width: 100%;
height:100%;
font-family: 'Open Sans', sans-serif;
background: #092756;
background: -moz-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%),-moz-linear-gradient(top, rgba(57,173,219,.25) 0%, rgba(42,60,87,.4) 100%), -moz-linear-gradient(-45deg, #670d10 0%, #092756 100%);
background: -webkit-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), -webkit-linear-gradient(top, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), -webkit-linear-gradient(-45deg, #670d10 0%,#092756 100%);
background: -o-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), -o-linear-gradient(top, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), -o-linear-gradient(-45deg, #670d10 0%,#092756 100%);
background: -ms-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), -ms-linear-gradient(top, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), -ms-linear-gradient(-45deg, #670d10 0%,#092756 100%);
background: -webkit-radial-gradient(0% 100%, ellipse cover, rgba(104,128,138,.4) 10%,rgba(138,114,76,0) 40%), linear-gradient(to bottom, rgba(57,173,219,.25) 0%,rgba(42,60,87,.4) 100%), linear-gradient(135deg, #670d10 0%,#092756 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#3E1D6D', endColorstr='#092756',GradientType=1 );
}
.login {
position: absolute;
top: 50%;
left: 50%;
margin: -150px 0 0 -150px;
width:300px;
height:300px;
}
.login h1 { color: #fff; text-shadow: 0 0 10px rgba(0,0,0,0.3); letter-spacing:1px; text-align:center; }
input {
width: 100%;
margin-bottom: 10px;
background: rgba(0,0,0,0.3);
border: none;
outline: none;
padding: 10px;
font-size: 13px;
color: #fff;
text-shadow: 1px 1px 1px rgba(0,0,0,0.3);
border: 1px solid rgba(0,0,0,0.3);
border-radius: 4px;
box-shadow: inset 0 -5px 45px rgba(100,100,100,0.2), 0 1px 1px rgba(255,255,255,0.2);
-webkit-transition: box-shadow .5s ease;
-moz-transition: box-shadow .5s ease;
-o-transition: box-shadow .5s ease;
-ms-transition: box-shadow .5s ease;
transition: box-shadow .5s ease;
}
input:focus { box-shadow: inset 0 -5px 45px rgba(100,100,100,0.4), 0 1px 1px rgba(255,255,255,0.2); }
</style>
</head>
<body>
<div class="login">
<h1>Login</h1>
<form action="auth" method="post">
<input type="text" name="u" placeholder="Username" required="required" />
<input type="password" name="p" placeholder="Password" required="required" />
<button type="submit" class="btn btn-primary btn-block btn-large">Let me in.</button>
</form>
</div>
</body>
</html>

262
src/v1/js/app.js Normal file
View File

@ -0,0 +1,262 @@
const _ = require('underscore');
const axios = require('axios');
class Recipe {
constructor() {
this.url = 'http://localhost:3000/recipes';
this.listData = [];
this.content = document.querySelector('#content');
this.editor = document.querySelector('#editor');
this.fields = ['_id', 'name', 'url', 'md', 'short', 'hash', 'lastused'];
this.selects = ['meat', 'mealtype'];
this.newButton = document.querySelector('#new');
this.saveButton = document.querySelector('#save');
this.deleteButton = document.querySelector('#delete');
this.closeButton = document.querySelector('#close');
}
show(div) {
div.style.display = '';
}
hide(div) {
div.style.display = 'none';
}
resetForm() {
this.fields.forEach((item) => {
const elm = document.querySelector(`#${item}`);
elm.value = '';
});
this.selects.forEach((item) => {
const elm = document.querySelector(`#${item}`);
elm.selectedIndex = 0;
});
}
loadForm(workData) {
this.fields.forEach((item) => {
const elm = document.querySelector(`#${item}`);
elm.value = workData[item];
});
this.selects.forEach((item) => {
const elm = document.querySelector(`#${item}`);
elm.selectedIndex = workData[item];
});
}
getForm() {
const newObj = {};
this.fields.forEach((item) => {
const elm = document.querySelector(`#${item}`);
newObj[item] = elm.value;
});
this.selects.forEach((item) => {
const elm = document.querySelector(`#${item}`);
newObj[item] = elm.options[elm.selectedIndex].value;
});
return newObj;
}
setupClicks() {
this.content.addEventListener('click', (event) => {
try {
const dataset = event.path[0].dataset;
this.getRecord(dataset.hash.toString());
}
catch (e) {
console.error(e);
}
});
this.newButton.addEventListener('click', () => {
setTimeout(() => {
this.newForm();
}, 100);
});
this.saveButton.addEventListener('click', () => {
setTimeout(() => {
this.sendRecord();
}, 100);
});
this.closeButton.addEventListener('click', () => {
setTimeout(() => {
this.closeForm();
}, 100);
});
this.deleteButton.addEventListener('click', () => {
setTimeout(() => {
this.deleteEntry();
}, 300);
});
}
deleteEntry() {
if (confirm('Are you sure you want to delete this entry?')) {
// Save it!
const newData = this.getForm();
this.deleteRecord(newData.hash);
}
else {
// Do nothing!
}
}
resetFormButtons() {
const elm = document.querySelector('#hash');
this.deleteButton.disabled = (elm.value === '');
}
newForm() {
this.resetForm();
this.resetFormButtons();
this.show(this.editor);
this.hide(this.content);
}
closeForm() {
this.resetForm();
this.resetFormButtons();
this.hide(this.editor);
this.show(this.content);
}
renderList() {
const list = this.listData.map((item) => {
return `<div class="dataRow" data-id="${item._id}" data-hash="${item.hash}">${item.name}</div>`;
});
this.content.innerHTML = list.join('');
}
getList() {
axios.get(this.url)
.then( (response) => {
// handle success
this.listData = response.data.slice();
this.renderList();
})
.catch( (error) => {
// handle error
console.error(error);
});
}
getRecord(hash) {
axios.get(`${this.url}/${hash}`)
.then( (response) => {
// handle success
const data = Object.assign({}, response.data);
this.resetForm();
this.loadForm(data);
this.resetFormButtons();
this.show(this.editor);
this.hide(this.content);
})
.catch( (error) => {
// handle error
console.error(error);
});
}
postData(data) {
axios.post(`${this.url}`, data)
.then( (response) => {
// handle success
if (response.data._id > 0) {
this.closeForm();
setTimeout(() => {
this.getList();
}, 250);
}
})
.catch( (error) => {
// handle error
console.error(error);
});
}
updateData(data) {
axios.put(`${this.url}/${data.hash}`, data)
.then( (response) => {
// handle success
if (response.data.changes > 0) {
this.closeForm();
setTimeout(() => {
this.getList();
}, 250);
}
})
.catch( (error) => {
// handle error
console.error(error);
});
}
deleteRecord(hash) {
axios.delete(`${this.url}/${hash}`)
.then( (response) => {
// handle success
if (response.data.changes > 0) {
this.closeForm();
setTimeout(() => {
this.getList();
}, 250);
}
})
.catch( (error) => {
// handle error
console.error(error);
});
}
sendRecord() {
const newData = this.getForm();
if (newData.hash === '')
this.postData(newData);
else
this.updateData(newData);
}
init() {
console.log('init');
this.resetForm();
this.setupClicks();
this.getList();
}
}
(function () {
const app = new Recipe();
app.init();
})();

208
src/v1/js/duff.js Normal file
View File

@ -0,0 +1,208 @@
const $ = require('jquery');
const _ = require('underscore');
const Backbone = require('backbone');
const tpl = require('./lib/utils');
const app = {
};
app.Recipe = Backbone.Model.extend({
'urlRoot':'http://localhost:3000/recipes',
'defaults':{
'_id':null,
'name':'',
'hash':''
}
});
app.RecipeCollection = Backbone.Collection.extend({
'model':app.Recipe,
'url':'http://localhost:3000/recipes'
});
app.HeaderView = Backbone.View.extend({
'initialize':function () {
console.log('>> HeaderView this', this);
this.template = _.template(tpl.get('header'));
},
'render':function (eventName) {
console.log('>> HeaderView render', this);
$(this.el).html(this.template());
return this;
},
'events':{
'click .new':'newWine'
},
'newRecipe':function (event) {
app.navigate('recipe/new', true);
return false;
}
});
app.RecipeListView = Backbone.View.extend({
'tagName':'ul',
'initialize':function () {
this.model.bind('reset', this.render, this);
var self = this;
this.model.bind('add', function (recipe) {
$(self.el).append(new app.RecipeListItemView({ 'model':recipe }).render().el);
});
},
'render':function (eventName) {
_.each(this.model.models, function (recipe) {
$(this.el).append(new app.RecipeListItemView({ 'model':recipe }).render().el);
}, this);
return this;
}
});
app.RecipeListItemView = Backbone.View.extend({
'tagName':'li',
'initialize':function () {
this.template = _.template(tpl.get('recipe-list-item'));
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.close, this);
},
'render':function (eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
app.RecipeView = Backbone.View.extend({
'tagName':'div', // Not required since 'div' is the default if no el or tagName specified
'initialize':function () {
this.template = _.template(tpl.get('recipe-details'));
this.model.bind('change', this.render, this);
},
'render':function (eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
'events':{
'change input':'change',
'click .save':'saveRecipe',
'click .delete':'deleteRecipe'
},
'change':function (event) {
var target = event.target;
console.log(`changing ${ target.id } from: ${ target.defaultValue } to: ${ target.value}`);
// You could change your model on the spot, like this:
// var change = {};
// change[target.name] = target.value;
// this.model.set(change);
},
'saveRecipe':function () {
this.model.set({
'name':$('#name').val()
});
if (this.model.isNew()) {
var self = this;
app.recipeList.create(this.model, {
'success':function () {
app.navigate(`recipe/${ self.model.id}`, false);
}
});
}
else
this.model.save();
return false;
},
'deleteRecipe':function () {
this.model.destroy({
'success':function () {
alert('Recipe deleted successfully');
window.history.back();
}
});
return false;
}
});
app.Router = Backbone.Router.extend({
'initialize':function () {
$('#header').html(new app.HeaderView().render().el);
},
'routes': {
'': 'list',
'recipe/new': 'newRecipe',
'recipe/:id': 'recipeDetails'
},
'list':function () {
this.before();
},
'recipeDetails':function (id) {
this.before(function () {
var recipe = app.router.recipeList.get(id);
app.showView('#content', new app.RecipeView({ 'model':recipe }));
});
},
'newRecipe':function () {
this.before(function () {
app.showView('#content', new app.RecipeView({ 'model':new app.recipe() }));
});
},
'showView':function (selector, view) {
if (this.currentView)
this.currentView.close();
$(selector).html(view.render().el);
this.currentView = view;
return view;
},
'before':function (callback) {
console.log(this);
if (this.recipeList) {
if (callback) callback();
}
else {
this.recipeList = new app.RecipeCollection();
this.recipeList.fetch({ 'success': () => {
console.log(this);
console.log(app.router.recipeList);
$('#sidebar').html(new app.RecipeListView({ 'model':app.router.recipeList }).render().el);
if (callback) callback();
} });
}
}
});
tpl.loadTemplates(['header', 'recipe-details', 'recipe-list-item'], function () {
app.router = new app.Router();
Backbone.history.start();
});

31
src/v1/js/headerview.js Normal file
View File

@ -0,0 +1,31 @@
const $ = require('jquery');
const _ = require('underscore');
const Backbone = require('backbone');
const tpl = require('./lib/utils');
const HeaderView = Backbone.View.extend({
'initialize':function () {
console.log('>> this', this);
this.template = _.template(tpl.get('header'));
},
'render':function (eventName) {
$(this.el).html(this.template());
return this;
},
'events':{
'click .new':'newWine'
},
'newWine':function (event) {
app.navigate('wines/new', true);
return false;
}
});
module.exports = { HeaderView };

18
src/v1/js/lib/models.js Normal file
View File

@ -0,0 +1,18 @@
const Backbone = require('backbone');
const RecipeModel = Backbone.Model.extend({
'urlRoot':'http://localhost:3000/recipes',
'defaults':{
'id':null,
'name':'',
'hash':''
}
});
const RecipeCollection = Backbone.Collection.extend({
'model':RecipeModel,
'url':'http://localhost:3000/recipes'
});
module.exports = { RecipeModel, RecipeCollection };

37
src/v1/js/lib/utils.js Normal file
View File

@ -0,0 +1,37 @@
const $ = require('jquery');
module.exports = {
// Hash of preloaded templates for the app
'templates':{},
// Recursively pre-load all the templates for the app.
// This implementation should be changed in a production environment. All the template files should be
// concatenated in a single file.
'loadTemplates':function (names, callback) {
var that = this;
var loadTemplate = function (index) {
var name = names[index];
console.log(`Loading template: ${ name}`);
$.get(`tpl/${ name }.html`, function (data) {
that.templates[name] = data;
index++;
if (index < names.length)
loadTemplate(index);
else
callback();
});
};
loadTemplate(0);
},
// Get template by name from hash of preloaded templates
'get':function (name) {
return this.templates[name];
}
};
// module.exports = { tpl };

7
test.json Normal file
View File

@ -0,0 +1,7 @@
{
"name": "Asian pepper steak crock pot recipe",
"url": "https://www.marksdailyapple.com/asian-pepper-steak-crock-pot-recipe",
"md": "Asian Pepper Steak Crock Pot Recipe\n ===\n [Source](https://www.marksdailyapple.com/asian-pepper-steak-crock-pot-recipe/)\n \n Ingredients:\n ---\n * 2 lbs. steak (sirloin is preferable, but any other good cut will do)\n * 2 tbsp coconut oil\n * 1-2 cloves of garlic, minced\n * 1/4 cup wheat-free tamari (similar to soy sauce)\n * 1 16 oz can bean sprouts, drained\n * 1 16 oz can diced tomatoes\n * 1 large green pepper, sliced in thin strips\n * 1 small onion, sliced\n * Salt and pepper to taste\n \n Method:\n ---\n 1. On a chopping board, cut the steak on an angle to make strips about a &frac12; inch thick. \n \n 2. In a large frying pan, add the oil and heat. Saute the steak until it lightly browns. \n \n 3. Drain excess fat, liberally coat the meat with ground pepper and put the meat in the crock pot. \n \n 4. Add garlic and tamari, and mix so that the steak is thoroughly coated.\n \n 5. Cook in a crock pot on low for 6 hours. \n \n 6. One hour before serving, add sprouts, tomatoes, green peppers and onions and turn crock pot to high. \n \n 7. Cook for one hour and then serve piping hot.",
"meat": 2,
"mealtype": 1
}