Upgrade shakapacker, vue; switch to pinia

This commit is contained in:
Dan Elbert 2024-09-28 20:58:25 -05:00
parent bb2e29f25c
commit f246f71aa9
66 changed files with 7743 additions and 4796 deletions

View File

@ -5,3 +5,5 @@ tmp/*.*
public/assets public/assets
public/packs public/packs
node_modules/ node_modules/
.yarn
.pnp.*

2
.gitignore vendored
View File

@ -35,3 +35,5 @@ yarn-debug.log*
/yarn-error.log /yarn-error.log
yarn-debug.log* yarn-debug.log*
.yarn-integrity .yarn-integrity
.yarn
.pnp.*

View File

@ -31,7 +31,7 @@ WORKDIR /parsley
COPY Gemfile* ./ COPY Gemfile* ./
RUN bundle install RUN bundle install
COPY package.json yarn.lock ./ COPY package.json.org yarn.lock ./
RUN yarn install --production=true --frozen-lockfile RUN yarn install --production=true --frozen-lockfile
COPY . . COPY . .

View File

@ -3,10 +3,11 @@ source 'https://rubygems.org'
gem 'rails', '7.2.1' gem 'rails', '7.2.1'
gem 'pg', '~> 1.5.8' gem 'pg', '~> 1.5.8'
gem 'shakapacker', '6.5.4' gem 'shakapacker', '8.0.2'
gem 'bootsnap', '>= 1.1.0', require: false gem 'bootsnap', '>= 1.1.0', require: false
gem 'oj', '~> 3.16.6' gem 'oj', '~> 3.16.6'
gem 'csv', '~> 3.3'
gem 'kaminari', '~> 1.2.2' gem 'kaminari', '~> 1.2.2'
gem 'unitwise', '~> 2.3.0' gem 'unitwise', '~> 2.3.0'

View File

@ -81,6 +81,7 @@ GEM
concurrent-ruby (1.3.4) concurrent-ruby (1.3.4)
connection_pool (2.4.1) connection_pool (2.4.1)
crass (1.0.6) crass (1.0.6)
csv (3.3.0)
dalli (3.2.8) dalli (3.2.8)
database_cleaner (2.0.2) database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (>= 2, < 3)
@ -176,6 +177,7 @@ GEM
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2) ostruct (>= 0.2)
ostruct (0.6.0) ostruct (0.6.0)
package_json (0.1.0)
parslet (2.0.0) parslet (2.0.0)
pg (1.5.8) pg (1.5.8)
pry (0.14.2) pry (0.14.2)
@ -261,8 +263,9 @@ GEM
rspec-support (3.13.1) rspec-support (3.13.1)
securerandom (0.3.1) securerandom (0.3.1)
semantic_range (3.0.0) semantic_range (3.0.0)
shakapacker (6.5.4) shakapacker (8.0.2)
activesupport (>= 5.2) activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
@ -296,6 +299,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
bcrypt (~> 3.1.18) bcrypt (~> 3.1.18)
bootsnap (>= 1.1.0) bootsnap (>= 1.1.0)
csv (~> 3.3)
dalli (~> 3.2.8) dalli (~> 3.2.8)
database_cleaner (~> 2.0.2) database_cleaner (~> 2.0.2)
factory_bot_rails (~> 6.4.3) factory_bot_rails (~> 6.4.3)
@ -309,7 +313,7 @@ DEPENDENCIES
rails-controller-testing rails-controller-testing
redcarpet (~> 3.6.0) redcarpet (~> 3.6.0)
rspec-rails (~> 7.0.1) rspec-rails (~> 7.0.1)
shakapacker (= 6.5.4) shakapacker (= 8.0.2)
sqlite3 (~> 2.1.0) sqlite3 (~> 2.1.0)
tzinfo-data tzinfo-data
unitwise (~> 2.3.0) unitwise (~> 2.3.0)

View File

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2020 Dan Elbert Copyright (c) 2024 Dan Elbert
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,2 +1,2 @@
rails: bundle exec rails s -b 0.0.0.0 rails: bundle exec rails s -b 0.0.0.0
webpacker: bin/webpack-dev-server shakapacker: bin/shakapacker-dev-server

View File

@ -5,7 +5,7 @@ A self hosted cookbook
Parsley is released under the MIT License. Parsley is released under the MIT License.
Copyright (C) 2020 Dan Elbert Copyright (C) 2024 Dan Elbert
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -4,13 +4,15 @@
<app-navbar></app-navbar> <app-navbar></app-navbar>
<section id="main" class=""> <section id="main" class="">
<div class="container"> <div class="container">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<router-view v-if="!hasError"></router-view> <component v-if="!hasError" :is="Component" />
<div v-else> <div v-else>
<h1>Error!</h1> <h1>Error!</h1>
<p>{{ error }}</p> <p>{{ error }}</p>
</div> </div>
</transition> </transition>
</router-view>
</div> </div>
</section> </section>
</div> </div>
@ -18,31 +20,31 @@
<script> <script>
import { mapMutations, mapState } from "vuex"; import { mapState } from "pinia";
import api from "../lib/Api";
import TWEEN from '@tweenjs/tween.js'; import TWEEN from '@tweenjs/tween.js';
import { useAppConfigStore } from "../stores/appConfig";
export default { export default {
data() { data() {
return { return {
api: api
}; };
}, },
computed: { computed: {
...mapState({ ...mapState(useAppConfigStore, {
hasError: state => state.error !== null, hasError: store => store.error !== null,
error: state => state.error, error: store => store.error,
authChecked: state => state.authChecked, authChecked: store => store.authChecked,
initialLoad: state => state.initialLoad initialLoad: store => store.initialLoad
}) })
}, },
watch: { watch: {
isLoading(val) { isLoading(val) {
if (val) { if (val) {
this.$Progress.start(); // this.$Progress.start();
} else { } else {
this.$Progress.finish(); // this.$Progress.finish();
} }
}, },

View File

@ -33,7 +33,7 @@
export default { export default {
props: { props: {
value: String, modelValue: String,
id: String, id: String,
placeholder: String, placeholder: String,
name: String, name: String,
@ -70,12 +70,12 @@
}, },
created() { created() {
this.rawValue = this.value; this.rawValue = this.modelValue;
}, },
watch: { watch: {
value(newValue) { modelValue(newValue) {
this.rawValue = newValue; this.rawValue = newValue;
} }
}, },
@ -156,7 +156,7 @@
this.rawValue = newValue; this.rawValue = newValue;
this.$emit("input", newValue); this.$emit("update:modelValue", newValue);
if (newValue.length >= Math.max(1, this.minLength)) { if (newValue.length >= Math.max(1, this.minLength)) {
this.debouncedUpdateOptions(newValue); this.debouncedUpdateOptions(newValue);
@ -192,7 +192,7 @@
selectOption(opt) { selectOption(opt) {
this.rawValue = this.optionValue(opt); this.rawValue = this.optionValue(opt);
this.$emit("input", this.rawValue); this.$emit("update:modelValue", this.rawValue);
this.$emit("optionSelected", opt); this.$emit("optionSelected", opt);
this.isListOpen = false; this.isListOpen = false;
}, },

View File

@ -8,7 +8,7 @@
export default { export default {
props: { props: {
value: { modelValue: {
required: false, required: false,
type: [Date, String] type: [Date, String]
}, },
@ -22,7 +22,7 @@
computed: { computed: {
stringValue() { stringValue() {
const d = DateTimeUtils.toDate(this.value); const d = DateTimeUtils.toDate(this.modelValue);
return DateTimeUtils.formatDateForEdit(d); return DateTimeUtils.formatDateForEdit(d);
} }
}, },
@ -30,7 +30,7 @@
methods: { methods: {
input(val) { input(val) {
let d = DateTimeUtils.toDate(val + " 00:00"); let d = DateTimeUtils.toDate(val + " 00:00");
this.$emit("input", d); this.$emit("update:modelValue", d);
} }
} }
} }

View File

@ -132,7 +132,13 @@
created() { created() {
if (LOADED_APIS[this.svgName] !== true) { if (LOADED_APIS[this.svgName] !== true) {
for (let sb of this.svgData.scriptBlocks) { for (let sb of this.svgData.scriptBlocks) {
try {
new Function(sb)(window); new Function(sb)(window);
} catch (e) {
console.log(sb);
console.log(e);
}
} }
LOADED_APIS[this.svgName] = true; LOADED_APIS[this.svgName] = true;
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div ref="container"> <Teleport to="body">
<div ref="modal" :class="['popup', 'modal', { 'is-wide': wide, 'is-active': open && error === null }]"> <div :class="['popup', '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">
@ -15,14 +15,17 @@
</section> </section>
</div> </div>
</div> </div>
</div> </Teleport>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapState } from "pinia";
import { useAppConfigStore } from "../stores/appConfig";
export default { export default {
emits: ["dismiss"],
props: { props: {
open: { open: {
type: Boolean, type: Boolean,
@ -35,16 +38,8 @@
} }
}, },
mounted() {
this.$root.$el.appendChild(this.$refs.modal);
},
beforeDestroy() {
this.$refs.container.appendChild(this.$refs.modal);
},
computed: { computed: {
...mapState([ ...mapState(useAppConfigStore, [
'error' 'error'
]) ])
}, },

View File

@ -54,7 +54,8 @@
<script> <script>
import UserLogin from "./UserLogin"; import UserLogin from "./UserLogin";
import { mapState } from "vuex"; import { mapState } from "pinia";
import { useAppConfigStore } from "../stores/appConfig";
import { swUpdate } from "../lib/ServiceWorker"; import { swUpdate } from "../lib/ServiceWorker";
export default { export default {
@ -65,7 +66,7 @@
}, },
computed: { computed: {
...mapState([ ...mapState(useAppConfigStore, [
'route', 'route',
'user', 'user',
'updateAvailable' 'updateAvailable'

View File

@ -32,7 +32,7 @@
default: 0.5 default: 0.5
}, },
value: { modelValue: {
required: false, required: false,
type: Number, type: Number,
default: 0 default: 0
@ -47,7 +47,7 @@
computed: { computed: {
ratingPercent() { ratingPercent() {
return ((this.value || 0) / this.starCount) * 100.0; return ((this.modelValue || 0) / this.starCount) * 100.0;
}, },
temporaryPercent() { temporaryPercent() {
@ -69,7 +69,7 @@
methods: { methods: {
handleClick(evt) { handleClick(evt) {
if (this.temporaryValue !== null) { if (this.temporaryValue !== null) {
this.$emit("input", this.temporaryValue); this.$emit("update:modelValue", this.temporaryValue);
} }
}, },

View File

@ -18,7 +18,7 @@ export default {
default: "" default: ""
}, },
value: { modelValue: {
required: false, required: false,
type: String, type: String,
default: "" default: ""
@ -37,7 +37,7 @@ export default {
methods: { methods: {
triggerInput: debounce(function() { triggerInput: debounce(function() {
this.$emit("input", this.text); this.$emit("update:modelValue", this.text);
}, 250, {leading: false, trailing: true}), }, 250, {leading: false, trailing: true}),
userUpdateText(text) { userUpdateText(text) {
@ -55,7 +55,7 @@ export default {
}, },
created() { created() {
this.$watch("value", this.$watch("modelValue",
val => this.propUpdateText(val), val => this.propUpdateText(val),
{ {
immediate: true immediate: true

View File

@ -2,7 +2,7 @@
<div class="tag-editor control"> <div class="tag-editor control">
<input ref="input" type="text" class="input" :value="tagText" @input="inputHandler" @focus="getFocus" @blur="loseFocus"> <input ref="input" type="text" class="input" :value="tagText" @input="inputHandler" @focus="getFocus" @blur="loseFocus">
<div class="tags"> <div class="tags">
<span v-for="t in value" :key="t" class="tag">{{t}}</span> <span v-for="t in modelValue" :key="t" class="tag">{{t}}</span>
</div> </div>
</div> </div>
</template> </template>
@ -11,7 +11,7 @@
export default { export default {
props: { props: {
value: { modelValue: {
required: true, required: true,
type: Array type: Array
} }
@ -25,7 +25,7 @@
computed: { computed: {
tagText() { tagText() {
return this.value.join(" "); return this.modelValue.join(" ");
} }
}, },
@ -54,8 +54,8 @@
const newTags = [...new Set(str.toString().split(/\s+/).filter(t => t.length > 0))]; const newTags = [...new Set(str.toString().split(/\s+/).filter(t => t.length > 0))];
if (!this.arraysEqual(newTags, this.value)) { if (!this.arraysEqual(newTags, this.modelValue)) {
this.$emit("input", newTags); this.$emit("update:modelValue", newTags);
} }
}, },

View File

@ -2,8 +2,8 @@
<div class="field"> <div class="field">
<label v-if="label.length" class="label is-small-mobile">{{ label }}</label> <label v-if="label.length" class="label is-small-mobile">{{ label }}</label>
<div :class="controlClasses"> <div :class="controlClasses">
<textarea v-if="isTextarea" :class="inputClasses" :value="value" @input="input" :disabled="disabled"></textarea> <textarea v-if="isTextarea" :class="inputClasses" :value="modelValue" @input="input" :disabled="disabled"></textarea>
<input v-else :type="type" :class="inputClasses" :value="value" @input="input" :disabled="disabled"> <input v-else :type="type" :class="inputClasses" :value="modelValue" @input="input" :disabled="disabled">
<app-icon class="is-right" icon="warning" v-if="validationError !== null"></app-icon> <app-icon class="is-right" icon="warning" v-if="validationError !== null"></app-icon>
</div> </div>
<p v-if="helpMessage !== null" :class="helpClasses"> <p v-if="helpMessage !== null" :class="helpClasses">
@ -21,7 +21,7 @@
type: String, type: String,
default: "" default: ""
}, },
value: { modelValue: {
required: false, required: false,
type: [String, Number], type: [String, Number],
default: "" default: ""
@ -83,7 +83,7 @@
methods: { methods: {
input(evt) { input(evt) {
this.$emit("input", evt.target.value); this.$emit("update:modelValue", evt.target.value);
} }
} }
} }

View File

@ -62,11 +62,14 @@
<button class="button" type="button" @click="addUnit">Add Unit</button> <button class="button" type="button" @click="addUnit">Add Unit</button>
<table class="table"> <table class="table">
<thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Grams</th> <th>Grams</th>
<th></th> <th></th>
</tr> </tr>
</thead>
<tbody>
<tr v-for="unit in visibleFoodUnits" :key="unit.id"> <tr v-for="unit in visibleFoodUnits" :key="unit.id">
<td> <td>
<div class="control"> <div class="control">
@ -82,6 +85,7 @@
<button type="button" class="button is-danger" @click="removeUnit(unit)">X</button> <button type="button" class="button is-danger" @click="removeUnit(unit)">X</button>
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</div> </div>
</div> </div>
@ -96,14 +100,18 @@
<div class="message-body"> <div class="message-body">
<table class="table"> <table class="table">
<thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Grams</th> <th>Grams</th>
</tr> </tr>
</thead>
<tbody>
<tr v-for="unit in food.ndbn_units"> <tr v-for="unit in food.ndbn_units">
<td>{{unit.description}}</td> <td>{{unit.description}}</td>
<td>{{unit.gram_weight}}</td> <td>{{unit.gram_weight}}</td>
</tr> </tr>
</tbody>
</table> </table>
</div> </div>
@ -139,7 +147,8 @@
<script> <script>
import api from "../lib/Api"; import api from "../lib/Api";
import { mapState } from "vuex"; import { mapState } from "pinia";
import { useNutrientStore } from "../stores/nutrient";
export default { export default {
props: { props: {
@ -166,7 +175,7 @@
}, },
computed: { computed: {
...mapState({ ...mapState(useNutrientStore, {
nutrients: 'nutrientList' nutrients: 'nutrientList'
}), }),

View File

@ -55,7 +55,8 @@
<script> <script>
import { mapState } from "vuex"; import { mapState } from "pinia";
import { useNutrientStore } from "../stores/nutrient";
export default { export default {
props: { props: {
@ -66,7 +67,7 @@
}, },
computed: { computed: {
...mapState({ ...mapState(useNutrientStore, {
nutrients: 'nutrientList' nutrients: 'nutrientList'
}), }),
} }

View File

@ -71,6 +71,7 @@
<th>Result</th> <th>Result</th>
</tr> </tr>
</thead> </thead>
<tbody>
<tr> <tr>
<td>Heading</td> <td>Heading</td>
<td> <td>
@ -155,6 +156,7 @@ _underline_
</p> </p>
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
<h3 class="title is-3">Basic Example</h3> <h3 class="title is-3">Basic Example</h3>
@ -206,7 +208,7 @@ _underline_
<script> <script>
import autosize from "autosize"; //import autosize from "autosize";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import api from "../lib/Api"; import api from "../lib/Api";

View File

@ -12,18 +12,22 @@
</div> </div>
<div class="column is-half"> <div class="column is-half">
<table class="table is-bordered is-narrow is-size-7"> <table class="table is-bordered is-narrow is-size-7">
<thead>
<tr> <tr>
<th>#</th> <th>#</th>
<th>Unit</th> <th>Unit</th>
<th>Name</th> <th>Name</th>
<th>Prep</th> <th>Prep</th>
</tr> </tr>
</thead>
<tbody>
<tr v-for="i in bulkIngredientPreview"> <tr v-for="i in bulkIngredientPreview">
<td>{{i.quantity}}</td> <td>{{i.quantity}}</td>
<td>{{i.units}}</td> <td>{{i.units}}</td>
<td>{{i.name}}</td> <td>{{i.name}}</td>
<td>{{i.preparation}}</td> <td>{{i.preparation}}</td>
</tr> </tr>
</tbody>
</table> </table>
</div> </div>
</div> </div>
@ -43,7 +47,8 @@
import RecipeEditIngredientItem from "./RecipeEditIngredientItem"; import RecipeEditIngredientItem from "./RecipeEditIngredientItem";
import { mapState } from "vuex"; import { mapState } from "pinia";
import { useMediaQueryStore } from "../stores/mediaQuery";
export default { export default {
props: { props: {
@ -61,8 +66,8 @@
}, },
computed: { computed: {
...mapState({ ...mapState(useMediaQueryStore, {
isMobile: state => state.mediaQueries.mobile isMobile: store => store.mobile
}), }),
bulkIngredientPreview() { bulkIngredientPreview() {
if (this.bulkEditText === null) { if (this.bulkEditText === null) {

View File

@ -65,14 +65,18 @@
<div class="message-header" @click="showNutrition = !showNutrition">Nutrition Data</div> <div class="message-header" @click="showNutrition = !showNutrition">Nutrition Data</div>
<div class="message-body" v-show="showNutrition"> <div class="message-body" v-show="showNutrition">
<table class="table"> <table class="table">
<thead>
<tr> <tr>
<th>Item</th> <th>Item</th>
<th>Value</th> <th>Value</th>
</tr> </tr>
</thead>
<tbody>
<tr v-for="nutrient in recipe.nutrition_data.nutrients" :key="nutrient.name"> <tr v-for="nutrient in recipe.nutrition_data.nutrients" :key="nutrient.name">
<td>{{nutrient.label}}</td> <td>{{nutrient.label}}</td>
<td>{{ roundValue(nutrient.value) }}</td> <td>{{ roundValue(nutrient.value) }}</td>
</tr> </tr>
</tbody>
</table> </table>
<h3 class="title is-5">Nutrition Calculation Warnings</h3> <h3 class="title is-5">Nutrition Calculation Warnings</h3>
@ -149,7 +153,8 @@
<script> <script>
import api from "../lib/Api"; import api from "../lib/Api";
import { mapActions, mapMutations, mapState } from "vuex"; import { mapActions, mapState } from "pinia";
import { useTaskStore } from "../stores/task";
export default { export default {
props: { props: {
@ -185,7 +190,7 @@
}, },
computed: { computed: {
...mapState([ ...mapState(useTaskStore, [
'taskLists' 'taskLists'
]), ]),
@ -235,11 +240,8 @@
}, },
methods: { methods: {
...mapActions([ ...mapActions(useTaskStore, [
'ensureTaskLists' 'ensureTaskLists',
]),
...mapMutations([
'setCurrentTaskList' 'setCurrentTaskList'
]), ]),

View File

@ -63,8 +63,8 @@
<script> <script>
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
import { mapActions } from "vuex"; import { mapActions } from "pinia";
import cloneDeep from "lodash/cloneDeep"; import { useTaskStore } from "../stores/task";
import TaskItemEdit from "./TaskItemEdit"; import TaskItemEdit from "./TaskItemEdit";
@ -115,7 +115,7 @@
}, },
methods: { methods: {
...mapActions([ ...mapActions(useTaskStore, [
'createTaskItem', 'createTaskItem',
'updateTaskItem', 'updateTaskItem',
'deleteTaskItems', 'deleteTaskItems',

View File

@ -53,19 +53,20 @@
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~styles/variables"; @use "bulma/sass" as bulma;
@use 'sass:color';
div.dropdown-item { div.dropdown-item {
cursor: pointer; cursor: pointer;
&.hovered { &.hovered {
color: $black; color: bulma.$black;
background-color: $background; background-color: bulma.$background;
} }
&.is-active { &.is-active {
color: $link-invert; color: color.invert(bulma.$link);
background-color: $link; background-color: bulma.$link;
} }
} }

View File

@ -7,7 +7,7 @@
</p> </p>
<p> <p>
Parsley is released under the MIT License. All code &copy; Dan Elbert 2020. Parsley is released under the MIT License. All code &copy; Dan Elbert 2024.
</p> </p>
<p> <p>

View File

@ -15,7 +15,6 @@
<script> <script>
import FoodShow from "./FoodShow"; import FoodShow from "./FoodShow";
import { mapState } from "vuex";
import api from "../lib/Api"; import api from "../lib/Api";
export default { export default {
@ -26,9 +25,9 @@
}, },
computed: { computed: {
...mapState({ foodId() {
foodId: state => state.route.params.id, return this.$route.params.id;
}) }
}, },
created() { created() {

View File

@ -12,7 +12,6 @@
<script> <script>
import FoodEdit from "./FoodEdit"; import FoodEdit from "./FoodEdit";
import { mapState } from "vuex";
import api from "../lib/Api"; import api from "../lib/Api";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';

View File

@ -17,7 +17,6 @@
<script> <script>
import FoodEdit from "./FoodEdit"; import FoodEdit from "./FoodEdit";
import { mapState } from "vuex";
import api from "../lib/Api"; import api from "../lib/Api";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
@ -30,9 +29,9 @@
}, },
computed: { computed: {
...mapState({ foodId() {
foodId: state => state.route.params.id, return this.$route.params.id;
}) }
}, },
methods: { methods: {

View File

@ -17,7 +17,6 @@
<script> <script>
import LogShow from "./LogShow"; import LogShow from "./LogShow";
import { mapState } from "vuex";
import api from "../lib/Api"; import api from "../lib/Api";
export default { export default {
@ -28,9 +27,9 @@
}, },
computed: { computed: {
...mapState({ logId() {
logId: state => state.route.params.id, return this.$route.params.id;
}) }
}, },
created() { created() {

View File

@ -19,7 +19,6 @@
<script> <script>
import LogEdit from "./LogEdit"; import LogEdit from "./LogEdit";
import { mapState } from "vuex";
import api from "../lib/Api"; import api from "../lib/Api";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
@ -37,9 +36,9 @@
}, },
computed: { computed: {
...mapState({ recipeId() {
recipeId: state => state.route.params.recipeId, return this.$route.params.recipeId;
}) }
}, },
methods: { methods: {

View File

@ -18,7 +18,6 @@
<script> <script>
import { mapState } from "vuex";
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";
@ -32,9 +31,9 @@
}, },
computed: { computed: {
...mapState({ logId() {
logId: state => state.route.params.id, return this.$route.params.id;
}) }
}, },
methods: { methods: {

View File

@ -5,19 +5,23 @@
</h1> </h1>
<table class="table"> <table class="table">
<thead>
<tr> <tr>
<th>Recipe</th> <th>Recipe</th>
<th>Date</th> <th>Date</th>
<th>Rating</th> <th>Rating</th>
<th>Notes</th> <th>Notes</th>
</tr> </tr>
</thead>
<tbody>
<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 :value="l.rating" readonly></app-rating></td>
<td>{{l.notes}}</td> <td>{{l.notes}}</td>
</tr> </tr>
</tbody>
</table> </table>
</div> </div>

View File

@ -9,12 +9,15 @@
</app-modal> </app-modal>
<table class="table"> <table class="table">
<thead>
<tr> <tr>
<th>Note</th> <th>Note</th>
<th>Date</th> <th>Date</th>
<th></th> <th></th>
</tr> </tr>
</thead>
<tbody>
<tr v-for="n in notes" :key="n.id"> <tr v-for="n in notes" :key="n.id">
<td> <td>
{{ n.content }} {{ n.content }}
@ -28,6 +31,7 @@
</button> </button>
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</div> </div>

View File

@ -25,7 +25,6 @@
<script> <script>
import RecipeShow from "./RecipeShow"; import RecipeShow from "./RecipeShow";
import { mapState } from "vuex";
import api from "../lib/Api"; import api from "../lib/Api";
export default { export default {
@ -37,13 +36,11 @@
}, },
computed: { computed: {
...mapState({ recipeId() { return this.$route.params.id },
recipeId: state => state.route.params.id, routeQuery() { return this.$route.query },
routeQuery: state => state.route.query, scale() { return this.$route.query.scale || null },
scale: state => state.route.query.scale || null, system() { return this.$route.query.system || null },
system: state => state.route.query.system || null, unit() { return this.$route.query.unit || null },
unit: state => state.route.query.unit || null
}),
isScaled() { isScaled() {
return this.recipe.converted_scale !== null && this.recipe.converted_scale.length > 0 && this.recipe.converted_scale !== "1"; return this.recipe.converted_scale !== null && this.recipe.converted_scale.length > 0 && this.recipe.converted_scale !== "1";

View File

@ -21,7 +21,6 @@
<script> <script>
import RecipeEdit from "./RecipeEdit"; import RecipeEdit from "./RecipeEdit";
import { mapState } from "vuex";
import api from "../lib/Api"; import api from "../lib/Api";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
@ -34,9 +33,9 @@
}, },
computed: { computed: {
...mapState({ recipeId() {
recipeId: state => state.route.params.id, return this.$route.params.id;
}) }
}, },
methods: { methods: {

View File

@ -11,7 +11,7 @@
<table class="table is-fullwidth" :class="{ small: mediaQueries.touch }"> <table class="table is-fullwidth" :class="{ small: isTouch }">
<thead> <thead>
<tr> <tr>
<th v-for="h in tableHeader" :key="h.name"> <th v-for="h in tableHeader" :key="h.name">
@ -91,9 +91,10 @@
<script> <script>
import api from "../lib/Api"; import api from "../lib/Api";
import debounce from "lodash/debounce"; import { mapWritableState, mapState } from "pinia";
import { mapMutations, mapState } from "vuex";
import AppLoading from "./AppLoading"; import AppLoading from "./AppLoading";
import { useAppConfigStore } from "../stores/appConfig";
import { useMediaQueryStore } from "../stores/mediaQuery";
export default { export default {
props: { props: {
@ -112,8 +113,9 @@
}, },
computed: { computed: {
...mapState([ ...mapState(useMediaQueryStore, { isTouch: store => store.touch }),
"mediaQueries" ...mapWritableState(useAppConfigStore, [
"initialLoad"
]), ]),
search() { search() {
@ -174,10 +176,6 @@
}, },
methods: { methods: {
...mapMutations([
"setInitialLoad"
]),
buildQueryParams() { buildQueryParams() {
return { return {
name: this.searchQuery.name, name: this.searchQuery.name,
@ -302,7 +300,7 @@
created() { created() {
this.$watch("search", this.$watch("search",
() => { () => {
this.getList().then(() => this.setInitialLoad(true)); this.getList().then(() => this.initialLoad = true);
}, },
{ {
deep: true, deep: true,

View File

@ -24,9 +24,9 @@
<script> <script>
import api from "../lib/Api";
import * as Errors from '../lib/Errors'; import * as Errors from '../lib/Errors';
import { mapActions, mapMutations, mapState } from "vuex"; import { mapActions, mapState } from "pinia";
import { useTaskStore } from "../stores/task";
import TaskListMiniForm from "./TaskListMiniForm"; import TaskListMiniForm from "./TaskListMiniForm";
import TaskListDropdownItem from "./TaskListDropdownItem"; import TaskListDropdownItem from "./TaskListDropdownItem";
@ -48,7 +48,7 @@
}, },
computed: { computed: {
...mapState([ ...mapState(useTaskStore, [
'taskLists', 'taskLists',
'currentTaskList' 'currentTaskList'
]), ]),
@ -62,14 +62,12 @@
}, },
methods: { methods: {
...mapActions([ ...mapActions(useTaskStore, [
'refreshTaskLists', 'refreshTaskLists',
'createTaskList', 'createTaskList',
'deleteTaskList', 'deleteTaskList',
'deleteTaskItems', 'deleteTaskItems',
'completeTaskItems' 'completeTaskItems',
]),
...mapMutations([
'setCurrentTaskList' 'setCurrentTaskList'
]), ]),

View File

@ -34,7 +34,7 @@
<button type="submit" class="button is-primary" :disabled="!enableSubmit">Login</button> <button type="submit" class="button is-primary" :disabled="!enableSubmit">Login</button>
</div> </div>
<div class="control"> <div class="control">
<button class="button is-secondary" @click="showLogin = false">Cancel</button> <button type="button" class="button is-secondary" @click="showLogin = false">Cancel</button>
</div> </div>
</div> </div>
@ -47,8 +47,8 @@
<script> <script>
import api from "../lib/Api"; import { mapActions, mapState } from "pinia";
import { mapActions, mapState } from "vuex"; import { useAppConfigStore } from "../stores/appConfig";
export default { export default {
data() { data() {
@ -61,7 +61,7 @@
}, },
computed: { computed: {
...mapState([ ...mapState(useAppConfigStore, [
'loginMessage' 'loginMessage'
]), ]),
enableSubmit() { enableSubmit() {
@ -70,7 +70,7 @@
}, },
methods: { methods: {
...mapActions([ ...mapActions(useAppConfigStore, [
'login' 'login'
]), ]),
@ -86,6 +86,7 @@
this.loadResource( this.loadResource(
this.login(params) this.login(params)
.then(data => { .then(data => {
console.log(data);
if (data.success) { if (data.success) {
this.showLogin = false; this.showLogin = false;
} }

View File

@ -1,54 +1,6 @@
import Vue from 'vue'; import { mapActions, mapState } from "pinia";
import { mapActions, mapGetters, mapMutations, mapState } from 'vuex'; import { useAppConfigStore } from "../stores/appConfig";
import api from "../lib/Api";
Vue.mixin({
data() {
return {
localLoadingCount: 0
};
},
computed: {
...mapGetters([
"isLoading",
"isLoggedIn",
"isAdmin"
]),
...mapState([
"user"
]),
localLoading() {
return this.localLoadingCount > 0;
}
},
methods: {
...mapActions([
'updateCurrentUser'
]),
...mapMutations([
'setError',
'setLoading'
]),
loadResource(promise) {
this.setLoading(true);
this.localLoadingCount = this.localLoadingCount + 1;
return promise
.catch(err => this.setError(err))
.then(res => {
this.setLoading(false);
this.localLoadingCount = this.localLoadingCount - 1;
return res;
});
},
checkAuthentication() {
return this.loadResource(this.updateCurrentUser());
}
}
});
function clickStrikeClick(evt) { function clickStrikeClick(evt) {
const isStrikable = el => el && el.tagName === "LI"; const isStrikable = el => el && el.tagName === "LI";
@ -73,12 +25,59 @@ function clickStrikeClick(evt) {
} }
} }
Vue.directive('click-strike', { export function installMixins(app) {
bind(el) { app.directive('click-strike', {
beforeMount(el) {
el.addEventListener("click", clickStrikeClick); el.addEventListener("click", clickStrikeClick);
}, },
unbind(el) { unmounted(el) {
el.removeEventListener("click", clickStrikeClick); el.removeEventListener("click", clickStrikeClick);
} }
}); });
app.mixin({
data() {
return {
localLoadingCount: 0
};
},
computed: {
...mapState(useAppConfigStore, [
"isLoading",
"isLoggedIn",
"isAdmin",
"user"
]),
localLoading() {
return this.localLoadingCount > 0;
}
},
methods: {
...mapActions(useAppConfigStore, [
'setError',
'setLoading',
'updateCurrentUser'
]),
loadResource(promise) {
this.setLoading(true);
this.localLoadingCount = this.localLoadingCount + 1;
return promise
.catch(err => this.setError(err))
.then(res => {
this.setLoading(false);
this.localLoadingCount = this.localLoadingCount - 1;
return res;
});
},
checkAuthentication() {
return this.loadResource(this.updateCurrentUser());
}
}
});
}

View File

@ -1,4 +1,6 @@
import { useAppConfigStore } from '../stores/appConfig';
function trackInstall(worker, cb) { function trackInstall(worker, cb) {
worker.addEventListener('statechange', function() { worker.addEventListener('statechange', function() {
if (worker.state == 'installed') { if (worker.state == 'installed') {
@ -18,7 +20,9 @@ function trackActive(worker, cb) {
export function swUpdate() { export function swUpdate() {
navigator.serviceWorker.getRegistration().then(reg => { navigator.serviceWorker.getRegistration().then(reg => {
if (reg && reg.waiting) { if (reg && reg.waiting) {
trackActive(reg.waiting, () => window.location.reload(true)); trackActive(reg.waiting, () => {
window.location.reload(true)
});
reg.waiting.postMessage("skipWaiting"); reg.waiting.postMessage("skipWaiting");
} else { } else {
window.location.reload(true); window.location.reload(true);
@ -27,10 +31,17 @@ export function swUpdate() {
} }
export function swInit(store) { export function swInit() {
const updateReady = () => store.commit("setUpdateAvailable", true); const updateReady = () => {
const clearUpdateReady = () => store.commit("setUpdateAvailable", false); const store = useAppConfigStore();
store.updateAvailable = true;
};
const clearUpdateReady = () => {
const store = useAppConfigStore();
store.updateAvailable = false;
}
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js') navigator.serviceWorker.register('/sw.js')
@ -53,6 +64,4 @@ export function swInit(store) {
console.log('Registration failed with ' + error); console.log('Registration failed with ' + error);
}); });
} }
} }

View File

@ -0,0 +1,54 @@
import '../styles';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { swInit } from "../lib/ServiceWorker";
import config from '../lib/config';
import { installMixins } from "../lib/GlobalMixins";
import router from '../router';
import AppAutocomplete from "../components/AppAutocomplete";
import AppConfirm from "../components/AppConfirm";
import AppDateTime from "../components/AppDateTime";
import AppDatePicker from "../components/AppDatePicker";
import AppDropdown from "../components/AppDropdown";
import AppExpandTransition from "../components/AppExpandTransition";
import AppIcon from "../components/AppIcon";
import AppIconicIcon from "../components/AppIconicIcon";
import AppModal from "../components/AppModal";
import AppNavbar from "../components/AppNavbar";
import AppPager from "../components/AppPager";
import AppRating from "../components/AppRating";
import AppSearchText from "../components/AppSearchText";
import AppTagEditor from "../components/AppTagEditor";
import AppTextField from "../components/AppTextField";
import AppValidationErrors from "../components/AppValidationErrors";
import App from '../components/App.vue'
document.addEventListener('DOMContentLoaded', () => {
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.use(router);
swInit();
installMixins(app);
app.component("AppAutocomplete", AppAutocomplete);
app.component("AppConfirm", AppConfirm);
app.component("AppDateTime", AppDateTime);
app.component("AppDatePicker", AppDatePicker);
app.component("AppDropdown", AppDropdown);
app.component("AppExpandTransition", AppExpandTransition);
app.component("AppIcon", AppIcon);
app.component("AppIconicIcon", AppIconicIcon);
app.component("AppModal", AppModal);
app.component("AppNavbar", AppNavbar);
app.component("AppPager", AppPager);
app.component("AppRating", AppRating);
app.component("AppSearchText", AppSearchText);
app.component("AppTagEditor", AppTagEditor);
app.component("AppTextField", AppTextField);
app.component("AppValidationErrors", AppValidationErrors);
app.mount('#app');
});

View File

@ -1,5 +1,5 @@
import Vue from 'vue'; import { createRouter, createWebHashHistory } from "vue-router";
import Router from 'vue-router'; import { nextTick } from "vue";
import The404Page from '../components/The404Page'; import The404Page from '../components/The404Page';
import TheAboutPage from '../components/TheAboutPage'; import TheAboutPage from '../components/TheAboutPage';
@ -28,26 +28,12 @@ import TheUserEditor from '../components/TheUserEditor';
import TheAdminUserList from '../components/TheAdminUserList'; import TheAdminUserList from '../components/TheAdminUserList';
import TheAdminUserEditor from '../components/TheAdminUserEditor'; import TheAdminUserEditor from '../components/TheAdminUserEditor';
import $store from '../store'; import { useAppConfigStore } from "../stores/appConfig";
Vue.use(Router);
const router = new Router({ const router = createRouter({
routes: [] history: createWebHashHistory(),
}); routes: [
router.afterEach((to, from) => {
if (to.meta.handleInitialLoad !== true && $store.state.initialLoad === false) {
$store.commit("setInitialLoad", true);
}
Vue.nextTick(() => {
document.title = to.meta.title || 'Parsley';
});
});
router.addRoutes(
[
{ {
path: '/', path: '/',
redirect: '/recipes' redirect: '/recipes'
@ -148,10 +134,10 @@ router.addRoutes(
{ {
path: "/logout", path: "/logout",
name: "logout", name: "logout",
beforeEnter: (to, from, next) => { beforeEnter: async (to, from, next) => {
const $store = router.app.$store; const appConfig = useAppConfigStore();
$store.dispatch("logout") await appConfig.logout();
.then(() => next("/")); return next("/");
} }
}, },
@ -179,10 +165,22 @@ router.addRoutes(
}, },
{ {
path: '*', path: '/:pathMatch(.*)*',
name: 'NotFound',
component: The404Page component: The404Page
} }
] ]
); });
router.afterEach((to, from) => {
const appConfigStore = useAppConfigStore();
if (to.meta.handleInitialLoad !== true && appConfigStore.initialLoad === false) {
appConfigStore.initialLoad = true;
}
nextTick(() => {
document.title = to.meta.title || 'Parsley';
});
});
export default router; export default router;

View File

@ -0,0 +1,102 @@
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { useRoute } from "vue-router";
import api from "../lib/Api";
export const useAppConfigStore = defineStore('appConfig', () => {
const authChecked = ref(false);
const errorObject = ref(null);
const initialLoad = ref(false);
const loadingCount = ref(0);
const loginMessage = ref(null);
const updateAvailable = ref(false);
const userObject = ref(null);
const route = useRoute();
const isLoading = computed(() => {
return loadingCount.value > 0;
});
const isLoggedIn = computed(() => {
return userObject.value !== null;
});
const isAdmin = computed(() => {
return userObject.value?.admin === true;
});
const error = computed({
get: () => errorObject.value,
set: (val) => {
console.log(val);
errorObject.value = val;
}
});
const user = computed({
get: () => userObject.value,
set: (val) => {
userObject.value = val;
authChecked.value = true;
}
});
function setError(value) {
error.value = value;
}
function setLoading(value) {
if (value) {
loadingCount.value = loadingCount.value + 1;
} else {
loadingCount.value = loadingCount.value - 1;
}
}
async function updateCurrentUser() {
user.value = await api.getCurrentUser();
return user.value;
}
function login(authData) {
return api.postLogin(authData.username, authData.password)
.then(data => {
if (data.success) {
user.value = data.user;
loginMessage.value = null;
} else {
user.value = null;
loginMessage.value = data.message;
}
return data;
});
}
function logout() {
return api.getLogout()
.then(() => {
user.value = null;
});
}
return {
authChecked,
error,
initialLoad,
loadingCount,
loginMessage,
updateAvailable,
user,
route,
isAdmin,
isLoading,
isLoggedIn,
login,
logout,
setError,
setLoading,
updateCurrentUser
};
});

View File

@ -0,0 +1,35 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useMediaQueryStore = defineStore('mediaQuery', () => {
// Hard coded values taken directly from Bulma css
const mobileBp = 768;
const desktopBp = 1024;
const widscreenBp = 1216;
const fullHdBp = 1408;
const mediaQueries = {
mobile: `screen and (max-width: ${mobileBp}px)`,
tablet: `screen and (min-width: ${mobileBp + 1}px)`,
tabletOnly: `screen and (min-width: ${mobileBp + 1}px) and (max-width: ${desktopBp - 1}px)`,
touch: `screen and (max-width: ${desktopBp - 1}px)`,
desktop: `screen and (min-width: ${desktopBp}px)`,
desktopOnly: `screen and (min-width: ${desktopBp}px) and (max-width: ${widscreenBp - 1}px)`,
widescreen: `screen and (min-width: ${widscreenBp}px)`,
widescreenOnly: `screen and (min-width: ${widscreenBp}px) and (max-width: ${fullHdBp - 1}px)`,
fullhd: `screen and (min-width: ${fullHdBp}px)`
};
const store = {};
for (let device in mediaQueries) {
const query = window.matchMedia(mediaQueries[device]);
store[device] = ref(query.matches);
query.onchange = (q) => {
store[device].value = q.matches;
};
}
return store;
});

View File

@ -0,0 +1,36 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useNutrientStore = defineStore('nutrient', () => {
const nutrientList = ref({
kcal: { label: "Calories", unit: "kcal" },
protein: { label: "Protein", unit: "g" },
lipids: { label: "Fat", unit: "g" },
carbohydrates: { label: "Carbohydrates", unit: "g" },
water: { label: "Water", unit: "g" },
sugar: { label: "Sugar", unit: "g" },
fiber: { label: "Fiber", unit: "g" },
cholesterol: { label: "Cholesterol", unit: "mg" },
sodium: { label: "Sodium", unit: "mg" },
calcium: { label: "Calcium", unit: "mg" },
iron: { label: "Iron", unit: "mg" },
magnesium: { label: "Magnesium", unit: "mg" },
phosphorus: { label: "Phosphorus", unit: "mg" },
potassium: { label: "Potassium", unit: "mg" },
zinc: { label: "Zinc", unit: "mg" },
copper: { label: "Copper", unit: "mg" },
manganese: { label: "Manganese", unit: "mg" },
vit_a: { label: "Vitamin A", unit: "μg" },
vit_b6: { label: "Vitamin B6", unit: "mg" },
vit_b12: { label: "Vitamin B12", unit: "μg" },
vit_c: { label: "Vitamin C", unit: "mg" },
vit_d: { label: "Vitamin D", unit: "μg" },
vit_e: { label: "Vitamin E", unit: "mg" },
vit_k: { label: "Vitamin K", unit: "μg" },
ash: { label: "ash", unit: "g" }
});
return {
nutrientList
};
});

View File

@ -0,0 +1,114 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { useAppConfigStore } from "./appConfig";
import api from "../lib/Api";
import { createChannel } from "../lib/ActionCable";
export const useTaskStore = defineStore('task', () => {
const taskLists = ref([]);
const currentTaskList = ref(null);
let taskChannel = null;
function replaceTaskList(list) {
if (taskLists.value) {
const listIdx = taskLists.value.findIndex(l => l.id === list.id);
if (listIdx >= 0) {
taskLists.value.splice(listIdx, 1, list);
}
if (currentTaskList.value && currentTaskList.value.id === list.id) {
currentTaskList.value = list;
}
}
}
function ensureTaskListChannel() {
if (taskChannel === null) {
taskChannel = createChannel(null, "TaskChannel", {
received(data) {
if (data && data.action === 'updated') {
replaceTaskList(data.task_list);
}
}
});
}
}
function refreshTaskLists() {
const cb = function(data) {
taskLists.value = data || [];
let ctl = null;
if (currentTaskList.value) {
ctl = data.find(l => l.id === currentTaskList.value.id);
}
ctl = ctl || data[0] || null;
setCurrentTaskList(ctl);
ensureTaskListChannel();
};
return api.getTaskLists(cb)
}
function ensureTaskLists() {
const appConfig = useAppConfigStore();
if (appConfig.user && taskLists.value.length === 0) {
return refreshTaskLists();
} else {
return Promise.resolve();
}
}
function setCurrentTaskList(taskList) {
currentTaskList.value = taskList || null;
}
async function createTaskList(newList) {
currentTaskList.value = await api.postTaskList(newList);
return refreshTaskLists();
}
async function deleteTaskList(taskList) {
await api.deleteTaskList(taskList);
return refreshTaskLists();
}
function createTaskItem(taskItem) {
return api.postTaskItem(taskItem.task_list_id, taskItem)
.then(data => {
return data;
});
}
function updateTaskItem(taskItem) {
return api.patchTaskItem(taskItem.task_list_id, taskItem)
.then(data => {
return data;
});
}
function deleteTaskItems(payload) {
return api.deleteTaskItems(payload.taskList.id, payload.taskItems);
}
function completeTaskItems(payload) {
return api.completeTaskItems(payload.taskList.id, payload.taskItems, !payload.completed);
}
return {
currentTaskList,
taskLists,
createTaskList,
deleteTaskList,
ensureTaskLists,
refreshTaskLists,
setCurrentTaskList,
createTaskItem,
updateTaskItem,
deleteTaskItems,
completeTaskItems
};
});

View File

@ -1,21 +1,25 @@
@import "./variables"; // coolors.co pallet
$coolors-dark: rgba(29, 30, 24, 1);
$coolors-blue: rgba(67, 127, 151, 1);
$coolors-green: rgba(121, 167, 54, 1);
$coolors-red: #ab4c34;
$coolors-yellow: rgba(240, 162, 2, 1);
@import "~bulma/sass/utilities/_all"; $family-serif: Georgia, "Times New Roman", Times, serif;
@import "~bulma/sass/base/_all";
@import "~bulma/sass/components/dropdown";
@import "~bulma/sass/components/navbar";
@import "~bulma/sass/components/level";
@import "~bulma/sass/components/message";
@import "~bulma/sass/components/modal";
@import "~bulma/sass/components/pagination";
@import "~bulma/sass/components/panel";
@import "~bulma/sass/elements/_all";
@import "~bulma/sass/form/_all";
@import "~bulma/sass/grid/columns";
@import "~bulma/sass/layout/section";
@import "./responsive_controls"; @use "bulma/sass" as bulma with (
@import "./wide_modal"; $family-primary: $family-serif,
$green: $coolors-green,
$blue: $coolors-blue,
$red: $coolors-red,
$yellow: $coolors-yellow,
$dark: $coolors-dark,
$primary: $coolors-green,
$modal-content-width: 750px
);
// @import "./responsive_controls";
// @import "./wide_modal";
@import "./iconic"; @import "./iconic";
@import "./transitions"; @import "./transitions";
@ -28,7 +32,7 @@ body {
} }
body { body {
background-color: $grey-dark; background-color: bulma.$grey-dark;
padding-bottom: 2rem; padding-bottom: 2rem;
} }
@ -37,13 +41,13 @@ body {
.container { .container {
padding: 1rem; padding: 1rem;
background-color: $white; background-color: bulma.$white;
min-height: 75vh; min-height: 75vh;
} }
} }
.title, .subtitle, .navbar, .button, .pagination, .modal-card-title, th { .title, .subtitle, .navbar, .button, .pagination, .modal-card-title, th {
font-family: $family-sans-serif; font-family: bulma.$family-sans-serif;
} }
.pagination:not(:last-child) { .pagination:not(:last-child) {

View File

@ -1,7 +1,7 @@
<% <%
manifest_data = Webpacker::manifest.refresh manifest_data = Shakapacker::manifest.refresh
manifest_timestamp = File.mtime(Webpacker::config.public_manifest_path).to_i manifest_timestamp = File.mtime(Shakapacker::config.public_manifest_path).to_i
pack_assets = manifest_data.select { |name, asset| name !~ /\.map$/ && name != 'entrypoints' }.values pack_assets = manifest_data.select { |name, asset| name !~ /\.map$/ && name != 'entrypoints' }.values
%> %>

View File

@ -17,6 +17,9 @@ FileUtils.chdir APP_ROOT do
system! "gem install bundler --conservative" system! "gem install bundler --conservative"
system("bundle check") || system!("bundle install") system("bundle check") || system!("bundle install")
# Install JavaScript dependencies
system!("yarn install --no-immutable")
# puts "\n== Copying sample files ==" # puts "\n== Copying sample files =="
# unless File.exist?("config/database.yml") # unless File.exist?("config/database.yml")
# FileUtils.cp "config/database.yml.sample", "config/database.yml" # FileUtils.cp "config/database.yml.sample", "config/database.yml"

View File

@ -4,10 +4,10 @@ ENV["RAILS_ENV"] ||= "development"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__) ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
require "bundler/setup" require "bundler/setup"
require "webpacker" require "shakapacker"
require "webpacker/webpack_runner" require "shakapacker/webpack_runner"
APP_ROOT = File.expand_path("..", __dir__) APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do Dir.chdir(APP_ROOT) do
Webpacker::WebpackRunner.run(ARGV) Shakapacker::WebpackRunner.run(ARGV)
end end

View File

@ -4,10 +4,10 @@ ENV["RAILS_ENV"] ||= "development"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__) ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
require "bundler/setup" require "bundler/setup"
require "webpacker" require "shakapacker"
require "webpacker/dev_server_runner" require "shakapacker/dev_server_runner"
APP_ROOT = File.expand_path("..", __dir__) APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do Dir.chdir(APP_ROOT) do
Webpacker::DevServerRunner.run(ARGV) Shakapacker::DevServerRunner.run(ARGV)
end end

View File

@ -1,19 +0,0 @@
#!/usr/bin/env ruby
ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
ENV["NODE_ENV"] ||= "development"
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
require "rubygems"
require "bundler/setup"
require "webpacker"
require "webpacker/webpack_runner"
APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do
Webpacker::WebpackRunner.run(ARGV)
end

View File

@ -1,19 +0,0 @@
#!/usr/bin/env ruby
ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
ENV["NODE_ENV"] ||= "development"
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
require "rubygems"
require "bundler/setup"
require "webpacker"
require "webpacker/dev_server_runner"
APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do
Webpacker::DevServerRunner.run(ARGV)
end

View File

@ -1,18 +0,0 @@
#!/usr/bin/env ruby
APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do
yarn = ENV["PATH"].split(File::PATH_SEPARATOR).
select { |dir| File.expand_path(dir) != __dir__ }.
product(["yarn", "yarnpkg", "yarn.cmd", "yarn.ps1"]).
map { |dir, file| File.expand_path(file, dir) }.
find { |file| File.executable?(file) }
if yarn
exec yarn, *ARGV
else
$stderr.puts "Yarn executable was not detected in the system."
$stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
exit 1
end
end

View File

@ -1,15 +1,17 @@
# Note: You must restart bin/webpacker-dev-server for changes to take effect # Note: You must restart bin/shakapacker-dev-server for changes to take effect
# This file contains the defaults used by shakapacker.
default: &default default: &default
source_path: app/javascript source_path: app/javascript
# You can have a subdirectory of the source_path, like 'packs' (recommended). # You can have a subdirectory of the source_path, like 'packs' (recommended).
# Alternatively, you can use '/' to use the whole source_path directory. # Alternatively, you can use '/' to use the whole source_path directory.
source_entry_path: / # Notice that this is a relative path to source_path
source_entry_path: packs
# If nested_entries is true, then we'll pick up subdirectories within the source_entry_path. # If nested_entries is true, then we'll pick up subdirectories within the source_entry_path.
# You cannot set this option to true if you set source_entry_path to '/' # You cannot set this option to true if you set source_entry_path to '/'
nested_entries: false nested_entries: true
# While using a File-System-based automated bundle generation feature, miscellaneous warnings suggesting css order # While using a File-System-based automated bundle generation feature, miscellaneous warnings suggesting css order
# conflicts may arise due to the mini-css-extract-plugin. For projects where css ordering has been mitigated through # conflicts may arise due to the mini-css-extract-plugin. For projects where css ordering has been mitigated through
@ -19,10 +21,10 @@ default: &default
public_root_path: public public_root_path: public
public_output_path: packs public_output_path: packs
cache_path: tmp/webpacker cache_path: tmp/shakapacker
webpack_compile_output: true webpack_compile_output: true
# See https://github.com/shakacode/shakapacker#deployment # See https://github.com/shakacode/shakapacker#deployment
webpacker_precompile: true shakapacker_precompile: true
# Location for manifest.json, defaults to {public_output_path}/manifest.json if unset # Location for manifest.json, defaults to {public_output_path}/manifest.json if unset
# manifest_path: public/packs/manifest.json # manifest_path: public/packs/manifest.json
@ -35,48 +37,63 @@ default: &default
cache_manifest: false cache_manifest: false
# Select loader to use, available options are 'babel' (default), 'swc' or 'esbuild' # Select loader to use, available options are 'babel' (default), 'swc' or 'esbuild'
webpack_loader: 'babel' webpack_loader: 'swc'
# Set to true to enable check for matching versions of shakapacker gem and NPM package - will raise an error if there is a mismatch or wildcard versioning is used # Raises an error if there is a mismatch in the shakapacker gem and npm package being used
ensure_consistent_versioning: false ensure_consistent_versioning: true
# Select whether the compiler will use SHA digest ('digest' option) or most most recent modified timestamp ('mtime') to determine freshness # Select whether the compiler will use SHA digest ('digest' option) or most most recent modified timestamp ('mtime') to determine freshness
compiler_strategy: digest compiler_strategy: digest
# Select whether the compiler will always use a content hash and not just in production
# Don't use contentHash except for production for performance
# https://webpack.js.org/guides/build-performance/#avoid-production-specific-tooling
useContentHash: false
# Setting the asset host here will override Rails.application.config.asset_host.
# Here, you can set different asset_host per environment. Note that
# SHAKAPACKER_ASSET_HOST will override both configurations.
# asset_host: custom-path
development: development:
<<: *default <<: *default
compile: true compile: true
compiler_strategy: mtime compiler_strategy: mtime
# Reference: https://webpack.js.org/configuration/dev-server/ # Reference: https://webpack.js.org/configuration/dev-server/
# Keys not described there are documented inline and in https://github.com/shakacode/shakapacker/
dev_server: dev_server:
https: false # For running dev server with https, set `server: https`.
# server: https
host: localhost host: localhost
port: 3035 port: 3035
# Hot Module Replacement updates modules while the application is running without a full reload # Hot Module Replacement updates modules while the application is running without a full reload
# Used instead of the `hot` key in https://webpack.js.org/configuration/dev-server/#devserverhot
hmr: false hmr: false
# If HMR is on, CSS will by inlined by delivering it as part of the script payload via style-loader. Be sure # If HMR is on, CSS will by inlined by delivering it as part of the script payload via style-loader. Be sure
# that you add style-loader to your project dependencies. # that you add style-loader to your project dependencies.
# #
# If you want to instead deliver CSS via <link> with the mini-extract-css-plugin, set inline_css to false. # If you want to instead deliver CSS via <link> with the mini-css-extract-plugin, set inline_css to false.
# In that case, style-loader is not needed as a dependency. # In that case, style-loader is not needed as a dependency.
# #
# mini-extract-css-plugin is a required dependency in both cases. # mini-css-extract-plugin is a required dependency in both cases.
inline_css: true inline_css: true
# Defaults to the inverse of hmr. Uncomment to manually set this. # Defaults to the inverse of hmr. Uncomment to manually set this.
# live_reload: true live_reload: false
client: client:
# Should we show a full-screen overlay in the browser when there are compiler errors or warnings? # Should we show a full-screen overlay in the browser when there are compiler errors or warnings?
overlay: true overlay: true
# May also be a string # May also be a string
# webSocketURL: # webSocketURL:
# hostname: "0.0.0.0" # hostname: '0.0.0.0'
# pathname: "/ws" # pathname: '/ws'
# port: 8080 # port: 8080
# Should we use gzip compression? # Should we use gzip compression?
compress: true compress: true
# Note that apps that do not check the host are vulnerable to DNS rebinding attacks # Note that apps that do not check the host are vulnerable to DNS rebinding attacks
allowed_hosts: "all" allowed_hosts: 'auto'
# Shows progress and colorizes output of bin/shakapacker[-dev-server]
pretty: true pretty: true
headers: headers:
'Access-Control-Allow-Origin': '*' 'Access-Control-Allow-Origin': '*'
@ -91,17 +108,14 @@ test:
# Compile test packs to a separate directory # Compile test packs to a separate directory
public_output_path: packs-test public_output_path: packs-test
production: &production production:
<<: *default <<: *default
# Production depends on precompilation of packs prior to booting for performance. # Production depends on precompilation of packs prior to booting for performance.
compile: false compile: false
# Use content hash for naming assets. Cannot be overridden by for production.
useContentHash: true
# Cache manifest.json for performance # Cache manifest.json for performance
cache_manifest: true cache_manifest: true
docker:
<<: *production
beta:
<<: *production

View File

@ -13,9 +13,11 @@ const iriElementsAndProperties = {
}; };
module.exports = function(content) { module.exports = function(content) {
this.cacheable && this.cacheable(); this.cacheable && this.cacheable();
const $ = cheerio.load(content, { const $ = cheerio.load(content, {
xmlMode: true,
lowerCaseTags: false lowerCaseTags: false
}); });
@ -71,6 +73,7 @@ module.exports = function(content) {
const svgMarkup = $("svg").html(); const svgMarkup = $("svg").html();
return `module.exports = ${JSON.stringify({ return `module.exports = ${JSON.stringify({
attributes: attributes, attributes: attributes,
content: svgMarkup, content: svgMarkup,

View File

@ -6,10 +6,9 @@ module.exports = {
{ {
test: /\/iconic\/svg\/.*\.svg$/, test: /\/iconic\/svg\/.*\.svg$/,
use: [ use: [
'vue-loader', { loader: path.join(__dirname, '../loaders/iconic_svg_loader.js') }
path.join(__dirname, '../loaders/iconic_svg_loader.js')
], ],
type: 'asset/source' type: 'javascript/auto'
} }
] ]
}, },

View File

@ -1,3 +1,5 @@
const { env } = require('shakapacker')
const { DefinePlugin } = require('webpack')
const { VueLoaderPlugin } = require('vue-loader') const { VueLoaderPlugin } = require('vue-loader')
module.exports = { module.exports = {
@ -9,7 +11,14 @@ module.exports = {
} }
] ]
}, },
plugins: [new VueLoaderPlugin()], plugins: [
new VueLoaderPlugin(),
new DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: env.isDevelopment,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: env.isDevelopment
})
],
resolve: { resolve: {
extensions: ['.vue'] extensions: ['.vue']
} }

View File

@ -1,7 +1,10 @@
const { merge, mergeWithRules, webpackConfig } = require('shakapacker') // See the shakacode/shakapacker README and docs directory for advice on customizing your webpackConfig.
const cssConfig = require('./rules/css'); const { generateWebpackConfig, merge, mergeWithRules } = require('shakapacker')
const vueConfig = require('./rules/vue'); const cssConfig = require('./rules/css')
const svgConfig = require('./rules/svg'); const svgConfig = require('./rules/svg')
const vueConfig = require('./rules/vue')
const webpackConfig = generateWebpackConfig()
let conf = merge(vueConfig, svgConfig); let conf = merge(vueConfig, svgConfig);
conf = merge(cssConfig, conf); conf = merge(cssConfig, conf);
@ -28,5 +31,4 @@ conf = mergeWithRules({
} }
})(conf, updateFileLoaderConf); })(conf, updateFileLoaderConf);
module.exports = conf module.exports = conf

View File

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name ="theme-color" content="rgba(121, 167, 54, 1)">
<link rel="manifest" href="/manifest.json">
<title>Parsley</title>
</head>
<body>
<div id="app" data-url="http://localhost:3000/">
<div id="app-placeholder">
Loading...
</div>
</div>
<script src="/packs/application-3067a90281a92d120ca6.js"></script>
</body>
</html>

View File

@ -1,54 +0,0 @@
namespace :dev do
desc 'Run both rails server and webpack dev server'
task :run do
rails_pid = fork do
exec 'rails s'
end
webpack_pid = fork do
exec 'bin/webpack-dev-server'
end
running = true
shutdown = false
shutdown_start = nil
Signal.trap('SIGINT') do
shutdown = true
end
while running
rails_check ||= Process.waitpid(rails_pid, Process::WNOHANG)
webpack_check ||= Process.waitpid(webpack_pid, Process::WNOHANG)
running = rails_check.nil? || webpack_check.nil?
if shutdown
if shutdown_start.nil?
puts "Shutting down..."
shutdown_start = Time.now
#Process.kill("SIGINT", rails_pid) rescue Errno::ESRCH
#Process.kill("SIGINT", webpack_pid) rescue Errno::ESRCH
end
if (Time.now - shutdown_start) > 5
if rails_check.nil?
puts "Force killing rails..."
Process.kill("KILL", rails_pid)
end
if webpack_check.nil?
puts "Force killing webpack..."
Process.kill("KILL", webpack_pid)
end
end
end
sleep 0.25
end
end
end

View File

@ -1,44 +1,11 @@
{ {
"name": "parsley", "packageManager": "yarn@4.5.0",
"name": "app",
"private": true, "private": true,
"dependencies": {
"@babel/core": "7",
"@babel/plugin-transform-runtime": "7",
"@babel/preset-env": "7",
"@babel/runtime": "7",
"@rails/actioncable": "^7.0.4",
"@tweenjs/tween.js": "^18.6.4",
"autosize": "^5.0.2",
"babel-loader": "8",
"bulma": "0.9.4",
"cheerio": "^1.0.0-rc.12",
"compression-webpack-plugin": "9",
"css-loader": "^6.7.2",
"css-minimizer-webpack-plugin": "^4.2.2",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.7.1",
"sass": "^1.56.1",
"sass-loader": "^13.2.0",
"shakapacker": "6.5.4",
"style-loader": "^3.3.1",
"svg-loader": "^0.0.2",
"terser-webpack-plugin": "5",
"url-loader": "^4.1.1",
"vue": "^2.6.14",
"vue-loader": "^15.10.1",
"vue-progressbar": "^0.7.5",
"vue-resize": "^1.0.1",
"vue-router": "^3.6.5",
"vue-template-compiler": "^2.6.14",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0",
"webpack": "5",
"webpack-assets-manifest": "5",
"webpack-cli": "4",
"webpack-dev-server": "^4.11.1",
"webpack-merge": "5"
},
"version": "0.1.0", "version": "0.1.0",
"scripts": {
"console": "node"
},
"babel": { "babel": {
"presets": [ "presets": [
"./node_modules/shakapacker/package/babel/preset.js" "./node_modules/shakapacker/package/babel/preset.js"
@ -47,5 +14,40 @@
"browserslist": [ "browserslist": [
"defaults" "defaults"
], ],
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "dependencies": {
"@babel/core": "7",
"@babel/plugin-transform-runtime": "7",
"@babel/preset-env": "7",
"@babel/runtime": "7",
"@rails/actioncable": "7.2.100",
"@swc/core": "1.7.26",
"@tweenjs/tween.js": "^25.0.0",
"@types/babel__core": "7",
"@types/webpack": "5",
"babel-loader": "8",
"bulma": "^1.0.2",
"cheerio": "^1.0.0",
"compression-webpack-plugin": "9",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
"lodash": "^4.17.21",
"mini-css-extract-plugin": "^2.9.1",
"pinia": "^2.2.2",
"sass": "~1.78.0",
"sass-loader": "^16.0.2",
"shakapacker": "8.0.2",
"style-loader": "^4.0.0",
"swc-loader": "^0.2.6",
"terser-webpack-plugin": "5",
"vue": "^3.5.10",
"vue-loader": "^17.4.2",
"vue-router": "^4.4.5",
"webpack": "5",
"webpack-assets-manifest": "5",
"webpack-cli": "4",
"webpack-merge": "5"
},
"devDependencies": {
"webpack-dev-server": "4"
}
} }

View File

@ -1,12 +0,0 @@
module.exports = {
plugins: [
require('postcss-import'),
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009'
},
stage: 3
})
]
}

11270
yarn.lock

File diff suppressed because it is too large Load Diff