Continue converting to composition api

This commit is contained in:
Dan Elbert 2024-10-02 14:34:50 -05:00
parent 0d35f50dbf
commit 67c23015ab
39 changed files with 1013 additions and 1324 deletions

View File

@ -42,9 +42,19 @@ class ApplicationController < ActionController::Base
if owner if owner
yield if block_given? yield if block_given?
else else
respond_to do |format|
format.html do
flash[:warning] = "Operation Not Permitted" flash[:warning] = "Operation Not Permitted"
redirect_to root_path redirect_to root_path
end end
format.json do
render json: { error: "Operation Not Permitted" }, status: current_user.nil? ? :unauthorized : :forbidden
end
end
end
end end
def current_user def current_user

View File

@ -34,8 +34,10 @@ class CalculatorController < ApplicationController
begin begin
input_unit = UnitConversion.parse(input) input_unit = UnitConversion.parse(input)
input_unit.unitwise
rescue UnitConversion::UnparseableUnitError => e rescue UnitConversion::UnparseableUnitError => e
data[:errors][:input] << 'invalid string' data[:errors][:input] << e.message
input_unit = nil
end end
if !input_unit.nil? if !input_unit.nil?

View File

@ -50,7 +50,7 @@ class UsersController < ApplicationController
if @user.save if @user.save
set_current_user(@user) set_current_user(@user)
format.html { redirect_to root_path, notice: 'User created.' } format.html { redirect_to root_path, notice: 'User created.' }
format.json { render :show, status: :created, location: @user } format.json { render json: UserSerializer.for(@user), status: :created, location: @user }
else else
format.html { render :new } format.html { render :new }
format.json { render json: @user.errors, status: :unprocessable_entity } format.json { render json: @user.errors, status: :unprocessable_entity }
@ -68,7 +68,7 @@ class UsersController < ApplicationController
respond_to do |format| respond_to do |format|
if @user.update(user_params) if @user.update(user_params)
format.html { redirect_to root_path, notice: 'User updated.' } format.html { redirect_to root_path, notice: 'User updated.' }
format.json { render :show, status: :created, location: @user } format.json { render json: UserSerializer.for(@user) , status: :created, location: @user }
else else
format.html { render :edit } format.html { render :edit }
format.json { render json: @user.errors, status: :unprocessable_entity } format.json { render json: @user.errors, status: :unprocessable_entity }

View File

@ -145,7 +145,7 @@
emit("update:modelValue", newValue); emit("update:modelValue", newValue);
if (newValue.length >= Math.max(1, props.minLength)) { if (newValue.length >= Math.max(1, props.minLength)) {
this.updateOptions(newValue); updateOptions(newValue);
} else { } else {
isListOpen.value = false; isListOpen.value = false;
} }

View File

@ -1,31 +1,33 @@
<template> <template>
<app-modal :open="open" :title="message" @dismiss="runCancel"> <app-modal :open="open" :title="title" @dismiss="runCancel">
<p class="is-size-5">{{ message }}</p>
<template #footer>
<div class="buttons"> <div class="buttons">
<button type="button" class="button is-primary" @click="runConfirm">OK</button> <button type="button" class="button is-primary" @click="runConfirm">OK</button>
<button type="button" class="button" @click="runCancel">Cancel</button> <button type="button" class="button" @click="runCancel">Cancel</button>
</div> </div>
</template>
</app-modal> </app-modal>
</template> </template>
<script setup> <script setup>
const emit = defineEmits(["cancel", "confirm"]);
const props = defineProps({ const props = defineProps({
cancel: {
type: Function,
required: true
},
confirm: {
type: Function,
required: true
},
message: { message: {
type: String, type: String,
required: false, required: false,
default: 'Are you sure?' default: 'Are you sure?'
}, },
title: {
type: String,
required: false,
default: "Confirm"
},
open: { open: {
type: Boolean, type: Boolean,
required: true required: true
@ -33,11 +35,11 @@ const props = defineProps({
}); });
function runConfirm() { function runConfirm() {
props.confirm(); emit("confirm");
} }
function runCancel() { function runCancel() {
props.cancel(); emit("cancel");
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<app-text-field :value="stringValue" @input="input" :label="label" type="date"></app-text-field> <app-text-field :value="stringValue" @update:modelValue="input" :label="label" type="date"></app-text-field>
</template> </template>
<script setup> <script setup>

View File

@ -1,6 +1,6 @@
<template> <template>
<Teleport to="body"> <Teleport to="body">
<div :class="['popup', 'modal', { 'is-wide': wide, 'is-active': open && error === null }]"> <div :class="['modal', { 'is-wide': wide, 'is-active': open && error === null }]">
<div class="modal-background" @click="close"></div> <div class="modal-background" @click="close"></div>
<div class="modal-card"> <div class="modal-card">
<header class="modal-card-head"> <header class="modal-card-head">
@ -13,6 +13,11 @@
<section class="modal-card-body"> <section class="modal-card-body">
<slot></slot> <slot></slot>
</section> </section>
<footer class="modal-card-foot">
<slot name="footer">
</slot>
</footer>
</div> </div>
</div> </div>
</Teleport> </Teleport>

View File

@ -1,7 +1,7 @@
<template> <template>
<nav v-show="totalPages > 1 || showWithSinglePage" class="pagination" role="navigation" :aria-label="pagedItemName + ' page navigation'"> <nav v-show="totalPages > 1 || showWithSinglePage" class="pagination" role="navigation" :aria-label="pagedItemName + ' page navigation'">
<a class="pagination-previous" :title="isFirstPage ? 'This is the first page' : ''" :disabled="isFirstPage" @click.prevent="changePage(currentPage - 1)">Previous</a> <a :class="{'pagination-previous': true, 'is-disabled': isFirstPage}" :title="isFirstPage ? 'This is the first page' : ''" @click.prevent="changePage(currentPage - 1)">Previous</a>
<a class="pagination-next" :title="isLastPage ? 'This is the last page' : ''" :disabled="isLastPage" @click.prevent="changePage(currentPage + 1)">Next page</a> <a :class="{'pagination-next': true, 'is-disabled': isLastPage}" :title="isLastPage ? 'This is the last page' : ''" @click.prevent="changePage(currentPage + 1)">Next page</a>
<ul class="pagination-list"> <ul class="pagination-list">
<li v-for="page in pageItems" :key="page"> <li v-for="page in pageItems" :key="page">
<a v-if="page > 0" class="pagination-link" :class="{'is-current': page === currentPage}" href="#" @click.prevent="changePage(page)">{{page}}</a> <a v-if="page > 0" class="pagination-link" :class="{'is-current': page === currentPage}" href="#" @click.prevent="changePage(page)">{{page}}</a>

View File

@ -21,7 +21,7 @@
</div> </div>
<div class="field-body"> <div class="field-body">
<div class="field"> <div class="field">
<app-rating readonly :value="log.rating"></app-rating> <app-rating readonly :model-value="log.rating"></app-rating>
</div> </div>
</div> </div>
</div> </div>

View File

@ -43,34 +43,28 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, ref } from "vue";
import { useMediaQueryStore } from "../stores/mediaQuery";
import RecipeEditIngredientItem from "./RecipeEditIngredientItem"; import RecipeEditIngredientItem from "./RecipeEditIngredientItem";
import { mapState } from "pinia"; const mediaQueryStore = useMediaQueryStore();
import { useMediaQueryStore } from "../stores/mediaQuery";
export default { const props = defineProps({
props: {
ingredients: { ingredients: {
required: true, required: true,
type: Array type: Array
} }
}, });
data() { const isBulkEditing = ref(false);
return { const bulkEditText = ref(null);
isBulkEditing: false, const isMobile = computed(() => mediaQueryStore.mobile);
bulkEditText: null const visibleIngredients = computed(() => props.ingredients.filter(i => i._destroy !== true));
};
},
computed: { const bulkIngredientPreview = computed(() => {
...mapState(useMediaQueryStore, { if (bulkEditText.value === null || bulkEditText.value === "") {
isMobile: store => store.mobile
}),
bulkIngredientPreview() {
if (this.bulkEditText === null) {
return []; return [];
} }
@ -85,7 +79,7 @@
}; };
const parsed = []; const parsed = [];
const lines = this.bulkEditText.replace("\r", "").split("\n"); const lines = bulkEditText.value.replace("\r", "").split("\n");
for (let line of lines) { for (let line of lines) {
if (line.length === 0) { continue; } if (line.length === 0) { continue; }
@ -102,16 +96,10 @@
} }
return parsed; return parsed;
}, });
visibleIngredients() { function createIngredient() {
return this.ingredients.filter(i => i._destroy !== true); const sort_orders = props.ingredients.map(i => i.sort_order);
}
},
methods: {
createIngredient() {
const sort_orders = this.ingredients.map(i => i.sort_order);
sort_orders.push(0); sort_orders.push(0);
const next_sort_order = Math.max(...sort_orders) + 5; const next_sort_order = Math.max(...sort_orders) + 5;
@ -124,27 +112,27 @@
ingredient_id: null, ingredient_id: null,
sort_order: next_sort_order sort_order: next_sort_order
}; };
}, }
addIngredient() { function addIngredient() {
this.ingredients.push(this.createIngredient()); props.ingredients.push(createIngredient());
}, }
deleteFood(food) { function deleteFood(food) {
if (food.id) { if (food.id) {
food._destroy = true; food._destroy = true;
} else { } else {
const idx = this.ingredients.findIndex(i => i === food); const idx = props.ingredients.findIndex(i => i === food);
this.ingredients.splice(idx, 1); props.ingredients.splice(idx, 1);
}
} }
},
bulkEditIngredients() { function bulkEditIngredients() {
this.isBulkEditing = true; isBulkEditing.value = true;
let text = []; let text = [];
for (let item of this.visibleIngredients) { for (let item of visibleIngredients.value) {
text.push( text.push(
item.quantity + " " + item.quantity + " " +
(item.units || "-") + " " + (item.units || "-") + " " +
@ -154,16 +142,16 @@
); );
} }
this.bulkEditText = text.join("\n"); bulkEditText.value = text.join("\n");
}, }
cancelBulkEditing() { function cancelBulkEditing() {
this.isBulkEditing = false; isBulkEditing.value = false;
}, }
saveBulkEditing() { function saveBulkEditing() {
const parsed = this.bulkIngredientPreview.filter(i => i !== null); const parsed = bulkIngredientPreview.value.filter(i => i !== null);
const existing = [...this.ingredients]; const existing = [...props.ingredients];
const newList = []; const newList = [];
for (let parsedIngredient of parsed) { for (let parsedIngredient of parsed) {
@ -182,7 +170,7 @@
} }
if (newIngredient === null) { if (newIngredient === null) {
newIngredient = this.createIngredient(); newIngredient = createIngredient();
} }
newIngredient.quantity = parsedIngredient.quantity; newIngredient.quantity = parsedIngredient.quantity;
@ -196,20 +184,14 @@
newList.push({id: oldExisting.id, _destroy: true}); newList.push({id: oldExisting.id, _destroy: true});
} }
this.ingredients.splice(0); props.ingredients.splice(0);
let sortIdx = 0; let sortIdx = 0;
for (let n of newList) { for (let n of newList) {
n.sort_order = sortIdx++; n.sort_order = sortIdx++;
this.ingredients.push(n); props.ingredients.push(n);
} }
this.isBulkEditing = false; isBulkEditing.value = false;
}
},
components: {
RecipeEditIngredientItem
}
} }
</script> </script>

View File

@ -44,12 +44,13 @@
</div> </div>
</template> </template>
<script> <script setup>
import { useTemplateRef, watch } from "vue";
import api from "../lib/Api"; import api from "../lib/Api";
export default { const emit = defineEmits(["deleteFood"]);
props: { const props = defineProps({
ingredient: { ingredient: {
required: true, required: true,
type: Object type: Object
@ -59,40 +60,34 @@
type: Boolean, type: Boolean,
default: false default: false
} }
}, });
methods: { const autocompleteElement = useTemplateRef("autocomplete");
deleteFood(ingredient) {
this.$emit("deleteFood", ingredient);
},
updateSearchItems(text) { watch(props.ingredient.name, (val) => {
if (props.ingredient.ingredient && props.ingredient.ingredient.name !== val) {
props.ingredient.ingredient_id = null;
props.ingredient.ingredient = null;
}
});
function deleteFood(ingredient) {
emit("deleteFood", ingredient);
}
function updateSearchItems(text) {
return api.getSearchIngredients(text); return api.getSearchIngredients(text);
},
searchItemSelected(ingredient) {
this.ingredient.ingredient_id = ingredient.id;
this.ingredient.ingredient = ingredient;
this.ingredient.name = ingredient.name;
},
nameClick() {
if (this.ingredient.ingredient_id === null && this.ingredient.name !== null && this.ingredient.name.length > 2) {
this.$refs.autocomplete.updateOptions(this.ingredient.name);
} }
}
},
watch: { function searchItemSelected(ingredient) {
'ingredient.name': function(val) { props.ingredient.ingredient_id = ingredient.id;
if (this.ingredient.ingredient && this.ingredient.ingredient.name !== val) { props.ingredient.ingredient = ingredient;
this.ingredient.ingredient_id = null; props.ingredient.name = ingredient.name;
this.ingredient.ingredient = null;
} }
}
},
components: { function nameClick() {
if (props.ingredient.ingredient_id === null && props.ingredient.name !== null && props.ingredient.name.length > 2) {
autocompleteElement.updateOptions(props.ingredient.name);
} }
} }

View File

@ -38,7 +38,7 @@
Ingredients Ingredients
<button class="button is-small is-primary" type="button" @click="showConvertDialog = true">Convert</button> <button class="button is-small is-primary" type="button" @click="showConvertDialog = true">Convert</button>
<app-dropdown :open="addToTasksMenuOpen" label="Add to list" button-class="is-small is-primary" @open="addToTasksMenuOpen = true" @close="addToTasksMenuOpen = false"> <app-dropdown :open="addToTasksMenuOpen" label="Add to list" button-class="is-small is-primary" @open="addToTasksMenuOpen = true" @close="addToTasksMenuOpen = false">
<button class="button primary" v-for="tl in taskLists" :key="tl.id" @click="addRecipeToList(tl)"> <button class="button primary" v-for="tl in taskStore.taskLists" :key="tl.id" @click="addRecipeToList(tl)">
{{tl.name}} {{tl.name}}
</button> </button>
</app-dropdown> </app-dropdown>
@ -150,31 +150,32 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import api from "../lib/Api"; import api from "../lib/Api";
import { mapActions, mapState } from "pinia";
import { useTaskStore } from "../stores/task"; import { useTaskStore } from "../stores/task";
export default { const taskStore = useTaskStore();
props: { const router = useRouter();
const props = defineProps({
recipe: { recipe: {
required: true, required: true,
type: Object type: Object
} }
}, });
data() { const showNutrition = ref(false);
return { const showConvertDialog = ref(false);
showNutrition: false, const addToTasksMenuOpen = ref(false);
showConvertDialog: false,
addToTasksMenuOpen: false,
scaleValue: '1', const scaleValue = ref('1');
systemConvertValue: "", const systemConvertValue = ref('');
unitConvertValue: "", const unitConvertValue = ref('');
scaleOptions: [ const scaleOptions = [
'1/4', '1/4',
'1/3', '1/3',
'1/2', '1/2',
@ -185,84 +186,60 @@
'2', '2',
'3', '3',
'4' '4'
] ];
};
},
computed: { const timeDisplay = computed(() => {
...mapState(useTaskStore, [ let a = formatMinutes(props.recipe.active_time);
'taskLists' const t = formatMinutes(props.recipe.total_time);
]),
timeDisplay() {
let a = this.formatMinutes(this.recipe.active_time);
const t = this.formatMinutes(this.recipe.total_time);
if (a) { if (a) {
a = ` (${a} active)`; a = ` (${a} active)`;
} }
return t + a; return t + a;
}, });
sourceUrl() { const sourceUrl = computed(() => {
try { try {
return new URL(this.recipe.source); return new URL(props.recipe.source);
} catch(err) { } catch(err) {
return null; return null;
} }
},
isSourceUrl() {
return this.sourceUrl !== null;
},
sourceText() {
if (this.isSourceUrl) {
return this.sourceUrl.host;
} else {
return this.recipe.source;
}
}
},
watch: {
recipe: {
handler: function(r) {
if (r) {
this.scaleValue = r.converted_scale || '1';
this.systemConvertValue = r.converted_system;
this.unitConvertValue = r.converted_unit;
}
},
immediate: true
}
},
methods: {
...mapActions(useTaskStore, [
'ensureTaskLists',
'setCurrentTaskList'
]),
addRecipeToList(list) {
api.addRecipeToTaskList(list.id, this.recipe.id)
.then(() => {
this.setCurrentTaskList(list);
this.$router.push({name: 'task_lists'})
}); });
},
convert() { const isSourceUrl = computed(() => sourceUrl.value !== null);
this.showConvertDialog = false; const sourceText = computed(() => isSourceUrl.value ? sourceUrl.value.host : props.recipe.source);
this.$router.push({name: 'recipe', query: { scale: this.scaleValue, system: this.systemConvertValue, unit: this.unitConvertValue }});
},
roundValue(v) { watch(props.recipe, (r) => {
if (r) {
scaleValue.value = r.converted_scale || '1';
systemConvertValue.value = r.converted_system;
unitConvertValue.value = r.converted_unit;
}
}, { immediate: true });
onMounted(() => {
taskStore.ensureTaskLists();
});
function addRecipeToList(list) {
api.addRecipeToTaskList(list.id, props.recipe.id)
.then(() => {
taskStore.setCurrentTaskList(list);
router.push({name: 'task_lists'})
});
}
function convert() {
showConvertDialog.value = false;
router.push({name: 'recipe', query: { scale: scaleValue.value, system: systemConvertValue.value, unit: unitConvertValue.value }});
}
function roundValue(v) {
return parseFloat(v).toFixed(2); return parseFloat(v).toFixed(2);
}, }
formatMinutes(min) { function formatMinutes(min) {
if (min) { if (min) {
const partUnits = [ const partUnits = [
{unit: "d", minutes: 60 * 24}, {unit: "d", minutes: 60 * 24},
@ -287,12 +264,6 @@
return ""; return "";
} }
} }
},
mounted() {
this.ensureTaskLists();
}
}
</script> </script>

View File

@ -24,38 +24,41 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { import { onMounted, useTemplateRef } from "vue";
props: {
const emit = defineEmits(["save"]);
const props = defineProps({
taskItem: { taskItem: {
required: true, required: true,
type: Object type: Object
} }
}, });
methods: { const nameElement = useTemplateRef("nameInput");
inputKeydown(evt) {
onMounted(() => focus());
function inputKeydown(evt) {
switch (evt.key) { switch (evt.key) {
case "Enter": case "Enter":
evt.preventDefault(); evt.preventDefault();
this.save(); save();
}
} }
},
save() { function save() {
this.$emit("save", this.taskItem); emit("save", props.taskItem);
}, }
focus() { function focus() {
this.$refs.nameInput.focus(); nameElement.value.focus();
} }
},
mounted() { defineExpose({
this.focus(); focus
} });
}
</script> </script>

View File

@ -60,130 +60,100 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, ref, useTemplateRef } from "vue";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
import { mapActions } from "pinia";
import { useTaskStore } from "../stores/task"; import { useTaskStore } from "../stores/task";
import { useLoadResource } from "../lib/useLoadResource";
import TaskItemEdit from "./TaskItemEdit"; import TaskItemEdit from "./TaskItemEdit";
const newItemTemplate = function(listId) { const { loadResource } = useLoadResource();
return { const taskStore = useTaskStore();
task_list_id: listId, const itemEditElement = useTemplateRef("itemEdit");
name: '',
quantity: '',
completed: false
};
};
export default { const props = defineProps({
props: {
taskList: { taskList: {
required: true, required: true,
type: Object type: Object
} }
}, });
data() { const showAddItem = ref(false);
const newItem = ref(null);
const newItemValidationErrors = ref({});
const completedTaskItems = computed(() => (props.taskList ? props.taskList.task_items : []).filter(i => i.completed));
const uncompletedTaskItems = computed(() => (props.taskList ? props.taskList.task_items : []).filter(i => !i.completed));
const completedItemCount = computed(() => completedTaskItems.value.length);
const uncompletedItemCount = computed(() => uncompletedTaskItems.value.length);
const taskItems = computed(() => uncompletedTaskItems.value.concat(completedTaskItems.value));
function newItemTemplate() {
return { return {
showAddItem: false, task_list_id: null,
newItem: null, name: '',
newItemValidationErrors: {} quantity: '',
completed: false
}; };
},
computed: {
completedItemCount() {
return this.taskList === null ? 0 : this.taskList.task_items.filter(i => i.completed).length;
},
uncompletedItemCount() {
return this.taskList === null ? 0 : this.taskList.task_items.filter(i => !i.completed).length;
},
completedTaskItems() {
return (this.taskList ? this.taskList.task_items : []).filter(i => i.completed);
},
uncompletedTaskItems() {
return (this.taskList ? this.taskList.task_items : []).filter(i => !i.completed);
},
taskItems() {
return this.uncompletedTaskItems.concat(this.completedTaskItems);
} }
},
methods: { function save() {
...mapActions(useTaskStore, [ newItem.value.task_list_id = props.taskList.id;
'createTaskItem', loadResource(
'updateTaskItem', taskStore.createTaskItem(newItem.value)
'deleteTaskItems',
'completeTaskItems'
]),
save() {
this.loadResource(
this.createTaskItem(this.newItem)
.then(() => { .then(() => {
this.newItem = newItemTemplate(this.taskList.id); newItem.value = newItemTemplate();
this.$refs.itemEdit.focus(); itemEditElement.value.focus();
}) })
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.newItemValidationErrors = err.validationErrors())) .catch(Errors.onlyFor(Errors.ApiValidationError, err => newItemValidationErrors.value = err.validationErrors()))
) )
}, }
toggleItem(i) { function toggleItem(i) {
this.loadResource( loadResource(
this.completeTaskItems({ taskStore.completeTaskItems({
taskList: this.taskList, taskList: props.taskList,
taskItems: [i], taskItems: [i],
completed: !i.completed completed: !i.completed
}) })
); );
}, }
toggleShowAddItem() { function toggleShowAddItem() {
this.newItem = newItemTemplate(this.taskList.id); newItem.value = newItemTemplate();
this.showAddItem = !this.showAddItem; showAddItem.value = !showAddItem.value;
}, }
completeAllItems() { function completeAllItems() {
const toComplete = this.taskList.task_items.filter(i => !i.completed); const toComplete = props.taskList.task_items.filter(i => !i.completed);
this.loadResource( loadResource(
this.completeTaskItems({ taskStore.completeTaskItems({
taskList: this.taskList, taskList: props.taskList,
taskItems: toComplete, taskItems: toComplete,
completed: true completed: true
}) })
) )
}, }
unCompleteAllItems() { function unCompleteAllItems() {
const toUnComplete = this.taskList.task_items.filter(i => i.completed); const toUnComplete = props.taskList.task_items.filter(i => i.completed);
this.loadResource( loadResource(
this.completeTaskItems({ taskStore.completeTaskItems({
taskList: this.taskList, taskList: props.taskList,
taskItems: toUnComplete, taskItems: toUnComplete,
completed: false completed: false
}) })
) )
}, }
deleteCompletedItems() { function deleteCompletedItems() {
this.loadResource( loadResource(
this.deleteTaskItems({ taskStore.deleteTaskItems({
taskList: this.taskList, taskList: props.taskList,
taskItems: this.taskList.task_items.filter(i => i.completed) taskItems: props.taskList.task_items.filter(i => i.completed)
}) })
); );
},
},
components: {
TaskItemEdit
}
} }
</script> </script>

View File

@ -14,10 +14,13 @@
</template> </template>
<script> <script setup>
export default { import { ref } from "vue";
props: {
const emit = defineEmits(["select", "delete"]);
const props = defineProps({
taskList: { taskList: {
type: Object, type: Object,
required: true required: true
@ -28,25 +31,18 @@
required: false, required: false,
default: false default: false
} }
}, });
data() { const hovering = ref(false);
return { const confirmingDelete = ref(false);
hovering: false,
confirmingDelete: false
};
},
methods: { function selectList() {
selectList() { emit("select", props.taskList);
this.$emit("select", this.taskList);
},
deleteList() {
this.confirmingDelete = false;
this.$emit("delete", this.taskList);
}
} }
function deleteList() {
confirmingDelete.value = false;
emit("delete", props.taskList);
} }
</script> </script>

View File

@ -14,10 +14,11 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { const emit = defineEmits(["save"]);
props: {
const props = defineProps({
taskList: { taskList: {
required: true, required: true,
type: Object type: Object
@ -28,20 +29,17 @@
type: Object, type: Object,
default: function() { return {}; } default: function() { return {}; }
} }
}, });
methods: { function save() {
save() { emit("save");
this.$emit("save"); }
},
nameKeydownHandler(evt) { function nameKeydownHandler(evt) {
switch (evt.key) { switch (evt.key) {
case "Enter": case "Enter":
evt.preventDefault(); evt.preventDefault();
this.save(); save();
}
}
} }
} }

View File

@ -5,10 +5,7 @@
</div> </div>
</template> </template>
<script> <script setup>
export default {
}
</script> </script>

View File

@ -19,11 +19,7 @@
</div> </div>
</template> </template>
<script> <script setup>
export default {
}
</script> </script>

View File

@ -2,11 +2,8 @@
</template> </template>
<script> <script setup>
export default {
}
</script> </script>

View File

@ -29,24 +29,21 @@
</template> </template>
<script> <script setup>
import { onBeforeMount, ref } from "vue";
import { useLoadResource } from "../lib/useLoadResource";
import api from "../lib/Api"; import api from "../lib/Api";
export default { const { loadResource } = useLoadResource();
data() { const userList = ref([]);
return {
userList: []
};
},
created() { onBeforeMount(() => {
this.loadResource( loadResource(
api.getAdminUserList() api.getAdminUserList()
.then(list => this.userList = list) .then(list => userList.value = list)
); );
} });
}
</script> </script>

View File

@ -40,93 +40,64 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, ref, watch } from "vue";
import api from "../lib/Api"; import api from "../lib/Api";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import { useLoadResource } from "../lib/useLoadResource";
export default { const { loadResource } = useLoadResource();
data() {
return {
input: '',
outputUnit: '',
ingredient_name: '',
ingredient: null,
density: '',
output: '',
errors: {}
};
},
computed: { const input = ref("");
inputErrors() { const outputUnit = ref("");
if (this.errors.input && this.errors.input.length > 0) { const ingredient_name = ref("");
return this.errors.input.join(", "); const ingredient = ref(null);
} else { const density = ref("");
return null; const output = ref("");
} const errors = ref({});
},
outputUnitErrors() { const inputErrors = computed(() => getErrors("input"));
if (this.errors.output_unit && this.errors.output_unit.length > 0) { const outputUnitErrors = computed(() => getErrors("output_unit"));
return this.errors.output_unit.join(", "); const densityErrors = computed(() => getErrors("density"));
} else {
return null;
}
},
densityErrors() { const updateOutput = debounce(function() {
if (this.errors.density && this.errors.density.length > 0) { if (input.value && input.value.length > 0) {
return this.errors.density.join(", "); loadResource(api.getCalculate(input.value, outputUnit.value, ingredient.value ? ingredient.value.ingredient_id : null, density.value)
} else {
return null;
}
}
},
methods: {
updateSearchItems(text) {
return api.getSearchIngredients(text);
},
searchItemSelected(ingredient) {
this.ingredient = ingredient || null;
this.ingredient_name = ingredient.name || null;
this.density = ingredient.density || null;
},
updateOutput: debounce(function() {
if (this.input && this.input.length > 0) {
this.loadResource(api.getCalculate(this.input, this.outputUnit, this.ingredient ? this.ingredient.ingredient_id : null, this.density)
.then(data => { .then(data => {
this.output = data.output; output.value = data.output;
this.errors = data.errors; errors.value = data.errors;
}) })
); );
} }
}, 500) }, 500);
},
watch: { watch(ingredient_name, function(val) {
'ingredient_name': function(val) { if (ingredient.value && ingredient.value.name !== val) {
if (this.ingredient && this.ingredient.name !== val) { ingredient.value = null;
this.ingredient = null;
} }
} });
},
created() { watch(
this.$watch( [input, outputUnit, density, ingredient],
function() { () => updateOutput()
return [this.input, this.outputUnit, this.density, this.ingredient]; );
},
function() {
this.updateOutput();
}
)
},
components: { function updateSearchItems(text) {
return api.getSearchIngredients(text);
}
function searchItemSelected(ingredient) {
ingredient.value = ingredient || null;
ingredient_name.value = ingredient.name || null;
density.value = ingredient.density || null;
}
function getErrors(type) {
if (errors.value[type] && errors.value[type].length > 0) {
return errors.value[type].join(", ");
} else {
return null;
} }
} }

View File

@ -7,40 +7,33 @@
<food-show :food="food"></food-show> <food-show :food="food"></food-show>
</div> </div>
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_food', params: { id: foodId }}">Edit</router-link> <router-link v-if="appConfig.isLoggedIn" class="button" :to="{name: 'edit_food', params: { id: foodId }}">Edit</router-link>
<router-link class="button" to="/foods">Back</router-link> <router-link class="button" to="/foods">Back</router-link>
</div> </div>
</template> </template>
<script> <script setup>
import { computed, onBeforeMount, ref } from "vue";
import { useRoute } from "vue-router";
import FoodShow from "./FoodShow"; import FoodShow from "./FoodShow";
import api from "../lib/Api"; import api from "../lib/Api";
import { useLoadResource } from "../lib/useLoadResource";
import { useAppConfigStore } from "../stores/appConfig";
export default { const { loadResource } = useLoadResource();
data: function () { const appConfig = useAppConfigStore();
return { const route = useRoute();
food: null
}
},
computed: { const food = ref(null);
foodId() { const foodId = computed(() => route.params.id);
return this.$route.params.id;
}
},
created() { onBeforeMount(() => {
this.loadResource( loadResource(
api.getFood(this.foodId) api.getFood(foodId.value)
.then(data => { this.food = data; return data; }) .then(data => { food.value = data; return data; })
); );
}, });
components: {
FoodShow
}
}
</script> </script>

View File

@ -9,16 +9,20 @@
</div> </div>
</template> </template>
<script> <script setup>
import { reactive, ref } from "vue";
import { useRouter } from "vue-router";
import FoodEdit from "./FoodEdit"; import FoodEdit from "./FoodEdit";
import api from "../lib/Api"; import api from "../lib/Api";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
import { useLoadResource } from "../lib/useLoadResource";
export default { const { loadResource } = useLoadResource();
data() { const router = useRouter();
return {
food: { const validationErrors = ref({});
const food = reactive({
name: null, name: null,
notes: null, notes: null,
ndbn: null, ndbn: null,
@ -49,26 +53,16 @@
cholesterol: null, cholesterol: null,
lipids: null, lipids: null,
food_units: [] food_units: []
}, });
validationErrors: {}
}
},
methods: { function save() {
save() { validationErrors.value = {}
this.validationErrors = {} loadResource(
this.loadResource( api.postFood(food)
api.postFood(this.food) .then(() => router.push('/foods'))
.then(() => this.$router.push('/foods')) .catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
); );
} }
},
components: {
FoodEdit
}
}
</script> </script>

View File

@ -14,47 +14,37 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, onBeforeMount, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import FoodEdit from "./FoodEdit"; import FoodEdit from "./FoodEdit";
import api from "../lib/Api"; import api from "../lib/Api";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
import { useLoadResource } from "../lib/useLoadResource";
export default { const { loadResource } = useLoadResource();
data: function () { const router = useRouter();
return { const route = useRoute();
food: null,
validationErrors: {}
};
},
computed: { const food = ref(null);
foodId() { const validationErrors = ref({});
return this.$route.params.id; const foodId = computed(() => route.params.id);
}
},
methods: { onBeforeMount(() => {
save() { loadResource(
this.validationErrors = {}; api.getFood(foodId.value)
this.loadResource( .then(data => { food.value = data; return data; })
api.patchFood(this.food)
.then(() => this.$router.push({name: 'food', params: {id: this.foodId }}))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
); );
} });
},
created() { function save() {
this.loadResource( validationErrors.value = {};
api.getFood(this.foodId) loadResource(
.then(data => { this.food = data; return data; }) api.patchFood(food.value)
.then(() => router.push({name: 'food', params: {id: foodId.value }}))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
); );
},
components: {
FoodEdit
}
} }
</script> </script>

View File

@ -3,7 +3,7 @@
<h1 class="title">Ingredients</h1> <h1 class="title">Ingredients</h1>
<div class="buttons"> <div class="buttons">
<router-link v-if="isLoggedIn" :to="{name: 'new_food'}" class="button is-primary">Create Ingredient</router-link> <router-link v-if="appConfig.isLoggedIn" :to="{name: 'new_food'}" class="button is-primary">Create Ingredient</router-link>
</div> </div>
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="food" @changePage="changePage"></app-pager> <app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="food" @changePage="changePage"></app-pager>
@ -35,12 +35,14 @@
<td>{{i.kcal}}</td> <td>{{i.kcal}}</td>
<td>{{i.density}}</td> <td>{{i.density}}</td>
<td> <td>
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_food', params: { id: i.id } }"> <template v-if="appConfig.isLoggedIn">
<router-link class="button" :to="{name: 'edit_food', params: { id: i.id } }">
<app-icon icon="pencil"></app-icon> <app-icon icon="pencil"></app-icon>
</router-link> </router-link>
<button v-if="isLoggedIn" type="button" class="button is-danger" @click="deleteFood(i)"> <button type="button" class="button is-danger" @click="deleteFood(i)">
<app-icon icon="x"></app-icon> <app-icon icon="x"></app-icon>
</button> </button>
</template>
</td> </td>
</tr> </tr>
</transition-group> </transition-group>
@ -49,114 +51,80 @@
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="food" @changePage="changePage"></app-pager> <app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="food" @changePage="changePage"></app-pager>
<div class="buttons"> <div class="buttons">
<router-link v-if="isLoggedIn" :to="{name: 'new_food'}" class="button is-primary">Create Ingredient</router-link> <router-link v-if="appConfig.isLoggedIn" :to="{name: 'new_food'}" class="button is-primary">Create Ingredient</router-link>
</div> </div>
<app-confirm :open="showConfirmFoodDelete" :message="confirmFoodDeleteMessage" :cancel="foodDeleteCancel" :confirm="foodDeleteConfirm"></app-confirm> <app-confirm :open="showConfirmFoodDelete" title="Delete Ingredient?" :message="confirmFoodDeleteMessage" @cancel="foodDeleteCancel" @confirm="foodDeleteConfirm"></app-confirm>
</div> </div>
</template> </template>
<script> <script setup>
import { computed, reactive, ref, watch } from "vue";
import api from "../lib/Api"; import api from "../lib/Api";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import { useAppConfigStore } from "../stores/appConfig";
import { useLoadResource } from "../lib/useLoadResource";
export default { const appConfig = useAppConfigStore();
data() { const { loadResource } = useLoadResource();
return {
foodData: null, const foodData = ref(null);
foodForDeletion: null, const foodForDeletion = ref(null);
search: { const search = reactive({
page: 1, page: 1,
per: 25, per: 25,
name: null name: null
} });
};
},
computed: { const foods = computed(() => foodData.value?.foods || []);
foods() { const totalPages = computed(() => foodData.value?.total_pages || 0);
if (this.foodData) { const currentPage = computed(() => foodData.value?.current_page || 0);
return this.foodData.foods; const showConfirmFoodDelete = computed(() => foodForDeletion.value !== null);
} else { const confirmFoodDeleteMessage = computed(() => {
return []; if (foodForDeletion.value !== null) {
} return `Are you sure you want to delete ${foodForDeletion.value.name}?`;
},
totalPages() {
if (this.foodData) {
return this.foodData.total_pages
}
return 0;
},
currentPage() {
if (this.foodData) {
return this.foodData.current_page
}
return 0;
},
showConfirmFoodDelete() {
return this.foodForDeletion !== null;
},
confirmFoodDeleteMessage() {
if (this.foodForDeletion !== null) {
return `Are you sure you want to delete ${this.foodForDeletion.name}?`;
} else { } else {
return "??"; return "??";
} }
} });
},
methods: { const getList = debounce(function() {
changePage(idx) { return loadResource(
this.search.page = idx; api.getFoodList(search.page, search.per, search.name)
}, .then(data => foodData.value = data)
getList: debounce(function() {
return this.loadResource(
api.getFoodList(this.search.page, this.search.per, this.search.name)
.then(data => this.foodData = data)
); );
}, 500, {leading: true, trailing: true}), }, 500, {leading: true, trailing: true});
deleteFood(food) { watch(search,
this.foodForDeletion = food; () => getList(),
},
foodDeleteCancel() {
this.foodForDeletion = null;
},
foodDeleteConfirm() {
if (this.foodForDeletion !== null) {
this.loadResource(
api.deleteFood(this.foodForDeletion.id).then(res => {
this.foodForDeletion = null;
return this.getList();
})
);
console.log("This is where the thing happens!!");
this.foodForDeletion = null;
}
}
},
created() {
this.$watch("search",
() => this.getList(),
{ {
deep: true, deep: true,
immediate: true immediate: true
} }
); );
},
components: { function changePage(idx) {
search.page = idx;
}
function deleteFood(food) {
foodForDeletion.value = food;
}
function foodDeleteCancel() {
foodForDeletion.value = null;
}
function foodDeleteConfirm() {
if (foodForDeletion.value !== null) {
loadResource(
api.deleteFood(foodForDeletion.value.id).then(res => {
foodForDeletion.value = null;
return getList();
})
);
} }
} }

View File

@ -14,35 +14,26 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, onBeforeMount, ref } from "vue";
import { useRoute } from "vue-router";
import LogShow from "./LogShow"; import LogShow from "./LogShow";
import api from "../lib/Api"; import api from "../lib/Api";
import { useLoadResource } from "../lib/useLoadResource";
export default { const { loadResource } = useLoadResource();
data: function () { const route = useRoute();
return {
log: null
}
},
computed: { const log = ref(null);
logId() { const logId = computed(() => route.params.id);
return this.$route.params.id;
}
},
created() { onBeforeMount(() => {
this.loadResource( loadResource(
api.getLog(this.logId) api.getLog(logId.value)
.then(data => { this.log = data; return data; }) .then(data => { log.value = data; return data; })
); );
}, });
components: {
LogShow
}
}
</script> </script>

View File

@ -16,53 +16,44 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, onBeforeMount, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import LogEdit from "./LogEdit"; import LogEdit from "./LogEdit";
import api from "../lib/Api"; import api from "../lib/Api";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
import { useLoadResource } from "../lib/useLoadResource";
export default { const { loadResource } = useLoadResource();
data() { const route = useRoute();
return { const router = useRouter();
validationErrors: {},
log: { const validationErrors = ref({});
const log = reactive({
date: null, date: null,
rating: null, rating: null,
notes: null, notes: null,
recipe: null recipe: null
} });
}
},
computed: { const recipeId = computed(() => route.params.recipeId);
recipeId() {
return this.$route.params.recipeId;
}
},
methods: { onBeforeMount(() => {
save() { loadResource(
this.log.original_recipe_id = this.recipeId; api.getRecipe(recipeId.value, null, null, null, data => log.recipe = data)
this.validationErrors = {};
this.loadResource(
api.postLog(this.log)
.then(() => this.$router.push('/'))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
); );
} });
},
created() { function save() {
this.loadResource( log.original_recipe_id = recipeId.value;
api.getRecipe(this.recipeId, null, null, null, data => this.log.recipe = data) validationErrors.value = {};
loadResource(
api.postLog(log)
.then(() => router.push('/'))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
); );
},
components: {
LogEdit
}
} }
</script> </script>

View File

@ -16,47 +16,38 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, onBeforeMount, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import api from "../lib/Api"; import api from "../lib/Api";
import * as Errors from "../lib/Errors"; import * as Errors from "../lib/Errors";
import LogEdit from "./LogEdit"; import LogEdit from "./LogEdit";
import { useLoadResource } from "../lib/useLoadResource";
export default { const { loadResource } = useLoadResource();
data() { const route = useRoute();
return { const router = useRouter();
validationErrors: {},
log: null
}
},
computed: { const validationErrors = ref({});
logId() { const log = ref(null);
return this.$route.params.id;
}
},
methods: { const logId = computed(() => route.params.id);
save() {
this.validationErrors = {}; onBeforeMount(() => {
this.loadResource( loadResource(
api.patchLog(this.log) api.getLog(logId.value)
.then(() => this.$router.push('/')) .then(data => { log.value = data; return data; })
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
); );
} });
},
created() { function save() {
this.loadResource( validationErrors.value = {};
api.getLog(this.logId) loadResource(
.then(data => { this.log = data; return data; }) api.patchLog(log.value)
.then(() => router.push('/'))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
); );
},
components: {
LogEdit
}
} }
</script> </script>

View File

@ -18,7 +18,7 @@
<tr v-for="l in logs" :key="l.id"> <tr v-for="l in logs" :key="l.id">
<td> <router-link :to="{name: 'log', params: {id: l.id}}">{{l.recipe.name}}</router-link></td> <td> <router-link :to="{name: 'log', params: {id: l.id}}">{{l.recipe.name}}</router-link></td>
<td><app-date-time :date-time="l.date" :show-time="false"></app-date-time> </td> <td><app-date-time :date-time="l.date" :show-time="false"></app-date-time> </td>
<td><app-rating :value="l.rating" readonly></app-rating></td> <td><app-rating :model-value="l.rating" readonly></app-rating></td>
<td>{{l.notes}}</td> <td>{{l.notes}}</td>
</tr> </tr>
</tbody> </tbody>
@ -27,68 +27,42 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, reactive, ref, watch } from "vue";
import api from "../lib/Api"; import api from "../lib/Api";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import { useLoadResource } from "../lib/useLoadResource";
export default { const { loadResource } = useLoadResource();
data() {
return { const logData = ref(null);
logData: null, const search = reactive({
search: {
page: 1, page: 1,
per: 25 per: 25
} });
};
},
computed: { const logs = computed(() => logData.value?.logs || []);
logs() { const totalPages = computed(() => logData.value?.total_pages || 0);
if (this.logData) { const currentPage = computed(() => logData.value?.current_page || 0);
return this.logData.logs;
} else {
return [];
}
},
totalPages() { const getList = debounce(function() {
if (this.logData) { loadResource(
return this.logData.total_pages api.getLogList(search.page, search.per)
} .then(data => logData.value = data)
return 0;
},
currentPage() {
if (this.logData) {
return this.logData.current_page
}
return 0;
}
},
methods: {
changePage(idx) {
this.search.page = idx;
},
getList: debounce(function() {
this.loadResource(
api.getLogList(this.search.page, this.search.per)
.then(data => this.logData = data)
); );
}, 500, {leading: true, trailing: true}) }, 500, {leading: true, trailing: true});
},
created() { watch(search,
this.$watch("search", () => getList(),
() => this.getList(),
{ {
deep: true, deep: true,
immediate: true immediate: true
} }
); );
}
function changePage(idx) {
search.page = idx;
} }
</script> </script>

View File

@ -38,63 +38,54 @@
</template> </template>
<script> <script setup>
import { onBeforeMount, ref } from "vue";
import api from "../lib/Api"; import api from "../lib/Api";
import NoteEdit from "./NoteEdit"; import NoteEdit from "./NoteEdit";
import { useLoadResource } from "../lib/useLoadResource";
export default { const { loadResource } = useLoadResource();
data() { const notes = ref([]);
return { const editNote = ref(null);
notes: [],
editNote: null
};
},
methods: { onBeforeMount(() => {
refreshList() { refreshList();
this.loadResource( });
function refreshList() {
loadResource(
api.getNoteList() api.getNoteList()
.then(data => this.notes = data) .then(data => notes.value = data)
); );
}, }
addNote() { function addNote() {
this.editNote = { id: null, content: "" }; editNote.value = { id: null, content: "" };
}, }
saveNote() { function saveNote() {
this.loadResource( loadResource(
api.postNote(this.editNote) api.postNote(editNote.value)
.then(() => { .then(() => {
this.editNote = null; editNote.value = null;
return this.refreshList(); return refreshList();
}) })
); );
}, }
cancelNote() { function cancelNote() {
this.editNote = null; editNote.value = null;
}, }
deleteNote(n) { function deleteNote(n) {
this.loadResource( loadResource(
api.deleteNote(n) api.deleteNote(n)
.then(() => { .then(() => {
return this.refreshList(); return refreshList();
}) })
); );
} }
},
created() {
this.refreshList();
},
components: {
NoteEdit
}
}
</script> </script>

View File

@ -22,59 +22,44 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, ref, watch } from "vue";
import { useRoute } from "vue-router";
import RecipeShow from "./RecipeShow"; import RecipeShow from "./RecipeShow";
import api from "../lib/Api"; import api from "../lib/Api";
import { useLoadResource } from "../lib/useLoadResource";
export default { const route = useRoute();
data: function () { const { loadResource } = useLoadResource();
return { const recipe = ref(null);
recipe: null,
showNutrition: false
}
},
computed: { const recipeId = computed(() => route.params.id);
recipeId() { return this.$route.params.id }, const scale = computed(() => route.query.scale || null);
routeQuery() { return this.$route.query }, const system = computed(() => route.query.system || null);
scale() { return this.$route.query.scale || null }, const unit = computed(() => route.query.unit || null);
system() { return this.$route.query.system || null }, const isScaled = computed(() => recipe.value?.converted_scale?.length !== undefined && recipe.value.converted_scale.length > 0 && recipe.value.converted_scale !== "1");
unit() { return this.$route.query.unit || null },
isScaled() { watch(
return this.recipe.converted_scale !== null && this.recipe.converted_scale.length > 0 && this.recipe.converted_scale !== "1"; () => route.query,
} () => refreshData(),
}, { immediate: true }
);
watch: { watch(
routeQuery() { () => recipe.value?.name,
this.refreshData(); (newRecipe) => {
},
recipe(newRecipe) {
if (newRecipe) { if (newRecipe) {
document.title = `${newRecipe.name}`; document.title = `${newRecipe.name}`;
} }
} }
}, )
methods: { function refreshData() {
refreshData() { loadResource(
this.loadResource( api.getRecipe(recipeId.value, scale.value, system.value, unit.value, data => recipe.value = data)
api.getRecipe(this.recipeId, this.scale, this.system, this.unit, data => this.recipe = data)
); );
} }
},
created() {
this.refreshData();
},
components: {
RecipeShow
}
}
</script> </script>

View File

@ -13,17 +13,20 @@
</div> </div>
</template> </template>
<script> <script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import RecipeEdit from "./RecipeEdit"; import RecipeEdit from "./RecipeEdit";
import api from "../lib/Api"; import api from "../lib/Api";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
import { useLoadResource } from "../lib/useLoadResource";
export default { const router = useRouter();
data() { const { loadResource } = useLoadResource();
return {
validationErrors: {}, const validationErrors = ref({});
recipe: { const recipe = ref({
name: null, name: null,
source: null, source: null,
description: null, description: null,
@ -33,25 +36,16 @@
step_text: null, step_text: null,
tags: [], tags: [],
ingredients: [] ingredients: []
} });
}
},
methods: { function save() {
save() { validationErrors.value = {};
this.validationErrors = {}; loadResource(
this.loadResource( api.postRecipe(recipe.value)
api.postRecipe(this.recipe) .then(() => router.push('/'))
.then(() => this.$router.push('/')) .catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
); );
} }
},
components: {
RecipeEdit
}
}
</script> </script>

View File

@ -18,47 +18,38 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, onBeforeMount, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useLoadResource } from "../lib/useLoadResource";
import RecipeEdit from "./RecipeEdit"; import RecipeEdit from "./RecipeEdit";
import api from "../lib/Api"; import api from "../lib/Api";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
export default { const { loadResource } = useLoadResource();
data: function () { const route = useRoute();
return { const router = useRouter();
recipe: null, const recipe = ref(null);
validationErrors: {} const validationErrors = ref({});
}
},
computed: { const recipeId = computed(() => route.params.id);
recipeId() {
return this.$route.params.id;
}
},
methods: { onBeforeMount(() => {
save() { loadResource(
this.validationErrors = {}; api.getRecipe(recipeId.value, null, null, null, data => { recipe.value = data; return data; })
this.loadResource( );
api.patchRecipe(this.recipe) });
.then(() => this.$router.push({name: 'recipe', params: {id: this.recipeId }}))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors())) function save() {
validationErrors.value = {};
loadResource(
api.patchRecipe(recipe.value)
.then(() => router.push({name: 'recipe', params: {id: recipeId.value }}))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
); );
} }
},
created() {
this.loadResource(
api.getRecipe(this.recipeId, null, null, null, data => { this.recipe = data; return data; })
);
},
components: {
RecipeEdit
}
}
</script> </script>

View File

@ -42,7 +42,7 @@
</div> </div>
</td> </td>
<td> <td>
<app-rating v-if="r.rating !== null" :value="r.rating" readonly></app-rating> <app-rating v-if="r.rating !== null" :model-value="r.rating" readonly></app-rating>
<span v-else>--</span> <span v-else>--</span>
</td> </td>
<td>{{ r.yields }}</td> <td>{{ r.yields }}</td>
@ -85,7 +85,7 @@
</div> </div>
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="recipe" @changePage="changePage"></app-pager> <app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="recipe" @changePage="changePage"></app-pager>
<app-confirm :open="showConfirmRecipeDelete" :message="confirmRecipeDeleteMessage" :cancel="recipeDeleteCancel" :confirm="recipeDeleteConfirm"></app-confirm> <app-confirm :open="showConfirmRecipeDelete" title="Delete Recipe?" :message="confirmRecipeDeleteMessage" @cancel="recipeDeleteCancel" @confirm="recipeDeleteConfirm"></app-confirm>
</div> </div>
</template> </template>

View File

@ -22,15 +22,16 @@
</div> </div>
</template> </template>
<script> <script setup>
import { computed, onBeforeMount, ref } from "vue";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
import { mapActions, mapState } from "pinia";
import { useTaskStore } from "../stores/task"; import { useTaskStore } from "../stores/task";
import TaskListMiniForm from "./TaskListMiniForm"; import TaskListMiniForm from "./TaskListMiniForm";
import TaskListDropdownItem from "./TaskListDropdownItem"; import TaskListDropdownItem from "./TaskListDropdownItem";
import TaskItemList from "./TaskItemList"; import TaskItemList from "./TaskItemList";
import { useLoadResource } from "../lib/useLoadResource";
const newListTemplate = function() { const newListTemplate = function() {
return { return {
@ -38,83 +39,53 @@
}; };
}; };
export default { const { loadResource } = useLoadResource();
data() { const taskStore = useTaskStore();
return {
showListDropdown: false,
newList: newListTemplate(),
newListValidationErrors: {}
}
},
computed: { const showListDropdown = ref(false);
...mapState(useTaskStore, [ const newList = ref(newListTemplate());
'taskLists', const newListValidationErrors = ref({});
'currentTaskList'
]), const taskLists = computed(() => taskStore.taskLists);
listSelectLabel() { const currentTaskList = computed(() => taskStore.currentTaskList);
if (this.currentTaskList === null) { const listSelectLabel = computed(() => {
if (currentTaskList.value === null) {
return "Select or Create a List"; return "Select or Create a List";
} else { } else {
return this.currentTaskList.name; return currentTaskList.value.name;
} }
});
onBeforeMount(() => {
loadResource(taskStore.refreshTaskLists());
});
function selectList(list) {
taskStore.setCurrentTaskList(list);
showListDropdown.value = false;
} }
},
methods: { function saveNewList() {
...mapActions(useTaskStore, [ loadResource(
'refreshTaskLists', taskStore.createTaskList(newList.value)
'createTaskList', .then(() => showListDropdown.value = false)
'deleteTaskList', .then(() => { newList.value = newListTemplate(); newListValidationErrors.value = {}; } )
'deleteTaskItems', .catch(Errors.onlyFor(Errors.ApiValidationError, err => newListValidationErrors.value = err.validationErrors()))
'completeTaskItems',
'setCurrentTaskList'
]),
selectList(list) {
this.setCurrentTaskList(list);
this.showListDropdown = false;
},
saveNewList() {
this.loadResource(
this.createTaskList(this.newList)
.then(() => this.showListDropdown = false)
.then(() => { this.newList = newListTemplate(); this.newListValidationErrors = {}; } )
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.newListValidationErrors = err.validationErrors()))
); );
}, }
deleteList(list) { function deleteList(list) {
this.loadResource( loadResource(taskStore.deleteTaskList(list));
this.deleteTaskList(list) }
);
},
function deleteAllItems() {
loadResource(
deleteAllItems() { taskStore.deleteTaskItems({
this.loadResource( taskList: currentTaskList.value,
this.deleteTaskItems({ taskItems: currentTaskList.value.task_items
taskList: this.currentTaskList,
taskItems: this.currentTaskList.task_items
}) })
); );
} }
},
created() {
this.loadResource(
this.refreshTaskLists()
);
},
components: {
TaskListMiniForm,
TaskListDropdownItem,
TaskItemList
}
}
</script> </script>

View File

@ -13,42 +13,38 @@
</div> </div>
</template> </template>
<script> <script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import UserEdit from "./UserEdit"; import UserEdit from "./UserEdit";
import api from "../lib/Api"; import api from "../lib/Api";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
import { useLoadResource } from "../lib/useLoadResource";
import { useCheckAuthentication } from "../lib/useCheckAuthentication";
export default { const { loadResource } = useLoadResource();
data() { const { checkAuthentication } = useCheckAuthentication(loadResource);
return { const router = useRouter();
validationErrors: {},
userObj: { const validationErrors = ref({});
const userObj = ref({
username: '', username: '',
full_name: '', full_name: '',
email: '', email: '',
password: '', password: '',
password_confirmation: '' password_confirmation: ''
} });
}
},
methods: { function save() {
save() { validationErrors.value = {};
this.validationErrors = {}; loadResource(
this.loadResource( api.postUser(userObj.value)
api.postUser(this.userObj) .then(() => checkAuthentication())
.then(() => this.checkAuthentication()) .then(() => router.push('/'))
.then(() => this.$router.push('/')) .catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
); );
} }
},
components: {
UserEdit
}
}
</script> </script>

View File

@ -13,62 +13,55 @@
</div> </div>
</template> </template>
<script> <script setup>
import { ref, watch } from "vue";
import { useRouter } from "vue-router";
import UserEdit from "./UserEdit"; import UserEdit from "./UserEdit";
import api from "../lib/Api"; import api from "../lib/Api";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
import { useAppConfigStore } from "../stores/appConfig";
import { useLoadResource } from "../lib/useLoadResource";
import { useCheckAuthentication } from "../lib/useCheckAuthentication";
export default { const appConfig = useAppConfigStore();
data() { const { loadResource } = useLoadResource();
return { const { checkAuthentication } = useCheckAuthentication(loadResource);
validationErrors: {}, const router = useRouter();
userObj: null
}
},
created() { const validationErrors = ref({});
this.refreshUser(); const userObj = ref(null);
},
watch: { watch(
user() { () => appConfig.user,
this.refreshUser(); () => refreshUser(),
} { immediate: true });
},
methods: { function refreshUser() {
refreshUser() { if (appConfig.user) {
if (this.user) { userObj.value = {
this.userObj = { username: appConfig.user.username,
username: this.user.username, full_name: appConfig.user.full_name,
full_name: this.user.full_name, email: appConfig.user.email,
email: this.user.email,
password: '', password: '',
password_confirmation: '' password_confirmation: ''
}; };
} else { } else {
this.userObj = null; userObj.value = null;
}
} }
},
save() { function save() {
this.validationErrors = {}; validationErrors.value = {};
this.loadResource( loadResource(
api.patchUser(this.userObj) api.patchUser(userObj.value)
.then(() => this.checkAuthentication()) .then(() => checkAuthentication())
.then(() => { .then(() => {
this.$router.push('/'); router.push('/');
}) })
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors())) .catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
); );
} }
},
components: {
UserEdit
}
}
</script> </script>

View File

@ -3,21 +3,19 @@
<app-text-field label="Username" v-model="userObj.username"></app-text-field> <app-text-field label="Username" v-model="userObj.username"></app-text-field>
<app-text-field label="Name" v-model="userObj.full_name"></app-text-field> <app-text-field label="Name" v-model="userObj.full_name"></app-text-field>
<app-text-field label="Email" v-model="userObj.email"></app-text-field> <app-text-field label="Email" v-model="userObj.email"></app-text-field>
<app-text-field label="Password" v-model="userObj.password"></app-text-field> <app-text-field type="password" label="Password" v-model="userObj.password"></app-text-field>
<app-text-field label="Password Confirmation" v-model="userObj.password_confirmation"></app-text-field> <app-text-field type="password" label="Password Confirmation" v-model="userObj.password_confirmation"></app-text-field>
</div> </div>
</template> </template>
<script> <script setup>
export default { const props = defineProps({
props: {
userObj: { userObj: {
required: true, required: true,
type: Object type: Object
} }
} });
}
</script> </script>

View File

@ -45,60 +45,46 @@
</div> </div>
</template> </template>
<script> <script setup>
import { mapActions, mapState } from "pinia"; import { computed, nextTick, ref, useTemplateRef } from "vue";
import { useAppConfigStore } from "../stores/appConfig"; import { useAppConfigStore } from "../stores/appConfig";
import { useLoadResource } from "../lib/useLoadResource";
export default { const appConfig = useAppConfigStore();
data() { const { loadResource } = useLoadResource();
return {
showLogin: false,
error: '',
username: '',
password: ''
};
},
computed: { const userNameElement = useTemplateRef("usernameInput");
...mapState(useAppConfigStore, [
'loginMessage' const showLogin = ref(false);
]), const error = ref('');
enableSubmit() { const username = ref("");
return this.username !== '' && this.password !== '' && !this.isLoading; const password = ref("");
const loginMessage = computed(() => appConfig.loginMessage);
const enableSubmit = computed(() => username.value !== "" && password.value !== "" && !appConfig.isLoading);
function openDialog() {
showLogin.value = true;
nextTick(() => {
userNameElement.value.focus();
})
} }
},
methods: { function performLogin() {
...mapActions(useAppConfigStore, [ if (username.value !== '' && password.value !== '') {
'login' const params = {username: username.value, password: password.value};
]),
openDialog() { loadResource(
this.showLogin = true; appConfig.login(params)
this.$nextTick(() => this.$refs.usernameInput.focus());
},
performLogin() {
if (this.username !== '' && this.password !== '') {
const params = {username: this.username, password: this.password};
this.loadResource(
this.login(params)
.then(data => { .then(data => {
console.log(data);
if (data.success) { if (data.success) {
this.showLogin = false; showLogin.value = false;
} }
}) })
); );
} }
} }
},
components: {
}
}
</script> </script>