This commit is contained in:
Martin Donnelly 2020-04-08 22:27:58 +01:00
commit 92844ad44f
28 changed files with 4750 additions and 0 deletions

62
.eslintrc.json Normal file
View File

@ -0,0 +1,62 @@
{
"parserOptions": {
"ecmaVersion": 2019,
"sourceType": "module"
},
"env": {
"es6": true,
"browser": true
},
"plugins": [
"svelte3"
],
"overrides": [
{
"files": [
"**/*.svelte"
],
"processor": "svelte3/svelte3"
}
],
"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, 240, 2], // 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": ["/"] }]
}
}

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/node_modules/
/public/build/
.DS_Store

5
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@ -0,0 +1,29 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="140" />
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="RIGHT_MARGIN" value="240" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="ELSE_ON_NEW_LINE" value="true" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_FOR" value="false" />
<option name="IF_BRACE_FORCE" value="1" />
<option name="DOWHILE_BRACE_FORCE" value="1" />
<option name="WHILE_BRACE_FORCE" value="1" />
<option name="FOR_BRACE_FORCE" value="1" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<custom-configuration-file used="true" path="$PROJECT_DIR$/.eslintrc.json" />
</component>
</project>

6
.idea/misc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/svelte_menu.iml" filepath="$PROJECT_DIR$/.idea/svelte_menu.iml" />
</modules>
</component>
</project>

8
.idea/svelte_menu.iml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

93
README.md Normal file
View File

@ -0,0 +1,93 @@
*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
---
# svelte app
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
```bash
npx degit sveltejs/template svelte-app
cd svelte-app
```
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
## Get started
Install the dependencies...
```bash
cd svelte-app
npm install
```
...then start [Rollup](https://rollupjs.org):
```bash
npm run dev
```
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
## Building and running in production mode
To create an optimised version of the app:
```bash
npm run build
```
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
## Single-page app mode
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
```js
"start": "sirv public --single"
```
## Deploying to the web
### With [now](https://zeit.co/now)
Install `now` if you haven't already:
```bash
npm install -g now
```
Then, from within your project folder:
```bash
cd public
now deploy --name my-project
```
As an alternative, use the [Now desktop client](https://zeit.co/download) and simply drag the unzipped project folder to the taskbar icon.
### With [surge](https://surge.sh/)
Install `surge` if you haven't already:
```bash
npm install -g surge
```
Then, from within your project folder:
```bash
npm run build
surge public my-project.surge.sh
```

3301
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "svelte-app",
"version": "1.0.0",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"start": "sirv public"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^11.0.0",
"@rollup/plugin-node-resolve": "^7.0.0",
"eslint": "^6.8.0",
"eslint-plugin-svelte3": "^2.7.3",
"rollup": "^1.20.0",
"rollup-plugin-livereload": "^1.0.0",
"rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^5.1.2",
"svelte": "^3.0.0"
},
"dependencies": {
"axios": "^0.19.2",
"debounce": "^1.2.0",
"rollup-plugin-replace": "^2.2.0",
"sirv-cli": "^0.4.4"
}
}

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

503
public/global.css Normal file
View File

@ -0,0 +1,503 @@
@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.6rem;
padding: 0.1rem 0.4rem;
text-align: center;
margin: 0.3rem;
background: var(--light-color);
color: #333;
border-radius: 3px;
}
.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;
}
}
:root {
--primary-color: #64B5F6;
--dark-color: #333333;
--light-color: #f4f4f4;
--danger-color: #dc3545;
--success-color: #28a745;
--medium-color: #999999;
}
.table-responsive {
display: block;
overflow-x: auto;
width: 100%;
}
.cardV2 {
border-radius: 4px;
background-color: #fff;
box-shadow: 0 0 4px 0 rgba(0,0,0,.14), 0 3px 4px 0 rgba(0,0,0,.12), 0 1px 5px 0 rgba(0,0,0,.2);
/*display: flex;
flex-direction: column;*/
min-width: 0;
/*position: relative;
word-wrap: break-word;*/
}
table {
max-width: 100%;
width: 100%;
border: 0;
margin-bottom: 1rem;
border-collapse: collapse;
}
tr {
border-top: 1px solid #ccc;
}
tbody tr:nth-of-type(odd){
background-color: rgba(0,0,0,0.04);
}
tbody td {
border-top: 1px solid #e1e1e1;
}
.modalWindow {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.2);
z-index: 99999;
opacity:0;
pointer-events: none;
text-align:center;
}
.modalWindow:target {
opacity:1;
pointer-events: auto;
}
.modalWindow > div {
width: 500px;
position: relative;
margin: 10% auto;
background: #fff;
}

18
public/index.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Menuizer</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/build/bundle.css'>
<script defer src='/build/bundle.js'></script>
</head>
<!-- svelte -->
<body>
</body>
</html>

BIN
recipes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

76
rollup.config.js Normal file
View File

@ -0,0 +1,76 @@
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import livereload from 'rollup-plugin-livereload';
import replace from 'rollup-plugin-replace';
import { terser } from 'rollup-plugin-terser';
const production = !process.env.ROLLUP_WATCH;
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js'
},
plugins: [
svelte({
// enable run-time checks when not in production
dev: !production,
// we'll extract any component CSS out into
// a separate file - better for performance
css: css => {
css.write('public/build/bundle.css');
}
}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
replace({
exclude: 'node_modules/**',
ENV: JSON.stringify(production ? 'production' : 'development'),
}),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
};
function serve() {
let started = false;
return {
writeBundle() {
if (!started) {
started = true;
require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
}
}
};
}

21
src/App.svelte Normal file
View File

@ -0,0 +1,21 @@
<script>
import Header from './components/Header.svelte';
import Editor from './components/Editor.svelte';
import FilterBar from './components/FilterBar.svelte';
import Recipes from './components/Recipes.svelte';
import Debug from './components/Debug.svelte';
</script>
<style>
</style>
<main>
<Header/>
<Editor/>
<FilterBar/>
<Recipes/>
</main>

View File

@ -0,0 +1,20 @@
<script>
import {state} from '../store/store';
state.recipes.subscribe(async (v) => {
console.log('>> recipes', v);
});
state.currentItem.subscribe(async (v) => {
console.log('>> currentItem', v);
});
state.filter.subscribe(async (v) => {
console.log('>> filter', v);
});
</script>

View File

@ -0,0 +1,157 @@
<script>
import { state } from '../store/store';
import debounce from 'debounce';
let _editMode;
let _currentItem;
let meat;
let mealtype;
let deleteEnabled = false;
$: {
meat = _currentItem.meat.toString();
mealtype = _currentItem.mealtype.toString();
}
$: deleteEnabled = (_currentItem.hash === '')
state.editMode.subscribe(async (v) => {
_editMode = v;
});
state.currentItem.subscribe(async (v) => {
_currentItem = v;
});
function deleteItem() {
console.log('>> DELETE');
}
function closeEditor() {
state.closeEditor();
}
async function saveRecipe() {
await state.saveRecipe(_currentItem);
}
function pasteHandler(v) {
debouncedPasteProcessor(v);
}
function pasteProcessor(item) {
const meats = [
'x',
'chicken',
'beef',
'pork',
'fish',
'egg',
'vegetable'
];
const newFragment = {};
const titleRegEx = /(?:#\s)(.*)(?:\n)/;
const linkRegEx = /(?:\[.*]\()(.*)(?:\))/;
const foodRegEx = /([vV]egetable|[pP]ork|[cC]hicken|[bB]eef|[fF]ish|[eE]gg)/g;
const mealTypeRegEx = /([sS]oup)/g;
const foodCount = {};
let winnerVal = 0;
let winnerId = 0;
const newTitle = titleRegEx.exec(item.target.value);
const newLink = linkRegEx.exec(item.target.value);
if (newTitle !== null) newFragment.name = newTitle[1];
if (newLink !== null) newFragment.url = newLink[1];
const matchedFoods = [...item.target.value.matchAll(foodRegEx)];
const mealTypes = [...item.target.value.matchAll(mealTypeRegEx)];
if (matchedFoods.length > 0) {
const deboxed = matchedFoods.map(fooditem => {
return fooditem[0].toLowerCase();
});
deboxed.forEach(el => {
foodCount[el] = foodCount[el] + 1 || 1;
});
for (const key in foodCount)
if (foodCount[key] > winnerVal) {
winnerVal = foodCount[key];
winnerId = meats.indexOf(key);
}
newFragment.meat = winnerId;
}
if (mealTypes.length > 0)
newFragment.mealtype = 2;
else
newFragment.mealtype = 1;
_currentItem = {..._currentItem, ...newFragment};
}
const debouncedPasteProcessor = debounce(pasteProcessor, 250);
</script>
<style>
</style>
{#if _editMode}
<div class="container">
<form autocomplete="off">
<label for="name">Name:</label>
<input type="text" name="name" id="name" bind:value={_currentItem.name} required/>
<label for="url">Url:</label>
<input type="text" name="url" id="url" bind:value={_currentItem.url} required/>
<label for="md">Markdown:</label>
<textarea id="md" name="md" cols="50" rows="10" bind:value={_currentItem.md} on:paste={pasteHandler}></textarea>
<label for="meat">Meat</label>
<select id="meat" name="meat" bind:value={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" bind:value={mealtype} required>
<option></option>
<option value="1">Main</option>
<option value="2">Soup</option>
<option value="128">Note</option>
</select>
<input id="_id" name="id" type="hidden" bind:value={_currentItem._id} disabled/>
<input type="hidden" id="short" name="short" bind:value={_currentItem.short} disabled/>
<input type="hidden" id="hash" name="hash" bind:value={_currentItem.hash} disabled/>
<input type="hidden" id="lastused" name="lastused" bind:value={_currentItem.lastused} disabled/>
<div class="my text-right">
<button class="btn btn-danger btn-sm" id="delete" type="button" disabled={deleteEnabled} on:click={deleteItem}>
Delete
</button>
<button class="btn btn-sm" type="button" on:click={closeEditor}>
Close
</button>
<button class="btn btn-primary btn-sm" id="save" type="button" on:click={saveRecipe}>
Save
</button>
</div>
</form>
</div>
{/if}

View File

@ -0,0 +1,45 @@
<script>
import { state } from '../store/store';
function updateMeat(event) {
const newVal = event.target.value;
state.updateMeatFilter(newVal);
}
function updateMeal(event) {
const newVal = event.target.value;
state.updateMealFilter(newVal);
}
</script>
<style>
.filterBar {
background: var(--medium-color);
margin-bottom: 1rem;
padding: 10px 5px;
}
</style>
<div class="container">
<div class="filterBar grid-4">
<select on:change={updateMeat}>
<option value="0">All</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>
<select on:change={updateMeal}>
<option value="0">All</option>
<option value="1">Mains</option>
<option value="2">Soups</option>
<option value="128">Notes</option>
</select>
</div>
</div>

View File

@ -0,0 +1,23 @@
<script>
import { state } from '../store/store';
function handleNewRecipe() {
console.log('newRecipe');
state.newRecipe();
}
</script>
<style>
</style>
<header class="navbar bg-primary">
<h2>
Recipes
</h2>
<ul>
<li>
<button class="btn btn-sm" on:click={handleNewRecipe} type="button">New Recipe</button>
</li>
</ul>
</header>

View File

@ -0,0 +1,89 @@
<script>
import { state } from '../store/store';
export let recipeItem = {};
let meatClass;
let meatText;
let url;
const meats = ['x', 'Chicken', 'Beef', 'Pork', 'Fish', 'Egg', 'Vegetable'];
$:{
meatText = meats[recipeItem.meat];
meatClass = (recipeItem.meat === '') ? '' : meats[recipeItem.meat].toLowerCase();
url = `/view/${recipeItem.short}`;
}
function editRecipe(hash) {
state.editRecipe(hash);
}
</script>
<style>
.recipeItem {
display: flex;
padding: 0.1rem;
border-bottom: 1px #ccc dotted;
}
.recipeItem:nth-of-type(odd) {
background-color: rgba(0, 0, 0, 0.04);
}
.listItemSix {
flex: 6;
}
.listItemThree {
flex: 3;
}
.chicken {
background: #8e5241;
color: #fff;
}
.beef {
background: #d72414;
color: #fff;
}
.pork {
background: #ef96d9;
color: #fff;
}
.fish {
background: #005ba0;
color: #fff;
}
.egg {
background: #fbc003;
color: #000;
}
.vegetable {
background: #00903e;
color: #fff;
}
</style>
<div class="recipeItem">
<div class="listItemSix"><a href={url}>{recipeItem.name}</a></div>
<div class="listItemThree">
{#if recipeItem.mealtype ===2}
<span class="badge badge-light">Soup</span>
{:else if recipeItem.mealtype===128}
<span class="badge badge-dark">Note</span>
{/if}
<span class="badge {meatClass}">{meatText}</span>
</div>
<div class="listItemThree all-center">
<button class="btn btn-primary btn-sm" type="button" on:click={editRecipe(recipeItem.hash)}>Edit</button>
</div>
</div>

View File

@ -0,0 +1,61 @@
<script>
import { state } from '../store/store';
import { onMount } from 'svelte';
import RecipeItem from './RecipeItem.svelte';
let _storedRecipes = [];
let _recipes = [];
let _filter = {
'meat': '0',
'meal': '0'
};
state.recipes.subscribe(async (v) => {
_storedRecipes = v;
_recipes = doFilter(_storedRecipes);
});
state.filter.subscribe(async (v) => {
_filter = v;
_recipes = doFilter(_storedRecipes);
});
onMount(async () => {
await state.fetchRecipes();
});
function doFilter(v) {
const meatFilterMode = parseInt(_filter.meat, 10);
const mealFilterMode = parseInt(_filter.meal, 10);
const mealsFilter = v.filter(item => mealFilterMode === 0 || item.mealtype === mealFilterMode);
const meatsFilter = mealsFilter.filter(item => meatFilterMode === 0 || item.meat === meatFilterMode);
return meatsFilter.sort((a, b) => {
var shortA = a.short; // ignore upper and lowercase
var shortB = b.short; // ignore upper and lowercase
if (shortA < shortB) {
return -1;
}
if (shortA > shortB) {
return 1;
}
// names must be equal
return 0;
});
}
</script>
<style>
</style>
<div class="container ">
{#each _recipes as item}
<RecipeItem recipeItem={item}/>
{/each}
</div>

10
src/main.js Normal file
View File

@ -0,0 +1,10 @@
import App from './App.svelte';
const app = new App({
target: document.body,
props: {
name: 'world'
}
});
export default app;

162
src/store/store.js Normal file
View File

@ -0,0 +1,162 @@
import { writable } from 'svelte/store';
import axios from 'axios';
const url = (ENV === 'production') ? 'https://menu.silvrtree.co.uk/recipes' : 'http://localhost:3000/recipes';
const oldstate = writable({
'recipes': [],
'currentItem': {
'name': '',
'url': '',
'md': '',
'meat': '',
'mealtype': '',
'_id': '',
'short': '',
'hash': '',
'lastused': ''
},
'editMode': false,
'meatFilterMode':0,
'mealFilterMode':0
});
function Filter() {
const { subscribe, set, update } = writable({
'meat':'0',
'meal':'0'
});
return {
subscribe,
'updateMeat': (newVal) => update(v => {
return {
...v, ...{ 'meat':newVal }
};
}),
'updateMeal': (newVal) => update(v => {
return {
...v, ...{ 'meal':newVal }
};
})
};
}
function Recipes() {
const { subscribe, set, update } = writable([]);
return {
subscribe,
set,
update
};
}
function EditMode() {
const { subscribe, set, update } = writable(false);
return {
subscribe,
'newRecipe': () => update(v => true),
'closeEditor': () => update( v => false)
};
}
function CurrentItem() {
const { subscribe, set, update } = writable({
'name': '',
'url': '',
'md': '',
'meat': '',
'mealtype': '',
'_id': '',
'short': '',
'hash': '',
'lastused': ''
});
return {
subscribe,
'clearItem': () => update(v => {
return {
'name': '',
'url': '',
'md': '',
'meat': '',
'mealtype': '',
'_id': '',
'short': '',
'hash': '',
'lastused': ''
};
}),
'updateItem': (payload) => update(v => {
return payload;
})
};
}
const state = {
'editMode': EditMode(),
'currentItem': CurrentItem(),
'recipes': Recipes(),
'filter': Filter(),
newRecipe() {
console.log('>> Action:newRecipe');
this.editMode.newRecipe();
this.currentItem.clearItem();
},
async editRecipe(hash) {
const response = await axios.get(`${url}/${hash}`).catch((err) => {
console.error(err);
});
this.currentItem.updateItem(response.data);
this.editMode.newRecipe();
},
async saveRecipe(payload) {
console.log('>> Action:saveRecipe');
const data = { ...payload };
let response;
if (data.hash === '') {
console.log('Create new');
response = await axios.post(`${url}`, data).catch((err) => {
console.error(err);
});
}
else {
console.log('Update existing');
response = await axios.put(`${url}/${data.hash}`, data).catch((err) => {
console.error(err);
});
}
if (response.data.changes > 0 || response.data.msg === 'Row inserted') {
this.closeEditor();
this.fetchRecipes();
}
},
async fetchRecipes() {
const response = await axios.get(url);
this.recipes.set(response.data);
},
closeEditor() {
this.editMode.closeEditor();
this.currentItem.clearItem();
},
updateMeatFilter(newVal) {
this.filter.updateMeat(newVal);
},
updateMealFilter(newVal) {
this.filter.updateMeal(newVal);
}
};
// export { currentItem, editMode, actions, state };
export { state };