feature/editor-improvements (#289)

* pin editor buttons on scroll

* scaler scratch

* fix langauge assignment 1st pass

* set lang on navigate

* refactor/breakup router

* unify style for language selectro

* refactor/code-cleanup

* refactor/page specific components to page folder

* Fix time card layout issue

* fix timecard display

* update mobile cards / fix overflow errors

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden
2021-04-21 21:52:12 -08:00
committed by GitHub
parent a5306c31c6
commit 284df44209
66 changed files with 778 additions and 664 deletions

View File

@@ -1,90 +0,0 @@
<template>
<div>
<ImportDialog
:name="selectedName"
:date="selectedDate"
ref="import_dialog"
@import="importBackup"
@delete="deleteBackup"
/>
<v-row>
<v-col
:cols="12"
:sm="6"
:md="6"
:lg="4"
:xl="4"
v-for="backup in backups"
:key="backup.name"
>
<v-card hover outlined @click="openDialog(backup)">
<v-card-text>
<v-row align="center">
<v-col cols="2">
<v-icon large color="primary">mdi-backup-restore</v-icon>
</v-col>
<v-col cols="10">
<div class="text-truncate">
<strong>{{ backup.name }}</strong>
</div>
<div class="text-truncate">{{ readableTime(backup.date) }}</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
</template>
<script>
import ImportDialog from "./ImportDialog";
import { api } from "@/api";
import utils from "@/utils";
export default {
props: {
backups: Array,
},
components: {
ImportDialog,
},
data() {
return {
selectedName: "",
selectedDate: "",
loading: false,
};
},
methods: {
openDialog(backup) {
this.selectedDate = this.readableTime(backup.date);
this.selectedName = backup.name;
this.$refs.import_dialog.open();
},
readableTime(timestamp) {
let date = new Date(timestamp);
return utils.getDateAsText(date);
},
async importBackup(data) {
this.$emit("loading");
let response = await api.backups.import(data.name, data);
let importData = response.data;
this.$emit("finished", importData);
},
deleteBackup(data) {
this.$emit("loading");
api.backups.delete(data.name);
this.selectedBackup = null;
this.backupLoading = false;
this.$emit("finished");
},
},
};
</script>
<style>
</style>

View File

@@ -1,90 +0,0 @@
<template>
<div>
<ImportDialog
:name="selectedName"
:date="selectedDate"
ref="import_dialog"
@import="importBackup"
@delete="deleteBackup"
/>
<v-row>
<v-col
:sm="6"
:md="6"
:lg="4"
:xl="4"
v-for="backup in backups"
:key="backup.name"
>
<v-card @click="openDialog(backup)">
<v-card-text>
<v-row align="center">
<v-col cols="12" sm="2">
<v-icon color="primary"> mdi-backup-restore </v-icon>
</v-col>
<v-col cols="12" sm="10">
<div>
<strong>{{ backup.name }}</strong>
</div>
<div>{{ readableTime(backup.date) }}</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
</template>
<script>
import ImportDialog from "./ImportDialog";
import { api } from "@/api";
import utils from "@/utils";
export default {
props: {
backups: Array,
},
components: {
ImportDialog,
},
data() {
return {
selectedName: "",
selectedDate: "",
loading: false,
};
},
methods: {
openDialog(backup) {
this.selectedDate = this.readableTime(backup.date);
this.selectedName = backup.name;
this.$refs.import_dialog.open();
},
readableTime(timestamp) {
let date = new Date(timestamp);
return utils.getDateAsText(date);
},
async importBackup(data) {
this.$emit("loading");
let response = await api.backups.import(data.name, data);
let failed = response.data.failed;
let succesful = response.data.successful;
this.$emit("finished", succesful, failed);
},
deleteBackup(data) {
this.$emit("loading");
api.backups.delete(data.name);
this.selectedBackup = null;
this.backupLoading = false;
this.$emit("finished");
},
},
};
</script>
<style>
</style>

View File

@@ -1,123 +0,0 @@
<template>
<div class="text-center">
<v-dialog
v-model="dialog"
width="500"
:fullscreen="$vuetify.breakpoint.xsOnly"
>
<v-card>
<v-toolbar dark color="primary" v-show="$vuetify.breakpoint.xsOnly">
<v-btn icon dark @click="dialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title></v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items>
<v-btn dark text @click="raiseEvent('import')">
{{ $t("general.import") }}
</v-btn>
</v-toolbar-items>
</v-toolbar>
<v-card-title> {{ name }} </v-card-title>
<v-card-subtitle class="mb-n3"> {{ date }} </v-card-subtitle>
<v-divider></v-divider>
<v-card-text>
<ImportOptions @update-options="updateOptions" class="mt-5 mb-2" />
<v-divider></v-divider>
<v-checkbox
dense
label="Remove existing entries matching imported entries"
v-model="forceImport"
></v-checkbox>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<TheDownloadBtn :download-url="downloadUrl" />
<v-spacer></v-spacer>
<v-btn color="error" text @click="raiseEvent('delete')">
{{ $t("general.delete") }}
</v-btn>
<v-btn
color="success"
outlined
@click="raiseEvent('import')"
v-show="$vuetify.breakpoint.smAndUp"
>
{{ $t("general.import") }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
import ImportOptions from "@/components/Admin/Backup/ImportOptions";
import TheDownloadBtn from "@/components/UI/TheDownloadBtn.vue";
import { backupURLs } from "@/api/backup";
export default {
components: { ImportOptions, TheDownloadBtn },
props: {
name: {
default: "Backup Name",
},
date: {
default: "Backup Date",
},
},
data() {
return {
options: {
recipes: true,
settings: true,
themes: true,
users: true,
groups: true,
},
dialog: false,
forceImport: false,
rebaseImport: false,
downloading: false,
};
},
computed: {
downloadUrl() {
return backupURLs.downloadBackup(this.name);
},
},
methods: {
updateOptions(options) {
this.options = options;
},
open() {
this.dialog = true;
},
close() {
this.dialog = false;
},
raiseEvent(event) {
let eventData = {
name: this.name,
force: this.forceImport,
rebase: this.rebaseImport,
recipes: this.options.recipes,
settings: this.options.settings,
themes: this.options.themes,
users: this.options.users,
groups: this.options.groups,
};
this.close();
this.$emit(event, eventData);
},
},
};
</script>
<style>
</style>

View File

@@ -1,67 +0,0 @@
<template>
<div>
<v-checkbox
v-for="option in options"
:key="option.text"
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: "Pages",
},
themes: {
value: true,
text: this.$t("general.themes"),
},
users: {
value: true,
text: this.$t("general.users"),
},
groups: {
value: true,
text: this.$t("general.groups"),
},
},
};
},
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,
});
},
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@@ -1,118 +0,0 @@
<template>
<v-card :loading="loading">
<v-card-title> {{ $t("settings.backup.create-heading") }} </v-card-title>
<v-card-text class="mt-n3">
<v-text-field
dense
:label="$t('settings.backup.backup-tag')"
v-model="tag"
></v-text-field>
</v-card-text>
<v-card-actions class="mt-n9 flex-wrap">
<v-switch v-model="fullBackup" :label="switchLabel"></v-switch>
<v-spacer></v-spacer>
<v-btn color="success" text @click="createBackup()">
{{ $t("general.create") }}
</v-btn>
</v-card-actions>
<v-expand-transition>
<div v-if="!fullBackup">
<v-card-text class="mt-n4">
<v-row>
<v-col sm="4">
<p>{{ $t("general.options") }}:</p>
<ImportOptions @update-options="updateOptions" class="mt-5" />
</v-col>
<v-col>
<p>{{ $t("general.templates") }}:</p>
<v-checkbox
v-for="template in availableTemplates"
:key="template"
class="mb-n4 mt-n3"
dense
:label="template"
@click="appendTemplate(template)"
></v-checkbox>
</v-col>
</v-row>
</v-card-text>
</div>
</v-expand-transition>
</v-card>
</template>
<script>
import ImportOptions from "@/components/Admin/Backup/ImportOptions";
import { api } from "@/api";
export default {
components: { ImportOptions },
data() {
return {
tag: null,
fullBackup: true,
loading: false,
options: {
recipes: true,
settings: true,
themes: true,
users: true,
groups: true,
},
availableTemplates: [],
selectedTemplates: [],
};
},
mounted() {
this.getAvailableBackups();
},
computed: {
switchLabel() {
if (this.fullBackup) {
return this.$t("settings.backup.full-backup");
} else return this.$t("settings.backup.partial-backup");
},
},
methods: {
updateOptions(options) {
this.options = options;
},
async getAvailableBackups() {
let response = await api.backups.requestAvailable();
response.templates.forEach(element => {
this.availableTemplates.push(element);
});
},
async createBackup() {
this.loading = true;
let data = {
tag: this.tag,
options: {
recipes: this.options.recipes,
settings: this.options.settings,
themes: this.options.themes,
users: this.options.users,
groups: this.options.groups,
},
templates: this.selectedTemplates,
};
await api.backups.create(data);
this.loading = false;
this.$emit("created");
},
appendTemplate(templateName) {
if (this.selectedTemplates.includes(templateName)) {
let index = this.selectedTemplates.indexOf(templateName);
if (index !== -1) {
this.selectedTemplates.splice(index, 1);
}
} else this.selectedTemplates.push(templateName);
},
},
};
</script>
<style>
</style>

View File

@@ -1,99 +0,0 @@
<template>
<v-dialog v-model="pageDialog" max-width="500">
<v-card>
<v-app-bar dark dense color="primary">
<v-icon left>
mdi-page-layout-body
</v-icon>
<v-toolbar-title class="headline">
{{ title }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-form ref="newGroup" @submit.prevent="submitForm">
<v-card-text>
<v-text-field
autofocus
v-model="page.name"
:label="$t('settings.page-name')"
></v-text-field>
<CategoryTagSelector
v-model="page.categories"
ref="categoryFormSelector"
@mounted="catMounted = true"
:tag-selector="false"
/>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="pageDialog = false">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="primary" type="submit">
{{ buttonText }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</template>
<script>
const NEW_PAGE_EVENT = "refresh-page";
import { api } from "@/api";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
export default {
components: {
CategoryTagSelector,
},
data() {
return {
catMounted: false,
title: "",
buttonText: "",
create: false,
pageDialog: false,
page: {
name: "",
position: 0,
categories: [],
},
};
},
watch: {
catMounted(val) {
if (val) this.pushSelected();
},
},
methods: {
open(parameters) {
this.page = parameters.data;
this.create = parameters.create;
this.buttonText = parameters.buttonText;
this.title = parameters.title;
this.pageDialog = true;
if (this.catMounted) this.pushSelected();
},
pushSelected() {
this.$refs.categoryFormSelector.setInit(this.page.categories);
},
async submitForm() {
if (this.create) {
await api.siteSettings.createPage(this.page);
} else {
await api.siteSettings.updatePage(this.page);
}
this.pageDialog = false;
this.page.categories = [];
this.$emit(NEW_PAGE_EVENT);
},
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@@ -1,129 +0,0 @@
<template>
<v-card flat>
<CreatePageDialog ref="createDialog" @refresh-page="getPages" />
<v-card-text>
<h2 class="mt-1 mb-1 ">
{{$t('settings.custom-pages')}}
<span>
<v-btn color="success" @click="newPage" small class="ml-3">
{{$t('general.create')}}
</v-btn>
</span>
</h2>
<draggable class="row mt-1" v-model="customPages">
<v-col
:sm="6"
:md="6"
:lg="4"
:xl="3"
v-for="(item, index) in customPages"
:key="item + item.id"
>
<v-card>
<v-card-text class="mb-0 pb-0">
<h3>{{ item.name }}</h3>
<v-divider></v-divider>
</v-card-text>
<v-card-text class="mt-0">
<div>
<v-chip
v-for="cat in item.categories"
:key="cat.slug + cat.id"
class="my-2 mr-2"
label
small
color="accent lighten-1"
>
{{ cat.name }}
</v-chip>
</div>
</v-card-text>
<v-card-actions>
<v-btn text small color="error" @click="deletePage(item.id)">
{{$t('general.delete')}}
</v-btn>
<v-spacer> </v-spacer>
<v-btn small text color="success" @click="editPage(index)">
{{$t('general.edit')}}
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</draggable>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="success" @click="savePages">
{{$t('general.save')}}
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
import draggable from "vuedraggable";
import CreatePageDialog from "@/components/Admin/General/CreatePageDialog";
import { api } from "@/api";
export default {
components: {
draggable,
CreatePageDialog,
},
data() {
return {
pageDialog: false,
customPages: [],
newPageData: {
create: true,
title: this.$t('settings.new-page'),
buttonText: this.$t('general.create'),
data: {
name: "",
categories: [],
position: 0,
},
},
editPageData: {
create: false,
title: this.$t('settings.edit-page'),
buttonText: this.$t('general.update'),
data: {},
},
};
},
async mounted() {
this.getPages();
},
methods: {
async getPages() {
this.customPages = await api.siteSettings.getPages();
this.customPages.sort((a, b) => a.position - b.position);
},
async deletePage(id) {
await api.siteSettings.deletePage(id);
this.getPages();
},
async savePages() {
this.customPages.forEach((element, index) => {
element.position = index;
});
await api.siteSettings.updateAllPages(this.customPages);
this.getPages();
},
editPage(index) {
this.editPageData.data = this.customPages[index];
this.$refs.createDialog.open(this.editPageData);
},
newPage() {
this.newPageData.position = this.customPages.length;
this.$refs.createDialog.open(this.newPageData);
},
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@@ -1,237 +0,0 @@
<template>
<v-card flat>
<v-card-text>
<h2 class="mt-1 mb-1">{{ $t("settings.homepage.home-page") }}</h2>
<v-row align="center" justify="center" dense class="mb-n7 pb-n5">
<v-col cols="12" sm="3" md="2">
<v-switch
v-model="settings.showRecent"
:label="$t('settings.homepage.show-recent')"
></v-switch>
</v-col>
<v-col cols="12" sm="5" md="5">
<v-slider
class="pt-sm-4"
:label="$t('settings.homepage.card-per-section')"
v-model="settings.cardsPerSection"
max="30"
dense
color="primary"
min="3"
thumb-label
>
</v-slider>
</v-col>
<v-spacer></v-spacer>
</v-row>
</v-card-text>
<v-card-text>
<v-row>
<v-col cols="12" sm="6">
<v-card outlined min-height="350px">
<v-app-bar dark dense color="primary">
<v-icon left>
mdi-home
</v-icon>
<v-toolbar-title class="headline">
{{ $t("settings.homepage.home-page-sections") }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-list height="300" dense style="overflow:auto">
<v-list-item-group>
<draggable
v-model="settings.categories"
group="categories"
:style="{
minHeight: `150px`,
}"
>
<v-list-item
v-for="(item, index) in settings.categories"
:key="`${item.name}-${index}`"
>
<v-list-item-icon>
<v-icon>mdi-menu</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title v-text="item.name"></v-list-item-title>
</v-list-item-content>
<v-list-item-icon @click="deleteActiveCategory(index)">
<v-icon>mdi-delete</v-icon>
</v-list-item-icon>
</v-list-item>
</draggable>
</v-list-item-group>
</v-list>
</v-card>
</v-col>
<v-col cols="12" sm="6">
<v-card outlined height="350px">
<v-app-bar dark dense color="primary">
<v-icon left>
mdi-tag
</v-icon>
<v-toolbar-title class="headline">
{{ $t("settings.homepage.all-categories") }}
</v-toolbar-title>
<v-spacer></v-spacer>
<NewCategoryTagDialog :tag-dialog="false" />
</v-app-bar>
<v-list height="300" dense style="overflow:auto">
<v-list-item-group>
<draggable
v-model="allCategories"
group="categories"
:style="{
minHeight: `150px`,
}"
>
<v-list-item
v-for="(item, index) in allCategories"
:key="`${item.name}-${index}`"
>
<v-list-item-icon>
<v-icon>mdi-menu</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title v-text="item.name"></v-list-item-title>
</v-list-item-content>
<v-list-item-icon
@click="deleteCategoryfromDatabase(item.slug)"
>
<v-icon>mdi-delete</v-icon>
</v-list-item-icon>
</v-list-item>
</draggable>
</v-list-item-group>
</v-list>
</v-card>
</v-col>
</v-row>
</v-card-text>
<v-card-text>
<h2 class="mt-1 mb-4">{{$t('settings.locale-settings')}}</h2>
<v-row>
<v-col cols="1">
<LanguageMenu @select-lang="writeLang" :site-settings="true" />
</v-col>
<v-col sm="3">
<v-select
dense
prepend-icon="mdi-calendar-week-begin"
v-model="settings.firstDayOfWeek"
:items="allDays"
item-text="name"
item-value="value"
:label="$t('settings.first-day-of-week')"
/>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="success" @click="saveSettings" class="mr-2">
<v-icon left> mdi-content-save </v-icon>
{{ $t("general.save") }}
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
import { api } from "@/api";
import LanguageMenu from "@/components/UI/LanguageMenu";
import draggable from "vuedraggable";
import NewCategoryTagDialog from "@/components/UI/Dialogs/NewCategoryTagDialog.vue";
export default {
components: {
draggable,
LanguageMenu,
NewCategoryTagDialog,
},
data() {
return {
settings: {
language: "en",
firstDayOfWeek: 0,
showRecent: null,
cardsPerSection: null,
categories: [],
},
};
},
mounted() {
this.getOptions();
},
computed: {
allCategories() {
return this.$store.getters.getAllCategories;
},
allDays() {
return [
{
name: this.$t('general.sunday'),
value: 0,
},
{
name: this.$t('general.monday'),
value: 1,
},
{
name: this.$t('general.tuesday'),
value: 2,
},
{
name: this.$t('general.wednesday'),
value: 3,
},
{
name: this.$t('general.thursday'),
value: 4,
},
{
name: this.$t('general.friday'),
value: 5,
},
{
name: this.$t('general.saturday'),
value: 6,
}
];
},
},
methods: {
writeLang(val) {
this.settings.language = val;
},
deleteCategoryfromDatabase(category) {
api.categories.delete(category);
},
async getOptions() {
this.settings = await api.siteSettings.get();
},
deleteActiveCategory(index) {
this.settings.categories.splice(index, 1);
},
async saveSettings() {
await api.siteSettings.update(this.settings);
this.$store.dispatch("setLang", {
currentVueComponent: this,
language: this.settings.language });
this.getOptions();
},
},
};
</script>
<style>
</style>

View File

@@ -1,131 +0,0 @@
<template>
<div>
<Confirmation
ref="deleteGroupConfirm"
:title="$t('user.confirm-group-deletion')"
:message="
$t('user.are-you-sure-you-want-to-delete-the-group', {
groupName: group.name,
})
"
icon="mdi-alert"
@confirm="deleteGroup"
:width="450"
@close="closeGroupDelete"
/>
<v-card class="ma-auto" tile min-height="325px">
<v-list dense>
<v-card-title class="py-1">{{ group.name }}</v-card-title>
<v-divider></v-divider>
<v-subheader>{{
$t("user.group-id-with-value", { groupID: group.id })
}}</v-subheader>
<v-list-item-group color="primary">
<v-list-item v-for="property in groupProps" :key="property.text">
<v-list-item-icon>
<v-icon> {{ property.icon || "mdi-account" }} </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="pl-4 flex row justify-space-between">
<div>{{ property.text }}</div>
<div>{{ property.value }}</div>
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
small
color="error"
@click="confirmDelete"
:disabled="ableToDelete"
>
{{ $t("general.delete") }}
</v-btn>
<!-- Coming Soon! -->
<v-btn small color="success" disabled>
{{ $t("general.edit") }}
</v-btn>
</v-card-actions>
</v-card>
</div>
</template>
<script>
const RENDER_EVENT = "update";
import Confirmation from "@/components/UI/Confirmation";
import { api } from "@/api";
export default {
components: { Confirmation },
props: {
group: {
default: {
name: "DEFAULT_NAME",
id: 1,
users: [],
mealplans: [],
categories: [],
webhookUrls: [],
webhookTime: "00:00",
webhookEnable: false,
},
},
},
data() {
return {
groupProps: {},
};
},
computed: {
ableToDelete() {
return this.group.users.length >= 1 ? true : false;
},
},
mounted() {
this.buildData();
},
methods: {
confirmDelete() {
this.$refs.deleteGroupConfirm.open();
},
async deleteGroup() {
await api.groups.delete(this.group.id);
this.$emit(RENDER_EVENT);
},
closeGroupDelete() {
console.log("Close Delete");
},
buildData() {
this.groupProps = [
{
text: this.$t("user.total-users"),
icon: "mdi-account",
value: this.group.users.length,
},
{
text: this.$t("user.total-mealplans"),
icon: "mdi-food",
value: this.group.mealplans.length,
},
{
text: this.$t("user.webhooks-enabled"),
icon: "mdi-webhook",
value: this.group.webhookEnable
? this.$t("general.yes")
: this.$t("general.no"),
},
{
text: this.$t("user.webhook-time"),
icon: "mdi-clock-outline",
value: this.group.webhookTime,
},
];
},
},
};
</script>
<style scoped>
</style>

View File

@@ -1,119 +0,0 @@
<template>
<div>
<v-card outlined class="mt-n1">
<v-card-actions>
<v-spacer></v-spacer>
<div width="100px">
<v-text-field
v-model="filter"
clearable
class="mr-2 pt-0"
append-icon="mdi-filter"
:label="$t('general.filter')"
single-line
hide-details
></v-text-field>
</div>
<v-dialog v-model="groupDialog" max-width="400">
<template v-slot:activator="{ on, attrs }">
<v-btn
class="mx-2"
small
color="success"
dark
v-bind="attrs"
v-on="on"
>
{{ $t("user.create-group") }}
</v-btn>
</template>
<v-card>
<v-app-bar dark dense color="primary">
<v-icon left>
mdi-account-group
</v-icon>
<v-toolbar-title class="headline">
{{ $t("user.create-group") }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-form ref="newGroup" @submit.prevent="createGroup">
<v-card-text>
<v-text-field
v-model="newGroupName"
:label="$t('user.group-name')"
:rules="[existsRule]"
></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="groupDialog = false">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="primary" type="submit">
{{ $t("general.create") }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</v-card-actions>
<v-card-text>
<v-row>
<v-col
:sm="6"
:md="6"
:lg="4"
:xl="3"
v-for="group in groups"
:key="group.id"
>
<GroupCard
:group="group"
@update="$store.dispatch('requestAllGroups')"
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
</div>
</template>
<script>
import { validators } from "@/mixins/validators";
import { api } from "@/api";
import GroupCard from "@/components/Admin/ManageUsers/GroupCard";
export default {
components: { GroupCard },
mixins: [validators],
data() {
return {
filter: "",
groupDialog: false,
newGroupName: "",
};
},
computed: {
groups() {
return this.$store.getters.getGroups;
},
},
methods: {
async createGroup() {
this.groupLoading = true;
let response = await api.groups.create(this.newGroupName);
if (response.created) {
this.groupLoading = false;
this.groupDialog = false;
this.$store.dispatch("requestAllGroups");
}
},
},
};
</script>
<style>
</style>

View File

@@ -1,242 +0,0 @@
<template>
<v-card outlined class="mt-n1">
<Confirmation
ref="deleteUserDialog"
:title="$t('user.confirm-link-deletion')"
:message="
$t('user.are-you-sure-you-want-to-delete-the-link', {
link: activeName,
})
"
icon="mdi-alert"
@confirm="deleteUser"
:width="450"
@close="closeDelete"
/>
<v-toolbar flat>
<v-icon large color="accent" class="mr-1">
mdi-link-variant
</v-icon>
<v-toolbar-title class="headine">
{{ $t("user.sign-up-links") }}
</v-toolbar-title>
<v-spacer> </v-spacer>
<v-dialog v-model="dialog" max-width="500">
<template v-slot:activator="{ on, attrs }">
<v-btn small color="success" dark v-bind="attrs" v-on="on">
{{ $t("user.create-link") }}
</v-btn>
</template>
<v-card>
<v-app-bar dark dense color="primary">
<v-icon left>
mdi-account
</v-icon>
<v-toolbar-title class="headline">
{{ $t("user.create-link") }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-form ref="newUser" @submit.prevent="save">
<v-card-text>
<v-text-field
v-model="editedItem.name"
:label="$t('user.link-name')"
:rules="[existsRule]"
validate-on-blur
></v-text-field>
<v-checkbox
v-model="editedItem.admin"
:label="$t('user.admin')"
></v-checkbox>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="close">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="primary" type="submit">
{{ $t("general.save") }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</v-toolbar>
<v-divider></v-divider>
<v-card-text>
<v-data-table :headers="headers" :items="links" sort-by="calories">
<template v-slot:item.token="{ item }">
{{ `${baseURL}/sign-up/${item.token}` }}
<v-btn
icon
class="mr-1"
small
color="accent"
@click="updateClipboard(`${baseURL}/sign-up/${item.token}`)"
>
<v-icon>
mdi-content-copy
</v-icon>
</v-btn>
</template>
<template v-slot:item.admin="{ item }">
<v-btn small :color="item.admin ? 'success' : 'error'" text>
<v-icon small left>
mdi-account-cog
</v-icon>
{{ item.admin ? $t("general.yes") : $t("general.no") }}
</v-btn>
</template>
<template v-slot:item.actions="{ item }">
<v-btn class="mr-1" small color="error" @click="deleteItem(item)">
<v-icon small left>
mdi-delete
</v-icon>
{{ $t("general.delete") }}
</v-btn>
</template>
</v-data-table>
</v-card-text>
</v-card>
</template>
<script>
import Confirmation from "@/components/UI/Confirmation";
import { api } from "@/api";
import { validators } from "@/mixins/validators";
export default {
components: { Confirmation },
mixins: [validators],
data() {
return {
dialog: false,
activeId: null,
activeName: null,
headers: [
{
text: this.$t("user.link-id"),
align: "start",
sortable: false,
value: "id",
},
{ text: this.$t("general.name"), value: "name" },
{ text: this.$t("general.token"), value: "token" },
{ text: this.$t("user.admin"), value: "admin", align: "center" },
{ text: "", value: "actions", sortable: false, align: "center" },
],
links: [],
editedIndex: -1,
editedItem: {
name: "",
admin: false,
token: "",
id: 0,
},
defaultItem: {
name: "",
token: "",
admin: false,
id: 0,
},
};
},
computed: {
baseURL() {
return window.location.origin;
},
},
watch: {
dialog(val) {
val || this.close();
},
dialogDelete(val) {
val || this.closeDelete();
},
},
created() {
this.initialize();
},
methods: {
updateClipboard(newClip) {
navigator.clipboard.writeText(newClip).then(
function() {
console.log("Copied", newClip);
},
function() {
console.log("Copy Failed", newClip);
}
);
},
async initialize() {
this.links = await api.signUps.getAll();
},
async deleteUser() {
await api.signUps.deleteToken(this.activeId);
this.initialize();
},
editItem(item) {
this.editedIndex = this.links.indexOf(item);
this.editedItem = Object.assign({}, item);
this.dialog = true;
},
deleteItem(item) {
this.activeId = item.token;
this.activeName = item.name;
this.editedIndex = this.links.indexOf(item);
this.editedItem = Object.assign({}, item);
this.$refs.deleteUserDialog.open();
},
deleteItemConfirm() {
this.links.splice(this.editedIndex, 1);
this.closeDelete();
},
close() {
this.dialog = false;
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem);
this.editedIndex = -1;
});
},
closeDelete() {
this.dialogDelete = false;
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem);
this.editedIndex = -1;
});
},
async save() {
if (this.editedIndex > -1) {
api.links.update(this.editedItem);
this.close();
} else if (this.$refs.newUser.validate()) {
api.signUps.createToken({
name: this.editedItem.name,
admin: this.editedItem.admin,
});
this.close();
}
await this.initialize();
},
},
};
</script>
<style>
</style>

View File

@@ -1,283 +0,0 @@
<template>
<v-card outlined class="mt-n1">
<Confirmation
ref="deleteUserDialog"
:title="$t('user.confirm-user-deletion')"
:message="
$t('user.are-you-sure-you-want-to-delete-the-user', {
activeName,
activeId,
})
"
icon="mdi-alert"
@confirm="deleteUser"
:width="450"
@close="closeDelete"
/>
<v-toolbar flat>
<v-spacer> </v-spacer>
<div width="100px">
<v-text-field
v-model="search"
class="mr-2"
append-icon="mdi-filter"
:label="$t('general.filter')"
single-line
hide-details
></v-text-field>
</div>
<v-dialog v-model="dialog" max-width="600px">
<template v-slot:activator="{ on, attrs }">
<v-btn small color="success" dark v-bind="attrs" v-on="on">
{{ $t("user.create-user") }}
</v-btn>
</template>
<v-card>
<v-app-bar dark dense color="primary">
<v-icon left>
mdi-account
</v-icon>
<v-toolbar-title class="headline">
{{ formTitle }}
</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-title class="headline">
{{ $t("user.user-id-with-value", { id: editedItem.id }) }}
</v-toolbar-title>
</v-app-bar>
<v-form ref="newUser" @submit.prevent="save">
<v-card-text>
<v-row>
<v-col cols="12" sm="12" md="6">
<v-text-field
v-model="editedItem.fullName"
:label="$t('user.full-name')"
:rules="[existsRule]"
validate-on-blur
></v-text-field>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-text-field
v-model="editedItem.email"
:label="$t('user.email')"
:rules="[existsRule, emailRule]"
validate-on-blur
></v-text-field>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-select
dense
v-model="editedItem.group"
:items="existingGroups"
:label="$t('user.user-group')"
></v-select>
</v-col>
<v-col cols="12" sm="12" md="6" v-if="showPassword">
<v-text-field
dense
v-model="editedItem.password"
:label="$t('user.user-password')"
:rules="[existsRule, minRule]"
></v-text-field>
</v-col>
<v-col cols="12" sm="12" md="3">
<v-switch
v-model="editedItem.admin"
:label="$t('user.admin')"
></v-switch>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-btn color="info" text @click="resetPassword">
Reset Password
</v-btn>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="close">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="primary" type="submit">
{{ $t("general.save") }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</v-toolbar>
<v-divider></v-divider>
<v-card-text>
<v-data-table
:headers="headers"
:items="users"
sort-by="calories"
:search="search"
>
<template v-slot:item.actions="{ item }">
<v-btn class="mr-1" small color="error" @click="deleteItem(item)">
<v-icon small left>
mdi-delete
</v-icon>
{{ $t("general.delete") }}
</v-btn>
<v-btn small color="success" @click="editItem(item)">
<v-icon small left class="mr-2">
mdi-pencil
</v-icon>
{{ $t("general.edit") }}
</v-btn>
</template>
<template v-slot:item.admin="{ item }">
{{ item.admin ? "Admin" : "User" }}
</template>
<template v-slot:no-data>
<v-btn color="primary" @click="initialize">
{{ $t("general.reset") }}
</v-btn>
</template>
</v-data-table>
</v-card-text>
</v-card>
</template>
<script>
import Confirmation from "@/components/UI/Confirmation";
import { api } from "@/api";
import { validators } from "@/mixins/validators";
export default {
components: { Confirmation },
mixins: [validators],
data() {
return {
search: "",
dialog: false,
activeId: null,
activeName: null,
headers: [
{
text: this.$t("user.user-id"),
align: "start",
sortable: false,
value: "id",
},
{ text: this.$t("user.full-name"), value: "fullName" },
{ text: this.$t("user.email"), value: "email" },
{ text: this.$t("user.group"), value: "group" },
{ text: this.$t("user.admin"), value: "admin" },
{ text: "", value: "actions", sortable: false, align: "center" },
],
users: [],
editedIndex: -1,
editedItem: {
id: 0,
fullName: "",
password: "",
email: "",
group: "",
admin: false,
},
defaultItem: {
id: 0,
fullName: "",
password: "",
email: "",
group: "",
admin: false,
},
};
},
computed: {
formTitle() {
return this.editedIndex === -1
? this.$t("user.new-user")
: this.$t("user.edit-user");
},
showPassword() {
return this.editedIndex === -1 ? true : false;
},
existingGroups() {
return this.$store.getters.getGroupNames;
},
},
watch: {
dialog(val) {
val || this.close();
},
dialogDelete(val) {
val || this.closeDelete();
},
},
created() {
this.initialize();
},
methods: {
async initialize() {
this.users = await api.users.allUsers();
},
async deleteUser() {
await api.users.delete(this.activeId);
this.initialize();
},
editItem(item) {
this.editedIndex = this.users.indexOf(item);
this.editedItem = Object.assign({}, item);
this.dialog = true;
},
deleteItem(item) {
this.activeId = item.id;
this.activeName = item.fullName;
this.editedIndex = this.users.indexOf(item);
this.editedItem = Object.assign({}, item);
this.$refs.deleteUserDialog.open();
},
deleteItemConfirm() {
this.users.splice(this.editedIndex, 1);
this.closeDelete();
},
close() {
this.dialog = false;
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem);
this.editedIndex = -1;
});
},
closeDelete() {
this.dialogDelete = false;
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem);
this.editedIndex = -1;
});
},
async save() {
if (this.editedIndex > -1) {
await api.users.update(this.editedItem);
this.close();
} else if (this.$refs.newUser.validate()) {
await api.users.create(this.editedItem);
this.close();
}
await this.initialize();
},
resetPassword() {
api.users.resetPassword(this.editedItem.id);
},
},
};
</script>
<style>
</style>

View File

@@ -1,110 +0,0 @@
<template>
<v-card outlined class="my-2" :loading="loading">
<MigrationDialog ref="migrationDialog" />
<v-card-title>
{{ title }}
<v-spacer></v-spacer>
<span>
<UploadBtn
class="mt-1"
:url="`/api/migrations/${folder}/upload`"
fileName="archive"
@uploaded="$emit('refresh')"
:post="true"
/>
</span>
</v-card-title>
<v-card-text> {{ description }}</v-card-text>
<div v-if="available[0]">
<v-card
outlined
v-for="migration in available"
:key="migration.name"
class="ma-2"
>
<v-card-text>
<v-row align="center">
<v-col cols="2">
<v-icon large color="primary">mdi-import</v-icon>
</v-col>
<v-col cols="10">
<div class="text-truncate">
<strong>{{ migration.name }}</strong>
</div>
<div class="text-truncate">
{{ readableTime(migration.date) }}
</div>
</v-col>
</v-row>
</v-card-text>
<v-card-actions class="mt-n6">
<v-spacer></v-spacer>
<v-btn color="error" text @click="deleteMigration(migration.name)">
{{ $t("general.delete") }}
</v-btn>
<v-btn
color="accent"
text
@click="importMigration(migration.name)"
:loading="loading"
:disabled="loading"
>
{{ $t("general.import") }}
</v-btn>
</v-card-actions>
</v-card>
</div>
<div v-else>
<v-card outlined class="text-center ma-2">
<v-card-text>
{{ $t("migration.no-migration-data-available") }}
</v-card-text>
</v-card>
</div>
<br />
</v-card>
</template>
<script>
import UploadBtn from "../../UI/UploadBtn";
import utils from "@/utils";
import { api } from "@/api";
import MigrationDialog from "@/components/Admin/Migration/MigrationDialog.vue";
export default {
props: {
folder: String,
title: String,
description: String,
available: Array,
},
components: {
UploadBtn,
MigrationDialog,
},
data() {
return {
loading: false,
};
},
methods: {
deleteMigration(file_name) {
api.migrations.delete(this.folder, file_name);
this.$emit("refresh");
},
async importMigration(file_name) {
this.loading = true;
let response = await api.migrations.import(this.folder, file_name);
this.$refs.migrationDialog.open(response);
// this.$emit("imported", response.successful, response.failed);
this.loading = false;
},
readableTime(timestamp) {
let date = new Date(timestamp);
return utils.getDateAsText(date);
},
},
};
</script>
<style>
</style>

View File

@@ -1,109 +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>
mdi-import
</v-icon>
<v-toolbar-title class="headline">
Migration 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">Success: {{ values.success }}</div>
<div class="error--text">Failed: {{ values.failure }}</div>
</v-card-text>
</div>
</v-row>
</v-card-text>
<v-tabs v-model="tab">
<v-tab>{{ $t("general.recipes") }}</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/Admin/Backup/ImportSummaryDialog/DataTable";
export default {
components: {
DataTable,
},
data: () => ({
tab: null,
dialog: false,
recipeData: [],
themeData: [],
settingsData: [],
userData: [],
groupData: [],
pageData: [],
importHeaders: [
{
text: "Status",
value: "status",
},
{
text: "Name",
align: "start",
sortable: true,
value: "name",
},
{ text: "Exception", value: "data-table-expand", align: "center" },
],
allDataTables: [],
}),
computed: {
recipeNumbers() {
return this.calculateNumbers(this.$t("general.recipes"), this.recipeData);
},
allNumbers() {
return [this.recipeNumbers];
},
allTables() {
return [this.recipeData];
},
},
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;
this.dialog = true;
},
},
};
</script>
<style>
</style>

View File

@@ -1,91 +0,0 @@
<template>
<div>
<v-btn text color="info" @click="dialog = true">
{{ $t("settings.add-a-new-theme") }}
</v-btn>
<v-dialog v-model="dialog" width="500">
<v-card>
<v-app-bar dense dark color="primary mb-2">
<v-icon large left class="mt-1">
mdi-format-color-fill
</v-icon>
<v-toolbar-title class="headline">
{{ $t("settings.add-a-new-theme") }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
<v-card-title> </v-card-title>
<v-form @submit.prevent="select">
<v-card-text>
<v-text-field
:label="$t('settings.theme.theme-name')"
v-model="themeName"
:rules="[rules.required]"
></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="dialog = false">
{{ $t("general.cancel") }}
</v-btn>
<v-btn color="success" text type="submit" :disabled="!themeName">
{{ $t("general.create") }}
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-dialog>
</div>
</template>
<script>
export default {
props: {
buttonText: String,
value: String,
},
data() {
return {
dialog: false,
themeName: "",
rules: {
required: val =>
!!val || this.$t("settings.theme.theme-name-is-required"),
},
};
},
watch: {
color() {
this.updateColor();
},
},
methods: {
randomColor() {
return "#" + Math.floor(Math.random() * 16777215).toString(16);
},
select() {
const newTheme = {
name: this.themeName,
colors: {
primary: "#E58325",
accent: "#00457A",
secondary: "#973542",
success: "#5AB1BB",
info: "#4990BA",
warning: "#FF4081",
error: "#EF5350",
},
};
this.$emit("new-theme", newTheme);
this.dialog = false;
},
},
};
</script>
<style>
</style>

View File

@@ -1,91 +0,0 @@
<template>
<div>
<Confirmation
:title="$t('settings.theme.delete-theme')"
:message="$t('settings.theme.are-you-sure-you-want-to-delete-this-theme')"
color="error"
icon="mdi-alert-circle"
ref="deleteThemeConfirm"
v-on:confirm="deleteSelectedTheme()"
/>
<v-card flat outlined class="ma-2">
<v-card-text class="mb-n5 mt-n2">
<h3>
{{ theme.name }}
{{ current ? $t("general.current-parenthesis") : "" }}
</h3>
</v-card-text>
<v-card-text>
<v-row flex align-center>
<v-card
v-for="(color, index) in theme.colors"
:key="index"
class="ma-1 mx-auto"
height="34"
width="36"
:color="color"
>
</v-card>
</v-row>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-btn text color="error" @click="confirmDelete">
{{ $t("general.delete") }}
</v-btn>
<v-spacer></v-spacer>
<!-- <v-btn text color="accent" @click="editTheme">Edit</v-btn> -->
<v-btn text color="success" @click="saveThemes">{{
$t("general.apply")
}}</v-btn>
</v-card-actions>
</v-card>
</div>
</template>
<script>
import Confirmation from "@/components/UI/Confirmation";
import { api } from "@/api";
const DELETE_EVENT = "delete";
const APPLY_EVENT = "apply";
const EDIT_EVENT = "edit";
export default {
components: {
Confirmation,
},
props: {
theme: Object,
current: {
default: false,
},
},
methods: {
confirmDelete() {
if (this.theme.name === "default") {
// Notify User Can't Delete Default
} else if (this.theme !== {}) {
this.$refs.deleteThemeConfirm.open();
}
},
async deleteSelectedTheme() {
//Delete Theme from DB
await api.themes.delete(this.theme.name);
//Get the new list of available from DB
this.availableThemes = await api.themes.requestAll();
this.$emit(DELETE_EVENT);
},
async saveThemes() {
this.$store.commit("setTheme", this.theme);
this.$emit(APPLY_EVENT, this.theme);
},
editTheme() {
this.$emit(EDIT_EVENT);
},
},
};
</script>
<style>
</style>

View File

@@ -1,38 +0,0 @@
<template>
<v-card>
<v-card-title>Last Scrapped JSON Data</v-card-title>
<v-card-text>
<VJsoneditor
@error="logError()"
v-model="lastRecipeJson"
height="1500px"
:options="jsonEditorOptions"
/>
</v-card-text>
</v-card>
</template>
<script>
import VJsoneditor from "v-jsoneditor";
import { api } from "@/api";
export default {
components: { VJsoneditor },
data() {
return {
lastRecipeJson: {},
jsonEditorOptions: {
mode: "code",
search: false,
mainMenuBar: false,
},
};
},
async mounted() {
this.lastRecipeJson = await api.meta.getLastJson();
},
};
</script>
<style>
</style>

View File

@@ -1,37 +0,0 @@
<template>
<v-card>
<v-card-title>Last Scrapped JSON Data</v-card-title>
<v-card-text>
<VJsoneditor
@error="logError()"
v-model="lastRecipeJson"
height="1500px"
:options="jsonEditorOptions"
/>
</v-card-text>
</v-card>
</template>
<script>
import VJsoneditor from "v-jsoneditor";
export default {
components: { VJsoneditor },
data() {
return {
lastRecipeJson: "",
jsonEditorOptions: {
mode: "code",
search: false,
mainMenuBar: false,
},
};
},
async mounted() {
this.lastRecipeJson = "Hello \n 123 \n 567"
},
};
</script>
<style>
</style>

View File

@@ -0,0 +1,48 @@
<template>
<v-select
dense
:items="allLanguages"
item-text="name"
:label="$t('settings.language')"
prepend-icon="mdi-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",
},
],
};
},
mounted() {
this.selectedItem = this.$store.getters.getActiveLang;
},
computed: {
allLanguages() {
return this.$store.getters.getAllLangs;
},
},
methods: {
setLanguage(selectedLanguage) {
this.$emit(SELECT_EVENT, selectedLanguage);
},
},
};
</script>

View File

@@ -45,7 +45,7 @@
</template>
<script>
import DataTable from "@/components/Admin/Backup/ImportSummaryDialog/DataTable";
import DataTable from "@/components/ImportSummaryDialog";
export default {
components: {
DataTable,

View File

@@ -31,11 +31,7 @@
v-on="on"
></v-text-field>
</template>
<DatePicker
v-model="startDate"
no-title
@input="menu2 = false"
/>
<DatePicker v-model="startDate" no-title @input="menu2 = false" />
</v-menu>
</v-col>
<v-col cols="12" lg="6" md="6" sm="12">
@@ -59,11 +55,7 @@
v-on="on"
></v-text-field>
</template>
<DatePicker
v-model="endDate"
no-title
@input="menu2 = false"
/>
<DatePicker v-model="endDate" no-title @input="menu2 = false" />
</v-menu>
</v-col>
</v-row>
@@ -87,7 +79,7 @@
<script>
const CREATE_EVENT = "created";
import DatePicker from "../UI/DatePicker";
import DatePicker from "@/components/FormHelpers/DatePicker";
import { api } from "@/api";
import utils from "@/utils";
import MealPlanCard from "./MealPlanCard";

View File

@@ -1,42 +1,51 @@
<template>
<v-toolbar class="card-btn" flat height="0" extension-height="0">
<template v-slot:extension>
<v-col></v-col>
<div v-if="open">
<v-btn
class="mr-2"
fab
dark
small
color="error"
@click="deleteRecipeConfrim"
>
<v-icon>mdi-delete</v-icon>
<v-expand-transition>
<v-toolbar
class="card-btn pt-1"
flat
:height="isSticky ? null : '0'"
:extension-height="isSticky ? '20' : '0'"
color="rgb(255, 0, 0, 0.0)"
>
<ConfirmationDialog
:title="$t('recipe.delete-recipe')"
:message="$t('recipe.delete-ConfirmationDialog')"
color="error"
icon="mdi-alert-circle"
ref="deleteRecipieConfirm"
v-on:confirm="deleteRecipe()"
/>
<template v-slot:extension>
<v-col></v-col>
<div v-if="open">
<v-btn
class="mr-2"
fab
dark
small
color="error"
@click="deleteRecipeConfrim"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
<v-btn class="mr-2" fab dark small color="success" @click="save">
<v-icon>mdi-content-save</v-icon>
</v-btn>
<v-btn class="mr-5" fab dark small color="secondary" @click="json">
<v-icon>mdi-code-braces</v-icon>
</v-btn>
</div>
<v-btn color="accent" fab dark small @click="editor">
<v-icon>mdi-square-edit-outline</v-icon>
</v-btn>
<Confirmation
:title="$t('recipe.delete-recipe')"
:message="$t('recipe.delete-confirmation')"
color="error"
icon="mdi-alert-circle"
ref="deleteRecipieConfirm"
v-on:confirm="deleteRecipe()"
/>
<v-btn class="mr-2" fab dark small color="success" @click="save">
<v-icon>mdi-content-save</v-icon>
</v-btn>
<v-btn class="mr-5" fab dark small color="secondary" @click="json">
<v-icon>mdi-code-braces</v-icon>
</v-btn>
</div>
<v-btn color="accent" fab dark small @click="editor">
<v-icon>mdi-square-edit-outline</v-icon>
</v-btn>
</template>
</v-toolbar>
</template>
</v-toolbar>
</v-expand-transition>
</template>
<script>
import Confirmation from "../../components/UI/Confirmation.vue";
import ConfirmationDialog from "@/components/UI/Dialogs/ConfirmationDialog.vue";
export default {
props: {
@@ -47,7 +56,25 @@ export default {
},
components: {
Confirmation,
ConfirmationDialog,
},
data() {
return {
stickyTop: 50,
scrollPosition: null,
};
},
mounted() {
window.addEventListener("scroll", this.updateScroll);
},
destroy() {
window.removeEventListener("scroll", this.updateScroll);
},
computed: {
isSticky() {
return this.scrollPosition >= 500;
},
},
methods: {
@@ -57,6 +84,9 @@ export default {
save() {
this.$emit("save");
},
updateScroll() {
this.scrollPosition = window.scrollY;
},
deleteRecipeConfrim() {
this.$refs.deleteRecipieConfirm.open();

View File

@@ -1,23 +1,39 @@
<template>
<v-card
class="mx-auto"
hover
:to="`/recipe/${slug}`"
max-height="125"
@click="$emit('selected')"
>
<v-list-item>
<v-list-item-avatar rounded size="125" class="mt-0 ml-n4">
<v-img :src="getImage(slug)"> </v-img>
</v-list-item-avatar>
<v-list-item-content class="align-self-start">
<v-list-item-title>
{{ name }}
</v-list-item-title>
<v-rating length="5" size="16" dense :value="rating"></v-rating>
<div class="text">
<v-list-item-action-text>
{{ description | truncate(115) }}
</v-list-item-action-text>
<v-list-item three-line>
<v-list-item-avatar
tile
size="125"
color="grey"
class="v-mobile-img rounded-sm my-0 ml-n4"
>
<v-img :src="getImage(slug)" lazy-src=""></v-img
></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">
<RecipeChips
:items="tags"
:title="false"
:limit="1"
:small="true"
:isCategory="false"
/>
<v-rating
color="secondary"
class="ml-auto"
background-color="secondary lighten-3"
dense
length="5"
size="15"
:value="rating"
></v-rating>
</div>
</v-list-item-content>
</v-list-item>
@@ -25,8 +41,12 @@
</template>
<script>
import RecipeChips from "@/components/Recipe/RecipeViewer/RecipeChips";
import { api } from "@/api";
export default {
components: {
RecipeChips,
},
props: {
name: String,
slug: String,
@@ -36,6 +56,9 @@ export default {
route: {
default: true,
},
tags: {
default: true,
},
},
methods: {
@@ -47,6 +70,11 @@ export default {
</script>
<style>
.v-mobile-img {
padding-top: 0;
padding-bottom: 0;
padding-left: 0;
}
.v-card--reveal {
align-items: center;
bottom: 0;

View File

@@ -11,7 +11,7 @@
<div>
Recipe Image
</div>
<UploadBtn
<TheUploadBtn
class="ml-auto"
url="none"
file-name="image"
@@ -44,12 +44,12 @@
<script>
const REFRESH_EVENT = "refresh";
const UPLOAD_EVENT = "upload";
import UploadBtn from "@/components/UI/UploadBtn";
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
import { api } from "@/api";
// import axios from "axios";
export default {
components: {
UploadBtn,
TheUploadBtn,
},
props: {
slug: String,

View File

@@ -33,7 +33,7 @@
class="my-3"
:label="$t('recipe.recipe-name')"
v-model="value.name"
:rules="[rules.required]"
:rules="[existsRule]"
>
</v-text-field>
<v-textarea
@@ -94,7 +94,7 @@
class="mr-n1"
slot="prepend"
color="error"
@click="removeIngredient(index)"
@click="removeByIndex(value.recipeIngredient, index)"
>
mdi-delete
</v-icon>
@@ -107,7 +107,7 @@
<v-btn color="secondary" fab dark small @click="addIngredient">
<v-icon>mdi-plus</v-icon>
</v-btn>
<BulkAdd @bulk-data="appendIngredients" />
<BulkAdd @bulk-data="addIngredient" />
<h2 class="mt-6">{{ $t("recipe.categories") }}</h2>
<CategoryTagSelector
@@ -140,7 +140,7 @@
color="white"
class="mr-2"
elevation="0"
@click="removeNote(index)"
@click="removeByIndex(value.notes, index)"
>
<v-icon color="error">mdi-delete</v-icon>
</v-btn>
@@ -183,7 +183,7 @@
color="white"
class="mr-2"
elevation="0"
@click="removeStep(index)"
@click="removeByIndex(value.recipeInstructions, index)"
>
<v-icon size="24" color="error">mdi-delete</v-icon>
</v-btn>
@@ -218,6 +218,7 @@
</template>
<script>
const UPLOAD_EVENT = "upload";
import draggable from "vuedraggable";
import utils from "@/utils";
import BulkAdd from "./BulkAdd";
@@ -225,6 +226,7 @@ import ExtrasEditor from "./ExtrasEditor";
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
import NutritionEditor from "./NutritionEditor";
import ImageUploadBtn from "./ImageUploadBtn.vue";
import { validators } from "@/mixins/validators";
export default {
components: {
BulkAdd,
@@ -237,26 +239,20 @@ export default {
props: {
value: Object,
},
mixins: [validators],
data() {
return {
drag: false,
fileObject: null,
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: {
uploadImage(fileObject) {
this.$emit("upload", fileObject);
this.$emit(UPLOAD_EVENT, fileObject);
},
toggleDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
let index = this.disabledSteps.indexOf(stepIndex);
const index = this.disabledSteps.indexOf(stepIndex);
if (index !== -1) {
this.disabledSteps.splice(index, 1);
}
@@ -265,66 +261,40 @@ export default {
}
},
isDisabled(stepIndex) {
if (this.disabledSteps.includes(stepIndex)) {
return "disabled-card";
} else {
return;
}
return this.disabledSteps.includes(stepIndex) ? "disabled-card" : null;
},
generateKey(item, index) {
return utils.generateUniqueKey(item, index);
},
appendIngredients(ingredients) {
this.value.recipeIngredient.push(...ingredients);
},
addIngredient() {
let list = this.value.recipeIngredient;
list.push("");
},
removeIngredient(index) {
this.value.recipeIngredient.splice(index, 1);
addIngredient(ingredients = null) {
if (ingredients) {
this.value.recipeIngredient.push(...ingredients);
} else {
this.value.recipeIngredient.push("");
}
},
appendSteps(steps) {
let processSteps = [];
steps.forEach(element => {
processSteps.push({ text: element });
});
this.value.recipeInstructions.push(...processSteps);
this.value.recipeInstructions.push(
...steps.map(x => ({
text: x,
}))
);
},
addStep() {
let list = this.value.recipeInstructions;
list.push({ text: "" });
this.value.recipeInstructions.push({ text: "" });
},
removeStep(index) {
this.value.recipeInstructions.splice(index, 1);
},
addNote() {
let list = this.value.notes;
list.push({ text: "" });
},
removeNote(index) {
this.value.notes.splice(index, 1);
},
removeCategory(index) {
this.value.recipeCategory.splice(index, 1);
},
removeTags(index) {
this.value.tags.splice(index, 1);
this.value.notes.push({ text: "" });
},
saveExtras(extras) {
this.value.extras = extras;
},
removeByIndex(list, index) {
list.splice(index, 1);
},
validateRecipe() {
if (this.$refs.form.validate()) {
return true;
} else {
return false;
}
return this.$refs.form.validate();
},
},
};

View File

@@ -1,57 +1,26 @@
<template>
<v-card
color="accent"
class="custom-transparent d-flex justify-start align-center text-center "
class="custom-transparent d-flex justify-start align-center text-center time-card-flex"
tile
:width="`${timeCardWidth}`"
height="55"
v-if="totalTime || prepTime || performTime"
v-if="showCards"
>
<v-card flat color="rgb(255, 0, 0, 0.0)">
<v-icon large color="white" class="mx-2"> mdi-clock-outline </v-icon>
</v-card>
<v-divider vertical color="white" class="py-1" v-if="totalTime">
</v-divider>
<v-card flat color="rgb(255, 0, 0, 0.0)" class=" my-2 " v-if="totalTime">
<v-card-text class="white--text">
<div>
<strong> {{ $t("recipe.total-time") }} </strong>
</div>
<div>{{ totalTime }}</div>
</v-card-text>
</v-card>
<v-divider vertical color="white" class="py-1" v-if="prepTime"> </v-divider>
<v-card
v-for="(time, index) in allTimes"
:key="index"
class="d-flex justify-start align-center text-center time-card-flex"
flat
color="rgb(255, 0, 0, 0.0)"
class="white--text my-2 "
v-if="prepTime"
>
<v-card-text class="white--text">
<v-card-text class="caption white--text py-2">
<div>
<strong> {{ $t("recipe.prep-time") }} </strong>
<strong> {{ time.name }} </strong>
</div>
<div>{{ prepTime }}</div>
</v-card-text>
</v-card>
<v-divider vertical color="white" class="my-1" v-if="performTime">
</v-divider>
<v-card
flat
color="rgb(255, 0, 0, 0.0)"
class="white--text py-2 "
v-if="performTime"
>
<v-card-text class="white--text">
<div>
<strong> {{ $t("recipe.perform-time") }} </strong>
</div>
<div>{{ performTime }}</div>
<div>{{ time.value }}</div>
</v-card-text>
</v-card>
</v-card>
@@ -64,52 +33,52 @@ export default {
totalTime: String,
performTime: String,
},
watch: {
showCards(val) {
console.log(val);
},
},
computed: {
timeLength() {
let times = [];
let timeArray = [this.totalTime, this.prepTime, this.performTime];
timeArray.forEach(element => {
if (element) {
times.push(element);
}
});
return times.length;
showCards() {
return [this.prepTime, this.totalTime, this.performTime].some(
x => !this.isEmpty(x)
);
},
iconColumn() {
switch (this.timeLength) {
case 0:
return null;
case 1:
return 4;
case 2:
return 3;
case 3:
return 2;
default:
return 1;
}
allTimes() {
return [
this.validateTotalTime,
this.validatePrepTime,
this.validatePerformTime,
].filter(x => x !== null);
},
timeCardWidth() {
let timeArray = [this.totalTime, this.prepTime, this.performTime];
let width = 80;
timeArray.forEach(element => {
if (element) {
width += 95;
}
});
if (this.$vuetify.breakpoint.name === "xs") {
return "100%";
}
return `${width}px`;
validateTotalTime() {
return !this.isEmpty(this.totalTime)
? { name: this.$t("recipe.total-time"), value: this.totalTime }
: null;
},
validatePrepTime() {
return !this.isEmpty(this.prepTime)
? { name: this.$t("recipe.prep-time"), value: this.prepTime }
: null;
},
validatePerformTime() {
return !this.isEmpty(this.performTime)
? { name: this.$t("recipe.perform-time"), value: this.performTime }
: null;
},
},
methods: {
isEmpty(str) {
return !str || str.length === 0;
},
},
};
</script>
<style scoped>
.time-card-flex {
width: fit-content;
}
.custom-transparent {
opacity: 0.7;
}

View File

@@ -73,6 +73,7 @@
:slug="recipe.slug"
:rating="recipe.rating"
:image="recipe.image"
:tags="recipe.tags"
/>
</v-col>
</v-row>

View File

@@ -8,7 +8,7 @@
@keydown.esc="cancel"
>
<v-card>
<v-app-bar v-if="Boolean(title)" :color="color" dense flat dark>
<v-app-bar v-if="Boolean(title)" :color="color" dense dark>
<v-icon v-if="Boolean(icon)" left> {{ icon }}</v-icon>
<v-toolbar-title v-text="title" />
</v-app-bar>
@@ -36,13 +36,13 @@
const CLOSE_EVENT = "close";
const OPEN_EVENT = "open";
/**
* Confirmation Component used to add a second validaion step to an action.
* ConfirmationDialog Component used to add a second validaion step to an action.
* @version 1.0.1
* @author [zackbcom](https://github.com/zackbcom)
* @since Version 1.0.0
*/
export default {
name: "Confirmation",
name: "ConfirmationDialog",
props: {
/**
* Message to be in body.

View File

@@ -1,87 +0,0 @@
<template>
<div class="text-center">
<v-menu
transition="slide-x-transition"
bottom
right
offset-y
close-delay="200"
>
<template v-slot:activator="{ on, attrs }">
<v-btn v-bind="attrs" v-on="on" icon>
<v-icon>mdi-translate</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item-group v-model="selectedItem" color="primary">
<v-list-item
v-for="(item, i) in allLanguages"
:key="i"
link
@click="setLanguage(item.value)"
>
<v-list-item-content>
<v-list-item-title>
{{ item.name }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</v-menu>
</div>
</template>
<script>
const SELECT_EVENT = "select-lang";
export default {
props: {
siteSettings: {
default: false,
},
},
data: function() {
return {
selectedItem: 0,
items: [
{
name: "English",
value: "en-US",
},
],
};
},
mounted() {
let active = this.$store.getters.getActiveLang;
this.allLanguages.forEach((element, index) => {
if (element.value === active) {
this.selectedItem = index;
return;
}
});
},
computed: {
allLanguages() {
return this.$store.getters.getAllLangs;
},
},
methods: {
setLanguage(selectedLanguage) {
if (this.siteSettings) {
this.$emit(SELECT_EVENT, selectedLanguage);
} else {
this.$store.dispatch("setLang", {
currentVueComponent: this,
language: selectedLanguage });
}
},
},
};
</script>
<style>
.menu-text {
text-align: left !important;
}
</style>

View File

@@ -1,66 +0,0 @@
<template>
<v-dialog
v-model="dialog"
max-width="900px"
:fullscreen="$vuetify.breakpoint.xsOnly"
>
<v-card>
<v-toolbar dark color="primary" v-show="$vuetify.breakpoint.xsOnly">
<v-btn icon dark @click="dialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title>{{ title }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items></v-toolbar-items>
</v-toolbar>
<v-card-title v-show="$vuetify.breakpoint.smAndUp">
{{ title }}
</v-card-title>
<v-card-text class="mt-3">
<v-row>
<v-col>
<v-alert outlined dense type="success">
<h4>{{ successHeader }}</h4>
<p v-for="success in this.success" :key="success" class="my-1">
- {{ success }}
</p>
</v-alert>
</v-col>
<v-col>
<v-alert v-if="failed[0]" outlined dense type="error">
<h4>{{ failedHeader }}</h4>
<p v-for="fail in this.failed" :key="fail" class="my-1">
- {{ fail }}
</p>
</v-alert>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
export default {
props: {
title: String,
successHeader: String,
success: Array,
failedHeader: String,
failed: Array,
},
data() {
return {
dialog: false,
};
},
methods: {
open() {
this.dialog = true;
},
},
};
</script>
<style>
</style>

View File

@@ -35,7 +35,7 @@
<v-icon>mdi-magnify</v-icon>
</v-btn>
<SiteMenu />
<TheSiteMenu />
</v-app-bar>
<v-app-bar
v-else
@@ -67,13 +67,13 @@
<v-icon>mdi-magnify</v-icon>
</v-btn>
<SiteMenu />
<TheSiteMenu />
</v-app-bar>
</div>
</template>
<script>
import SiteMenu from "@/components/UI/SiteMenu";
import TheSiteMenu from "@/components/UI/TheSiteMenu";
import SearchBar from "@/components/UI/Search/SearchBar";
import SearchDialog from "@/components/UI/Search/SearchDialog";
import { user } from "@/mixins/user";
@@ -82,7 +82,7 @@ export default {
mixins: [user],
components: {
SiteMenu,
TheSiteMenu,
SearchBar,
SearchDialog,
},