1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-03 04:25:24 +02:00

nuxt init

This commit is contained in:
hay-kot 2021-07-31 14:00:28 -08:00
parent 79b3985a49
commit 8d3db89327
275 changed files with 13274 additions and 4003 deletions

13
frontend/.editorconfig Normal file
View file

@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

View file

@ -1,2 +0,0 @@
VUE_APP_API_BASE_URL=http://localhost:9000
PREVIEW_BUNDLE=true

16
frontend/.eslintrc.js Normal file
View file

@ -0,0 +1,16 @@
module.exports = {
root: true,
env: {
browser: true,
node: true
},
extends: [
'@nuxtjs/eslint-config-typescript',
'plugin:nuxt/recommended',
'prettier'
],
plugins: [
],
// add your custom rules here
rules: {}
}

32
frontend/.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,32 @@
version: 2
updates:
# Fetch and update latest `npm` packages
- package-ecosystem: npm
directory: '/'
schedule:
interval: daily
time: '00:00'
open-pull-requests-limit: 10
reviewers:
- hay-kot
assignees:
- hay-kot
commit-message:
prefix: fix
prefix-development: chore
include: scope
# Fetch and update latest `github-actions` pkgs
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: daily
time: '00:00'
open-pull-requests-limit: 10
reviewers:
- hay-kot
assignees:
- hay-kot
commit-message:
prefix: fix
prefix-development: chore
include: scope

5
frontend/.github/semantic.yml vendored Normal file
View file

@ -0,0 +1,5 @@
# Always validate the PR title AND all the commits
titleAndCommits: true
# Allows use of Merge commits (eg on github: "Merge branch 'master' into feature/ride-unicorns")
# this is only relevant when using commitsOnly: true (or titleAndCommits: true)
allowMergeCommits: true

50
frontend/.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,50 @@
name: ci
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
jobs:
ci:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
node: [14]
steps:
- name: Checkout 🛎
uses: actions/checkout@master
- name: Setup node env 🏗
uses: actions/setup-node@v2.1.5
with:
node-version: ${{ matrix.node }}
check-latest: true
- name: Get yarn cache directory path 🛠
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache node_modules 📦
uses: actions/cache@v2.1.4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies 👨🏻‍💻
run: yarn
- name: Run linter 👀
run: yarn lint

90
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,90 @@
# 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
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp

1
frontend/.husky/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
_

View file

5
frontend/.husky/pre-commit Executable file
View file

@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
yarn lint-staged

4
frontend/.prettierrc Normal file
View file

@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

View file

@ -1,24 +1,69 @@
# frontend
## Project setup
```
npm install
## Build Setup
```bash
# install dependencies
$ yarn install
# serve with hot reload at localhost:3000
$ yarn dev
# build for production and launch server
$ yarn build
$ yarn start
# generate static project
$ yarn generate
```
### Compiles and hot-reloads for development
```
npm run serve
```
For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org).
### Compiles and minifies for production
```
npm run build
```
## Special Directories
### Lints and fixes files
```
npm run lint
```
You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality.
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
### `assets`
The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets).
### `components`
The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components).
### `layouts`
Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts).
### `pages`
This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing).
### `plugins`
The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins).
### `static`
This directory contains your static files. Each file inside this directory is mapped to `/`.
Example: `/static/robots.txt` is mapped as `/robots.txt`.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static).
### `store`
This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store).

View file

@ -0,0 +1,4 @@
// Ref: https://github.com/nuxt-community/vuetify-module#customvariables
//
// The variables you want to modify
// $font-size-root: 20px;

View file

@ -1,3 +0,0 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

View file

@ -0,0 +1,11 @@
<template>
<svg class="nuxt-logo" viewBox="0 0 45 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.7203 29.704H41.1008C41.6211 29.7041 42.1322 29.5669 42.5828 29.3061C43.0334 29.0454 43.4075 28.6704 43.6675 28.2188C43.9275 27.7672 44.0643 27.2549 44.0641 26.7335C44.0639 26.2121 43.9266 25.6999 43.6662 25.2485L32.6655 6.15312C32.4055 5.70162 32.0315 5.32667 31.581 5.06598C31.1305 4.8053 30.6195 4.66805 30.0994 4.66805C29.5792 4.66805 29.0682 4.8053 28.6177 5.06598C28.1672 5.32667 27.7932 5.70162 27.5332 6.15312L24.7203 11.039L19.2208 1.48485C18.9606 1.03338 18.5864 0.658493 18.1358 0.397853C17.6852 0.137213 17.1741 0 16.6538 0C16.1336 0 15.6225 0.137213 15.1719 0.397853C14.7213 0.658493 14.3471 1.03338 14.0868 1.48485L0.397874 25.2485C0.137452 25.6999 0.000226653 26.2121 2.8053e-07 26.7335C-0.000226092 27.2549 0.136554 27.7672 0.396584 28.2188C0.656614 28.6704 1.03072 29.0454 1.48129 29.3061C1.93185 29.5669 2.44298 29.7041 2.96326 29.704H13.2456C17.3195 29.704 20.3239 27.9106 22.3912 24.4118L27.4102 15.7008L30.0986 11.039L38.1667 25.0422H27.4102L24.7203 29.704ZM13.0779 25.0374L5.9022 25.0358L16.6586 6.36589L22.0257 15.7008L18.4322 21.9401C17.0593 24.2103 15.4996 25.0374 13.0779 25.0374Z" fill="#00DC82" />
</svg>
</template>
<style>
.nuxt-logo {
height: 180px;
}
</style>

View file

@ -0,0 +1,46 @@
<!-- Please remove this file from your project -->
<template>
<div class="relative flex items-top justify-center min-h-screen bg-gray-100 sm:items-center sm:pt-0">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.1.2/dist/tailwind.min.css" rel="stylesheet">
<div class="max-w-4xl mx-auto sm:px-6 lg:px-8">
<a class="flex justify-center pt-8 sm:pt-0" href="https://nuxtjs.org" target="_blank">
<svg width="218" height="45" viewBox="0 0 159 30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M55.5017 6.81866H60.1727L70.0719 22.9912V6.81866H74.3837V29.7345H69.7446L59.8135 13.5955V29.7345H55.5017V6.81866Z" fill="#003543" /> <path d="M93.657 29.7344H89.6389V27.1747C88.7241 28.9761 86.8628 29.9904 84.5113 29.9904C80.7869 29.9904 78.3684 27.3059 78.3684 23.4423V13.2339H82.3865V22.5976C82.3865 24.8566 83.7594 26.4276 85.8171 26.4276C88.0712 26.4276 89.6389 24.6598 89.6389 22.2377V13.2339H93.657V29.7344Z" fill="#003543" /> <path d="M107.64 29.7344L103.784 24.2342L99.9291 29.7344H95.6492L101.596 21.1242L96.1074 13.2339H100.485L103.784 17.9821L107.051 13.2339H111.461L105.94 21.1242L111.886 29.7344H107.64Z" fill="#003543" /> <path d="M120.053 8.25848V13.2339H124.627V16.6063H120.053V24.7974C120.053 25.0725 120.162 25.3363 120.356 25.531C120.55 25.7257 120.813 25.8353 121.087 25.8357H124.627V29.728H121.98C118.386 29.728 116.035 27.6323 116.035 23.9687V16.6095H112.801V13.2339H114.83C115.776 13.2339 116.327 12.6692 116.327 11.7349V8.25848H120.053Z" fill="#003543" /> <path d="M134.756 24.5446V6.81866H139.066V23.1864C139.066 27.6067 136.943 29.7345 133.349 29.7345H128.332V25.8421H133.461C133.804 25.8421 134.134 25.7054 134.377 25.4621C134.619 25.2188 134.756 24.8888 134.756 24.5446Z" fill="#003543" /> <path d="M141.649 22.0409H145.799C146.029 24.6006 147.728 26.2308 150.472 26.2308C152.923 26.2308 154.623 25.2501 154.623 23.2199C154.623 18.3085 142.331 21.7129 142.331 12.9395C142.334 9.17515 145.568 6.55945 150.215 6.55945C155.05 6.55945 158.317 9.34153 158.516 13.6306H154.388C154.193 11.6341 152.632 10.2918 150.207 10.2918C147.953 10.2918 146.548 11.3397 146.548 12.9427C146.548 18.0173 159 14.2226 159 23.1576C159 27.4131 155.504 30 150.474 30C145.279 30 141.882 26.8563 141.654 22.0441" fill="#003543" /> <path d="M24.7203 29.704H41.1008C41.6211 29.7041 42.1322 29.5669 42.5828 29.3061C43.0334 29.0454 43.4075 28.6704 43.6675 28.2188C43.9275 27.7672 44.0643 27.2549 44.0641 26.7335C44.0639 26.2121 43.9266 25.6999 43.6662 25.2485L32.6655 6.15312C32.4055 5.70162 32.0315 5.32667 31.581 5.06598C31.1305 4.8053 30.6195 4.66805 30.0994 4.66805C29.5792 4.66805 29.0682 4.8053 28.6177 5.06598C28.1672 5.32667 27.7932 5.70162 27.5332 6.15312L24.7203 11.039L19.2208 1.48485C18.9606 1.03338 18.5864 0.658493 18.1358 0.397853C17.6852 0.137213 17.1741 0 16.6538 0C16.1336 0 15.6225 0.137213 15.1719 0.397853C14.7213 0.658493 14.3471 1.03338 14.0868 1.48485L0.397874 25.2485C0.137452 25.6999 0.000226653 26.2121 2.8053e-07 26.7335C-0.000226092 27.2549 0.136554 27.7672 0.396584 28.2188C0.656614 28.6704 1.03072 29.0454 1.48129 29.3061C1.93185 29.5669 2.44298 29.7041 2.96326 29.704H13.2456C17.3195 29.704 20.3239 27.9106 22.3912 24.4118L27.4102 15.7008L30.0986 11.039L38.1667 25.0422H27.4102L24.7203 29.704ZM13.0779 25.0374L5.9022 25.0358L16.6586 6.36589L22.0257 15.7008L18.4322 21.9401C17.0593 24.2103 15.4996 25.0374 13.0779 25.0374Z" fill="#00DC82" /></svg>
</a>
<div class="mt-8 bg-white overflow-hidden shadow sm:rounded-lg p-6">
<h2 class="text-2xl leading-7 font-semibold">
Welcome to your Nuxt Application
</h2>
<p class="mt-3 text-gray-600">
We recommend you take a look at the <a href="https://nuxtjs.org" target="_blank" class="text-green-500 hover:underline">Nuxt documentation</a>, whether you are new or have previous experience with the framework.<br>
</p>
<p class="mt-4 pt-4 text-gray-800 border-t border-dashed">
To get started, remove <code class="bg-gray-100 text-sm p-1 rounded border">components/Tutorial.vue</code> and start coding in <code class="bg-gray-100 text-sm p-1 rounded border">pages/index.vue</code>. Have fun!
</p>
</div>
<div class="flex justify-center pt-4 space-x-2">
<a href="https://github.com/nuxt/nuxt.js" target="_blank"><svg
class="w-6 h-6 text-gray-600 hover:text-gray-800"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
width="32"
height="32"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
><path d="M12 2.247a10 10 0 0 0-3.162 19.487c.5.088.687-.212.687-.475c0-.237-.012-1.025-.012-1.862c-2.513.462-3.163-.613-3.363-1.175a3.636 3.636 0 0 0-1.025-1.413c-.35-.187-.85-.65-.013-.662a2.001 2.001 0 0 1 1.538 1.025a2.137 2.137 0 0 0 2.912.825a2.104 2.104 0 0 1 .638-1.338c-2.225-.25-4.55-1.112-4.55-4.937a3.892 3.892 0 0 1 1.025-2.688a3.594 3.594 0 0 1 .1-2.65s.837-.262 2.75 1.025a9.427 9.427 0 0 1 5 0c1.912-1.3 2.75-1.025 2.75-1.025a3.593 3.593 0 0 1 .1 2.65a3.869 3.869 0 0 1 1.025 2.688c0 3.837-2.338 4.687-4.563 4.937a2.368 2.368 0 0 1 .675 1.85c0 1.338-.012 2.413-.012 2.75c0 .263.187.575.687.475A10.005 10.005 0 0 0 12 2.247z" fill="currentColor" /></svg></a>
<a href="https://twitter.com/nuxt_js" target="_blank"><svg
class="w-6 h-6 text-gray-600 hover:text-gray-800"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
width="32"
height="32"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
><path d="M22.46 6c-.77.35-1.6.58-2.46.69c.88-.53 1.56-1.37 1.88-2.38c-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29c0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15c0 1.49.75 2.81 1.91 3.56c-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07a4.28 4.28 0 0 0 4 2.98a8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21C16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56c.84-.6 1.56-1.36 2.14-2.23z" fill="currentColor" /></svg></a>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,22 @@
<template>
<img
class="VuetifyLogo"
alt="Vuetify Logo"
src="/vuetify-logo.svg"
>
</template>
<style>
.VuetifyLogo {
height: 180px;
width: 180px;
transform: rotateY(560deg);
animation: turn 3.5s ease-out forwards 1s;
}
@keyframes turn {
100% {
transform: rotateY(0deg);
}
}
</style>

View file

@ -1,22 +0,0 @@
FROM node:lts-alpine
# # install simple http server for serving static content
# RUN npm install -g http-server
# make the 'app' folder the current working directory
WORKDIR /app
# copy both 'package.json' and 'package-lock.json' (if available)
COPY package*.json ./
# install project dependencies
RUN npm install
# copy project files and folders to the current working directory (i.e. 'app' folder)
# COPY . .
# build app for production with minification
# RUN npm run build
EXPOSE 8080
CMD [ "npm", "run", "serve" ]

View file

@ -1,9 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

View file

@ -0,0 +1,117 @@
<template>
<v-app dark>
<v-navigation-drawer
v-model="drawer"
:mini-variant="miniVariant"
:clipped="clipped"
fixed
app
>
<v-list>
<v-list-item
v-for="(item, i) in items"
:key="i"
:to="item.to"
router
exact
>
<v-list-item-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title v-text="item.title" />
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-app-bar
:clipped-left="clipped"
fixed
app
>
<v-app-bar-nav-icon @click.stop="drawer = !drawer" />
<v-btn
icon
@click.stop="miniVariant = !miniVariant"
>
<v-icon>mdi-{{ `chevron-${miniVariant ? 'right' : 'left'}` }}</v-icon>
</v-btn>
<v-btn
icon
@click.stop="clipped = !clipped"
>
<v-icon>mdi-application</v-icon>
</v-btn>
<v-btn
icon
@click.stop="fixed = !fixed"
>
<v-icon>mdi-minus</v-icon>
</v-btn>
<v-toolbar-title v-text="title" />
<v-spacer />
<v-btn
icon
@click.stop="rightDrawer = !rightDrawer"
>
<v-icon>mdi-menu</v-icon>
</v-btn>
</v-app-bar>
<v-main>
<v-container>
<Nuxt />
</v-container>
</v-main>
<v-navigation-drawer
v-model="rightDrawer"
:right="right"
temporary
fixed
>
<v-list>
<v-list-item @click.native="right = !right">
<v-list-item-action>
<v-icon light>
mdi-repeat
</v-icon>
</v-list-item-action>
<v-list-item-title>Switch drawer (click me)</v-list-item-title>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-footer
:absolute="!fixed"
app
>
<span>&copy; {{ new Date().getFullYear() }}</span>
</v-footer>
</v-app>
</template>
<script>
export default {
data () {
return {
clipped: false,
drawer: false,
fixed: false,
items: [
{
icon: 'mdi-apps',
title: 'Welcome',
to: '/'
},
{
icon: 'mdi-chart-bubble',
title: 'Inspire',
to: '/inspire'
}
],
miniVariant: false,
right: true,
rightDrawer: false,
title: 'Vuetify.js'
}
}
}
</script>

View file

@ -0,0 +1,44 @@
<template>
<v-app dark>
<h1 v-if="error.statusCode === 404">
{{ pageNotFound }}
</h1>
<h1 v-else>
{{ otherError }}
</h1>
<NuxtLink to="/">
Home page
</NuxtLink>
</v-app>
</template>
<script>
export default {
layout: 'empty',
props: {
error: {
type: Object,
default: null
}
},
data () {
return {
pageNotFound: '404 Not Found',
otherError: 'An error occurred'
}
},
head () {
const title =
this.error.statusCode === 404 ? this.pageNotFound : this.otherError
return {
title
}
}
}
</script>
<style scoped>
h1 {
font-size: 20px;
}
</style>

78
frontend/nuxt.config.js Normal file
View file

@ -0,0 +1,78 @@
import colors from 'vuetify/es5/util/colors'
export default {
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
titleTemplate: '%s - frontend',
title: 'frontend',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ name: 'format-detection', content: 'telephone=no' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: [
],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [
],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
// https://go.nuxtjs.dev/typescript
'@nuxt/typescript-build',
// https://go.nuxtjs.dev/vuetify
'@nuxtjs/vuetify',
],
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
// https://go.nuxtjs.dev/axios
'@nuxtjs/axios',
// https://go.nuxtjs.dev/pwa
'@nuxtjs/pwa',
],
// Axios module configuration: https://go.nuxtjs.dev/config-axios
axios: {},
// PWA module configuration: https://go.nuxtjs.dev/pwa
pwa: {
manifest: {
lang: 'en'
}
},
// Vuetify module configuration: https://go.nuxtjs.dev/config-vuetify
vuetify: {
customVariables: ['~/assets/variables.scss'],
theme: {
dark: true,
themes: {
dark: {
primary: colors.blue.darken2,
accent: colors.grey.darken3,
secondary: colors.amber.darken3,
info: colors.teal.lighten1,
warning: colors.amber.base,
error: colors.deepOrange.accent4,
success: colors.green.accent3
}
}
}
},
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {
}
}

View file

@ -1,76 +1,39 @@
{
"name": "frontend",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'"
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate",
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
"prepare": "husky install",
"lint": "yarn lint:js"
},
"lint-staged": {
"*.{js,vue}": "eslint"
},
"dependencies": {
"@adapttive/vue-markdown": "^4.0.1",
"@vue/composition-api": "^1.0.4",
"axios": "^0.21.1",
"core-js": "^3.14.0",
"fuse.js": "^6.4.6",
"register-service-worker": "^1.7.1",
"v-jsoneditor": "^1.4.4",
"vue": "^2.6.14",
"vue-i18n": "^8.24.1",
"vue-router": "^3.5.1",
"vuedraggable": "^2.24.3",
"vuetify": "^2.5.3",
"vuex": "^3.6.2",
"vuex-persistedstate": "^4.0.0-beta.3"
"@nuxtjs/axios": "^5.13.6",
"@nuxtjs/pwa": "^3.3.5",
"core-js": "^3.15.1",
"nuxt": "^2.15.7",
"vuetify": "^2.5.5"
},
"devDependencies": {
"@intlify/vue-i18n-loader": "^1.1.0",
"@mdi/font": "^5.9.55",
"@mdi/js": "^5.9.55",
"@vue/cli-plugin-babel": "^4.5.13",
"@vue/cli-plugin-eslint": "^4.5.13",
"@vue/cli-plugin-pwa": "~4.5.0",
"@vue/cli-service": "^4.5.13",
"@vue/preload-webpack-plugin": "^2.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"html-webpack-plugin": "^5.3.1",
"preload-webpack-plugin": "^2.3.0",
"sass": "^1.34.1",
"sass-loader": "^8.0.2",
"typeface-roboto": "^1.1.13",
"vue-cli-plugin-i18n": "~1.0.1",
"vue-cli-plugin-vuetify": "^2.4.1",
"vue-cli-plugin-webpack-bundle-analyzer": "^4.0.0",
"vue-template-compiler": "^2.6.14",
"vuetify-loader": "^1.7.2"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"prettier": {
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"printWidth": 120
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
"@babel/eslint-parser": "^7.14.7",
"@nuxt/types": "^2.15.7",
"@nuxt/typescript-build": "^2.1.0",
"@nuxtjs/eslint-config-typescript": "^6.0.1",
"@nuxtjs/eslint-module": "^3.0.2",
"@nuxtjs/vuetify": "^1.12.1",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-nuxt": "^2.0.0",
"eslint-plugin-vue": "^7.12.1",
"husky": "^6.0.0",
"lint-staged": "^10.5.4",
"prettier": "^2.3.2"
}
}

77
frontend/pages/index.vue Normal file
View file

@ -0,0 +1,77 @@
<template>
<v-row justify="center" align="center">
<v-col cols="12" sm="8" md="6">
<v-card class="logo py-4 d-flex justify-center">
<NuxtLogo />
<VuetifyLogo />
</v-card>
<v-card>
<v-card-title class="headline">
Welcome to the Vuetify + Nuxt.js template
</v-card-title>
<v-card-text>
<p>Vuetify is a progressive Material Design component framework for Vue.js. It was designed to empower developers to create amazing applications.</p>
<p>
For more information on Vuetify, check out the <a
href="https://vuetifyjs.com"
target="_blank"
rel="noopener noreferrer"
>
documentation
</a>.
</p>
<p>
If you have questions, please join the official <a
href="https://chat.vuetifyjs.com/"
target="_blank"
rel="noopener noreferrer"
title="chat"
>
discord
</a>.
</p>
<p>
Find a bug? Report it on the github <a
href="https://github.com/vuetifyjs/vuetify/issues"
target="_blank"
rel="noopener noreferrer"
title="contribute"
>
issue board
</a>.
</p>
<p>Thank you for developing with Vuetify and I look forward to bringing more exciting features in the future.</p>
<div class="text-xs-right">
<em><small>&mdash; John Leider</small></em>
</div>
<hr class="my-3">
<a
href="https://nuxtjs.org/"
target="_blank"
rel="noopener noreferrer"
>
Nuxt Documentation
</a>
<br>
<a
href="https://github.com/nuxt/nuxt.js"
target="_blank"
rel="noopener noreferrer"
>
Nuxt GitHub
</a>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
color="primary"
nuxt
to="/inspire"
>
Continue
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</template>

View file

@ -0,0 +1,19 @@
<template>
<v-row>
<v-col class="text-center">
<img
src="/v.png"
alt="Vuetify.js"
class="mb-5"
>
<blockquote class="blockquote">
&#8220;First, solve the problem. Then, write the code.&#8221;
<footer>
<small>
<em>&mdash;John Johnson</em>
</small>
</footer>
</blockquote>
</v-col>
</v-row>
</template>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" version="1.1">
<path d="M 162.083 54.642 C 148.745 68.272, 137.170 80.703, 136.362 82.266 C 133.689 87.435, 133.522 94.130, 135.929 99.573 C 137.122 102.269, 139.070 105.510, 140.258 106.775 L 142.418 109.074 90.974 160.526 L 39.529 211.979 46.999 219.499 L 54.470 227.020 91.235 190.265 L 128 153.510 164.765 190.265 L 201.530 227.020 209 219.500 L 216.470 211.980 179.725 175.225 L 142.980 138.470 150.320 131.178 C 156.858 124.685, 157.808 124.063, 159.001 125.501 C 162.066 129.195, 168.873 132.163, 174.392 132.213 C 183.508 132.295, 186.374 130.174, 212.477 104.038 L 236.454 80.030 231.501 75.001 L 226.548 69.973 209.288 87.212 L 192.027 104.452 187 99.500 L 181.973 94.548 199.212 77.288 L 216.452 60.027 211.500 55 L 206.548 49.973 189.288 67.212 L 172.027 84.452 167 79.500 L 161.973 74.548 179.225 57.275 L 196.477 40.001 191.406 34.930 L 186.335 29.859 162.083 54.642 M 38.429 41.250 C 31.557 49.376, 28.011 62.815, 29.835 73.824 C 31.955 86.615, 34.508 90.093, 61.720 117.253 L 86.520 142.005 101.501 126.999 L 116.482 111.993 79.496 74.996 C 59.154 54.648, 42.210 38, 41.844 38 C 41.478 38, 39.941 39.462, 38.429 41.250" stroke="none" fill="black" fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="description" content="Mealie is a self hosted recipe manager and meal planner.">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title> Mealie </title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View file

@ -1,82 +0,0 @@
{
"name": "Mealie",
"short_name": "Mealie",
"icons": [
{
"src": "./img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "./img/icons/android-chrome-maskable-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./img/icons/android-chrome-maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./img/icons/apple-touch-icon-60x60.png",
"sizes": "60x60",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon-76x76.png",
"sizes": "76x76",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon-120x120.png",
"sizes": "120x120",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon-180x180.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "./img/icons/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "./img/icons/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "./img/icons/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "./img/icons/msapplication-icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "./img/icons/mstile-150x150.png",
"sizes": "150x150",
"type": "image/png"
}
],
"start_url": ".",
"display": "standalone",
"background_color": "#FFFFFF",
"theme_color": "#E58325"
}

View file

@ -1,2 +0,0 @@
User-agent: *
Disallow:

View file

@ -1 +0,0 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#7289DA;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="602px" height="602px" viewBox="57 57 602 602" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="layer1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(58.964119, 58.887520)" opacity="0.91">
<ellipse style="fill: rgb(36, 157, 241); fill-rule: evenodd; stroke: rgb(255, 255, 255); stroke-width: 0;" transform="matrix(-0.674571, 0.73821, -0.73821, -0.674571, 556.833239, 241.613465)" cx="216.308" cy="152.076" rx="296.855" ry="296.855"/>
<path d="M 280.949 172.514 L 355.429 162.714 L 282.909 326.374 L 282.909 326.374 C 295.649 325.394 308.142 321.067 320.389 313.394 L 320.389 313.394 L 320.389 313.394 C 332.642 305.714 343.916 296.077 354.209 284.484 L 354.209 284.484 L 354.209 284.484 C 364.496 272.884 373.396 259.981 380.909 245.774 L 380.909 245.774 L 380.909 245.774 C 388.422 231.561 393.812 217.594 397.079 203.874 L 397.079 203.874 L 397.079 203.874 C 399.039 195.381 399.939 187.214 399.779 179.374 L 399.779 179.374 L 399.779 179.374 C 399.612 171.534 397.569 164.674 393.649 158.794 L 393.649 158.794 L 393.649 158.794 C 389.729 152.914 383.766 148.177 375.759 144.584 L 375.759 144.584 L 375.759 144.584 C 367.759 140.991 356.899 139.194 343.179 139.194 L 343.179 139.194 L 343.179 139.194 C 327.172 139.194 311.409 141.807 295.889 147.034 L 295.889 147.034 L 295.889 147.034 C 280.376 152.261 266.002 159.857 252.769 169.824 L 252.769 169.824 L 252.769 169.824 C 239.542 179.784 228.029 192.197 218.229 207.064 L 218.229 207.064 L 218.229 207.064 C 208.429 221.924 201.406 238.827 197.159 257.774 L 197.159 257.774 L 197.159 257.774 C 195.526 263.981 194.546 268.961 194.219 272.714 L 194.219 272.714 L 194.219 272.714 C 193.892 276.474 193.812 279.577 193.979 282.024 L 193.979 282.024 L 193.979 282.024 C 194.139 284.477 194.462 286.357 194.949 287.664 L 194.949 287.664 L 194.949 287.664 C 195.442 288.971 195.852 290.277 196.179 291.584 L 196.179 291.584 L 196.179 291.584 C 179.519 291.584 167.349 288.234 159.669 281.534 L 159.669 281.534 L 159.669 281.534 C 151.996 274.841 150.119 263.164 154.039 246.504 L 154.039 246.504 L 154.039 246.504 C 157.959 229.191 166.862 212.694 180.749 197.014 L 180.749 197.014 L 180.749 197.014 C 194.629 181.334 211.122 167.531 230.229 155.604 L 230.229 155.604 L 230.229 155.604 C 249.342 143.684 270.249 134.214 292.949 127.194 L 292.949 127.194 L 292.949 127.194 C 315.656 120.167 337.789 116.654 359.349 116.654 L 359.349 116.654 L 359.349 116.654 C 378.296 116.654 394.219 119.347 407.119 124.734 L 407.119 124.734 L 407.119 124.734 C 420.026 130.127 430.072 137.234 437.259 146.054 L 437.259 146.054 L 437.259 146.054 C 444.446 154.874 448.936 165.164 450.729 176.924 L 450.729 176.924 L 450.729 176.924 C 452.529 188.684 451.959 200.934 449.019 213.674 L 449.019 213.674 L 449.019 213.674 C 445.426 229.027 438.646 244.464 428.679 259.984 L 428.679 259.984 L 428.679 259.984 C 418.719 275.497 406.226 289.544 391.199 302.124 L 391.199 302.124 L 391.199 302.124 C 376.172 314.697 358.939 324.904 339.499 332.744 L 339.499 332.744 L 339.499 332.744 C 320.066 340.584 299.406 344.504 277.519 344.504 L 277.519 344.504 L 275.069 344.504 L 212.839 484.154 L 142.279 484.154 L 280.949 172.514 Z" transform="matrix(1, 0, 0, 1, 0, 0)" style="fill: rgb(255, 255, 255); fill-rule: nonzero; white-space: pre;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -1,138 +0,0 @@
<template>
<v-app>
<!-- Dummpy Comment -->
<TheAppBar />
<v-main>
<v-banner v-if="demo" sticky>
<div class="text-center">
<b> This is a Demo of the v0.5.0 (BETA) </b> | Username: changeme@email.com | Password: demo
</div>
</v-banner>
<GlobalSnackbar />
<v-snackbar v-model="snackWithButtons" bottom left timeout="-1">
{{ snackWithBtnText }}
<template v-slot:action="{ attrs }">
<v-btn text color="primary" v-bind="attrs" @click.stop="refreshApp">
{{ snackBtnText }}
</v-btn>
<v-btn icon class="ml-4" @click="snackWithButtons = false">
<v-icon>{{ $globals.icons.close }}</v-icon>
</v-btn>
</template>
</v-snackbar>
<router-view></router-view>
</v-main>
</v-app>
</template>
<script>
import TheAppBar from "@/components/UI/TheAppBar";
import GlobalSnackbar from "@/components/UI/GlobalSnackbar";
import Vuetify from "./plugins/vuetify";
import { user } from "@/mixins/user";
export default {
name: "App",
components: {
TheAppBar,
GlobalSnackbar,
},
mixins: [user],
computed: {
demo() {
const appInfo = this.$store.getters.getAppInfo;
return appInfo.demoStatus;
},
},
async created() {
// Initial API Requests
this.$store.dispatch("initTheme");
this.$store.dispatch("refreshToken");
this.$store.dispatch("requestUserData");
this.$store.dispatch("requestCurrentGroup");
this.$store.dispatch("requestTags");
this.$store.dispatch("requestAppInfo");
this.$store.dispatch("requestSiteSettings");
// Listen for swUpdated event and display refresh snackbar as required.
document.addEventListener("swUpdated", this.showRefreshUI, { once: true });
// Refresh all open app tabs when a new service worker is installed.
if (navigator.serviceWorker) {
navigator.serviceWorker.addEventListener("controllerchange", () => {
if (this.refreshing) return;
this.refreshing = true;
window.location.reload();
});
}
},
mounted() {
this.darkModeSystemCheck();
this.darkModeAddEventListener();
},
data() {
return {
refreshing: false,
registration: null,
snackBtnText: "",
snackWithBtnText: "",
snackWithButtons: false,
};
},
methods: {
// For Later!
/**
* Checks if 'system' is set for dark mode and then sets the corrisponding value for vuetify
*/
darkModeSystemCheck() {
if (this.$store.getters.getDarkMode === "system")
Vuetify.framework.theme.dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
},
/**
* This will monitor the OS level darkmode and call to update dark mode.
*/
darkModeAddEventListener() {
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
darkMediaQuery.addEventListener("change", () => {
this.darkModeSystemCheck();
});
},
showRefreshUI(e) {
// Display a snackbar inviting the user to refresh/reload the app due
// to an app update being available.
// The new service worker is installed, but not yet active.
// Store the ServiceWorkerRegistration instance for later use.
this.registration = e.detail;
this.snackBtnText = this.$t("events.refresh");
this.snackWithBtnText = this.$t("events.new-version");
this.snackWithButtons = true;
},
refreshApp() {
this.snackWithButtons = false;
// Protect against missing registration.waiting.
if (!this.registration || !this.registration.waiting) {
return;
}
this.registration.waiting.postMessage("skipWaiting");
},
},
};
</script>
<style>
.top-dialog {
align-self: flex-start;
}
:root {
scrollbar-color: transparent transparent;
}
</style>

View file

@ -1,80 +0,0 @@
import { apiReq } from "./api-utils";
import i18n from "@/i18n.js";
import { API_ROUTES } from "./apiRoutes";
export const aboutAPI = {
async getEvents() {
const resposne = await apiReq.get(API_ROUTES.aboutEvents);
return resposne.data;
},
async deleteEvent(id) {
const resposne = await apiReq.delete(API_ROUTES.aboutEventsId(id));
return resposne.data;
},
async deleteAllEvents() {
const resposne = await apiReq.delete(API_ROUTES.aboutEvents);
return resposne.data;
},
async allEventNotifications() {
const response = await apiReq.get(API_ROUTES.aboutEventsNotifications);
return response.data;
},
async createNotification(data) {
const response = await apiReq.post(API_ROUTES.aboutEventsNotifications, data);
return response.data;
},
async deleteNotification(id) {
const response = await apiReq.delete(API_ROUTES.aboutEventsNotificationsId(id));
return response.data;
},
async testNotificationByID(id) {
const response = await apiReq.post(
API_ROUTES.aboutEventsNotificationsTest,
{ id: id },
() => i18n.t("events.something-went-wrong"),
() => i18n.t("events.test-message-sent")
);
return response.data;
},
async testNotificationByURL(url) {
const response = await apiReq.post(
API_ROUTES.aboutEventsNotificationsTest,
{ test_url: url },
() => i18n.t("events.something-went-wrong"),
() => i18n.t("events.test-message-sent")
);
return response.data;
},
// async getAppInfo() {
// const response = await apiReq.get(aboutURLs.version);
// return response.data;
// },
// async getDebugInfo() {
// const response = await apiReq.get(aboutURLs.debug);
// return response.data;
// },
// async getLogText(num) {
// const response = await apiReq.get(aboutURLs.log(num));
// return response.data;
// },
// async getLastJson() {
// const response = await apiReq.get(aboutURLs.lastRecipe);
// return response.data;
// },
// async getIsDemo() {
// const response = await apiReq.get(aboutURLs.demo);
// return response.data;
// },
// async getStatistics() {
// const response = await apiReq.get(aboutURLs.statistics);
// return response.data;
// },
};

View file

@ -1,121 +0,0 @@
import { prefix } from "./apiRoutes";
import axios from "axios";
import { store } from "../store";
import { utils } from "@/utils";
axios.defaults.headers.common["Authorization"] = `Bearer ${store.getters.getToken}`;
function handleError(error, getText) {
if (getText) {
utils.notify.error(getText(error.response));
}
return false;
}
function handleResponse(response, getText) {
if (response && getText) {
const successText = getText(response);
utils.notify.success(successText);
}
return response;
}
function defaultErrorText(response) {
return response.statusText;
}
function defaultSuccessText(response) {
return response.statusText;
}
const requests = {
/**
*
* @param {*} funcCall Callable Axios Function
* @param {*} url Destination url
* @param {*} data Request Data
* @param {*} getErrorText Error Text Function
* @param {*} getSuccessText Success Text Function
* @returns Object response
*/
unsafe: async function(funcCall, url, data, getErrorText = defaultErrorText, getSuccessText) {
const response = await funcCall(url, data).catch(function(error) {
handleError(error, getErrorText);
});
return handleResponse(response, getSuccessText);
},
/**
*
* @param {*} funcCall Callable Axios Function
* @param {*} url Destination url
* @param {*} data Request Data
* @param {*} getErrorText Error Text Function
* @param {*} getSuccessText Success Text Function
* @returns Array [response, error]
*/
safe: async function(funcCall, url, data, getErrorText = defaultErrorText, getSuccessText) {
const response = await funcCall(url, data).catch(function(error) {
handleError(error, getErrorText);
return [null, error];
});
return [handleResponse(response, getSuccessText), null];
},
};
const apiReq = {
get: async function(url, getErrorText = defaultErrorText) {
return axios.get(url).catch(function(error) {
handleError(error, getErrorText);
});
},
getSafe: async function(url) {
let error = null;
const response = await axios.get(url).catch(e => {
error = e;
});
return [response, error];
},
post: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
return await requests.unsafe(axios.post, url, data, getErrorText, getSuccessText);
},
postSafe: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
return await requests.safe(axios.post, url, data, getErrorText, getSuccessText);
},
put: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
return await requests.unsafe(axios.put, url, data, getErrorText, getSuccessText);
},
putSafe: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
return await requests.safe(axios.put, url, data, getErrorText, getSuccessText);
},
patch: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
return await requests.unsafe(axios.patch, url, data, getErrorText, getSuccessText);
},
patchSafe: async function(url, data, getErrorText = defaultErrorText, getSuccessText) {
return await requests.safe(axios.patch, url, data, getErrorText, getSuccessText);
},
delete: async function(url, data, getErrorText = defaultErrorText, getSuccessText = defaultSuccessText) {
return await requests.unsafe(axios.delete, url, data, getErrorText, getSuccessText);
},
deleteSafe: async function(url, data, getErrorText = defaultErrorText, getSuccessText = defaultSuccessText) {
return await requests.unsafe(axios.delete, url, data, getErrorText, getSuccessText);
},
download: async function(url) {
const response = await this.get(url);
const token = response.data.fileToken;
const tokenURL = prefix + "/utils/download?token=" + token;
window.open(tokenURL, "_blank");
return response.data;
},
};
export { apiReq };

View file

@ -1,90 +0,0 @@
// This Content is Auto Generated
export const prefix = "/api";
export const API_ROUTES = {
aboutEvents: `${prefix}/about/events`,
aboutEventsNotifications: `${prefix}/about/events/notifications`,
aboutEventsNotificationsTest: `${prefix}/about/events/notifications/test`,
aboutRecipesDefaults: `${prefix}/about/recipes/defaults`,
authRefresh: `${prefix}/auth/refresh`,
authToken: `${prefix}/auth/token`,
authTokenLong: `${prefix}/auth/token/long`,
backupsAvailable: `${prefix}/backups/available`,
backupsExportDatabase: `${prefix}/backups/export/database`,
backupsUpload: `${prefix}/backups/upload`,
categories: `${prefix}/categories`,
categoriesEmpty: `${prefix}/categories/empty`,
debug: `${prefix}/debug`,
debugLastRecipeJson: `${prefix}/debug/last-recipe-json`,
debugLog: `${prefix}/debug/log`,
debugStatistics: `${prefix}/debug/statistics`,
debugVersion: `${prefix}/debug/version`,
groups: `${prefix}/groups`,
groupsSelf: `${prefix}/groups/self`,
mealPlansAll: `${prefix}/meal-plans/all`,
mealPlansCreate: `${prefix}/meal-plans/create`,
mealPlansThisWeek: `${prefix}/meal-plans/this-week`,
mealPlansToday: `${prefix}/meal-plans/today`,
mealPlansTodayImage: `${prefix}/meal-plans/today/image`,
migrations: `${prefix}/migrations`,
recipesCategory: `${prefix}/recipes/category`,
recipesCreate: `${prefix}/recipes/create`,
recipesCreateFromZip: `${prefix}/recipes/create-from-zip`,
recipesCreateUrl: `${prefix}/recipes/create-url`,
recipesSummary: `${prefix}/recipes/summary`,
recipesSummaryUncategorized: `${prefix}/recipes/summary/uncategorized`,
recipesSummaryUntagged: `${prefix}/recipes/summary/untagged`,
recipesTag: `${prefix}/recipes/tag`,
recipesTestScrapeUrl: `${prefix}/recipes/test-scrape-url`,
shoppingLists: `${prefix}/shopping-lists`,
siteSettings: `${prefix}/site-settings`,
siteSettingsCustomPages: `${prefix}/site-settings/custom-pages`,
siteSettingsWebhooksTest: `${prefix}/site-settings/webhooks/test`,
tags: `${prefix}/tags`,
tagsEmpty: `${prefix}/tags/empty`,
themes: `${prefix}/themes`,
themesCreate: `${prefix}/themes/create`,
users: `${prefix}/users`,
usersApiTokens: `${prefix}/users/api-tokens`,
usersSelf: `${prefix}/users/self`,
usersSignUps: `${prefix}/users/sign-ups`,
utilsDownload: `${prefix}/utils/download`,
aboutEventsId: id => `${prefix}/about/events/${id}`,
aboutEventsNotificationsId: id => `${prefix}/about/events/notifications/${id}`,
backupsFileNameDelete: file_name => `${prefix}/backups/${file_name}/delete`,
backupsFileNameDownload: file_name => `${prefix}/backups/${file_name}/download`,
backupsFileNameImport: file_name => `${prefix}/backups/${file_name}/import`,
categoriesCategory: category => `${prefix}/categories/${category}`,
debugLogNum: num => `${prefix}/debug/log/${num}`,
groupsId: id => `${prefix}/groups/${id}`,
mealPlansId: id => `${prefix}/meal-plans/${id}`,
mealPlansIdShoppingList: id => `${prefix}/meal-plans/${id}/shopping-list`,
mealPlansPlanId: plan_id => `${prefix}/meal-plans/${plan_id}`,
mediaRecipesRecipeSlugAssetsFileName: (recipe_slug, file_name) =>
`${prefix}/media/recipes/${recipe_slug}/assets/${file_name}`,
mediaRecipesRecipeSlugImagesFileName: (recipe_slug, file_name) =>
`${prefix}/media/recipes/${recipe_slug}/images/${file_name}`,
migrationsImportTypeFileNameDelete: (import_type, file_name) =>
`${prefix}/migrations/${import_type}/${file_name}/delete`,
migrationsImportTypeFileNameImport: (import_type, file_name) =>
`${prefix}/migrations/${import_type}/${file_name}/import`,
migrationsImportTypeUpload: import_type => `${prefix}/migrations/${import_type}/upload`,
recipesRecipeSlug: recipe_slug => `${prefix}/recipes/${recipe_slug}`,
recipesRecipeSlugAssets: recipe_slug => `${prefix}/recipes/${recipe_slug}/assets`,
recipesRecipeSlugImage: recipe_slug => `${prefix}/recipes/${recipe_slug}/image`,
recipesRecipeSlugZip: recipe_slug => `${prefix}/recipes/${recipe_slug}/zip`,
recipesSlugComments: slug => `${prefix}/recipes/${slug}/comments`,
recipesSlugCommentsId: (slug, id) => `${prefix}/recipes/${slug}/comments/${id}`,
shoppingListsId: id => `${prefix}/shopping-lists/${id}`,
siteSettingsCustomPagesId: id => `${prefix}/site-settings/custom-pages/${id}`,
tagsTag: tag => `${prefix}/tags/${tag}`,
themesId: id => `${prefix}/themes/${id}`,
usersApiTokensTokenId: token_id => `${prefix}/users/api-tokens/${token_id}`,
usersId: id => `${prefix}/users/${id}`,
usersIdFavorites: id => `${prefix}/users/${id}/favorites`,
usersIdFavoritesSlug: (id, slug) => `${prefix}/users/${id}/favorites/${slug}`,
usersIdImage: id => `${prefix}/users/${id}/image`,
usersIdPassword: id => `${prefix}/users/${id}/password`,
usersIdResetPassword: id => `${prefix}/users/${id}/reset-password`,
usersSignUpsToken: token => `${prefix}/users/sign-ups/${token}`,
};

View file

@ -1,62 +0,0 @@
import { apiReq } from "./api-utils";
import { store } from "@/store";
import i18n from "@/i18n.js";
import { API_ROUTES } from "./apiRoutes";
export const backupAPI = {
/**
* Request all backups available on the server
* @returns {Array} List of Available Backups
*/
async requestAvailable() {
let response = await apiReq.get(API_ROUTES.backupsAvailable);
return response.data;
},
/**
* Calls for importing a file on the server
* @param {string} fileName
* @param {object} data
* @returns A report containing status of imported items
*/
async import(fileName, data) {
let response = await apiReq.post(API_ROUTES.backupsFileNameImport(fileName), data);
store.dispatch("requestRecentRecipes");
return response;
},
/**
* Removes a file from the server
* @param {string} fileName
*/
async delete(fileName) {
return apiReq.delete(
API_ROUTES.backupsFileNameDelete(fileName),
null,
() => i18n.t("settings.backup.unable-to-delete-backup"),
() => i18n.t("settings.backup.backup-deleted")
);
},
/**
* Creates a backup on the serve given a set of options
* @param {object} data
* @returns
*/
async create(options) {
return apiReq.post(
API_ROUTES.backupsExportDatabase,
options,
() => i18n.t("settings.backup.error-creating-backup-see-log-file"),
response => {
return i18n.t("settings.backup.backup-created-at-response-export_path", { path: response.data.export_path });
}
);
},
/**
* Downloads a file from the server. I don't actually think this is used?
* @param {string} fileName
* @returns Download URL
*/
async download(fileName) {
const url = API_ROUTES.backupsFileNameDownload(fileName);
apiReq.download(url);
},
};

View file

@ -1,111 +0,0 @@
import { apiReq } from "./api-utils";
import { store } from "@/store";
import i18n from "@/i18n.js";
import { API_ROUTES } from "./apiRoutes";
export const categoryAPI = {
async getAll() {
let response = await apiReq.get(API_ROUTES.categories);
return response.data;
},
async getEmpty() {
let response = await apiReq.get(API_ROUTES.categoriesEmpty);
return response.data;
},
async create(name) {
const response = await apiReq.post(
API_ROUTES.categories,
{ name: name },
() => i18n.t("category.category-creation-failed"),
() => i18n.t("category.category-created")
);
if (response) {
store.dispatch("requestCategories");
return response.data;
}
},
async getRecipesInCategory(category) {
let response = await apiReq.get(API_ROUTES.categoriesCategory(category));
return response.data;
},
async update(name, newName, overrideRequest = false) {
const response = await apiReq.put(
API_ROUTES.categoriesCategory(name),
{ name: newName },
() => i18n.t("category.category-update-failed"),
() => i18n.t("category.category-updated")
);
if (response && !overrideRequest) {
store.dispatch("requestCategories");
return response.data;
}
},
async delete(category, overrideRequest = false) {
const response = await apiReq.delete(
API_ROUTES.categoriesCategory(category),
null,
() => i18n.t("category.category-deletion-failed"),
() => i18n.t("category.category-deleted")
);
if (response && !overrideRequest) {
store.dispatch("requestCategories");
}
return response;
},
};
export const tagAPI = {
async getAll() {
let response = await apiReq.get(API_ROUTES.tags);
return response.data;
},
async getEmpty() {
let response = await apiReq.get(API_ROUTES.tagsEmpty);
return response.data;
},
async create(name) {
const response = await apiReq.post(
API_ROUTES.tags,
{ name: name },
() => i18n.t("tag.tag-creation-failed"),
() => i18n.t("tag.tag-created")
);
if (response) {
store.dispatch("requestTags");
return response.data;
}
},
async getRecipesInTag(tag) {
let response = await apiReq.get(API_ROUTES.tagsTag(tag));
return response.data;
},
async update(name, newName, overrideRequest = false) {
const response = await apiReq.put(
API_ROUTES.tagsTag(name),
{ name: newName },
() => i18n.t("tag.tag-update-failed"),
() => i18n.t("tag.tag-updated")
);
if (response) {
if (!overrideRequest) {
store.dispatch("requestTags");
}
return response.data;
}
},
async delete(tag, overrideRequest = false) {
const response = await apiReq.delete(
API_ROUTES.tagsTag(tag),
null,
() => i18n.t("tag.tag-deletion-failed"),
() => i18n.t("tag.tag-deleted")
);
if (response) {
if (!overrideRequest) {
store.dispatch("requestTags");
}
return response.data;
}
},
};

View file

@ -1,53 +0,0 @@
import { apiReq } from "./api-utils";
import i18n from "@/i18n.js";
import { API_ROUTES } from "./apiRoutes";
function deleteErrorText(response) {
switch (response.data.detail) {
case "GROUP_WITH_USERS":
return i18n.t("group.cannot-delete-group-with-users");
case "GROUP_NOT_FOUND":
return i18n.t("group.group-not-found");
case "DEFAULT_GROUP":
return i18n.t("group.cannot-delete-default-group");
default:
return i18n.t("group.group-deletion-failed");
}
}
export const groupAPI = {
async allGroups() {
let response = await apiReq.get(API_ROUTES.groups);
return response.data;
},
create(name) {
return apiReq.post(
API_ROUTES.groups,
{ name: name },
() => i18n.t("group.user-group-creation-failed"),
() => i18n.t("group.user-group-created")
);
},
delete(id) {
return apiReq.delete(API_ROUTES.groupsId(id), null, deleteErrorText, function() {
return i18n.t("group.group-deleted");
});
},
async current() {
const response = await apiReq.get(API_ROUTES.groupsSelf, null, null);
if (response) {
return response.data;
}
},
update(data) {
return apiReq.put(
API_ROUTES.groupsId(data.id),
data,
() => i18n.t("group.error-updating-group"),
() => i18n.t("settings.group-settings-updated")
);
},
};

View file

@ -1,37 +0,0 @@
import { backupAPI } from "./backup";
import { recipeAPI } from "./recipe";
import { mealplanAPI } from "./mealplan";
import { settingsAPI } from "./settings";
import { themeAPI } from "./themes";
import { migrationAPI } from "./migration";
import { utilsAPI } from "./upload";
import { categoryAPI, tagAPI } from "./category";
import { metaAPI } from "./meta";
import { userAPI } from "./users";
import { signupAPI } from "./signUps";
import { groupAPI } from "./groups";
import { siteSettingsAPI } from "./siteSettings";
import { aboutAPI } from "./about";
import { shoppingListsAPI } from "./shoppingLists";
/**
* The main object namespace for interacting with the backend database
*/
export const api = {
recipes: recipeAPI,
siteSettings: siteSettingsAPI,
backups: backupAPI,
mealPlans: mealplanAPI,
settings: settingsAPI,
themes: themeAPI,
migrations: migrationAPI,
utils: utilsAPI,
categories: categoryAPI,
tags: tagAPI,
meta: metaAPI,
users: userAPI,
signUps: signupAPI,
groups: groupAPI,
about: aboutAPI,
shoppingLists: shoppingListsAPI,
};

View file

@ -1,57 +0,0 @@
import { apiReq } from "./api-utils";
import i18n from "@/i18n.js";
import { API_ROUTES } from "./apiRoutes";
export const mealplanAPI = {
create(postBody) {
return apiReq.post(
API_ROUTES.mealPlansCreate,
postBody,
() => i18n.t("meal-plan.mealplan-creation-failed"),
() => i18n.t("meal-plan.mealplan-created")
);
},
async all() {
let response = await apiReq.get(API_ROUTES.mealPlansAll);
return response;
},
async thisWeek() {
let response = await apiReq.get(API_ROUTES.mealPlansThisWeek);
return response.data;
},
async today() {
let response = await apiReq.get(API_ROUTES.mealPlansToday);
return response;
},
async getById(id) {
let response = await apiReq.get(API_ROUTES.mealPlansId(id));
return response.data;
},
delete(id) {
return apiReq.delete(
API_ROUTES.mealPlansId(id),
null,
() => i18n.t("meal-plan.mealplan-deletion-failed"),
() => i18n.t("meal-plan.mealplan-deleted")
);
},
update(id, body) {
return apiReq.put(
API_ROUTES.mealPlansId(id),
body,
() => i18n.t("meal-plan.mealplan-update-failed"),
() => i18n.t("meal-plan.mealplan-updated")
);
},
async shoppingList(id) {
let response = await apiReq.get(API_ROUTES.mealPlansIdShoppingList(id));
return response.data;
},
};

View file

@ -1,29 +0,0 @@
import { apiReq } from "./api-utils";
import { API_ROUTES } from "./apiRoutes";
export const metaAPI = {
async getAppInfo() {
const response = await apiReq.get(API_ROUTES.debugVersion);
return response.data;
},
async getDebugInfo() {
const response = await apiReq.get(API_ROUTES.debug);
return response.data;
},
async getLogText(num) {
const response = await apiReq.get(API_ROUTES.debugLogNum(num));
return response.data;
},
async getLastJson() {
const response = await apiReq.get(API_ROUTES.debugLastRecipeJson);
return response.data;
},
async getStatistics() {
const response = await apiReq.get(API_ROUTES.debugStatistics);
return response.data;
},
};

View file

@ -1,25 +0,0 @@
import { apiReq } from "./api-utils";
import { store } from "../store";
import i18n from "@/i18n.js";
import { API_ROUTES } from "./apiRoutes";
export const migrationAPI = {
async getMigrations() {
let response = await apiReq.get(API_ROUTES.migrations);
return response.data;
},
async delete(folder, file) {
const response = await apiReq.delete(
API_ROUTES.migrationsImportTypeFileNameDelete(folder, file),
null,
() => i18n.t("general.file-folder-not-found"),
() => i18n.t("migration.migration-data-removed")
);
return response;
},
async import(folder, file) {
let response = await apiReq.post(API_ROUTES.migrationsImportTypeFileNameImport(folder, file));
store.dispatch("requestRecentRecipes");
return response.data;
},
};

View file

@ -1,181 +0,0 @@
import { API_ROUTES } from "./apiRoutes";
import { apiReq } from "./api-utils";
import { store } from "../store";
import i18n from "@/i18n.js";
export const recipeAPI = {
/**
* Returns the Default Recipe Settings for the Site
* @returns {AxoisResponse} Axois Response Object
*/
async getDefaultSettings() {
const response = await apiReq.get(API_ROUTES.aboutRecipesDefaults);
return response;
},
/**
* Create a Recipe by URL
* @param {string} recipeURL
* @returns {string} Recipe Slug
*/
async createByURL(recipeURL) {
const response = await apiReq.post(API_ROUTES.recipesCreateUrl, { url: recipeURL }, false, () =>
i18n.t("recipe.recipe-created")
);
store.dispatch("requestRecentRecipes");
return response;
},
async getAllByCategory(categories) {
let response = await apiReq.post(API_ROUTES.recipesCategory, categories);
return response.data;
},
async create(recipeData) {
const response = await apiReq.post(
API_ROUTES.recipesCreate,
recipeData,
() => i18n.t("recipe.recipe-creation-failed"),
() => i18n.t("recipe.recipe-created")
);
store.dispatch("requestRecentRecipes");
return response.data;
},
async requestDetails(recipeSlug) {
const response = await apiReq.getSafe(API_ROUTES.recipesRecipeSlug(recipeSlug));
return response;
},
updateImage(recipeSlug, fileObject, overrideSuccessMsg = false) {
const formData = new FormData();
formData.append("image", fileObject);
formData.append("extension", fileObject.name.split(".").pop());
let successMessage = null;
if (!overrideSuccessMsg) {
successMessage = function() {
return overrideSuccessMsg ? null : i18n.t("recipe.recipe-image-updated");
};
}
return apiReq.put(
API_ROUTES.recipesRecipeSlugImage(recipeSlug),
formData,
() => i18n.t("general.image-upload-failed"),
successMessage
);
},
async createAsset(recipeSlug, fileObject, name, icon) {
const fd = new FormData();
fd.append("file", fileObject);
fd.append("extension", fileObject.name.split(".").pop());
fd.append("name", name);
fd.append("icon", icon);
const response = apiReq.post(API_ROUTES.recipesRecipeSlugAssets(recipeSlug), fd);
return response;
},
updateImagebyURL(slug, url) {
return apiReq.post(
API_ROUTES.recipesRecipeSlugImage(slug),
{ url: url },
() => i18n.t("general.image-upload-failed"),
() => i18n.t("recipe.recipe-image-updated")
);
},
async update(data) {
let response = await apiReq.put(
API_ROUTES.recipesRecipeSlug(data.slug),
data,
() => i18n.t("recipe.recipe-update-failed"),
() => i18n.t("recipe.recipe-updated")
);
if (response) {
store.dispatch("patchRecipe", response.data);
return response.data.slug; // ! Temporary until I rewrite to refresh page without additional request
}
},
async patch(data) {
let response = await apiReq.patch(API_ROUTES.recipesRecipeSlug(data.slug), data);
store.dispatch("patchRecipe", response.data);
return response.data;
},
async delete(recipeSlug) {
const response = await apiReq.delete(
API_ROUTES.recipesRecipeSlug(recipeSlug),
null,
() => i18n.t("recipe.unable-to-delete-recipe"),
() => i18n.t("recipe.recipe-deleted")
);
store.dispatch("dropRecipe", response.data);
return response;
},
async allSummary(start = 0, limit = 9999) {
const response = await apiReq.get(API_ROUTES.recipesSummary, {
params: { start: start, limit: limit },
});
return response.data;
},
async allUntagged() {
const response = await apiReq.get(API_ROUTES.recipesSummaryUntagged);
return response.data;
},
async allUnategorized() {
const response = await apiReq.get(API_ROUTES.recipesSummaryUncategorized);
return response.data;
},
recipeImage(recipeSlug, version = null, key = null) {
return `/api/media/recipes/${recipeSlug}/images/original.webp?&rnd=${key}&version=${version}`;
},
recipeSmallImage(recipeSlug, version = null, key = null) {
return `/api/media/recipes/${recipeSlug}/images/min-original.webp?&rnd=${key}&version=${version}`;
},
recipeTinyImage(recipeSlug, version = null, key = null) {
return `/api/media/recipes/${recipeSlug}/images/tiny-original.webp?&rnd=${key}&version=${version}`;
},
recipeAssetPath(recipeSlug, assetName) {
return `/api/media/recipes/${recipeSlug}/assets/${assetName}`;
},
/** Create comment in the Database
* @param slug
*/
async createComment(slug, data) {
const response = await apiReq.post(API_ROUTES.recipesSlugComments(slug), data);
return response.data;
},
/** Update comment in the Database
* @param slug
* @param id
*/
async updateComment(slug, id, data) {
const response = await apiReq.put(API_ROUTES.recipesSlugCommentsId(slug, id), data);
return response.data;
},
/** Delete comment from the Database
* @param slug
* @param id
*/
async deleteComment(slug, id) {
const response = await apiReq.delete(API_ROUTES.recipesSlugCommentsId(slug, id));
return response.data;
},
async testScrapeURL(url) {
const response = await apiReq.post(API_ROUTES.recipesTestScrapeUrl, { url: url });
return response.data;
},
};

View file

@ -1,19 +0,0 @@
import { apiReq } from "./api-utils";
import { API_ROUTES } from "./apiRoutes";
export const settingsAPI = {
async requestAll() {
let response = await apiReq.get(API_ROUTES.siteSettings);
return response.data;
},
async testWebhooks() {
let response = await apiReq.post(API_ROUTES.siteSettingsWebhooksTest);
return response.data;
},
async update(body) {
let response = await apiReq.put(API_ROUTES.siteSettings, body);
return response.data;
},
};

View file

@ -1,33 +0,0 @@
// This Content is Auto Generated
import { API_ROUTES } from "./apiRoutes";
import { apiReq } from "./api-utils";
export const shoppingListsAPI = {
/** Create Shopping List in the Database
*/
async createShoppingList(data) {
const response = await apiReq.post(API_ROUTES.shoppingLists, data);
return response.data;
},
/** Get Shopping List from the Database
* @param id
*/
async getShoppingList(id) {
const response = await apiReq.get(API_ROUTES.shoppingListsId(id));
return response.data;
},
/** Update Shopping List in the Database
* @param id
*/
async updateShoppingList(id, data) {
const response = await apiReq.put(API_ROUTES.shoppingListsId(id), data);
return response.data;
},
/** Delete Shopping List from the Database
* @param id
*/
async deleteShoppingList(id) {
const response = await apiReq.delete(API_ROUTES.shoppingListsId(id));
return response.data;
},
};

View file

@ -1,35 +0,0 @@
import { apiReq } from "./api-utils";
import i18n from "@/i18n.js";
import { API_ROUTES } from "./apiRoutes";
export const signupAPI = {
async getAll() {
let response = await apiReq.get(API_ROUTES.usersSignUps);
return response.data;
},
async createToken(data) {
let response = await apiReq.post(
API_ROUTES.usersSignUps,
data,
() => i18n.t("signup.sign-up-link-creation-failed"),
() => i18n.t("signup.sign-up-link-created")
);
return response.data;
},
async deleteToken(token) {
return await apiReq.delete(
API_ROUTES.usersSignUpsToken(token),
null,
() => i18n.t("signup.sign-up-token-deletion-failed"),
() => i18n.t("signup.sign-up-token-deleted")
);
},
async createUser(token, data) {
return apiReq.post(
API_ROUTES.usersSignUpsToken(token),
data,
() => i18n.t("user.you-are-not-allowed-to-create-a-user"),
() => i18n.t("user.user-created")
);
},
};

View file

@ -1,71 +0,0 @@
import { apiReq } from "./api-utils";
import { store } from "@/store";
import i18n from "@/i18n.js";
import { API_ROUTES } from "./apiRoutes";
export const siteSettingsAPI = {
async get() {
let response = await apiReq.get(API_ROUTES.siteSettings);
return response.data;
},
async update(body) {
const response = await apiReq.put(
API_ROUTES.siteSettings,
body,
() => i18n.t("settings.settings-update-failed"),
() => i18n.t("settings.settings-updated")
);
if (response) {
store.dispatch("requestSiteSettings");
}
return response;
},
async getPages() {
let response = await apiReq.get(API_ROUTES.siteSettingsCustomPages);
return response.data;
},
async getPage(id) {
let response = await apiReq.get(API_ROUTES.siteSettingsCustomPagesId(id));
return response.data;
},
createPage(body) {
return apiReq.post(
API_ROUTES.siteSettingsCustomPages,
body,
() => i18n.t("page.page-creation-failed"),
() => i18n.t("page.new-page-created")
);
},
async deletePage(id) {
return await apiReq.delete(
API_ROUTES.siteSettingsCustomPagesId(id),
null,
() => i18n.t("page.page-deletion-failed"),
() => i18n.t("page.page-deleted")
);
},
updatePage(body) {
return apiReq.put(
API_ROUTES.siteSettingsCustomPagesId(body.id),
body,
() => i18n.t("page.page-update-failed"),
() => i18n.t("page.page-updated")
);
},
async updateAllPages(allPages) {
let response = await apiReq.put(
API_ROUTES.siteSettingsCustomPages,
allPages,
() => i18n.t("page.pages-update-failed"),
() => i18n.t("page.pages-updated")
);
return response;
},
};

View file

@ -1,42 +0,0 @@
import { apiReq } from "./api-utils";
import i18n from "@/i18n.js";
import { API_ROUTES } from "./apiRoutes";
export const themeAPI = {
async requestAll() {
let response = await apiReq.get(API_ROUTES.themes);
return response.data;
},
async requestByName(name) {
let response = await apiReq.get(API_ROUTES.themesId(name));
return response.data;
},
async create(postBody) {
return await apiReq.post(
API_ROUTES.themesCreate,
postBody,
() => i18n.t("settings.theme.error-creating-theme-see-log-file"),
() => i18n.t("settings.theme.theme-saved")
);
},
update(data) {
return apiReq.put(
API_ROUTES.themesId(data.id),
data,
() => i18n.t("settings.theme.error-updating-theme"),
() => i18n.t("settings.theme.theme-updated")
);
},
delete(id) {
return apiReq.delete(
API_ROUTES.themesId(id),
null,
() => i18n.t("settings.theme.error-deleting-theme"),
() => i18n.t("settings.theme.theme-deleted")
);
},
};

View file

@ -1,14 +0,0 @@
import { apiReq } from "./api-utils";
import i18n from "@/i18n.js";
export const utilsAPI = {
// import { api } from "@/api";
uploadFile(url, fileObject) {
return apiReq.post(
url,
fileObject,
() => i18n.t("general.failure-uploading-file"),
() => i18n.t("general.file-uploaded")
);
},
};

View file

@ -1,107 +0,0 @@
import { API_ROUTES } from "./apiRoutes";
import { apiReq } from "./api-utils";
import i18n from "@/i18n.js";
export const userAPI = {
async login(formData) {
let response = await apiReq.post(API_ROUTES.authToken, formData, null, () => {
return i18n.t("user.user-successfully-logged-in");
});
return response;
},
async refresh() {
return apiReq.getSafe(API_ROUTES.authRefresh);
},
async allUsers() {
let response = await apiReq.get(API_ROUTES.users);
return response.data;
},
create(user) {
return apiReq.post(
API_ROUTES.users,
user,
() => i18n.t("user.user-creation-failed"),
() => i18n.t("user.user-created")
);
},
async self() {
return apiReq.getSafe(API_ROUTES.usersSelf);
},
async byID(id) {
let response = await apiReq.get(API_ROUTES.usersId(id));
return response.data;
},
update(user) {
return apiReq.put(
API_ROUTES.usersId(user.id),
user,
() => i18n.t("user.user-update-failed"),
() => i18n.t("user.user-updated")
);
},
changePassword(id, password) {
return apiReq.put(
API_ROUTES.usersIdPassword(id),
password,
() => i18n.t("user.existing-password-does-not-match"),
() => i18n.t("user.password-updated")
);
},
delete(id) {
return apiReq.delete(API_ROUTES.usersId(id), null, deleteErrorText, () => {
return i18n.t("user.user-deleted");
});
},
resetPassword(id) {
return apiReq.put(
API_ROUTES.usersIdResetPassword(id),
null,
() => i18n.t("user.password-reset-failed"),
() => i18n.t("user.password-has-been-reset-to-the-default-password")
);
},
async createAPIToken(name) {
const response = await apiReq.post(API_ROUTES.usersApiTokens, { name });
return response.data;
},
async deleteAPIToken(id) {
const response = await apiReq.delete(API_ROUTES.usersApiTokensTokenId(id));
return response.data;
},
/** Adds a Recipe to the users favorites
* @param id
*/
async getFavorites(id) {
const response = await apiReq.get(API_ROUTES.usersIdFavorites(id));
return response.data;
},
/** Adds a Recipe to the users favorites
* @param id
*/
async addFavorite(id, slug) {
const response = await apiReq.post(API_ROUTES.usersIdFavoritesSlug(id, slug));
return response.data;
},
/** Adds a Recipe to the users favorites
* @param id
*/
async removeFavorite(id, slug) {
const response = await apiReq.delete(API_ROUTES.usersIdFavoritesSlug(id, slug));
return response.data;
},
userProfileImage(id) {
if (!id || id === undefined) return;
return `/api/users/${id}/image`;
},
};
const deleteErrorText = response => {
switch (response.data.detail) {
case "SUPER_USER":
return i18n.t("user.error-cannot-delete-super-user");
default:
return i18n.t("user.you-are-not-allowed-to-delete-this-user");
}
};

View file

@ -1,17 +0,0 @@
<template>
<div>
<The404>
<h1 class="mx-auto">{{ $t('general.no-recipe-found') }}</h1>
</The404>
</div>
</template>
<script>
import The404 from "./The404.vue";
export default {
components: { The404 },
};
</script>

View file

@ -1,51 +0,0 @@
<template>
<div>
<v-card-title>
<slot>
<h1 class="mx-auto">{{ $t('page.404-page-not-found') }}</h1>
</slot>
</v-card-title>
<div class="d-flex justify-space-around">
<div class="d-flex">
<p>4</p>
<v-icon color="primary" class="mx-auto" size="200">
{{ $globals.icons.primary }}
</v-icon>
<p>4</p>
</div>
</div>
<v-card-actions>
<v-spacer></v-spacer>
<slot name="actions">
<v-btn v-for="(button, index) in buttons" :key="index" :to="button.to" color="primary">
<v-icon left> {{ button.icon }} </v-icon>
{{ button.text }}
</v-btn>
</slot>
<v-spacer></v-spacer>
</v-card-actions>
</div>
</template>
<script>
export default {
computed: {
buttons() {
return[
{ icon: this.$globals.icons.home, to: "/", text: this.$t('general.home') },
{ icon: this.$globals.icons.primary, to: "/recipes/all", text: this.$t('page.all-recipes') },
{ icon: this.$globals.icons.search, to: "/search", text: this.$t('search.search') },
];
},
}
};
</script>
<style scoped>
p {
padding-bottom: 0 !important;
margin-bottom: 0 !important;
color: var(--v-primary-base);
font-size: 200px;
}
</style>

View file

@ -1,134 +0,0 @@
<template>
<v-autocomplete
:items="activeItems"
v-model="selected"
:value="value"
:label="inputLabel"
chips
deletable-chips
:dense="dense"
item-text="name"
persistent-hint
multiple
:hint="hint"
:solo="solo"
:return-object="returnObject"
:flat="flat"
@input="emitChange"
>
<template v-slot:selection="data">
<v-chip
class="ma-1"
:input-value="data.selected"
close
@click:close="removeByIndex(data.index)"
label
color="accent"
dark
:key="data.index"
>
{{ data.item.name || data.item }}
</v-chip>
</template>
<template v-slot:append-outer="">
<NewCategoryTagDialog v-if="showAdd" :tag-dialog="tagSelector" @created-item="pushToItem" />
</template>
</v-autocomplete>
</template>
<script>
import NewCategoryTagDialog from "@/components/UI/Dialogs/NewCategoryTagDialog";
const MOUNTED_EVENT = "mounted";
export default {
components: {
NewCategoryTagDialog,
},
props: {
value: Array,
solo: {
default: false,
},
dense: {
default: true,
},
returnObject: {
default: true,
},
tagSelector: {
default: false,
},
hint: {
default: null,
},
showAdd: {
default: false,
},
showLabel: {
default: true,
},
},
data() {
return {
selected: [],
};
},
async created() {
if (this.tagSelector) {
this.$store.dispatch("requestTags");
} else {
this.$store.dispatch("requestCategories");
}
},
mounted() {
this.$emit(MOUNTED_EVENT);
this.setInit(this.value);
},
watch: {
value(val) {
this.selected = val;
},
},
computed: {
inputLabel() {
if (!this.showLabel) return null;
return this.tagSelector ? this.$t("tag.tags") : this.$t("recipe.categories");
},
activeItems() {
let ItemObjects = [];
if (this.tagSelector) ItemObjects = this.$store.getters.getAllTags;
else {
ItemObjects = this.$store.getters.getAllCategories;
}
if (this.returnObject) return ItemObjects;
else {
return ItemObjects.map(x => x.name);
}
},
flat() {
if (this.selected) {
return this.selected.length > 0 && this.solo;
}
return false;
},
},
methods: {
emitChange() {
this.$emit("input", this.selected);
},
setInit(val) {
this.selected = val;
},
removeByIndex(index) {
this.selected.splice(index, 1);
},
pushToItem(createdItem) {
createdItem = this.returnObject ? createdItem : createdItem.name;
this.selected.push(createdItem);
},
},
};
</script>
<style lang="scss" scoped></style>

View file

@ -1,65 +0,0 @@
<template>
<div>
<div class="text-center">
<h3>{{ buttonText }}</h3>
</div>
<v-text-field v-model="color" hide-details class="ma-0 pa-0" solo >
<template v-slot:append>
<v-menu v-model="menu" top nudge-bottom="105" nudge-left="16" :close-on-content-click="false">
<template v-slot:activator="{ on }">
<div :style="swatchStyle" v-on="on" swatches-max-height="300" />
</template>
<v-card>
<v-card-text class="pa-0">
<v-color-picker v-model="color" flat mode="hexa" show-swatches />
</v-card-text>
</v-card>
</v-menu>
</template>
</v-text-field>
</div>
</template>
<script>
export default {
props: {
buttonText: String,
value: String,
},
data() {
return {
dialog: false,
swatches: false,
color: this.value || "#1976D2",
mask: "!#XXXXXXXX",
menu: false,
};
},
computed: {
swatchStyle() {
const { value, menu } = this;
return {
backgroundColor: value,
cursor: "pointer",
height: "30px",
width: "30px",
borderRadius: menu ? "50%" : "4px",
transition: "border-radius 200ms ease-in-out",
};
},
},
watch: {
color() {
this.updateColor();
},
},
methods: {
updateColor() {
this.$emit("input", this.color);
},
},
};
</script>
<style></style>

View file

@ -1,27 +0,0 @@
<template>
<v-date-picker :first-day-of-week="firstDayOfWeek" v-on="$listeners"></v-date-picker>
</template>
<script>
import { api } from "@/api";
export default {
data() {
return {
firstDayOfWeek: 0,
};
},
mounted() {
this.getOptions();
},
methods: {
async getOptions() {
const settings = await api.siteSettings.get();
this.firstDayOfWeek = settings.firstDayOfWeek;
},
},
};
</script>
<style></style>

View file

@ -1,69 +0,0 @@
<template>
<div>
<v-checkbox
v-for="(option, index) in options"
:key="index"
class="mb-n4 mt-n3"
dense
:label="option.text"
v-model="option.value"
@change="emitValue()"
></v-checkbox>
</div>
</template>
<script>
const UPDATE_EVENT = "update-options";
export default {
data() {
return {
options: {
recipes: {
value: true,
text: this.$t("general.recipes"),
},
settings: {
value: true,
text: this.$t("general.settings"),
},
pages: {
value: true,
text: this.$t("settings.pages"),
},
themes: {
value: true,
text: this.$t("general.themes"),
},
users: {
value: true,
text: this.$t("user.users"),
},
groups: {
value: true,
text: this.$t("group.groups"),
},
notifications: {
value: true,
text: this.$t("events.notification"),
},
},
};
},
mounted() {
this.emitValue();
},
methods: {
emitValue() {
this.$emit(UPDATE_EVENT, {
recipes: this.options.recipes.value,
settings: this.options.settings.value,
themes: this.options.themes.value,
pages: this.options.pages.value,
users: this.options.users.value,
groups: this.options.groups.value,
notifications: this.options.notifications.value,
});
},
},
};
</script>

View file

@ -1,48 +0,0 @@
<template>
<v-select
dense
:items="allLanguages"
item-text="name"
:label="$t('settings.language')"
:prepend-icon="$globals.icons.translate"
:value="selectedItem"
@input="setLanguage"
>
</v-select>
</template>
<script>
const SELECT_EVENT = "select-lang";
export default {
props: {
siteSettings: {
default: false,
},
},
data: function() {
return {
selectedItem: 0,
items: [
{
name: "English",
value: "en-US",
},
],
};
},
created() {
this.selectedItem = this.$store.getters.getActiveLang;
},
computed: {
allLanguages() {
return this.$store.getters.getAllLangs;
},
},
methods: {
setLanguage(selectedLanguage) {
this.$emit(SELECT_EVENT, selectedLanguage);
},
},
};
</script>

View file

@ -1,42 +0,0 @@
<template>
<v-dialog ref="dialog" v-model="modal2" :return-value.sync="time" persistent width="290px">
<template v-slot:activator="{ on, attrs }">
<v-text-field
v-model="time"
:label="$t('settings.set-new-time')"
:prepend-icon="$globals.icons.clockOutline"
readonly
v-bind="attrs"
v-on="on"
></v-text-field>
</template>
<v-time-picker v-if="modal2" v-model="time" full-width>
<v-spacer></v-spacer>
<v-btn text color="primary" @click="modal2 = false"> {{ $t("general.cancel") }} </v-btn>
<v-btn text color="primary" @click="saveTime"> {{ $t("general.ok") }} </v-btn>
</v-time-picker>
</v-dialog>
</template>
<script>
export default {
data() {
return {
time: null,
modal2: false,
};
},
methods: {
saveTime() {
this.$refs.dialog.save(this.time);
this.$emit("save-time", this.time);
},
},
};
</script>
<style scoped>
.v-text-field {
max-width: 300px;
}
</style>

View file

@ -1,46 +0,0 @@
<template>
<div>
<v-data-table
dense
:headers="dataHeaders"
:items="dataSet"
item-key="name"
class="elevation-1 mt-2"
show-expand
:expanded.sync="expanded"
:footer-props="{
'items-per-page-options': [100, 200, 300, 400, -1],
}"
:items-per-page="100"
>
<template v-slot:item.status="{ item }">
<div :class="item.status ? 'success--text' : 'error--text'">
{{ item.status ? "Imported" : "Failed" }}
</div>
</template>
<template v-slot:expanded-item="{ headers, item }">
<td :colspan="headers.length">
<div class="ma-2">
{{ item.exception }}
</div>
</td>
</template>
</v-data-table>
</div>
</template>
<script>
export default {
props: {
dataSet: Array,
dataHeaders: Array,
},
data: () => ({
singleExpand: false,
expanded: [],
}),
};
</script>
<style></style>

View file

@ -1,142 +0,0 @@
<template>
<div class="text-center">
<v-dialog v-model="dialog" width="70%">
<v-card>
<v-app-bar dark color="primary mb-2">
<v-icon large left>
{{ $globals.icons.import }}
</v-icon>
<v-toolbar-title class="headline">
{{ $t("settings.backup.import-summary") }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-card-text class="mb-n4">
<v-row>
<div v-for="values in allNumbers" :key="values.title">
<v-card-text>
<div>
<h3>{{ values.title }}</h3>
</div>
<div class="success--text">{{ $t("general.success-count", { count: values.success }) }}</div>
<div class="error--text">{{ $t("general.failed-count", { count: values.failure }) }}</div>
</v-card-text>
</div>
</v-row>
</v-card-text>
<v-tabs v-model="tab" show-arrows="">
<v-tab>{{ $t("general.recipes") }}</v-tab>
<v-tab>{{ $t("general.themes") }}</v-tab>
<v-tab>{{ $t("general.settings") }}</v-tab>
<v-tab> {{ $t("settings.pages") }} </v-tab>
<v-tab>{{ $t("user.users") }}</v-tab>
<v-tab>{{ $t("group.groups") }}</v-tab>
</v-tabs>
<v-tabs-items v-model="tab">
<v-tab-item v-for="(table, index) in allTables" :key="index">
<v-card flat>
<DataTable :data-headers="importHeaders" :data-set="table" />
</v-card>
</v-tab-item>
</v-tabs-items>
</v-card>
</v-dialog>
</div>
</template>
<script>
import DataTable from "@/components/ImportSummaryDialog/DataTable";
export default {
components: {
DataTable,
},
data: () => ({
tab: null,
dialog: false,
recipeData: [],
themeData: [],
settingsData: [],
userData: [],
groupData: [],
pageData: [],
allDataTables: [],
}),
computed: {
importHeaders() {
return [
{
text: this.$t("general.status"),
value: "status",
},
{
text: this.$t("general.name"),
align: "start",
sortable: true,
value: "name",
},
{
text: this.$t("general.exception"),
value: "data-table-expand",
align: "center",
},
];
},
recipeNumbers() {
return this.calculateNumbers(this.$t("general.recipes"), this.recipeData);
},
settingsNumbers() {
return this.calculateNumbers(this.$t("general.settings"), this.settingsData);
},
themeNumbers() {
return this.calculateNumbers(this.$t("general.themes"), this.themeData);
},
userNumbers() {
return this.calculateNumbers(this.$t("user.users"), this.userData);
},
groupNumbers() {
return this.calculateNumbers(this.$t("group.groups"), this.groupData);
},
pageNumbers() {
return this.calculateNumbers(this.$t("settings.pages"), this.pageData);
},
allNumbers() {
return [
this.recipeNumbers,
this.themeNumbers,
this.settingsNumbers,
this.pageNumbers,
this.userNumbers,
this.groupNumbers,
];
},
allTables() {
return [this.recipeData, this.themeData, this.settingsData, this.pageData, this.userData, this.groupData];
},
},
methods: {
calculateNumbers(title, list_array) {
if (!list_array) return;
let numbers = { title: title, success: 0, failure: 0 };
list_array.forEach(element => {
if (element.status) {
numbers.success++;
} else numbers.failure++;
});
return numbers;
},
open(importData) {
this.recipeData = importData.recipeImports;
this.themeData = importData.themeImports;
this.settingsData = importData.settingsImports;
this.userData = importData.userImports;
this.groupData = importData.groupImports;
this.pageData = importData.pageImports;
this.dialog = true;
},
},
};
</script>
<style></style>

View file

@ -1,28 +0,0 @@
<template>
<div class="text-center">
<v-dialog v-model="dialog" width="500px">
<LoginForm @logged-in="dialog = false" />
</v-dialog>
</div>
</template>
<script>
import LoginForm from "./LoginForm";
export default {
components: {
LoginForm,
},
data() {
return {
dialog: false,
};
},
methods: {
open() {
this.dialog = true;
},
},
};
</script>
<style></style>

View file

@ -1,94 +0,0 @@
<template>
<v-card width="500px">
<v-divider></v-divider>
<v-app-bar dark color="primary" class="mt-n1 mb-2">
<v-icon large left v-if="!loading">
{{ $globals.icons.user }}
</v-icon>
<v-progress-circular v-else indeterminate color="white" large class="mr-2"> </v-progress-circular>
<v-toolbar-title class="headline">{{ $t("user.login") }}</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-form @submit.prevent="login">
<v-card-text>
<v-text-field
v-model="user.email"
:prepend-icon="$globals.icons.email"
validate-on-blur
autocomplete
autofocus
:label="`${$t('user.email')} or ${$t('user.username')} `"
type="email"
></v-text-field>
<v-text-field
v-model="user.password"
class="mb-2s"
autocomplete
:prepend-icon="$globals.icons.lock"
:label="$t('user.password')"
:type="showPassword ? 'text' : 'password'"
:append-icon="showPassword ? $globals.icons.eye : $globals.icons.eyeOff"
@click:append="showPassword = !showPassword"
></v-text-field>
<v-card-actions>
<v-btn v-if="options.isLoggingIn" color="primary" block large type="submit">{{ $t("user.sign-in") }} </v-btn>
</v-card-actions>
<v-alert v-if="error" class="mt-3 mb-0" type="error">
{{ $t("user.could-not-validate-credentials") }}
</v-alert>
</v-card-text>
</v-form>
</v-card>
</template>
<script>
import { api } from "@/api";
export default {
props: {},
data() {
return {
loading: false,
error: false,
showLogin: false,
showPassword: false,
user: {
email: "",
password: "",
},
options: {
isLoggingIn: true,
},
};
},
mounted() {
this.clear();
},
methods: {
clear() {
this.user = { email: "", password: "" };
},
async login() {
this.loading = true;
this.error = false;
let formData = new FormData();
formData.append("username", this.user.email);
formData.append("password", this.user.password);
const response = await api.users.login(formData);
if (!response) {
this.error = true;
} else {
this.clear();
this.$store.commit("setToken", response.data.access_token);
this.$emit("logged-in");
this.$store.dispatch("requestUserData");
}
this.loading = false;
},
},
};
</script>
<style></style>

View file

@ -1,141 +0,0 @@
<template>
<v-card width="500px">
<v-divider></v-divider>
<v-app-bar dark color="primary" class="mt-n1">
<v-icon large left v-if="!loading">
{{ $globals.icons.user }}
</v-icon>
<v-progress-circular v-else indeterminate color="white" large class="mr-2"> </v-progress-circular>
<v-toolbar-title class="headline">
{{ $t("signup.sign-up") }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-card-text>
{{ $t("signup.welcome-to-mealie") }}
<v-divider class="mt-3"></v-divider>
<v-form ref="signUpForm" @submit.prevent="signUp">
<v-text-field
v-model="user.name"
light="light"
:prepend-icon="$globals.icons.user"
validate-on-blur
:rules="[existsRule]"
:label="$t('user.full-name')"
></v-text-field>
<v-text-field
v-model="user.username"
light="light"
:prepend-icon="$globals.icons.user"
validate-on-blur
:rules="[existsRule]"
:label="$t('user.username')"
></v-text-field>
<v-text-field
v-model="user.email"
light="light"
:prepend-icon="$globals.icons.email"
validate-on-blur
:rules="[existsRule, emailRule]"
:label="$t('user.email')"
type="email"
></v-text-field>
<v-text-field
v-model="user.password"
light="light"
class="mb-2s"
:prepend-icon="$globals.icons.lock"
validate-on-blur
:label="$t('user.password')"
:type="showPassword ? 'text' : 'password'"
:rules="[minRule]"
></v-text-field>
<v-text-field
v-model="user.passwordConfirm"
light="light"
class="mb-2s"
:prepend-icon="$globals.icons.lock"
:label="$t('user.password')"
:type="showPassword ? 'text' : 'password'"
:append-icon="showPassword ? $globals.icons.eye : $globals.icons.eyeOff"
:rules="[user.password === user.passwordConfirm || $t('user.password-must-match')]"
@click:append="showPassword = !showPassword"
></v-text-field>
<v-card-actions>
<v-btn v-if="options.isLoggingIn" dark color="primary" block="block" type="submit">
{{ $t("signup.sign-up") }}
</v-btn>
</v-card-actions>
<v-alert dense v-if="error" outlined class="mt-3 mb-0" type="error">
{{ $t("signup.error-signing-up") }}
</v-alert>
</v-form>
</v-card-text>
</v-card>
</template>
<script>
import { api } from "@/api";
import { validators } from "@/mixins/validators";
export default {
mixins: [validators],
data() {
return {
loading: false,
error: false,
showPassword: false,
user: {
name: "",
email: "",
password: "",
passwordConfirm: "",
},
options: {
isLoggingIn: true,
},
};
},
mounted() {
this.clear();
},
computed: {
token() {
return this.$route.params.token;
},
},
methods: {
clear() {
this.user = {
name: "",
email: "",
password: "",
passwordConfirm: "",
};
},
async signUp() {
this.loading = true;
this.error = false;
const userData = {
fullName: this.user.name,
username: this.user.username,
email: this.user.email,
group: "default",
password: this.user.password,
admin: false,
};
if (this.$refs.signUpForm.validate()) {
if (await api.signUps.createUser(this.token, userData)) {
this.$emit("user-created");
this.$router.push("/");
}
}
this.loading = false;
},
},
};
</script>
<style></style>

View file

@ -1,173 +0,0 @@
<template>
<v-row>
<SearchDialog ref="mealselect" @selected="setSlug" />
<BaseDialog
title="Custom Meal"
:title-icon="$globals.icons.primary"
:submit-text="$t('general.save')"
:top="true"
ref="customMealDialog"
@submit="pushCustomMeal"
>
<v-card-text>
<v-text-field autofocus v-model="customMeal.name" :label="$t('general.name')"> </v-text-field>
<v-textarea v-model="customMeal.description" :label="$t('recipe.description')"> </v-textarea>
</v-card-text>
</BaseDialog>
<v-col cols="12" sm="12" md="6" lg="4" xl="3" v-for="(planDay, index) in value" :key="index">
<v-hover v-slot="{ hover }" :open-delay="50">
<v-card :class="{ 'on-hover': hover }" :elevation="hover ? 12 : 2">
<CardImage large :slug="planDay.meals[0].slug" icon-size="200" @click="openSearch(index, modes.primary)">
<div>
<v-fade-transition>
<v-btn v-if="hover" small color="info" class="ma-1" @click.stop="addCustomItem(index, modes.primary)">
<v-icon left>
{{ $globals.icons.edit }}
</v-icon>
{{ $t("reicpe.no-recipe") }}
</v-btn>
</v-fade-transition>
</div>
</CardImage>
<v-card-title class="my-n3 mb-n6">
{{ $d(new Date(planDay.date.replaceAll("-", "/")), "short") }}
</v-card-title>
<v-card-subtitle class="mb-0 pb-0"> {{ planDay.meals[0].name }}</v-card-subtitle>
<v-hover v-slot="{ hover }">
<v-card-actions>
<v-spacer></v-spacer>
<v-fade-transition>
<v-btn v-if="hover" small color="info" text @click.stop="addCustomItem(index, modes.sides)">
<v-icon left>
{{ $globals.icons.edit }}
</v-icon>
{{ $t("reicpe.no-recipe") }}
</v-btn>
</v-fade-transition>
<v-btn color="info" outlined small @click="openSearch(index, modes.sides)">
<v-icon small class="mr-1">
{{ $globals.icons.create }}
</v-icon>
{{ $t("meal-plan.side") }}
</v-btn>
</v-card-actions>
</v-hover>
<v-divider class="mx-2"></v-divider>
<v-list dense>
<v-list-item v-for="(recipe, i) in planDay.meals.slice(1)" :key="i">
<v-list-item-avatar color="accent">
<v-img :alt="recipe.slug" :src="getImage(recipe.slug)"></v-img>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text="recipe.name"></v-list-item-title>
</v-list-item-content>
<v-list-item-icon>
<v-btn icon @click="removeSide(index, i + 1)">
<v-icon color="error">
{{ $globals.icons.delete }}
</v-icon>
</v-btn>
</v-list-item-icon>
</v-list-item>
</v-list>
</v-card>
</v-hover>
</v-col>
</v-row>
</template>
<script>
import SearchDialog from "../UI/Dialogs/SearchDialog";
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import { api } from "@/api";
import CardImage from "../Recipe/CardImage.vue";
export default {
components: {
SearchDialog,
CardImage,
BaseDialog,
},
props: {
value: Array,
},
data() {
return {
activeIndex: 0,
mode: "PRIMARY",
modes: {
primary: "PRIMARY",
sides: "SIDES",
},
customMeal: {
slug: null,
name: "",
description: "",
},
};
},
methods: {
getImage(slug) {
if (slug) {
return api.recipes.recipeSmallImage(slug);
}
},
setSide(name, slug = null, description = "") {
const meal = { name: name, slug: slug, description: description };
this.value[this.activeIndex]["meals"].push(meal);
},
setPrimary(name, slug, description = "") {
this.value[this.activeIndex]["meals"][0]["slug"] = slug;
this.value[this.activeIndex]["meals"][0]["name"] = name;
this.value[this.activeIndex]["meals"][0]["description"] = description;
},
setSlug(recipe) {
switch (this.mode) {
case this.modes.primary:
this.setPrimary(recipe.name, recipe.slug);
break;
default:
this.setSide(recipe.name, recipe.slug);
break;
}
},
openSearch(index, mode) {
this.mode = mode;
this.activeIndex = index;
this.$refs.mealselect.open();
},
removeSide(dayIndex, sideIndex) {
this.value[dayIndex]["meals"].splice(sideIndex, 1);
},
addCustomItem(index, mode) {
this.mode = mode;
this.activeIndex = index;
this.$refs.customMealDialog.open();
},
pushCustomMeal() {
switch (this.mode) {
case this.modes.primary:
this.setPrimary(this.customMeal.name, this.customMeal.slug, this.customMeal.description);
break;
default:
this.setSide(this.customMeal.name, this.customMeal.slug, this.customMeal.description);
break;
}
this.customMeal = { name: "", slug: null, description: "" };
},
},
};
</script>
<style>
.relative-card {
position: relative;
}
.custom-button {
z-index: -1;
}
</style>

View file

@ -1,46 +0,0 @@
<template>
<v-card>
<v-card-title class="headline">
{{ $t("meal-plan.edit-meal-plan") }}
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<MealPlanCard v-model="mealPlan.planDays" />
<v-row align="center" justify="end">
<v-card-actions>
<TheButton update @click="update" />
<v-spacer></v-spacer>
</v-card-actions>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
import { api } from "@/api";
import { utils } from "@/utils";
import MealPlanCard from "./MealPlanCard";
export default {
components: {
MealPlanCard,
},
props: {
mealPlan: Object,
},
methods: {
formatDate(timestamp) {
let dateObject = new Date(timestamp);
return utils.getDateAsPythonDate(dateObject);
},
async update() {
if (await api.mealPlans.update(this.mealPlan.uid, this.mealPlan)) {
this.$emit("updated");
}
},
},
};
</script>
<style></style>

View file

@ -1,227 +0,0 @@
<template>
<v-card>
<v-card-title class=" headline">
{{ $t("meal-plan.create-a-new-meal-plan") }}
<v-btn color="info" class="ml-auto" @click="setQuickWeek()">
<v-icon left> {{ $globals.icons.calendarMinus }} </v-icon>
{{ $t("meal-plan.quick-week") }}
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-row dense>
<v-col cols="12" lg="6" md="6" sm="12">
<v-menu
ref="menu1"
v-model="menu1"
:close-on-content-click="true"
transition="scale-transition"
offset-y
max-width="290px"
min-width="290px"
>
<template v-slot:activator="{ on, attrs }">
<v-text-field
v-model="startComputedDateFormatted"
:label="$t('meal-plan.start-date')"
persistent-hint
:prepend-icon="$globals.icons.calendarMinus"
readonly
v-bind="attrs"
v-on="on"
></v-text-field>
</template>
<DatePicker v-model="startDate" no-title @input="menu2 = false" />
</v-menu>
</v-col>
<v-col cols="12" lg="6" md="6" sm="12">
<v-menu
ref="menu2"
v-model="menu2"
:close-on-content-click="true"
transition="scale-transition"
offset-y
max-width="290px"
min-width="290px"
>
<template v-slot:activator="{ on, attrs }">
<v-text-field
v-model="endComputedDateFormatted"
:label="$t('meal-plan.end-date')"
persistent-hint
:prepend-icon="$globals.icons.calendarMinus"
readonly
v-bind="attrs"
v-on="on"
></v-text-field>
</template>
<DatePicker v-model="endDate" no-title @input="menu2 = false" />
</v-menu>
</v-col>
</v-row>
</v-card-text>
<v-card-text v-if="startDate">
<MealPlanCard v-model="planDays" />
</v-card-text>
<v-row align="center" justify="end">
<v-card-actions class="mr-5">
<TheButton edit @click="random" v-if="planDays.length > 0" text>
<template v-slot:icon>
{{ $globals.icons.diceMultiple }}
</template>
{{ $t("general.random") }}
</TheButton>
<TheButton create @click="save" :disabled="planDays.length == 0" />
</v-card-actions>
</v-row>
</v-card>
</template>
<script>
const CREATE_EVENT = "created";
import DatePicker from "@/components/FormHelpers/DatePicker";
import { api } from "@/api";
import { utils } from "@/utils";
import MealPlanCard from "./MealPlanCard";
export default {
components: {
MealPlanCard,
DatePicker,
},
data() {
return {
isLoading: false,
planDays: [],
items: [],
// Dates
startDate: null,
endDate: null,
menu1: false,
menu2: false,
usedRecipes: [1],
};
},
watch: {
dateDif() {
this.planDays = [];
for (let i = 0; i < this.dateDif; i++) {
this.planDays.push({
date: this.getDate(i),
meals: [
{
name: "",
slug: "empty",
description: "empty",
},
],
});
}
},
},
async created() {
await this.$store.dispatch("requestCurrentGroup");
await this.$store.dispatch("requestAllRecipes");
await this.buildMealStore();
},
computed: {
groupSettings() {
return this.$store.getters.getCurrentGroup;
},
actualStartDate() {
if (!this.startDate) return null;
return Date.parse(this.startDate.replaceAll("-", "/"));
},
actualEndDate() {
if (!this.endDate) return null;
return Date.parse(this.endDate.replaceAll("-", "/"));
},
dateDif() {
if (!this.actualEndDate || !this.actualStartDate) return null;
let dateDif = (this.actualEndDate - this.actualStartDate) / (1000 * 3600 * 24) + 1;
if (dateDif < 1) {
return null;
}
return dateDif;
},
startComputedDateFormatted() {
return this.formatDate(this.actualStartDate);
},
endComputedDateFormatted() {
return this.formatDate(this.actualEndDate);
},
filteredRecipes() {
const recipes = this.items.filter(x => !this.usedRecipes.includes(x));
return recipes.length > 0 ? recipes : this.items;
},
allRecipes() {
return this.$store.getters.getAllRecipes;
},
},
methods: {
async buildMealStore() {
const categories = Array.from(this.groupSettings.categories, x => x.name);
this.items = await api.recipes.getAllByCategory(categories);
if (this.items.length === 0) {
this.items = this.allRecipes;
}
},
getRandom(list) {
return list[Math.floor(Math.random() * list.length)];
},
random() {
this.usedRecipes = [1];
this.planDays.forEach((_, index) => {
let recipe = this.getRandom(this.filteredRecipes);
this.planDays[index]["meals"][0]["slug"] = recipe.slug;
this.planDays[index]["meals"][0]["name"] = recipe.name;
this.usedRecipes.push(recipe);
});
},
getDate(index) {
const dateObj = new Date(this.actualStartDate.valueOf() + 1000 * 3600 * 24 * index);
return utils.getDateAsPythonDate(dateObj);
},
async save() {
const mealBody = {
group: this.groupSettings.name,
startDate: this.startDate,
endDate: this.endDate,
planDays: this.planDays,
};
if (await api.mealPlans.create(mealBody)) {
this.$emit(CREATE_EVENT);
this.planDays = [];
this.startDate = null;
this.endDate = null;
}
},
formatDate(date) {
if (!date) return null;
return this.$d(date);
},
getNextDayOfTheWeek(dayName, excludeToday = true, refDate = new Date()) {
const dayOfWeek = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"].indexOf(dayName.slice(0, 3).toLowerCase());
if (dayOfWeek < 0) return;
refDate.setUTCHours(0, 0, 0, 0);
refDate.setDate(refDate.getDate() + +!!excludeToday + ((dayOfWeek + 7 - refDate.getDay() - +!!excludeToday) % 7));
return refDate;
},
setQuickWeek() {
const nextMonday = this.getNextDayOfTheWeek("Monday", false);
const nextEndDate = new Date(nextMonday);
nextEndDate.setDate(nextEndDate.getDate() + 4);
this.startDate = utils.getDateAsPythonDate(nextMonday);
this.endDate = utils.getDateAsPythonDate(nextEndDate);
},
},
};
</script>

View file

@ -1,100 +0,0 @@
<template>
<div class="text-center">
<v-dialog v-model="dialog" width="650">
<v-card>
<v-card-title class="headline">
{{ $t("shopping-list.shopping-list") }}
<v-spacer></v-spacer>
<v-btn text color="accent" @click="group = !group">
{{ $t("meal-plan.group") }}
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text v-if="group == false">
<v-list dense v-for="(recipe, index) in ingredients" :key="`${index}-recipe`">
<v-subheader>{{ recipe.name }} </v-subheader>
<v-divider></v-divider>
<v-list-item-group color="primary">
<v-list-item v-for="(item, i) in recipe.recipe_ingredient" :key="i">
<v-list-item-content>
<v-list-item-title v-text="item"></v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</v-card-text>
<v-card-text v-else>
<v-list dense>
<v-list-item-group color="primary">
<v-list-item v-for="(item, i) in rawIngredients" :key="i">
<v-list-item-content>
<v-list-item-title v-text="item"></v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</v-card-text>
<v-divider></v-divider>
</v-card>
</v-dialog>
</div>
</template>
<script>
import { api } from "@/api";
const levenshtein = require("fast-levenshtein");
export default {
data() {
return {
dialog: false,
planID: 0,
ingredients: [],
rawIngredients: [],
group: false,
};
},
methods: {
openDialog: function(id) {
this.dialog = true;
this.planID = id;
this.getIngredients();
},
async getIngredients() {
this.ingredients = await api.mealPlans.shoppingList(this.planID);
this.getRawIngredients();
},
getRawIngredients() {
this.rawIngredients = [];
this.ingredients.forEach(element => {
this.rawIngredients.push(element.recipe_ingredient);
});
this.rawIngredients = this.rawIngredients.flat();
this.rawIngredients = this.levenshteinFilter(this.rawIngredients);
},
levenshteinFilter(source, maximum = 5) {
let _source, matches, x, y;
_source = source.slice();
matches = [];
for (x = _source.length - 1; x >= 0; x--) {
let output = _source.splice(x, 1);
for (y = _source.length - 1; y >= 0; y--) {
if (levenshtein.get(output[0], _source[y]) <= maximum) {
output.push(_source[y]);
_source.splice(y, 1);
x--;
}
}
matches.push(output);
}
return matches.flat();
},
},
};
</script>
<style></style>

View file

@ -1,100 +0,0 @@
<template>
<v-img
@click="$emit('click')"
:height="height"
v-if="!fallBackImage"
:src="getImage(slug)"
@load="fallBackImage = false"
@error="fallBackImage = true"
>
<slot> </slot>
</v-img>
<div class="icon-slot" v-else @click="$emit('click')">
<v-icon color="primary" class="icon-position" :size="iconSize">
{{ $globals.icons.primary }}
</v-icon>
<slot> </slot>
</div>
</template>
<script>
import { api } from "@/api";
export default {
props: {
tiny: {
type: Boolean,
default: null,
},
small: {
type: Boolean,
default: null,
},
large: {
type: Boolean,
default: null,
},
iconSize: {
default: 100,
},
slug: {
default: null,
},
imageVersion: {
default: null,
},
height: {
default: 200,
},
},
computed: {
imageSize() {
if (this.tiny) return "tiny";
if (this.small) return "small";
if (this.large) return "large";
return "large";
},
},
watch: {
slug() {
this.fallBackImage = false;
},
},
data() {
return {
fallBackImage: false,
};
},
methods: {
getImage(slug) {
switch (this.imageSize) {
case "tiny":
return api.recipes.recipeTinyImage(slug, this.imageVersion);
case "small":
return api.recipes.recipeSmallImage(slug, this.imageVersion);
case "large":
return api.recipes.recipeImage(slug, this.imageVersion);
}
},
},
};
</script>
<style scoped>
.icon-slot {
position: relative;
}
.icon-slot > div {
top: 0;
position: absolute;
z-index: 1;
}
.icon-position {
opacity: 0.8;
display: flex !important;
position: relative;
margin-left: auto !important;
margin-right: auto !important;
}
</style>

View file

@ -1,117 +0,0 @@
<template>
<v-card>
<v-card-title class="headline">
<v-icon large class="mr-2">
{{ $globals.icons.commentTextMultipleOutline }}
</v-icon>
{{ $t("recipe.comments") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card class="ma-2" v-for="(comment, index) in comments" :key="comment.id">
<v-list-item two-line>
<v-list-item-avatar color="accent" class="white--text">
<img :src="getProfileImage(comment.user.id)" />
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title> {{ comment.user.username }}</v-list-item-title>
<v-list-item-subtitle> {{ $d(new Date(comment.dateAdded), "short") }} </v-list-item-subtitle>
</v-list-item-content>
<v-card-actions v-if="loggedIn">
<TheButton
small
minor
v-if="!editKeys[comment.id] && (user.admin || comment.user.id === user.id)"
delete
@click="deleteComment(comment.id)"
/>
<TheButton
small
v-if="!editKeys[comment.id] && comment.user.id === user.id"
edit
@click="editComment(comment.id)"
/>
<TheButton small v-else-if="editKeys[comment.id]" update @click="updateComment(comment.id, index)" />
</v-card-actions>
</v-list-item>
<div>
<v-card-text>
{{ !editKeys[comment.id] ? comment.text : null }}
<v-textarea v-if="editKeys[comment.id]" v-model="comment.text"> </v-textarea>
</v-card-text>
</div>
</v-card>
<v-card-text v-if="loggedIn">
<v-textarea auto-grow row-height="1" outlined v-model="newComment"> </v-textarea>
<div class="d-flex">
<TheButton class="ml-auto" create @click="createNewComment"> {{ $t("recipe.comment-action") }} </TheButton>
</div>
</v-card-text>
</v-card>
</template>
<script>
import { api } from "@/api";
const NEW_COMMENT_EVENT = "new-comment";
const UPDATE_COMMENT_EVENT = "update-comment";
export default {
props: {
comments: {
type: Array,
},
slug: {
type: String,
},
},
data() {
return {
newComment: "",
editKeys: {},
};
},
computed: {
user() {
return this.$store.getters.getUserData;
},
loggedIn() {
return this.$store.getters.getIsLoggedIn;
},
},
watch: {
comments() {
for (const comment of this.comments) {
this.$set(this.editKeys, comment.id, false);
}
},
},
methods: {
resetImage() {
this.hideImage == false;
},
getProfileImage(id) {
return api.users.userProfileImage(id);
},
editComment(id) {
this.$set(this.editKeys, id, true);
},
async updateComment(id, index) {
this.$set(this.editKeys, id, false);
await api.recipes.updateComment(this.slug, id, this.comments[index]);
this.$emit(UPDATE_COMMENT_EVENT);
},
async createNewComment() {
await api.recipes.createComment(this.slug, { text: this.newComment });
this.$emit(NEW_COMMENT_EVENT);
this.newComment = "";
},
async deleteComment(id) {
await api.recipes.deleteComment(this.slug, id);
this.$emit(UPDATE_COMMENT_EVENT);
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,204 +0,0 @@
<template>
<div class="text-center">
<ConfirmationDialog
:title="$t('recipe.delete-recipe')"
:message="$t('recipe.delete-confirmation')"
color="error"
:icon="$globals.icons.alertCircle"
ref="deleteRecipieConfirm"
v-on:confirm="deleteRecipe()"
/>
<v-menu
offset-y
left
:bottom="!menuTop"
:nudge-bottom="!menuTop ? '5' : '0'"
:top="menuTop"
:nudge-top="menuTop ? '5' : '0'"
allow-overflow
close-delay="125"
open-on-hover
content-class="d-print-none"
>
<template v-slot:activator="{ on, attrs }">
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
<v-icon>{{ effMenuIcon }}</v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item v-for="(item, index) in displayedMenu" :key="index" @click="menuAction(item.action)">
<v-list-item-icon>
<v-icon v-text="item.icon" :color="item.color"></v-icon>
</v-list-item-icon>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</template>
<script>
import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog.vue";
import { api } from "@/api";
import { utils } from "@/utils";
export default {
components: {
ConfirmationDialog,
},
props: {
menuTop: {
type: Boolean,
default: true,
},
showPrint: {
type: Boolean,
default: false,
},
fab: {
type: Boolean,
default: false,
},
color: {
type: String,
default: "primary",
},
slug: {
type: String,
},
menuIcon: {
default: null,
},
name: {
type: String,
},
cardMenu: {
type: Boolean,
default: true,
},
},
computed: {
effMenuIcon() {
return this.menuIcon ? this.menuIcon : this.$globals.icons.dotsVertical;
},
loggedIn() {
return this.$store.getters.getIsLoggedIn;
},
baseURL() {
return window.location.origin;
},
recipeURL() {
return `${this.baseURL}/recipe/${this.slug}`;
},
printerMenu() {
return {
title: this.$t("general.print"),
icon: this.$globals.icons.printer,
color: "accent",
action: "print",
};
},
defaultMenu() {
return [
{
title: this.$t("general.share"),
icon: this.$globals.icons.shareVariant,
color: "accent",
action: "share",
},
{
title: this.$t("general.download"),
icon: this.$globals.icons.download,
color: "accent",
action: "download",
},
];
},
userMenu() {
return [
{
title: this.$t("general.delete"),
icon: this.$globals.icons.delete,
color: "error",
action: "delete",
},
{
title: this.$t("general.edit"),
icon: this.$globals.icons.edit,
color: "accent",
action: "edit",
},
];
},
displayedMenu() {
let menu = this.defaultMenu;
if (this.loggedIn && this.cardMenu) {
menu = [...this.userMenu, ...menu];
}
if (this.showPrint) {
menu = [this.printerMenu, ...menu];
}
return menu;
},
recipeText() {
return this.$t("recipe.share-recipe-message", [this.name]);
},
},
data() {
return {
loading: true,
};
},
methods: {
async menuAction(action) {
this.loading = true;
switch (action) {
case "delete":
this.$refs.deleteRecipieConfirm.open();
break;
case "share":
if (navigator.share) {
navigator
.share({
title: this.name,
text: this.recipeText,
url: this.recipeURL,
})
.then(() => console.log("Successful share"))
.catch(error => {
console.log("WebShareAPI not supported", error);
this.updateClipboard();
});
} else this.updateClipboard();
break;
case "edit":
this.$router.push(`/recipe/${this.slug}` + "?edit=true");
break;
case "print":
this.$router.push(`/recipe/${this.slug}` + "?print=true");
break;
case "download":
window.open(`/api/recipes/${this.slug}/zip`);
break;
default:
break;
}
this.loading = false;
},
async deleteRecipe() {
await api.recipes.delete(this.slug);
},
updateClipboard() {
const copyText = this.recipeURL;
navigator.clipboard.writeText(copyText).then(
() => {
console.log("Copied to Clipboard", copyText);
utils.notify.success("Copied to Clipboard");
},
() => console.log("Copied Failed", copyText)
);
},
},
};
</script>

View file

@ -1,61 +0,0 @@
<template>
<v-tooltip bottom nudge-right="50" :color="buttonStyle ? 'info' : 'secondary'">
<template v-slot:activator="{ on, attrs }">
<v-btn
small
@click.prevent="toggleFavorite"
v-if="isFavorite || showAlways"
:color="buttonStyle ? 'info' : 'secondary'"
:icon="!buttonStyle"
:fab="buttonStyle"
v-bind="attrs"
v-on="on"
>
<v-icon :small="!buttonStyle" :color="buttonStyle ? 'white' : 'secondary'">
{{ isFavorite ? $globals.icons.heart : $globals.icons.heartOutline }}
</v-icon>
</v-btn>
</template>
<span>{{ isFavorite ? $t("recipe.remove-from-favorites") : $t("recipe.add-to-favorites") }}</span>
</v-tooltip>
</template>
<script>
import { api } from "@/api";
export default {
props: {
slug: {
default: "",
},
showAlways: {
type: Boolean,
default: false,
},
buttonStyle: {
type: Boolean,
default: false,
},
},
computed: {
user() {
return this.$store.getters.getUserData;
},
isFavorite() {
return this.user.favoriteRecipes.indexOf(this.slug) !== -1;
},
},
methods: {
async toggleFavorite() {
if (!this.isFavorite) {
await api.users.addFavorite(this.user.id, this.slug);
} else {
await api.users.removeFavorite(this.user.id, this.slug);
}
this.$store.dispatch("requestUserData");
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,112 +0,0 @@
<template>
<v-expand-transition>
<v-card
:ripple="false"
class="mx-auto"
hover
:to="this.$listeners.selected ? undefined : `/recipe/${slug}`"
@click="$emit('selected')"
>
<v-list-item three-line>
<v-list-item-avatar tile size="125" class="v-mobile-img rounded-sm my-0 ml-n4">
<v-img
v-if="!fallBackImage"
:src="getImage(slug)"
@load="fallBackImage = false"
@error="fallBackImage = true"
></v-img>
<v-icon v-else color="primary" class="icon-position" size="100">
{{ $globals.icons.primary }}
</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title class=" mb-1">{{ name }} </v-list-item-title>
<v-list-item-subtitle> {{ description }} </v-list-item-subtitle>
<div class="d-flex justify-center align-center">
<FavoriteBadge v-if="loggedIn" :slug="slug" show-always />
<v-rating
color="secondary"
class="ml-auto"
background-color="secondary lighten-3"
dense
length="5"
size="15"
:value="rating"
></v-rating>
<v-spacer></v-spacer>
<ContextMenu :slug="slug" :menu-icon="$globals.icons.dotsHorizontal" :name="name" />
</div>
</v-list-item-content>
</v-list-item>
</v-card>
</v-expand-transition>
</template>
<script>
import FavoriteBadge from "@/components/Recipe/FavoriteBadge";
import ContextMenu from "@/components/Recipe/ContextMenu";
import { api } from "@/api";
export default {
components: {
FavoriteBadge,
ContextMenu,
},
props: {
name: String,
slug: String,
description: String,
rating: Number,
image: String,
route: {
default: true,
},
tags: {
default: true,
},
},
data() {
return {
fallBackImage: false,
};
},
methods: {
getImage(slug) {
return api.recipes.recipeSmallImage(slug, this.image);
},
},
computed: {
loggedIn() {
return this.$store.getters.getIsLoggedIn;
},
},
};
</script>
<style>
.v-mobile-img {
padding-top: 0;
padding-bottom: 0;
padding-left: 0;
}
.v-card--reveal {
align-items: center;
bottom: 0;
justify-content: center;
opacity: 0.8;
position: absolute;
width: 100%;
}
.v-card--text-show {
opacity: 1 !important;
}
.headerClass {
white-space: nowrap;
word-break: normal;
overflow: hidden;
text-overflow: ellipsis;
}
.text-top {
align-self: start !important;
}
</style>

View file

@ -1,167 +0,0 @@
<template>
<div v-if="value.length > 0 || edit">
<v-card class="mt-2">
<v-card-title class="py-2">
{{ $t("asset.assets") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-list :flat="!edit" v-if="value.length > 0">
<v-list-item v-for="(item, i) in value" :key="i">
<v-list-item-icon class="ma-auto">
<v-tooltip bottom>
<template v-slot:activator="{ on, attrs }">
<v-icon v-text="getIconDefinition(item.icon).icon" v-bind="attrs" v-on="on"></v-icon>
</template>
<span>{{ getIconDefinition(item.icon).title }}</span>
</v-tooltip>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="pl-2" v-text="item.name"></v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-btn v-if="!edit" color="primary" icon :href="assetURL(item.fileName)" target="_blank" top>
<v-icon> {{ $globals.icons.download }} </v-icon>
</v-btn>
<div v-else>
<v-btn color="error" icon @click="deleteAsset(i)" top>
<v-icon>{{ $globals.icons.delete }}</v-icon>
</v-btn>
<TheCopyButton :copy-text="copyLink(item.fileName)" />
</div>
</v-list-item-action>
</v-list-item>
</v-list>
</v-card>
<div class="d-flex ml-auto mt-2">
<v-spacer></v-spacer>
<base-dialog @submit="addAsset" :title="$t('asset.new-asset')" :title-icon="getIconDefinition(newAsset.icon).icon">
<template v-slot:open="{ open }">
<v-btn color="secondary" dark @click="open" v-if="edit">
<v-icon>{{ $globals.icons.create }}</v-icon>
</v-btn>
</template>
<v-card-text class="pt-2">
<v-text-field dense v-model="newAsset.name" :label="$t('general.name')"></v-text-field>
<div class="d-flex justify-space-between">
<v-select
dense
:prepend-icon="getIconDefinition(newAsset.icon).icon"
v-model="newAsset.icon"
:items="iconOptions"
item-text="title"
item-value="name"
class="mr-2"
>
<template v-slot:item="{ item }">
<v-list-item-avatar>
<v-icon class="mr-auto">
{{ item.icon }}
</v-icon>
</v-list-item-avatar>
{{ item.title }}
</template>
</v-select>
<TheUploadBtn @uploaded="setFileObject" :post="false" file-name="file" :text-btn="false" />
</div>
{{ fileObject.name }}
</v-card-text>
</base-dialog>
</div>
</div>
</template>
<script>
import TheCopyButton from "@/components/UI/Buttons/TheCopyButton";
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import { api } from "@/api";
export default {
components: {
BaseDialog,
TheUploadBtn,
TheCopyButton,
},
props: {
slug: String,
value: {
type: Array,
},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
fileObject: {},
newAsset: {
name: "",
icon: "mdi-file",
},
};
},
computed: {
baseURL() {
return window.location.origin;
},
iconOptions() {
return [
{
name: "mdi-file",
title: this.$i18n.t('asset.file'),
icon: this.$globals.icons.file
},
{
name: "mdi-file-pdf-box",
title: this.$i18n.t('asset.pdf'),
icon: this.$globals.icons.filePDF
},
{
name: "mdi-file-image",
title: this.$i18n.t('asset.image'),
icon: this.$globals.icons.fileImage
},
{
name: "mdi-code-json",
title: this.$i18n.t('asset.code'),
icon: this.$globals.icons.codeJson
},
{
name: "mdi-silverware-fork-knife",
title: this.$i18n.t('asset.recipe'),
icon: this.$globals.icons.primary
},
];
},
},
methods: {
getIconDefinition(val) {
return this.iconOptions.find(({ name }) => name === val );
},
assetURL(assetName) {
return api.recipes.recipeAssetPath(this.slug, assetName);
},
setFileObject(obj) {
this.fileObject = obj;
},
async addAsset() {
const serverAsset = await api.recipes.createAsset(
this.slug,
this.fileObject,
this.newAsset.name,
this.newAsset.icon
);
this.value.push(serverAsset.data);
this.newAsset = { name: "", icon: "mdi-file" };
},
deleteAsset(index) {
this.value.splice(index, 1);
},
copyLink(fileName) {
const assetLink = api.recipes.recipeAssetPath(this.slug, fileName);
return `<img src="${this.baseURL}${assetLink}" height="100%" width="100%"> </img>`;
},
},
};
</script>

View file

@ -1,57 +0,0 @@
<template>
<div class="text-center">
<v-dialog v-model="dialog" width="600">
<template v-slot:activator="{ on, attrs }">
<v-btn color="secondary lighten-2" dark v-bind="attrs" v-on="on" @click="inputText = ''">
{{ $t("new-recipe.bulk-add") }}
</v-btn>
</template>
<v-card>
<v-card-title class="headline"> {{ $t("new-recipe.bulk-add") }} </v-card-title>
<v-card-text>
<p>
{{ $t("new-recipe.paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list") }}
</p>
<v-textarea v-model="inputText"> </v-textarea>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="success" text @click="save"> {{ $t("general.save") }} </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
data() {
return {
dialog: false,
inputText: "",
};
},
methods: {
splitText() {
let split = this.inputText.split("\n");
split.forEach((element, index) => {
if ((element === "\n") | (element == false)) {
split.splice(index, 1);
}
});
return split;
},
save() {
this.$emit("bulk-data", this.splitText());
this.dialog = false;
},
},
};
</script>

View file

@ -1,85 +0,0 @@
<template>
<div class="text-center">
<v-dialog v-model="dialog" width="700">
<template v-slot:activator="{ on, attrs }">
<v-btn color="accent" dark v-bind="attrs" v-on="on"> {{ $t("recipe.api-extras") }} </v-btn>
</template>
<v-card>
<v-card-title> {{ $t("recipe.api-extras") }} </v-card-title>
<v-card-text :key="formKey">
<v-row align="center" v-for="(value, key, index) in extras" :key="index">
<v-col cols="12" sm="1">
<v-btn fab text x-small color="white" elevation="0" @click="removeExtra(key)">
<v-icon color="error">{{ $globals.icons.delete }}</v-icon>
</v-btn>
</v-col>
<v-col cols="12" md="3" sm="6">
<v-text-field :label="$t('recipe.object-key')" :value="key" @input="updateKey(index)"> </v-text-field>
</v-col>
<v-col cols="12" md="8" sm="6">
<v-text-field :label="$t('recipe.object-value')" v-model="extras[key]"> </v-text-field>
</v-col>
</v-row>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-form ref="addKey">
<v-text-field
:label="$t('recipe.new-key-name')"
v-model="newKeyName"
class="pr-4"
:rules="[rules.required, rules.whiteSpace]"
></v-text-field>
</v-form>
<v-btn color="info" text @click="append"> {{ $t("recipe.add-key") }} </v-btn>
<v-spacer></v-spacer>
<v-btn color="success" text @click="save"> {{ $t("general.save") }} </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
props: {
extras: Object,
},
data() {
return {
newKeyName: null,
dialog: false,
formKey: 1,
rules: {
required: v => !!v || this.$i18n.t("recipe.key-name-required"),
whiteSpace: v => !v || v.split(" ").length <= 1 || this.$i18n.t("recipe.no-white-space-allowed"),
},
};
},
methods: {
save() {
this.$emit("save", this.extras);
this.dialog = false;
},
append() {
if (this.$refs.addKey.validate()) {
this.extras[this.newKeyName] = "value";
this.formKey += 1;
}
},
removeExtra(key) {
delete this.extras[key];
this.formKey += 1;
},
},
};
</script>
<style></style>

View file

@ -1,76 +0,0 @@
<template>
<div class="text-center">
<v-menu offset-y top nudge-top="6" :close-on-content-click="false">
<template v-slot:activator="{ on, attrs }">
<v-btn color="accent" dark v-bind="attrs" v-on="on">
<v-icon left>
{{ $globals.icons.fileImage }}
</v-icon>
{{ $t("general.image") }}
</v-btn>
</template>
<v-card width="400">
<v-card-title class="headline flex mb-0">
<div>
{{ $t("recipe.recipe-image") }}
</div>
<TheUploadBtn
class="ml-auto"
url="none"
file-name="image"
:text-btn="false"
@uploaded="uploadImage"
:post="false"
/>
</v-card-title>
<v-card-text class="mt-n5">
<div>
<v-text-field :label="$t('general.url')" class="pt-5" clearable v-model="url" :messages="getMessages()">
<template v-slot:append-outer>
<v-btn class="ml-2" color="primary" @click="getImageFromURL" :loading="loading" :disabled="!slug">
{{ $t("general.get") }}
</v-btn>
</template>
</v-text-field>
</div>
</v-card-text>
</v-card>
</v-menu>
</div>
</template>
<script>
const REFRESH_EVENT = "refresh";
const UPLOAD_EVENT = "upload";
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import { api } from "@/api";
export default {
components: {
TheUploadBtn,
},
props: {
slug: String,
},
data: () => ({
url: "",
loading: false,
}),
methods: {
uploadImage(fileObject) {
this.$emit(UPLOAD_EVENT, fileObject);
},
async getImageFromURL() {
this.loading = true;
if (await api.recipes.updateImagebyURL(this.slug, this.url)) {
this.$emit(REFRESH_EVENT);
}
this.loading = false;
},
getMessages() {
return this.slug ? [""] : [this.$i18n.t("recipe.save-recipe-before-use")];
},
},
};
</script>
<style lang="scss" scoped></style>

View file

@ -1,60 +0,0 @@
<template>
<div class="text-center">
<v-menu offset-y top nudge-top="6" :close-on-content-click="false">
<template v-slot:activator="{ on, attrs }">
<v-btn color="accent" dark v-bind="attrs" v-on="on">
<v-icon left>
{{ $globals.icons.cog }}
</v-icon>
{{ $t("general.settings") }}
</v-btn>
</template>
<v-card>
<v-card-title class="py-2">
<div>
{{ $t("recipe.recipe-settings") }}
</div>
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text class="mt-n5">
<v-switch
dense
v-for="(itemValue, key) in value"
:key="key"
v-model="value[key]"
flat
inset
:label="labels[key]"
hide-details
></v-switch>
</v-card-text>
</v-card>
</v-menu>
</div>
</template>
<script>
export default {
components: {},
props: {
value: Object,
},
computed: {
labels() {
return {
public: this.$t("recipe.public-recipe"),
showNutrition: this.$t("recipe.show-nutrition-values"),
showAssets: this.$t("asset.show-assets"),
landscapeView: this.$t("recipe.landscape-view-coming-soon"),
disableComments: this.$t("recipe.disable-comments"),
disableAmount: this.$t("recipe.disable-amount"),
};
},
},
methods: {},
};
</script>
<style lang="scss" scoped></style>

View file

@ -1,167 +0,0 @@
<template>
<div v-if="edit || (value && value.length > 0)">
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
<div v-if="edit">
<draggable :value="value" @input="updateIndex" @start="drag = true" @end="drag = false" handle=".handle">
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
<div v-for="(ingredient, index) in value" :key="generateKey('ingredient', index)">
<v-row align="center">
<v-text-field
v-if="edit && showTitleEditor[index]"
class="mx-3 mt-3"
v-model="value[index].title"
dense
:label="$t('recipe.section-title')"
>
</v-text-field>
<v-textarea
class="mr-2"
:label="$t('recipe.ingredient')"
v-model="value[index].note"
auto-grow
solo
dense
rows="1"
>
<template slot="append">
<v-tooltip right nudge-right="10">
<template v-slot:activator="{ on, attrs }">
<v-btn icon small class="mt-n1" v-bind="attrs" v-on="on" @click="toggleShowTitle(index)">
<v-icon>{{ showTitleEditor[index] ? $globals.icons.minus : $globals.icons.createAlt }}</v-icon>
</v-btn>
</template>
<span>{{
showTitleEditor[index] ? $t("recipe.remove-section") : $t("recipe.insert-section")
}}</span>
</v-tooltip>
</template>
<template slot="append-outer">
<v-icon class="handle">{{ $globals.icons.arrowUpDown }}</v-icon>
</template>
<v-icon class="mr-n1" slot="prepend" color="error" @click="removeByIndex(value, index)">
{{ $globals.icons.delete }}
</v-icon>
</v-textarea>
</v-row>
</div>
</transition-group>
</draggable>
<div class="d-flex row justify-end">
<BulkAdd @bulk-data="addIngredient" class="mr-2" />
<v-btn color="secondary" dark @click="addIngredient" class="mr-4">
<v-icon>{{ $globals.icons.create }}</v-icon>
</v-btn>
</div>
</div>
<div v-else>
<div v-for="(ingredient, index) in value" :key="generateKey('ingredient', index)">
<h3 class="mt-2" v-if="showTitleEditor[index]">{{ ingredient.title }}</h3>
<v-divider v-if="showTitleEditor[index]"></v-divider>
<v-list-item dense @click="toggleChecked(index)">
<v-checkbox hide-details :value="checked[index]" class="pt-0 my-auto py-auto" color="secondary"> </v-checkbox>
<v-list-item-content>
<vue-markdown class="ma-0 pa-0 text-subtitle-1 dense-markdown" :source="ingredient.note"> </vue-markdown>
</v-list-item-content>
</v-list-item>
</div>
</div>
</div>
</template>
<script>
import BulkAdd from "@/components/Recipe/Parts/Helpers/BulkAdd";
import VueMarkdown from "@adapttive/vue-markdown";
import draggable from "vuedraggable";
import { utils } from "@/utils";
export default {
components: {
BulkAdd,
draggable,
VueMarkdown,
},
props: {
value: {
type: Array,
},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
drag: false,
checked: [],
showTitleEditor: [],
};
},
mounted() {
this.checked = this.value.map(() => false);
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
},
watch: {
value: {
handler() {
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
},
},
},
methods: {
addIngredient(ingredients = null) {
if (ingredients.length) {
const newIngredients = ingredients.map(x => {
return {
title: null,
note: x,
unit: null,
food: null,
disableAmount: true,
quantity: 1,
};
});
this.value.push(...newIngredients);
} else {
this.value.push({
title: null,
note: "",
unit: null,
food: null,
disableAmount: true,
quantity: 1,
});
}
},
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
updateIndex(data) {
this.$emit("input", data);
},
toggleChecked(index) {
this.$set(this.checked, index, !this.checked[index]);
},
removeByIndex(list, index) {
list.splice(index, 1);
},
validateTitle(title) {
return !(title === null || title === "");
},
toggleShowTitle(index) {
const newVal = !this.showTitleEditor[index];
if (!newVal) {
this.value[index].title = "";
}
this.$set(this.showTitleEditor, index, newVal);
},
},
};
</script>
<style>
.dense-markdown p {
margin: auto !important;
}
</style>

View file

@ -1,169 +0,0 @@
<template>
<div>
<h2 class="mb-4">{{ $t("recipe.instructions") }}</h2>
<div>
<draggable
:disabled="!edit"
:value="value"
@input="updateIndex"
@start="drag = true"
@end="drag = false"
handle=".handle"
>
<div v-for="(step, index) in value" :key="index">
<v-app-bar v-if="showTitleEditor[index]" class="primary mx-1 mt-6" dark dense rounded>
<v-toolbar-title class="headline" v-if="!edit">
<v-app-bar-title v-text="step.title"> </v-app-bar-title>
</v-toolbar-title>
<v-text-field
v-if="edit"
class="headline pa-0 mt-5"
v-model="step.title"
dense
solo
flat
:placeholder="$t('recipe.section-title')"
background-color="primary"
>
</v-text-field>
</v-app-bar>
<v-hover v-slot="{ hover }">
<v-card
class="ma-1"
:class="[{ 'on-hover': hover }, isChecked(index)]"
:elevation="hover ? 12 : 2"
:ripple="!edit"
@click="toggleDisabled(index)"
>
<v-card-title :class="{ 'pb-0': !isChecked(index) }">
<v-btn
v-if="edit"
fab
x-small
color="white"
class="mr-2"
elevation="0"
@click="removeByIndex(value, index)"
>
<v-icon size="24" color="error">{{ $globals.icons.delete }}</v-icon>
</v-btn>
{{ $t("recipe.step-index", { step: index + 1 }) }}
<v-btn v-if="edit" text color="primary" class="ml-auto" @click="toggleShowTitle(index)">
{{ !showTitleEditor[index] ? $t("recipe.insert-section") : $t("recipe.remove-section") }}
</v-btn>
<v-icon v-if="edit" class="handle">{{ $globals.icons.arrowUpDown }}</v-icon>
<v-fade-transition>
<v-icon v-show="isChecked(index)" size="24" class="ml-auto" color="success">
{{ $globals.icons.checkboxMarkedCircle }}
</v-icon>
</v-fade-transition>
</v-card-title>
<v-card-text v-if="edit">
<v-textarea
auto-grow
dense
v-model="value[index]['text']"
:key="generateKey('instructions', index)"
rows="4"
>
</v-textarea>
</v-card-text>
<v-expand-transition>
<div class="m-0 p-0" v-show="!isChecked(index) && !edit">
<v-card-text>
<vue-markdown :source="step.text"> </vue-markdown>
</v-card-text>
</div>
</v-expand-transition>
</v-card>
</v-hover>
</div>
</draggable>
</div>
</div>
</template>
<script>
import draggable from "vuedraggable";
import VueMarkdown from "@adapttive/vue-markdown";
import { utils } from "@/utils";
export default {
components: {
VueMarkdown,
draggable,
},
props: {
value: {
type: Array,
},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
disabledSteps: [],
showTitleEditor: [],
};
},
mounted() {
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
},
watch: {
value: {
handler() {
this.disabledSteps = [];
this.showTitleEditor = this.value.map(x => this.validateTitle(x.title));
},
},
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
removeByIndex(list, index) {
list.splice(index, 1);
},
validateTitle(title) {
return !(title === null || title === "");
},
toggleDisabled(stepIndex) {
if (this.edit) return;
if (this.disabledSteps.includes(stepIndex)) {
let index = this.disabledSteps.indexOf(stepIndex);
if (index !== -1) {
this.disabledSteps.splice(index, 1);
}
} else {
this.disabledSteps.push(stepIndex);
}
},
isChecked(stepIndex) {
if (this.disabledSteps.includes(stepIndex) && !this.edit) {
return "disabled-card";
} else {
return;
}
},
toggleShowTitle(index) {
const newVal = !this.showTitleEditor[index];
if (!newVal) {
this.value[index].title = "";
}
this.$set(this.showTitleEditor, index, newVal);
},
updateIndex(data) {
this.$emit("input", data);
},
},
};
</script>

View file

@ -1,67 +0,0 @@
<template>
<div v-if="value.length > 0 || edit">
<h2 class="my-4">{{ $t("recipe.note") }}</h2>
<v-card class="mt-1" v-for="(note, index) in value" :key="generateKey('note', index)">
<div v-if="edit">
<v-card-text>
<v-row align="center">
<v-btn fab x-small color="white" class="mr-2" elevation="0" @click="removeByIndex(value, index)">
<v-icon color="error">{{ $globals.icons.delete }}</v-icon>
</v-btn>
<v-text-field :label="$t('recipe.title')" v-model="value[index]['title']"></v-text-field>
</v-row>
<v-textarea auto-grow :placeholder="$t('recipe.note')" v-model="value[index]['text']"> </v-textarea>
</v-card-text>
</div>
<div v-else>
<v-card-title class="py-2">
{{ note.title }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text>
<vue-markdown :source="note.text"> </vue-markdown>
</v-card-text>
</div>
</v-card>
<div class="d-flex justify-end" v-if="edit">
<v-btn class="mt-1" color="secondary" dark @click="addNote">
<v-icon>{{ $globals.icons.create }}</v-icon>
</v-btn>
</div>
</div>
</template>
<script>
import VueMarkdown from "@adapttive/vue-markdown";
import { utils } from "@/utils";
export default {
components: {
VueMarkdown,
},
props: {
value: {
type: Array,
},
edit: {
type: Boolean,
default: true,
},
},
methods: {
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
addNote() {
this.value.push({ title: "", text: "" });
},
removeByIndex(list, index) {
list.splice(index, 1);
},
},
};
</script>
<style></style>

View file

@ -1,100 +0,0 @@
<template>
<div v-if="valueNotNull || edit">
<v-card class="mt-2">
<v-card-title class="py-2">
{{ $t("recipe.nutrition") }}
</v-card-title>
<v-divider class="mx-2"></v-divider>
<v-card-text v-if="edit">
<div v-for="(item, key, index) in value" :key="index">
<v-text-field
dense
:value="value[key]"
:label="labels[key].label"
:suffix="labels[key].suffix"
type="number"
autocomplete="off"
@input="updateValue(key, $event)"
></v-text-field>
</div>
</v-card-text>
<v-list dense v-if="showViewer" class="mt-0 pt-0">
<v-list-item v-for="(item, key, index) in labels" :key="index">
<v-list-item-content>
<v-list-item-title class="pl-4 text-subtitle-1 flex row ">
<div>{{ item.label }}</div>
<div class="ml-auto mr-1">{{ value[key] }}</div>
<div>{{ item.suffix }}</div>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-card>
</div>
</template>
<script>
export default {
props: {
value: {},
edit: {
type: Boolean,
default: true,
},
},
data() {
return {
labels: {
calories: {
label: this.$t("recipe.calories"),
suffix: this.$t("recipe.calories-suffix"),
},
fatContent: {
label: this.$t("recipe.fat-content"),
suffix: this.$t("recipe.grams"),
},
fiberContent: {
label: this.$t("recipe.fiber-content"),
suffix: this.$t("recipe.grams"),
},
proteinContent: {
label: this.$t("recipe.protein-content"),
suffix: this.$t("recipe.grams"),
},
sodiumContent: {
label: this.$t("recipe.sodium-content"),
suffix: this.$t("recipe.milligrams"),
},
sugarContent: {
label: this.$t("recipe.sugar-content"),
suffix: this.$t("recipe.grams"),
},
carbohydrateContent: {
label: this.$t("recipe.carbohydrate-content"),
suffix: this.$t("recipe.grams"),
},
},
};
},
computed: {
showViewer() {
return !this.edit && this.valueNotNull;
},
valueNotNull() {
for (const property in this.value) {
const valueProperty = this.value[property];
if (valueProperty && valueProperty !== "") return true;
}
return false;
},
},
methods: {
updateValue(key, value) {
this.$emit("input", { ...this.value, [key]: value });
},
},
};
</script>
<style lang="scss" scoped></style>

View file

@ -1,62 +0,0 @@
<template>
<div @click.prevent>
<v-rating
:readonly="!loggedIn"
color="secondary"
background-color="secondary lighten-3"
length="5"
:dense="small ? true : undefined"
:size="small ? 15 : undefined"
hover
v-model="rating"
:value="value"
@input="updateRating"
@click="updateRating"
></v-rating>
</div>
</template>
<script>
import { api } from "@/api";
export default {
props: {
emitOnly: {
default: false,
},
name: String,
slug: String,
value: Number,
small: {
default: false,
},
},
mounted() {
this.rating = this.value;
},
data() {
return {
rating: 0,
};
},
computed: {
loggedIn() {
return this.$store.getters.getIsLoggedIn;
},
},
methods: {
updateRating(val) {
if (this.emitOnly) {
this.$emit("input", val);
return;
}
api.recipes.patch({
name: this.name,
slug: this.slug,
rating: val,
});
},
},
};
</script>
<style lang="scss" scoped></style>

Some files were not shown because too many files have changed in this diff Show more