Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
c26d94e504 | |||
659d7405b2 | |||
169dda4d3b | |||
f5e32cf888 | |||
1b6b3135c7 | |||
808c805cde | |||
7ead02ad7e | |||
67c23015ab | |||
0d35f50dbf | |||
a071e6b21e | |||
b957d44aed | |||
f246f71aa9 | |||
bb2e29f25c | |||
552524e78b | |||
2b77bc9fb8 | |||
855b4ac779 | |||
b311a7d7e8 | |||
7f1cf99237 | |||
dd915624a3 | |||
a306f72215 | |||
1a695b795e |
@ -1 +0,0 @@
|
|||||||
> 1%, not ie <= 12
|
|
@ -4,4 +4,6 @@ db/*.sqlite*
|
|||||||
tmp/*.*
|
tmp/*.*
|
||||||
public/assets
|
public/assets
|
||||||
public/packs
|
public/packs
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.yarn
|
||||||
|
.pnp.*
|
12
.gitignore
vendored
12
.gitignore
vendored
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
# Ignore the default SQLite database.
|
# Ignore the default SQLite database.
|
||||||
/db/*.sqlite3
|
/db/*.sqlite3
|
||||||
/db/*.sqlite3-journal
|
/db/*.sqlite3*
|
||||||
|
|
||||||
# Ignore all logfiles and tempfiles.
|
# Ignore all logfiles and tempfiles.
|
||||||
/log/*
|
/log/*
|
||||||
@ -27,3 +27,13 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
|
/dist
|
||||||
|
|
||||||
|
/public/packs
|
||||||
|
/public/packs-test
|
||||||
|
/node_modules
|
||||||
|
/yarn-error.log
|
||||||
|
yarn-debug.log*
|
||||||
|
.yarn-integrity
|
||||||
|
.yarn
|
||||||
|
.pnp.*
|
@ -1 +1 @@
|
|||||||
3.0.1
|
3.3.5
|
||||||
|
12
Dockerfile
12
Dockerfile
@ -1,17 +1,13 @@
|
|||||||
FROM ruby:3.0.1-buster
|
FROM ruby:3.3.5-bookworm
|
||||||
|
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash - && \
|
RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash - && \
|
||||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
|
||||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list && \
|
|
||||||
apt-get update && apt-get dist-upgrade -y && \
|
apt-get update && apt-get dist-upgrade -y && \
|
||||||
apt-get install -y \
|
apt-get install -y \
|
||||||
nodejs \
|
nodejs \
|
||||||
yarn \
|
|
||||||
nginx && \
|
nginx && \
|
||||||
apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
|
apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN gem update --system && gem install bundler
|
RUN gem update --no-document --system && gem install bundler --no-document && corepack enable
|
||||||
|
|
||||||
|
|
||||||
# Install nginx config files
|
# Install nginx config files
|
||||||
RUN rm /etc/nginx/sites-enabled/default
|
RUN rm /etc/nginx/sites-enabled/default
|
||||||
@ -32,12 +28,12 @@ COPY Gemfile* ./
|
|||||||
RUN bundle install
|
RUN bundle install
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock ./
|
||||||
RUN yarn install --production=true --frozen-lockfile
|
RUN yarn install --immutable
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Compile assets
|
# Compile assets
|
||||||
RUN env RAILS_ENV=production bundle exec rails webpacker:clobber webpacker:compile
|
RUN env RAILS_ENV=production bundle exec rails shakapacker:clobber shakapacker:compile
|
||||||
|
|
||||||
ENV PORT=80
|
ENV PORT=80
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
32
Gemfile
32
Gemfile
@ -1,30 +1,30 @@
|
|||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
gem 'rails', '6.1.3.2'
|
gem 'rails', '7.2.1'
|
||||||
gem 'pg', '~> 1.2.3'
|
gem 'pg', '~> 1.5.8'
|
||||||
|
|
||||||
gem 'webpacker', '5.3.0'
|
gem 'shakapacker', '8.0.2'
|
||||||
gem 'bootsnap', '>= 1.1.0', require: false
|
gem 'bootsnap', '>= 1.1.0', require: false
|
||||||
|
|
||||||
gem 'oj', '~> 3.11.5'
|
gem 'oj', '~> 3.16.6'
|
||||||
|
gem 'csv', '~> 3.3'
|
||||||
|
|
||||||
gem 'kaminari', '~> 1.2.1'
|
gem 'kaminari', '~> 1.2.2'
|
||||||
gem 'unitwise', '~> 2.2.0'
|
gem 'unitwise', '~> 2.3.0'
|
||||||
gem 'redcarpet', '~> 3.5.1'
|
gem 'redcarpet', '~> 3.6.0'
|
||||||
|
|
||||||
gem 'dalli', '~> 2.7.11'
|
gem 'dalli', '~> 3.2.8'
|
||||||
gem 'puma', '~> 5.3'
|
gem 'puma', '~> 6.4'
|
||||||
gem 'bcrypt', '~> 3.1.16'
|
gem 'bcrypt', '~> 3.1.18'
|
||||||
gem 'tzinfo-data'
|
gem 'tzinfo-data'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'sqlite3', '~> 1.4.2'
|
gem 'sqlite3', '~> 2.1.0'
|
||||||
|
|
||||||
gem 'guard', '~> 2.16.2'
|
gem 'guard', '~> 2.18.0'
|
||||||
gem 'guard-rspec', require: false
|
gem 'guard-rspec', require: false
|
||||||
gem 'rspec-rails', '~> 5.0.1'
|
gem 'rspec-rails', '~> 7.0.1'
|
||||||
gem 'rails-controller-testing'
|
gem 'rails-controller-testing'
|
||||||
gem 'factory_bot_rails', '~> 6.2.0'
|
gem 'factory_bot_rails', '~> 6.4.3'
|
||||||
gem 'database_cleaner', '~> 2.0.1'
|
gem 'database_cleaner', '~> 2.0.2'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
439
Gemfile.lock
439
Gemfile.lock
@ -1,97 +1,114 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.3.2)
|
actioncable (7.2.1)
|
||||||
actionpack (= 6.1.3.2)
|
actionpack (= 7.2.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 7.2.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.3.2)
|
zeitwerk (~> 2.6)
|
||||||
actionpack (= 6.1.3.2)
|
actionmailbox (7.2.1)
|
||||||
activejob (= 6.1.3.2)
|
actionpack (= 7.2.1)
|
||||||
activerecord (= 6.1.3.2)
|
activejob (= 7.2.1)
|
||||||
activestorage (= 6.1.3.2)
|
activerecord (= 7.2.1)
|
||||||
activesupport (= 6.1.3.2)
|
activestorage (= 7.2.1)
|
||||||
mail (>= 2.7.1)
|
activesupport (= 7.2.1)
|
||||||
actionmailer (6.1.3.2)
|
mail (>= 2.8.0)
|
||||||
actionpack (= 6.1.3.2)
|
actionmailer (7.2.1)
|
||||||
actionview (= 6.1.3.2)
|
actionpack (= 7.2.1)
|
||||||
activejob (= 6.1.3.2)
|
actionview (= 7.2.1)
|
||||||
activesupport (= 6.1.3.2)
|
activejob (= 7.2.1)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
activesupport (= 7.2.1)
|
||||||
rails-dom-testing (~> 2.0)
|
mail (>= 2.8.0)
|
||||||
actionpack (6.1.3.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionview (= 6.1.3.2)
|
actionpack (7.2.1)
|
||||||
activesupport (= 6.1.3.2)
|
actionview (= 7.2.1)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
activesupport (= 7.2.1)
|
||||||
rack-test (>= 0.6.3)
|
|
||||||
rails-dom-testing (~> 2.0)
|
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
|
||||||
actiontext (6.1.3.2)
|
|
||||||
actionpack (= 6.1.3.2)
|
|
||||||
activerecord (= 6.1.3.2)
|
|
||||||
activestorage (= 6.1.3.2)
|
|
||||||
activesupport (= 6.1.3.2)
|
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.3.2)
|
racc
|
||||||
activesupport (= 6.1.3.2)
|
rack (>= 2.2.4, < 3.2)
|
||||||
|
rack-session (>= 1.0.1)
|
||||||
|
rack-test (>= 0.6.3)
|
||||||
|
rails-dom-testing (~> 2.2)
|
||||||
|
rails-html-sanitizer (~> 1.6)
|
||||||
|
useragent (~> 0.16)
|
||||||
|
actiontext (7.2.1)
|
||||||
|
actionpack (= 7.2.1)
|
||||||
|
activerecord (= 7.2.1)
|
||||||
|
activestorage (= 7.2.1)
|
||||||
|
activesupport (= 7.2.1)
|
||||||
|
globalid (>= 0.6.0)
|
||||||
|
nokogiri (>= 1.8.5)
|
||||||
|
actionview (7.2.1)
|
||||||
|
activesupport (= 7.2.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activejob (6.1.3.2)
|
activejob (7.2.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 7.2.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.3.2)
|
activemodel (7.2.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 7.2.1)
|
||||||
activerecord (6.1.3.2)
|
activerecord (7.2.1)
|
||||||
activemodel (= 6.1.3.2)
|
activemodel (= 7.2.1)
|
||||||
activesupport (= 6.1.3.2)
|
activesupport (= 7.2.1)
|
||||||
activestorage (6.1.3.2)
|
timeout (>= 0.4.0)
|
||||||
actionpack (= 6.1.3.2)
|
activestorage (7.2.1)
|
||||||
activejob (= 6.1.3.2)
|
actionpack (= 7.2.1)
|
||||||
activerecord (= 6.1.3.2)
|
activejob (= 7.2.1)
|
||||||
activesupport (= 6.1.3.2)
|
activerecord (= 7.2.1)
|
||||||
marcel (~> 1.0.0)
|
activesupport (= 7.2.1)
|
||||||
mini_mime (~> 1.0.2)
|
marcel (~> 1.0)
|
||||||
activesupport (6.1.3.2)
|
activesupport (7.2.1)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
base64
|
||||||
|
bigdecimal
|
||||||
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
|
connection_pool (>= 2.2.5)
|
||||||
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
|
logger (>= 1.4.2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
tzinfo (~> 2.0)
|
securerandom (>= 0.3)
|
||||||
zeitwerk (~> 2.3)
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
bcrypt (3.1.16)
|
base64 (0.2.0)
|
||||||
bootsnap (1.7.5)
|
bcrypt (3.1.20)
|
||||||
msgpack (~> 1.0)
|
bigdecimal (3.1.8)
|
||||||
builder (3.2.4)
|
bootsnap (1.18.4)
|
||||||
|
msgpack (~> 1.2)
|
||||||
|
builder (3.3.0)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
concurrent-ruby (1.1.8)
|
concurrent-ruby (1.3.4)
|
||||||
|
connection_pool (2.4.1)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
dalli (2.7.11)
|
csv (3.3.0)
|
||||||
database_cleaner (2.0.1)
|
dalli (3.2.8)
|
||||||
database_cleaner-active_record (~> 2.0.0)
|
database_cleaner (2.0.2)
|
||||||
database_cleaner-active_record (2.0.1)
|
database_cleaner-active_record (>= 2, < 3)
|
||||||
|
database_cleaner-active_record (2.2.0)
|
||||||
activerecord (>= 5.a)
|
activerecord (>= 5.a)
|
||||||
database_cleaner-core (~> 2.0.0)
|
database_cleaner-core (~> 2.0.0)
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
diff-lcs (1.4.4)
|
date (3.3.4)
|
||||||
erubi (1.10.0)
|
diff-lcs (1.5.1)
|
||||||
factory_bot (6.2.0)
|
drb (2.2.1)
|
||||||
|
erubi (1.13.0)
|
||||||
|
factory_bot (6.5.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
factory_bot_rails (6.2.0)
|
factory_bot_rails (6.4.3)
|
||||||
factory_bot (~> 6.2.0)
|
factory_bot (~> 6.4)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
ffi (1.15.0)
|
ffi (1.17.0)
|
||||||
formatador (0.2.5)
|
formatador (1.1.0)
|
||||||
globalid (0.4.2)
|
globalid (1.2.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 6.1)
|
||||||
guard (2.16.2)
|
guard (2.18.1)
|
||||||
formatador (>= 0.2.4)
|
formatador (>= 0.2.4)
|
||||||
listen (>= 2.7, < 4.0)
|
listen (>= 2.7, < 4.0)
|
||||||
lumberjack (>= 1.0.12, < 2.0)
|
lumberjack (>= 1.0.12, < 2.0)
|
||||||
nenv (~> 0.1)
|
nenv (~> 0.1)
|
||||||
notiffany (~> 0.0)
|
notiffany (~> 0.0)
|
||||||
pry (>= 0.9.12)
|
pry (>= 0.13.0)
|
||||||
shellany (~> 0.0)
|
shellany (~> 0.0)
|
||||||
thor (>= 0.18.1)
|
thor (>= 0.18.1)
|
||||||
guard-compat (1.2.1)
|
guard-compat (1.2.1)
|
||||||
@ -99,171 +116,207 @@ GEM
|
|||||||
guard (~> 2.1)
|
guard (~> 2.1)
|
||||||
guard-compat (~> 1.1)
|
guard-compat (~> 1.1)
|
||||||
rspec (>= 2.99.0, < 4.0)
|
rspec (>= 2.99.0, < 4.0)
|
||||||
i18n (1.8.10)
|
i18n (1.14.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
kaminari (1.2.1)
|
io-console (0.7.2)
|
||||||
|
irb (1.14.1)
|
||||||
|
rdoc (>= 4.0.0)
|
||||||
|
reline (>= 0.4.2)
|
||||||
|
kaminari (1.2.2)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
kaminari-actionview (= 1.2.1)
|
kaminari-actionview (= 1.2.2)
|
||||||
kaminari-activerecord (= 1.2.1)
|
kaminari-activerecord (= 1.2.2)
|
||||||
kaminari-core (= 1.2.1)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-actionview (1.2.1)
|
kaminari-actionview (1.2.2)
|
||||||
actionview
|
actionview
|
||||||
kaminari-core (= 1.2.1)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-activerecord (1.2.1)
|
kaminari-activerecord (1.2.2)
|
||||||
activerecord
|
activerecord
|
||||||
kaminari-core (= 1.2.1)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-core (1.2.1)
|
kaminari-core (1.2.2)
|
||||||
liner (0.2.4)
|
liner (0.2.4)
|
||||||
listen (3.5.1)
|
listen (3.9.0)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
loofah (2.9.1)
|
logger (1.6.1)
|
||||||
|
loofah (2.22.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.12.0)
|
||||||
lumberjack (1.2.8)
|
lumberjack (1.2.10)
|
||||||
mail (2.7.1)
|
mail (2.8.1)
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
marcel (1.0.1)
|
net-imap
|
||||||
|
net-pop
|
||||||
|
net-smtp
|
||||||
|
marcel (1.0.4)
|
||||||
memoizable (0.4.2)
|
memoizable (0.4.2)
|
||||||
thread_safe (~> 0.3, >= 0.3.1)
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
method_source (1.0.0)
|
method_source (1.1.0)
|
||||||
mini_mime (1.0.3)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.5.1)
|
mini_portile2 (2.8.7)
|
||||||
minitest (5.14.4)
|
minitest (5.25.1)
|
||||||
msgpack (1.4.2)
|
msgpack (1.7.2)
|
||||||
nenv (0.3.0)
|
nenv (0.3.0)
|
||||||
nio4r (2.5.7)
|
net-imap (0.4.16)
|
||||||
nokogiri (1.11.3)
|
date
|
||||||
mini_portile2 (~> 2.5.0)
|
net-protocol
|
||||||
|
net-pop (0.1.2)
|
||||||
|
net-protocol
|
||||||
|
net-protocol (0.2.2)
|
||||||
|
timeout
|
||||||
|
net-smtp (0.5.0)
|
||||||
|
net-protocol
|
||||||
|
nio4r (2.7.3)
|
||||||
|
nokogiri (1.16.7)
|
||||||
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
notiffany (0.1.3)
|
notiffany (0.1.3)
|
||||||
nenv (~> 0.1)
|
nenv (~> 0.1)
|
||||||
shellany (~> 0.0)
|
shellany (~> 0.0)
|
||||||
oj (3.11.5)
|
oj (3.16.6)
|
||||||
parslet (1.8.2)
|
bigdecimal (>= 3.0)
|
||||||
pg (1.2.3)
|
ostruct (>= 0.2)
|
||||||
pry (0.14.1)
|
ostruct (0.6.0)
|
||||||
|
package_json (0.1.0)
|
||||||
|
parslet (2.0.0)
|
||||||
|
pg (1.5.8)
|
||||||
|
pry (0.14.2)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
puma (5.3.0)
|
psych (5.1.2)
|
||||||
|
stringio
|
||||||
|
puma (6.4.3)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.5.2)
|
racc (1.8.1)
|
||||||
rack (2.2.3)
|
rack (3.1.7)
|
||||||
rack-proxy (0.6.5)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-session (2.0.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 3.0.0)
|
||||||
rails (6.1.3.2)
|
rack-test (2.1.0)
|
||||||
actioncable (= 6.1.3.2)
|
rack (>= 1.3)
|
||||||
actionmailbox (= 6.1.3.2)
|
rackup (2.1.0)
|
||||||
actionmailer (= 6.1.3.2)
|
rack (>= 3)
|
||||||
actionpack (= 6.1.3.2)
|
webrick (~> 1.8)
|
||||||
actiontext (= 6.1.3.2)
|
rails (7.2.1)
|
||||||
actionview (= 6.1.3.2)
|
actioncable (= 7.2.1)
|
||||||
activejob (= 6.1.3.2)
|
actionmailbox (= 7.2.1)
|
||||||
activemodel (= 6.1.3.2)
|
actionmailer (= 7.2.1)
|
||||||
activerecord (= 6.1.3.2)
|
actionpack (= 7.2.1)
|
||||||
activestorage (= 6.1.3.2)
|
actiontext (= 7.2.1)
|
||||||
activesupport (= 6.1.3.2)
|
actionview (= 7.2.1)
|
||||||
|
activejob (= 7.2.1)
|
||||||
|
activemodel (= 7.2.1)
|
||||||
|
activerecord (= 7.2.1)
|
||||||
|
activestorage (= 7.2.1)
|
||||||
|
activesupport (= 7.2.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.3.2)
|
railties (= 7.2.1)
|
||||||
sprockets-rails (>= 2.0.0)
|
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
actionview (>= 5.0.1.rc1)
|
actionview (>= 5.0.1.rc1)
|
||||||
activesupport (>= 5.0.1.rc1)
|
activesupport (>= 5.0.1.rc1)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 5.0.0)
|
||||||
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.3.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.21)
|
||||||
railties (6.1.3.2)
|
nokogiri (~> 1.14)
|
||||||
actionpack (= 6.1.3.2)
|
railties (7.2.1)
|
||||||
activesupport (= 6.1.3.2)
|
actionpack (= 7.2.1)
|
||||||
method_source
|
activesupport (= 7.2.1)
|
||||||
rake (>= 0.8.7)
|
irb (~> 1.13)
|
||||||
thor (~> 1.0)
|
rackup (>= 1.0.0)
|
||||||
rake (13.0.3)
|
rake (>= 12.2)
|
||||||
rb-fsevent (0.11.0)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
rb-inotify (0.10.1)
|
zeitwerk (~> 2.6)
|
||||||
|
rake (13.2.1)
|
||||||
|
rb-fsevent (0.11.2)
|
||||||
|
rb-inotify (0.11.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
redcarpet (3.5.1)
|
rdoc (6.7.0)
|
||||||
rspec (3.10.0)
|
psych (>= 4.0.0)
|
||||||
rspec-core (~> 3.10.0)
|
redcarpet (3.6.0)
|
||||||
rspec-expectations (~> 3.10.0)
|
reline (0.5.10)
|
||||||
rspec-mocks (~> 3.10.0)
|
io-console (~> 0.5)
|
||||||
rspec-core (3.10.1)
|
rspec (3.13.0)
|
||||||
rspec-support (~> 3.10.0)
|
rspec-core (~> 3.13.0)
|
||||||
rspec-expectations (3.10.1)
|
rspec-expectations (~> 3.13.0)
|
||||||
|
rspec-mocks (~> 3.13.0)
|
||||||
|
rspec-core (3.13.1)
|
||||||
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-expectations (3.13.3)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.10.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-mocks (3.10.2)
|
rspec-mocks (3.13.1)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.10.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (5.0.1)
|
rspec-rails (7.0.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 7.0)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 7.0)
|
||||||
railties (>= 5.2)
|
railties (>= 7.0)
|
||||||
rspec-core (~> 3.10)
|
rspec-core (~> 3.13)
|
||||||
rspec-expectations (~> 3.10)
|
rspec-expectations (~> 3.13)
|
||||||
rspec-mocks (~> 3.10)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.10)
|
rspec-support (~> 3.13)
|
||||||
rspec-support (3.10.2)
|
rspec-support (3.13.1)
|
||||||
|
securerandom (0.3.1)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
shellany (0.0.1)
|
shakapacker (8.0.2)
|
||||||
signed_multiset (0.2.1)
|
|
||||||
sprockets (4.0.2)
|
|
||||||
concurrent-ruby (~> 1.0)
|
|
||||||
rack (> 1, < 3)
|
|
||||||
sprockets-rails (3.2.2)
|
|
||||||
actionpack (>= 4.0)
|
|
||||||
activesupport (>= 4.0)
|
|
||||||
sprockets (>= 3.0.0)
|
|
||||||
sqlite3 (1.4.2)
|
|
||||||
thor (1.1.0)
|
|
||||||
thread_safe (0.3.6)
|
|
||||||
tzinfo (2.0.4)
|
|
||||||
concurrent-ruby (~> 1.0)
|
|
||||||
tzinfo-data (1.2021.1)
|
|
||||||
tzinfo (>= 1.0.0)
|
|
||||||
unitwise (2.2.0)
|
|
||||||
liner (~> 0.2)
|
|
||||||
memoizable (~> 0.4)
|
|
||||||
parslet (~> 1.5)
|
|
||||||
signed_multiset (~> 0.2)
|
|
||||||
webpacker (5.3.0)
|
|
||||||
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)
|
||||||
websocket-driver (0.7.3)
|
shellany (0.0.1)
|
||||||
|
signed_multiset (0.2.1)
|
||||||
|
sqlite3 (2.1.0)
|
||||||
|
mini_portile2 (~> 2.8.0)
|
||||||
|
stringio (3.1.1)
|
||||||
|
thor (1.3.2)
|
||||||
|
thread_safe (0.3.6)
|
||||||
|
timeout (0.4.1)
|
||||||
|
tzinfo (2.0.6)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
tzinfo-data (1.2024.2)
|
||||||
|
tzinfo (>= 1.0.0)
|
||||||
|
unitwise (2.3.0)
|
||||||
|
liner (~> 0.2)
|
||||||
|
memoizable (~> 0.4)
|
||||||
|
parslet (~> 2.0)
|
||||||
|
signed_multiset (~> 0.2)
|
||||||
|
useragent (0.16.10)
|
||||||
|
webrick (1.8.2)
|
||||||
|
websocket-driver (0.7.6)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
zeitwerk (2.4.2)
|
zeitwerk (2.6.18)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
bcrypt (~> 3.1.16)
|
bcrypt (~> 3.1.18)
|
||||||
bootsnap (>= 1.1.0)
|
bootsnap (>= 1.1.0)
|
||||||
dalli (~> 2.7.11)
|
csv (~> 3.3)
|
||||||
database_cleaner (~> 2.0.1)
|
dalli (~> 3.2.8)
|
||||||
factory_bot_rails (~> 6.2.0)
|
database_cleaner (~> 2.0.2)
|
||||||
guard (~> 2.16.2)
|
factory_bot_rails (~> 6.4.3)
|
||||||
|
guard (~> 2.18.0)
|
||||||
guard-rspec
|
guard-rspec
|
||||||
kaminari (~> 1.2.1)
|
kaminari (~> 1.2.2)
|
||||||
oj (~> 3.11.5)
|
oj (~> 3.16.6)
|
||||||
pg (~> 1.2.3)
|
pg (~> 1.5.8)
|
||||||
puma (~> 5.3)
|
puma (~> 6.4)
|
||||||
rails (= 6.1.3.2)
|
rails (= 7.2.1)
|
||||||
rails-controller-testing
|
rails-controller-testing
|
||||||
redcarpet (~> 3.5.1)
|
redcarpet (~> 3.6.0)
|
||||||
rspec-rails (~> 5.0.1)
|
rspec-rails (~> 7.0.1)
|
||||||
sqlite3 (~> 1.4.2)
|
shakapacker (= 8.0.2)
|
||||||
|
sqlite3 (~> 2.1.0)
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
unitwise (~> 2.2.0)
|
unitwise (~> 2.3.0)
|
||||||
webpacker (= 5.3.0)
|
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.2.17
|
2.5.20
|
||||||
|
2
LICENSE
2
LICENSE
@ -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
|
||||||
|
2
Procfile
2
Procfile
@ -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
|
||||||
|
@ -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
|
||||||
|
4
app/channels/application_cable/channel.rb
Normal file
4
app/channels/application_cable/channel.rb
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module ApplicationCable
|
||||||
|
class Channel < ActionCable::Channel::Base
|
||||||
|
end
|
||||||
|
end
|
18
app/channels/application_cable/connection.rb
Normal file
18
app/channels/application_cable/connection.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
module ApplicationCable
|
||||||
|
class Connection < ActionCable::Connection::Base
|
||||||
|
identified_by :current_user
|
||||||
|
|
||||||
|
def connect
|
||||||
|
self.current_user = find_verified_user
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def find_verified_user
|
||||||
|
if verified_user = User.find_by(id: cookies.encrypted['_parsley_session']['user_id'])
|
||||||
|
verified_user
|
||||||
|
else
|
||||||
|
reject_unauthorized_connection
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
12
app/channels/task_channel.rb
Normal file
12
app/channels/task_channel.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
class TaskChannel < ApplicationCable::Channel
|
||||||
|
|
||||||
|
def subscribed
|
||||||
|
stream_for current_user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.update_task_list(task_list)
|
||||||
|
task_list.reload
|
||||||
|
self.broadcast_to task_list.user_id, { task_list: TaskListSerializer.for(task_list), action: 'updated' }
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
@ -4,7 +4,7 @@ class ApplicationController < ActionController::Base
|
|||||||
protect_from_forgery with: :exception
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
def verified_request?
|
def verified_request?
|
||||||
if request.content_type == "application/json"
|
if request.media_type == "application/json"
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
super()
|
super()
|
||||||
@ -42,8 +42,18 @@ class ApplicationController < ActionController::Base
|
|||||||
if owner
|
if owner
|
||||||
yield if block_given?
|
yield if block_given?
|
||||||
else
|
else
|
||||||
flash[:warning] = "Operation Not Permitted"
|
respond_to do |format|
|
||||||
redirect_to root_path
|
format.html do
|
||||||
|
flash[:warning] = "Operation Not Permitted"
|
||||||
|
redirect_to root_path
|
||||||
|
end
|
||||||
|
|
||||||
|
format.json do
|
||||||
|
render json: { error: "Operation Not Permitted" }, status: current_user.nil? ? :unauthorized : :forbidden
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -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?
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
class NotesController < ApplicationController
|
class NotesController < ApplicationController
|
||||||
before_action :set_note, only: [:show, :edit, :update, :destroy]
|
before_action :set_note, only: [:show, :update, :destroy]
|
||||||
before_action :ensure_valid_user
|
before_action :ensure_valid_user
|
||||||
|
|
||||||
# GET /notes
|
# GET /notes
|
||||||
|
@ -9,6 +9,7 @@ class TaskItemsController < ApplicationController
|
|||||||
@task_item.task_list = @task_list
|
@task_item.task_list = @task_list
|
||||||
|
|
||||||
if @task_item.save
|
if @task_item.save
|
||||||
|
TaskChannel.update_task_list(@task_list)
|
||||||
render json: TaskItemSerializer.for(@task_item), status: :created, location: [@task_list, @task_item]
|
render json: TaskItemSerializer.for(@task_item), status: :created, location: [@task_list, @task_item]
|
||||||
else
|
else
|
||||||
render json: @task_item.errors, status: :unprocessable_entity
|
render json: @task_item.errors, status: :unprocessable_entity
|
||||||
@ -17,6 +18,7 @@ class TaskItemsController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
if @task_item.update(task_item_params)
|
if @task_item.update(task_item_params)
|
||||||
|
TaskChannel.update_task_list(@task_list)
|
||||||
render json: TaskItemSerializer.for(@task_item), status: :ok, location: [@task_list, @task_item]
|
render json: TaskItemSerializer.for(@task_item), status: :ok, location: [@task_list, @task_item]
|
||||||
else
|
else
|
||||||
render json: @task_item.errors, status: :unprocessable_entity
|
render json: @task_item.errors, status: :unprocessable_entity
|
||||||
@ -30,6 +32,8 @@ class TaskItemsController < ApplicationController
|
|||||||
@task_items.each { |i| i.destroy }
|
@task_items.each { |i| i.destroy }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
TaskChannel.update_task_list(@task_list)
|
||||||
|
|
||||||
head :no_content
|
head :no_content
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -41,6 +45,8 @@ class TaskItemsController < ApplicationController
|
|||||||
@task_items.each { |i| i.update_attribute(:completed, new_status) }
|
@task_items.each { |i| i.update_attribute(:completed, new_status) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
TaskChannel.update_task_list(@task_list)
|
||||||
|
|
||||||
head :no_content
|
head :no_content
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -59,4 +65,4 @@ class TaskItemsController < ApplicationController
|
|||||||
@task_item = @task_list.task_items.find(params[:id])
|
@task_item = @task_list.task_items.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -27,6 +27,7 @@ class TaskListsController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
ensure_owner(@task_list) do
|
ensure_owner(@task_list) do
|
||||||
if @task_list.update(task_list_params)
|
if @task_list.update(task_list_params)
|
||||||
|
TaskChannel.update_task_list(@task_list)
|
||||||
render json: TaskListSerializer.for(@task_list), status: :ok, location: @task_list
|
render json: TaskListSerializer.for(@task_list), status: :ok, location: @task_list
|
||||||
else
|
else
|
||||||
render json: @task_list.errors, status: :unprocessable_entity
|
render json: @task_list.errors, status: :unprocessable_entity
|
||||||
@ -40,13 +41,14 @@ class TaskListsController < ApplicationController
|
|||||||
head :no_content
|
head :no_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_recipe
|
def add_recipe
|
||||||
ensure_owner(@task_list) do
|
ensure_owner(@task_list) do
|
||||||
recipe = Recipe.find(params[:recipe_id])
|
recipe = Recipe.find(params[:recipe_id])
|
||||||
|
|
||||||
@task_list.add_recipe_ingredients(recipe)
|
@task_list.add_recipe_ingredients(recipe)
|
||||||
|
TaskChannel.update_task_list(@task_list)
|
||||||
|
|
||||||
head :no_content
|
head :no_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -61,4 +63,4 @@ class TaskListsController < ApplicationController
|
|||||||
@task_list = TaskList.find(params[:id])
|
@task_list = TaskList.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -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 }
|
||||||
|
@ -1,83 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<vue-progress-bar></vue-progress-bar>
|
<app-progress-bar></app-progress-bar>
|
||||||
<app-navbar></app-navbar>
|
<app-navbar></app-navbar>
|
||||||
<section id="main" class="">
|
<section id="main" class="">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<transition name="fade" mode="out-in">
|
<router-view v-slot="{ Component }">
|
||||||
<router-view v-if="!hasError"></router-view>
|
<transition name="fade" mode="out-in">
|
||||||
<div v-else>
|
<component v-if="!hasError" :is="Component" />
|
||||||
<h1>Error!</h1>
|
<div v-else>
|
||||||
<p>{{error}}</p>
|
<h1>Error!</h1>
|
||||||
</div>
|
<p>{{ appConfig.error }}</p>
|
||||||
</transition>
|
</div>
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||||
|
import { useGlobalTweenGroup } from "../lib/useGlobalTweenGroup";
|
||||||
|
import { useAppConfigStore } from "../stores/appConfig";
|
||||||
|
import { useLoadResource } from "../lib/useLoadResource";
|
||||||
|
import { useCheckAuthentication } from "../lib/useCheckAuthentication";
|
||||||
|
import AppProgressBar from "./AppProgressBar.vue";
|
||||||
|
|
||||||
import { mapMutations, mapState } from "vuex";
|
const globalTweenGroup = useGlobalTweenGroup();
|
||||||
import api from "../lib/Api";
|
let animationLoop = true;
|
||||||
import TWEEN from '@tweenjs/tween.js';
|
|
||||||
|
|
||||||
export default {
|
const appConfig = useAppConfigStore();
|
||||||
data() {
|
const hasError = computed(() => appConfig.error !== null);
|
||||||
return {
|
|
||||||
api: api
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState({
|
|
||||||
hasError: state => state.error !== null,
|
|
||||||
error: state => state.error,
|
|
||||||
authChecked: state => state.authChecked,
|
|
||||||
initialLoad: state => state.initialLoad
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
const { loadResource } = useLoadResource();
|
||||||
isLoading(val) {
|
const { checkAuthentication } = useCheckAuthentication(loadResource);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => appConfig.initialLoad,
|
||||||
|
(val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
this.$Progress.start();
|
nextTick(() => document.body.classList.remove("loading"));
|
||||||
} else {
|
|
||||||
this.$Progress.finish();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
initialLoad(val) {
|
onMounted(() => {
|
||||||
if (val) {
|
// Setup global animation loop
|
||||||
this.$nextTick(() => {
|
function animate() {
|
||||||
document.body.classList.remove("loading");
|
if (animationLoop) {
|
||||||
});
|
globalTweenGroup.update();
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
// Setup global animation loop
|
|
||||||
function animate () {
|
|
||||||
TWEEN.update();
|
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
animate();
|
|
||||||
|
|
||||||
if (this.user === null && this.authChecked === false) {
|
|
||||||
this.checkAuthentication();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
if (this.initialLoad) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
document.body.classList.remove("loading");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
}
|
}
|
||||||
}
|
animate();
|
||||||
|
|
||||||
|
if (appConfig.user === null && appConfig.authChecked === false) {
|
||||||
|
checkAuthentication();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
animationLoop = false;
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
@ -27,206 +27,190 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
export default {
|
const emit = defineEmits(["update:modelValue", "inputClick", "optionSelected"]);
|
||||||
props: {
|
|
||||||
value: String,
|
|
||||||
id: String,
|
|
||||||
placeholder: String,
|
|
||||||
name: String,
|
|
||||||
inputClass: {
|
|
||||||
type: [String, Object, Array],
|
|
||||||
required: false,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
minLength: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
debounce: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
default: 250
|
|
||||||
},
|
|
||||||
|
|
||||||
valueAttribute: String,
|
const props = defineProps({
|
||||||
labelAttribute: String,
|
modelValue: String,
|
||||||
keyAttribute: String,
|
id: String,
|
||||||
|
placeholder: String,
|
||||||
onGetOptions: Function,
|
name: String,
|
||||||
searchOptions: Array
|
inputClass: {
|
||||||
|
type: [String, Object, Array],
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
minLength: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
debounce: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 250
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
valueAttribute: String,
|
||||||
return {
|
labelAttribute: String,
|
||||||
options: [],
|
keyAttribute: String,
|
||||||
rawValue: "",
|
|
||||||
isListOpen: false,
|
|
||||||
activeListIndex: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
onGetOptions: Function,
|
||||||
this.rawValue = this.value;
|
searchOptions: Array
|
||||||
|
});
|
||||||
|
|
||||||
},
|
const options = ref([]);
|
||||||
|
const rawValue = ref("");
|
||||||
|
const isListOpen = ref(false);
|
||||||
|
const activeListIndex = ref(0);
|
||||||
|
|
||||||
watch: {
|
const finalInputClass = computed(() => {
|
||||||
value(newValue) {
|
let cls = ['input'];
|
||||||
this.rawValue = newValue;
|
if (props.inputClass === null) {
|
||||||
}
|
return cls;
|
||||||
},
|
} else if (Array.isArray(props.inputClass)) {
|
||||||
|
return cls.concat(props.inputClass);
|
||||||
|
} else {
|
||||||
|
cls.push(props.inputClass);
|
||||||
|
return cls;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
computed: {
|
watch(
|
||||||
finalInputClass() {
|
() => props.modelValue,
|
||||||
let cls = ['input'];
|
(newValue) => { rawValue.value = newValue; },
|
||||||
if (this.inputClass === null) {
|
{ immediate: true }
|
||||||
return cls;
|
);
|
||||||
} else if (Array.isArray(this.inputClass)) {
|
|
||||||
return cls.concat(this.inputClass);
|
|
||||||
} else {
|
|
||||||
cls.push(this.inputClass);
|
|
||||||
return cls;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
debouncedUpdateOptions() {
|
function optionClass(idx) {
|
||||||
return debounce(this.updateOptions, this.debounce);
|
return activeListIndex.value === idx ? 'option active' : 'option';
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
function optionClick(opt) {
|
||||||
optionClass(idx) {
|
selectOption(opt);
|
||||||
return this.activeListIndex === idx ? 'option active' : 'option';
|
}
|
||||||
},
|
|
||||||
|
|
||||||
optionClick(opt) {
|
function optionKey(opt) {
|
||||||
this.selectOption(opt);
|
if (props.keyAttribute) {
|
||||||
},
|
return opt[props.keyAttribute]
|
||||||
|
} else if (props.valueAttribute) {
|
||||||
|
return opt[props.valueAttribute];
|
||||||
|
} else {
|
||||||
|
return opt.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
optionKey(opt) {
|
function optionValue(opt) {
|
||||||
if (this.keyAttribute) {
|
if (props.valueAttribute) {
|
||||||
return opt[this.keyAttribute]
|
return opt[props.valueAttribute];
|
||||||
} else if (this.valueAttribute) {
|
} else {
|
||||||
return opt[this.valueAttribute];
|
return opt.toString();
|
||||||
} else {
|
}
|
||||||
return opt.toString();
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
optionValue(opt) {
|
function optionLabel(opt) {
|
||||||
if (this.valueAttribute) {
|
if (props.labelAttribute) {
|
||||||
return opt[this.valueAttribute];
|
return opt[props.labelAttribute];
|
||||||
} else {
|
} else {
|
||||||
return opt.toString();
|
return null;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
optionLabel(opt) {
|
function optionMousemove(idx) {
|
||||||
if (this.labelAttribute) {
|
activeListIndex.value = idx;
|
||||||
return opt[this.labelAttribute];
|
}
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
optionMousemove(idx) {
|
function clickHandler(evt) {
|
||||||
this.activeListIndex = idx;
|
emit('inputClick', evt);
|
||||||
},
|
}
|
||||||
|
|
||||||
clickHandler(evt) {
|
function blurHandler(evt) {
|
||||||
this.$emit("inputClick", evt);
|
// blur fires before click. If the blur was fired because the user clicked a list item, immediately hiding the list here
|
||||||
},
|
// would prevent the click event from firing
|
||||||
|
setTimeout(() => {
|
||||||
|
isListOpen.value = false;
|
||||||
|
},250);
|
||||||
|
}
|
||||||
|
|
||||||
blurHandler(evt) {
|
function inputHandler(evt) {
|
||||||
// blur fires before click. If the blur was fired because the user clicked a list item, immediately hiding the list here
|
const newValue = evt.target.value;
|
||||||
// would prevent the click event from firing
|
|
||||||
setTimeout(() => {
|
|
||||||
this.isListOpen = false;
|
|
||||||
},250);
|
|
||||||
},
|
|
||||||
|
|
||||||
inputHandler(evt) {
|
if (rawValue.value !== newValue) {
|
||||||
const newValue = evt.target.value;
|
|
||||||
|
|
||||||
if (this.rawValue !== newValue) {
|
rawValue.value = newValue;
|
||||||
|
|
||||||
this.rawValue = newValue;
|
emit("update:modelValue", newValue);
|
||||||
|
|
||||||
this.$emit("input", newValue);
|
if (newValue.length >= Math.max(1, props.minLength)) {
|
||||||
|
updateOptions(newValue);
|
||||||
if (newValue.length >= Math.max(1, this.minLength)) {
|
} else {
|
||||||
this.debouncedUpdateOptions(newValue);
|
isListOpen.value = false;
|
||||||
} else {
|
|
||||||
this.isListOpen = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
keydownHandler(evt) {
|
|
||||||
if (this.isListOpen === false)
|
|
||||||
return;
|
|
||||||
|
|
||||||
switch (evt.key) {
|
|
||||||
case "ArrowUp":
|
|
||||||
evt.preventDefault();
|
|
||||||
this.activeListIndex = Math.max(0, this.activeListIndex - 1);
|
|
||||||
break;
|
|
||||||
case "ArrowDown":
|
|
||||||
evt.preventDefault();
|
|
||||||
this.activeListIndex = Math.min(this.options.length - 1, this.activeListIndex + 1);
|
|
||||||
break;
|
|
||||||
case "Enter":
|
|
||||||
evt.preventDefault();
|
|
||||||
this.selectOption(this.options[this.activeListIndex]);
|
|
||||||
break;
|
|
||||||
case "Escape":
|
|
||||||
evt.preventDefault();
|
|
||||||
this.isListOpen = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
selectOption(opt) {
|
|
||||||
this.rawValue = this.optionValue(opt);
|
|
||||||
this.$emit("input", this.rawValue);
|
|
||||||
this.$emit("optionSelected", opt);
|
|
||||||
this.isListOpen = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateOptions(value) {
|
|
||||||
let p = null;
|
|
||||||
if (this.searchOptions) {
|
|
||||||
const reg = new RegExp("^" + value, "i");
|
|
||||||
const matcher = o => reg.test(this.optionValue(o));
|
|
||||||
p = Promise.resolve(this.searchOptions.filter(matcher));
|
|
||||||
} else {
|
|
||||||
p = this.onGetOptions(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.then(opts => {
|
|
||||||
this.options = opts;
|
|
||||||
this.isListOpen = opts.length > 0;
|
|
||||||
this.activeListIndex = 0;
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function keydownHandler(evt) {
|
||||||
|
if (isListOpen.value === false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (evt.key) {
|
||||||
|
case "ArrowUp":
|
||||||
|
evt.preventDefault();
|
||||||
|
activeListIndex.value = Math.max(0, activeListIndex.value - 1);
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
evt.preventDefault();
|
||||||
|
activeListIndex.value = Math.min(options.value.length - 1, activeListIndex.value + 1);
|
||||||
|
break;
|
||||||
|
case "Enter":
|
||||||
|
evt.preventDefault();
|
||||||
|
selectOption(options.value[activeListIndex.value]);
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
evt.preventDefault();
|
||||||
|
isListOpen.value = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectOption(opt) {
|
||||||
|
rawValue.value = optionValue(opt);
|
||||||
|
emit("update:modelValue", rawValue.value);
|
||||||
|
emit("optionSelected", opt);
|
||||||
|
isListOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOptions = debounce(function(value) {
|
||||||
|
let p = null;
|
||||||
|
if (props.searchOptions) {
|
||||||
|
const reg = new RegExp("^" + value, "i");
|
||||||
|
const matcher = o => reg.test(optionValue(o));
|
||||||
|
p = Promise.resolve(props.searchOptions.filter(matcher));
|
||||||
|
} else {
|
||||||
|
p = props.onGetOptions(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.then(opts => {
|
||||||
|
options.value = opts;
|
||||||
|
isListOpen.value = opts.length > 0;
|
||||||
|
activeListIndex.value = 0;
|
||||||
|
})
|
||||||
|
}, props.debounce);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
@import "../styles/variables";
|
@use "bulma/sass/utilities" as bulma;
|
||||||
|
|
||||||
$labelLineHeight: 0.8rem;
|
$labelLineHeight: 0.8rem;
|
||||||
|
|
||||||
input.input {
|
input.input {
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: $grey-darker;
|
color: bulma.$grey-darker;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,7 +235,7 @@
|
|||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $turquoise;
|
background-color: bulma.$turquoise;
|
||||||
}
|
}
|
||||||
|
|
||||||
.opt_value {
|
.opt_value {
|
||||||
|
@ -1,47 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<app-modal :open="open" :title="message" @dismiss="runCancel">
|
<app-modal :open="open" :title="title" @dismiss="runCancel">
|
||||||
<div class="buttons">
|
<p class="is-size-5">{{ message }}</p>
|
||||||
<button type="button" class="button is-primary" @click="runConfirm">OK</button>
|
|
||||||
<button type="button" class="button" @click="runCancel">Cancel</button>
|
<template #footer>
|
||||||
</div>
|
<div class="buttons">
|
||||||
|
<button type="button" class="button is-primary" @click="runConfirm">OK</button>
|
||||||
|
<button type="button" class="button" @click="runCancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</app-modal>
|
</app-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
export default {
|
const emit = defineEmits(["cancel", "confirm"]);
|
||||||
props: {
|
|
||||||
cancel: {
|
|
||||||
type: Function,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
|
|
||||||
confirm: {
|
const props = defineProps({
|
||||||
type: Function,
|
message: {
|
||||||
required: true
|
type: String,
|
||||||
},
|
required: false,
|
||||||
|
default: 'Are you sure?'
|
||||||
|
},
|
||||||
|
|
||||||
message: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'Are you sure?'
|
default: "Confirm"
|
||||||
},
|
},
|
||||||
|
|
||||||
open: {
|
open: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
runConfirm() {
|
|
||||||
this.confirm();
|
|
||||||
},
|
|
||||||
|
|
||||||
runCancel() {
|
|
||||||
this.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function runConfirm() {
|
||||||
|
emit("confirm");
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCancel() {
|
||||||
|
emit("cancel");
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
@ -1,38 +1,34 @@
|
|||||||
<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>
|
<script setup>
|
||||||
|
|
||||||
|
import { computed } from "vue";
|
||||||
import DateTimeUtils from "../lib/DateTimeUtils";
|
import DateTimeUtils from "../lib/DateTimeUtils";
|
||||||
|
|
||||||
export default {
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
props: {
|
const props = defineProps({
|
||||||
value: {
|
modelValue: {
|
||||||
required: false,
|
required: false,
|
||||||
type: [Date, String]
|
type: [Date, String]
|
||||||
},
|
|
||||||
|
|
||||||
label: {
|
|
||||||
required: false,
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
label: {
|
||||||
stringValue() {
|
required: false,
|
||||||
const d = DateTimeUtils.toDate(this.value);
|
type: String,
|
||||||
return DateTimeUtils.formatDateForEdit(d);
|
default: null
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
input(val) {
|
|
||||||
let d = DateTimeUtils.toDate(val + " 00:00");
|
|
||||||
this.$emit("input", d);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const stringValue = computed(() => {
|
||||||
|
const d = DateTimeUtils.toDate(props.modelValue);
|
||||||
|
return DateTimeUtils.formatDateForEdit(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
function input(val) {
|
||||||
|
let d = DateTimeUtils.toDate(val + " 00:00");
|
||||||
|
emit("update:modelValue", d);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -5,57 +5,49 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
|
import { computed } from "vue";
|
||||||
import DateTimeUtils from "../lib/DateTimeUtils";
|
import DateTimeUtils from "../lib/DateTimeUtils";
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
props: {
|
dateTime: {
|
||||||
dateTime: {
|
required: true,
|
||||||
required: true,
|
type: [Date, String]
|
||||||
type: [Date, String]
|
|
||||||
},
|
|
||||||
|
|
||||||
showDate: {
|
|
||||||
required: false,
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
|
|
||||||
showTime: {
|
|
||||||
required: false,
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
|
|
||||||
useInput: {
|
|
||||||
required: false,
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
showDate: {
|
||||||
dateObj() {
|
required: false,
|
||||||
return DateTimeUtils.toDate(this.dateTime);
|
type: Boolean,
|
||||||
},
|
default: true
|
||||||
|
},
|
||||||
|
|
||||||
friendlyString() {
|
showTime: {
|
||||||
const parts = [];
|
required: false,
|
||||||
if (this.showDate) {
|
type: Boolean,
|
||||||
parts.push(DateTimeUtils.formatDate(this.dateObj));
|
default: true
|
||||||
}
|
},
|
||||||
if (this.showTime) {
|
|
||||||
parts.push(DateTimeUtils.formatTime(this.dateObj, true));
|
|
||||||
}
|
|
||||||
return parts.join(" ");
|
|
||||||
},
|
|
||||||
|
|
||||||
fullString() {
|
useInput: {
|
||||||
return DateTimeUtils.formatTimestamp(this.dateObj);
|
required: false,
|
||||||
}
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
const dateObj = computed(() => DateTimeUtils.toDate(props.dateTime));
|
||||||
|
const fullString = computed(() => DateTimeUtils.formatTimestamp(dateObj.value));
|
||||||
|
|
||||||
|
const friendlyString = computed(() => {
|
||||||
|
const parts = [];
|
||||||
|
if (props.showDate) {
|
||||||
|
parts.push(DateTimeUtils.formatDate(dateObj.value));
|
||||||
|
}
|
||||||
|
if (props.showTime) {
|
||||||
|
parts.push(DateTimeUtils.formatTime(dateObj.value, true));
|
||||||
|
}
|
||||||
|
return parts.join(" ");
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dropdown" :class="{'is-active': open, 'is-hoverable': hover}">
|
<div ref="dropdown" class="dropdown" :class="{'is-active': open, 'is-hoverable': hover}">
|
||||||
<div class="dropdown-trigger">
|
<div class="dropdown-trigger">
|
||||||
<slot name="button">
|
<slot name="button">
|
||||||
<button type="button" class="button" :class="buttonClass" @click="toggle">
|
<button type="button" class="button" :class="buttonClass" @click="toggle">
|
||||||
@ -19,67 +19,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
export default {
|
import { useTemplateRef } from "vue";
|
||||||
props: {
|
import { onClickOutside } from '@vueuse/core'
|
||||||
open: {
|
|
||||||
required: false,
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
|
|
||||||
hover: {
|
const emit = defineEmits(["close", "open"]);
|
||||||
required: false,
|
const props = defineProps({
|
||||||
type: Boolean,
|
open: {
|
||||||
default: false
|
required: false,
|
||||||
},
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
|
||||||
label: {
|
hover: {
|
||||||
required: false,
|
required: false,
|
||||||
type: String,
|
type: Boolean,
|
||||||
default: 'Select'
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
buttonClass: {
|
label: {
|
||||||
required: false,
|
required: false,
|
||||||
default: ""
|
type: String,
|
||||||
}
|
default: 'Select'
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
buttonClass: {
|
||||||
toggle() {
|
required: false,
|
||||||
if (this.open) {
|
default: ""
|
||||||
this.triggerClose();
|
}
|
||||||
} else {
|
});
|
||||||
this.triggerOpen();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
triggerOpen() {
|
const dropdownElement = useTemplateRef("dropdown");
|
||||||
this.$emit("open");
|
|
||||||
},
|
|
||||||
|
|
||||||
triggerClose() {
|
onClickOutside(dropdownElement, event => handleOutsideClick(event))
|
||||||
this.$emit("close");
|
|
||||||
},
|
|
||||||
|
|
||||||
handleOutsideClick(evt) {
|
function toggle() {
|
||||||
if (this.open) {
|
if (props.open) {
|
||||||
if (!this.$el.contains(evt.target)) {
|
triggerClose();
|
||||||
this.triggerClose();
|
} else {
|
||||||
}
|
triggerOpen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
function triggerOpen() {
|
||||||
document.addEventListener("click", this.handleOutsideClick);
|
emit("open");
|
||||||
},
|
}
|
||||||
|
|
||||||
beforeDestroy() {
|
function triggerClose() {
|
||||||
document.removeEventListener("click", this.handleOutsideClick);
|
emit("close");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOutsideClick(evt) {
|
||||||
|
if (props.open) {
|
||||||
|
if (!dropdownElement.value.contains(evt.target)) {
|
||||||
|
triggerClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
@ -9,92 +9,87 @@
|
|||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
import TWEEN from '@tweenjs/tween.js';
|
import TWEEN from '@tweenjs/tween.js';
|
||||||
|
import { useGlobalTweenGroup } from "../lib/useGlobalTweenGroup";
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
props: {
|
expandTime: {
|
||||||
expandTime: {
|
type: Number,
|
||||||
type: Number,
|
default: 250
|
||||||
default: 250
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
animation: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
cancel () {
|
|
||||||
if (this.animation) {
|
|
||||||
this.animation.stop();
|
|
||||||
this.animation = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
enter(element, done) {
|
|
||||||
const width = parseInt(getComputedStyle(element).width);
|
|
||||||
const paddingTop = parseInt(getComputedStyle(element).paddingTop);
|
|
||||||
const paddingBottom = parseInt(getComputedStyle(element).paddingBottom);
|
|
||||||
|
|
||||||
element.style.width = width;
|
|
||||||
element.style.position = 'absolute';
|
|
||||||
element.style.visibility = 'hidden';
|
|
||||||
element.style.height = 'auto';
|
|
||||||
|
|
||||||
const height = parseInt(getComputedStyle(element).height);
|
|
||||||
|
|
||||||
element.style.width = null;
|
|
||||||
element.style.position = null;
|
|
||||||
element.style.visibility = null;
|
|
||||||
element.style.overflow = 'hidden';
|
|
||||||
element.style.height = 0;
|
|
||||||
|
|
||||||
this.animation = new TWEEN.Tween({height: 0, paddingTop: 0, paddingBottom: 0})
|
|
||||||
.to({height: height, paddingTop: paddingTop, paddingBottom: paddingBottom}, this.expandTime)
|
|
||||||
.onUpdate(obj => {
|
|
||||||
element.style.height = obj.height + "px";
|
|
||||||
element.style.paddingBottom = obj.paddingBottom + "px";
|
|
||||||
element.style.paddingTop = obj.paddingTop + "px";
|
|
||||||
})
|
|
||||||
.onComplete(() => {
|
|
||||||
this.animation = null;
|
|
||||||
element.removeAttribute('style');
|
|
||||||
element.style.opacity = 0.99;
|
|
||||||
setTimeout(() => {
|
|
||||||
// Fixes odd drawing bug in Chrome
|
|
||||||
element.style.opacity = 1.0;
|
|
||||||
}, 1000);
|
|
||||||
done();
|
|
||||||
})
|
|
||||||
.start();
|
|
||||||
},
|
|
||||||
|
|
||||||
leave(element, done) {
|
|
||||||
const height = parseInt(getComputedStyle(element).height);
|
|
||||||
const paddingTop = parseInt(getComputedStyle(element).paddingTop);
|
|
||||||
const paddingBottom = parseInt(getComputedStyle(element).paddingBottom);
|
|
||||||
|
|
||||||
element.style.overflow = 'hidden';
|
|
||||||
|
|
||||||
this.animation = new TWEEN.Tween({height: height, paddingTop: paddingTop, paddingBottom: paddingBottom})
|
|
||||||
.to({height: 0, paddingTop: 0, paddingBottom: 0}, this.expandTime)
|
|
||||||
.onUpdate(obj => {
|
|
||||||
element.style.height = obj.height + "px";
|
|
||||||
element.style.paddingBottom = obj.paddingBottom + "px";
|
|
||||||
element.style.paddingTop = obj.paddingTop + "px";
|
|
||||||
})
|
|
||||||
.onComplete(() => {
|
|
||||||
this.animation = null;
|
|
||||||
element.removeAttribute('style');
|
|
||||||
done();
|
|
||||||
})
|
|
||||||
.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let animation = null;
|
||||||
|
|
||||||
|
function cancel () {
|
||||||
|
if (animation) {
|
||||||
|
animation.stop();
|
||||||
|
animation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enter(element, done) {
|
||||||
|
const width = parseInt(getComputedStyle(element).width);
|
||||||
|
const paddingTop = parseInt(getComputedStyle(element).paddingTop);
|
||||||
|
const paddingBottom = parseInt(getComputedStyle(element).paddingBottom);
|
||||||
|
|
||||||
|
element.style.width = width;
|
||||||
|
element.style.position = 'absolute';
|
||||||
|
element.style.visibility = 'hidden';
|
||||||
|
element.style.height = 'auto';
|
||||||
|
|
||||||
|
const height = parseInt(getComputedStyle(element).height);
|
||||||
|
|
||||||
|
element.style.width = null;
|
||||||
|
element.style.position = null;
|
||||||
|
element.style.visibility = null;
|
||||||
|
element.style.overflow = 'hidden';
|
||||||
|
element.style.height = 0;
|
||||||
|
|
||||||
|
animation = new TWEEN.Tween({height: 0, paddingTop: 0, paddingBottom: 0})
|
||||||
|
.to({height: height, paddingTop: paddingTop, paddingBottom: paddingBottom}, props.expandTime)
|
||||||
|
.onUpdate(obj => {
|
||||||
|
element.style.height = obj.height + "px";
|
||||||
|
element.style.paddingBottom = obj.paddingBottom + "px";
|
||||||
|
element.style.paddingTop = obj.paddingTop + "px";
|
||||||
|
})
|
||||||
|
.onComplete(() => {
|
||||||
|
animation = null;
|
||||||
|
element.removeAttribute('style');
|
||||||
|
element.style.opacity = 0.99;
|
||||||
|
setTimeout(() => {
|
||||||
|
// Fixes odd drawing bug in Chrome
|
||||||
|
element.style.opacity = 1.0;
|
||||||
|
}, 1000);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.group(useGlobalTweenGroup())
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function leave(element, done) {
|
||||||
|
const height = parseInt(getComputedStyle(element).height);
|
||||||
|
const paddingTop = parseInt(getComputedStyle(element).paddingTop);
|
||||||
|
const paddingBottom = parseInt(getComputedStyle(element).paddingBottom);
|
||||||
|
|
||||||
|
element.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
animation = new TWEEN.Tween({height: height, paddingTop: paddingTop, paddingBottom: paddingBottom})
|
||||||
|
.to({height: 0, paddingTop: 0, paddingBottom: 0}, props.expandTime)
|
||||||
|
.onUpdate(obj => {
|
||||||
|
element.style.height = obj.height + "px";
|
||||||
|
element.style.paddingBottom = obj.paddingBottom + "px";
|
||||||
|
element.style.paddingTop = obj.paddingTop + "px";
|
||||||
|
})
|
||||||
|
.onComplete(() => {
|
||||||
|
animation = null;
|
||||||
|
element.removeAttribute('style');
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.group(useGlobalTweenGroup())
|
||||||
|
.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
class IconData {
|
class IconData {
|
||||||
constructor(iconicIcon, dataAttributes) {
|
constructor(iconicIcon, dataAttributes) {
|
||||||
this.iconicIcon = iconicIcon;
|
this.iconicIcon = iconicIcon;
|
||||||
@ -49,6 +51,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
emits: ["click"],
|
||||||
props: {
|
props: {
|
||||||
icon: {
|
icon: {
|
||||||
validator: (i) => iconMap[i] !== undefined
|
validator: (i) => iconMap[i] !== undefined
|
||||||
@ -66,38 +69,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
setup(props) {
|
||||||
|
const iconData = computed(() => iconMap[props.icon]);
|
||||||
|
const sizeData = computed(() => sizeMap[props.size]);
|
||||||
|
const iconClasses = computed(() => [sizeData.value.bulmaIconClass, sizeData.value.customIconClass]);
|
||||||
|
const iconicSize = computed(() => sizeData.value.iconicSize);
|
||||||
|
const iconicIcon = computed(() => iconData.value.iconicIcon);
|
||||||
|
const iconicAttributes = computed(() => iconData.value.dataAttributes);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
injectedSvg: null
|
iconClasses,
|
||||||
}
|
iconData,
|
||||||
},
|
sizeData,
|
||||||
|
iconicAttributes,
|
||||||
computed: {
|
iconicIcon,
|
||||||
iconData() {
|
iconicSize
|
||||||
return iconMap[this.icon];
|
|
||||||
},
|
|
||||||
|
|
||||||
sizeData() {
|
|
||||||
return sizeMap[this.size];
|
|
||||||
},
|
|
||||||
|
|
||||||
iconClasses() {
|
|
||||||
return [
|
|
||||||
this.sizeData.bulmaIconClass,
|
|
||||||
this.sizeData.customIconClass
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
iconicSize() {
|
|
||||||
return this.sizeData.iconicSize;
|
|
||||||
},
|
|
||||||
|
|
||||||
iconicIcon() {
|
|
||||||
return this.iconData.iconicIcon;
|
|
||||||
},
|
|
||||||
|
|
||||||
iconicAttributes() {
|
|
||||||
return this.iconData.dataAttributes;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg v-bind="svgAttributes" v-html="svgContent" :class="calculatedClasses"></svg>
|
<svg ref="svg" v-bind="svgAttributes" v-html="svgContent" :class="calculatedClasses"></svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
import { computed, nextTick, onMounted, onUpdated, useTemplateRef } from "vue";
|
||||||
|
|
||||||
import Caret from "../iconic/svg/smart/caret";
|
import Caret from "../iconic/svg/smart/caret";
|
||||||
import Check from "../iconic/svg/smart/check";
|
import Check from "../iconic/svg/smart/check";
|
||||||
import CircleCheck from "../iconic/svg/smart/circle-check";
|
import CircleCheck from "../iconic/svg/smart/circle-check";
|
||||||
@ -68,29 +70,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
setup(props) {
|
||||||
return {
|
const svgElement = useTemplateRef("svg");
|
||||||
calculatedClasses: []
|
const svgData = computed(() => iconMap[props.icon]);
|
||||||
}
|
const svgAttributes = computed(() => svgData.value.attributes);
|
||||||
},
|
const svgName = computed(() => svgAttributes.value['data-icon']);
|
||||||
|
const svgContent = computed(() => {
|
||||||
|
let content = String(svgData.value.content);
|
||||||
|
|
||||||
computed: {
|
for (let idRep of svgData.value.idReplacements) {
|
||||||
svgData() {
|
|
||||||
return iconMap[this.icon];
|
|
||||||
},
|
|
||||||
|
|
||||||
svgAttributes() {
|
|
||||||
return this.svgData.attributes;
|
|
||||||
},
|
|
||||||
|
|
||||||
svgName() {
|
|
||||||
return this.svgAttributes['data-icon'];
|
|
||||||
},
|
|
||||||
|
|
||||||
svgContent() {
|
|
||||||
let content = String(this.svgData.content);
|
|
||||||
|
|
||||||
for (let idRep of this.svgData.idReplacements) {
|
|
||||||
let newId = `__new_id_${globalIdCounter}`;
|
let newId = `__new_id_${globalIdCounter}`;
|
||||||
globalIdCounter += 1;
|
globalIdCounter += 1;
|
||||||
|
|
||||||
@ -98,57 +86,70 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
|
});
|
||||||
|
|
||||||
|
const calculatedClasses = computed(() => {
|
||||||
|
const classes = (svgAttributes.value.class || "").split(" ");
|
||||||
|
|
||||||
|
classes.push(`iconic-${props.size}`);
|
||||||
|
|
||||||
|
if (props.iconSizeOverride) {
|
||||||
|
classes.push(`iconic-icon-${props.iconSizeOverride}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.displaySizeOverride) {
|
||||||
|
classes.push(`iconic-size-${props.displaySizeOverride}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes;
|
||||||
|
});
|
||||||
|
|
||||||
|
function ensureSvgApi(name, scripts) {
|
||||||
|
if (!name) { return; }
|
||||||
|
if (LOADED_APIS[name] !== true) {
|
||||||
|
for (let sb of scripts) {
|
||||||
|
try {
|
||||||
|
new Function(sb)(window);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(sb);
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOADED_APIS[name] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
function setupSvgApi(name) {
|
||||||
|
|
||||||
rebind() {
|
|
||||||
const apis = APIS;
|
const apis = APIS;
|
||||||
|
if (apis && apis[name]) {
|
||||||
if (apis && this.svgName && apis[this.svgName]) {
|
const iconApi = apis[name](svgElement.value);
|
||||||
const iconApi = apis[this.svgName](this.$el);
|
for (let func in iconApi) svgElement.value[func] = iconApi[func]
|
||||||
for (let func in iconApi) this.$el[func] = iconApi[func]
|
|
||||||
} else {
|
} else {
|
||||||
this.$el.update = function() {}
|
svgElement.value.update = function() {}
|
||||||
}
|
}
|
||||||
|
svgElement.value.update();
|
||||||
this.calculatedClasses = (this.svgAttributes.class || "").split(" ");
|
|
||||||
|
|
||||||
this.calculatedClasses.push(`iconic-${this.size}`);
|
|
||||||
|
|
||||||
if (this.iconSizeOverride) {
|
|
||||||
this.calculatedClasses.push(`iconic-icon-${this.iconSizeOverride}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.displaySizeOverride) {
|
|
||||||
this.calculatedClasses.push(`iconic-size-${this.displaySizeOverride}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$el.update();
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
function updateScripts() {
|
||||||
if (LOADED_APIS[this.svgName] !== true) {
|
ensureSvgApi(svgName.value, svgData.value.scriptBlocks);
|
||||||
for (let sb of this.svgData.scriptBlocks) {
|
setupSvgApi(svgName.value);
|
||||||
new Function(sb)(window);
|
|
||||||
}
|
|
||||||
LOADED_APIS[this.svgName] = true;
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
onMounted(() => {
|
||||||
|
updateScripts();
|
||||||
|
});
|
||||||
|
|
||||||
this.$watch(
|
onUpdated(() => {
|
||||||
function() { return [this.$attrs, this.icon, this.fluid] },
|
updateScripts();
|
||||||
function() {
|
});
|
||||||
this.rebind()
|
|
||||||
},
|
return {
|
||||||
{
|
svgData,
|
||||||
immediate: true
|
svgAttributes,
|
||||||
}
|
svgName,
|
||||||
);
|
svgContent,
|
||||||
|
calculatedClasses
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,10 +4,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -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="['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,50 +13,40 @@
|
|||||||
<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>
|
||||||
</div>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
import { mapState } from "vuex";
|
import { computed } from "vue";
|
||||||
|
import { useAppConfigStore } from "../stores/appConfig";
|
||||||
|
|
||||||
export default {
|
const emit = defineEmits(["dismiss"]);
|
||||||
props: {
|
|
||||||
open: {
|
const props = defineProps({
|
||||||
type: Boolean,
|
open: {
|
||||||
default: false
|
type: Boolean,
|
||||||
},
|
default: false
|
||||||
title: String,
|
|
||||||
wide: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
title: String,
|
||||||
mounted() {
|
wide: {
|
||||||
this.$root.$el.appendChild(this.$refs.modal);
|
type: Boolean,
|
||||||
},
|
default: false
|
||||||
|
|
||||||
beforeDestroy() {
|
|
||||||
this.$refs.container.appendChild(this.$refs.modal);
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapState([
|
|
||||||
'error'
|
|
||||||
])
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.$emit("dismiss");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const appConfig = useAppConfigStore();
|
||||||
|
const error = computed(() => appConfig.error);
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit("dismiss");
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -51,46 +51,27 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
|
import { ref, watch } from "vue";
|
||||||
import UserLogin from "./UserLogin";
|
import UserLogin from "./UserLogin";
|
||||||
import { mapState } from "vuex";
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useAppConfigStore } from "../stores/appConfig";
|
||||||
import { swUpdate } from "../lib/ServiceWorker";
|
import { swUpdate } from "../lib/ServiceWorker";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
export default {
|
const appConfig = useAppConfigStore();
|
||||||
data() {
|
const menuActive = ref(false);
|
||||||
return {
|
const route = useRoute();
|
||||||
menuActive: false
|
const { isAdmin, isLoggedIn, updateAvailable, user } = storeToRefs(appConfig);
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
function updateApp() {
|
||||||
...mapState([
|
swUpdate();
|
||||||
'route',
|
|
||||||
'user',
|
|
||||||
'updateAvailable'
|
|
||||||
])
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
updateApp() {
|
|
||||||
swUpdate();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
route() {
|
|
||||||
this.menuActive = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
user() {
|
|
||||||
this.menuActive = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
UserLogin
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [route, appConfig.user],
|
||||||
|
() => menuActive.value = false
|
||||||
|
);
|
||||||
|
|
||||||
</script>
|
</script>
|
@ -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>
|
||||||
@ -11,96 +11,81 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
export default {
|
import { computed } from "vue";
|
||||||
props: {
|
|
||||||
pagedItemName: {
|
|
||||||
required: false,
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
|
|
||||||
currentPage: {
|
const emit = defineEmits(["changePage"]);
|
||||||
required: true,
|
|
||||||
type: Number
|
|
||||||
},
|
|
||||||
|
|
||||||
totalPages: {
|
const props = defineProps({
|
||||||
required: true,
|
pagedItemName: {
|
||||||
type: Number
|
required: false,
|
||||||
},
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
|
||||||
pageWindow: {
|
currentPage: {
|
||||||
required: false,
|
required: true,
|
||||||
type: Number,
|
type: Number
|
||||||
default: 4
|
},
|
||||||
},
|
|
||||||
|
|
||||||
pageOuterWindow: {
|
totalPages: {
|
||||||
required: false,
|
required: true,
|
||||||
type: Number,
|
type: Number
|
||||||
default: 1
|
},
|
||||||
},
|
|
||||||
|
|
||||||
showWithSinglePage: {
|
pageWindow: {
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean,
|
type: Number,
|
||||||
default: false
|
default: 4
|
||||||
}
|
},
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
pageOuterWindow: {
|
||||||
pageItems() {
|
required: false,
|
||||||
const items = new Set();
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
|
||||||
for (let x = 0; x < this.pageOuterWindow; x++) {
|
showWithSinglePage: {
|
||||||
items.add(x + 1);
|
required: false,
|
||||||
items.add(this.totalPages - x);
|
type: Boolean,
|
||||||
}
|
default: false
|
||||||
|
|
||||||
const start = this.currentPage - Math.ceil(this.pageWindow / 2);
|
|
||||||
const end = this.currentPage + Math.floor(this.pageWindow / 2);
|
|
||||||
|
|
||||||
for (let x = start; x <= end; x++) {
|
|
||||||
items.add(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
let emptySpace = -1;
|
|
||||||
const finalList = [];
|
|
||||||
|
|
||||||
[...items.values()].filter(p => p > 0 && p <= this.totalPages).sort((a, b) => a - b).forEach((p, idx, list) => {
|
|
||||||
finalList.push(p);
|
|
||||||
if (list[idx + 1] && list[idx + 1] !== p + 1) {
|
|
||||||
finalList.push(emptySpace--);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return finalList;
|
|
||||||
},
|
|
||||||
|
|
||||||
isLastPage() {
|
|
||||||
return this.currentPage === this.totalPages;
|
|
||||||
},
|
|
||||||
|
|
||||||
isFirstPage() {
|
|
||||||
return this.currentPage === 1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
changePage(idx) {
|
|
||||||
this.$emit("changePage", idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageItems = computed(() => {
|
||||||
|
const items = new Set();
|
||||||
|
|
||||||
|
for (let x = 0; x < props.pageOuterWindow; x++) {
|
||||||
|
items.add(x + 1);
|
||||||
|
items.add(props.totalPages - x);
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = props.currentPage - Math.ceil(props.pageWindow / 2);
|
||||||
|
const end = props.currentPage + Math.floor(props.pageWindow / 2);
|
||||||
|
|
||||||
|
for (let x = start; x <= end; x++) {
|
||||||
|
items.add(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
let emptySpace = -1;
|
||||||
|
const finalList = [];
|
||||||
|
|
||||||
|
[...items.values()].filter(p => p > 0 && p <= props.totalPages).sort((a, b) => a - b).forEach((p, idx, list) => {
|
||||||
|
finalList.push(p);
|
||||||
|
if (list[idx + 1] && list[idx + 1] !== p + 1) {
|
||||||
|
finalList.push(emptySpace--);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return finalList;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLastPage = computed(() => props.currentPage === props.totalPages);
|
||||||
|
const isFirstPage = computed(() => props.currentPage === 1);
|
||||||
|
|
||||||
|
function changePage(idx) {
|
||||||
|
emit("changePage", idx);
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
ul.pagination {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
69
app/javascript/components/AppProgressBar.vue
Normal file
69
app/javascript/components/AppProgressBar.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="progress-bar" :style="progressStyle"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { useAppConfigStore } from "../stores/appConfig";
|
||||||
|
import TWEEN from '@tweenjs/tween.js';
|
||||||
|
import { useGlobalTweenGroup } from "../lib/useGlobalTweenGroup";
|
||||||
|
|
||||||
|
const appConfig = useAppConfigStore();
|
||||||
|
|
||||||
|
const showProgress = ref(false);
|
||||||
|
const loadingPercent = ref(0);
|
||||||
|
let animation = null;
|
||||||
|
|
||||||
|
const progressStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
opacity: showProgress.value ? "1" : "0",
|
||||||
|
width: `${loadingPercent.value}%`,
|
||||||
|
height: "4px"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => appConfig.isLoading, val => {
|
||||||
|
if (val) {
|
||||||
|
start();
|
||||||
|
} else {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
if (!animation) {
|
||||||
|
showProgress.value = true;
|
||||||
|
animation = new TWEEN.Tween({ percent: 0 }, useGlobalTweenGroup())
|
||||||
|
.to({ percent: 90 })
|
||||||
|
.easing(TWEEN.Easing.Quartic.Out)
|
||||||
|
.duration(3000)
|
||||||
|
.onUpdate(({ percent }) => { loadingPercent.value = percent; })
|
||||||
|
.onComplete(({ percent }) => {})
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
if (animation) {
|
||||||
|
showProgress.value = false;
|
||||||
|
animation.stop();
|
||||||
|
animation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
@use "bulma/sass/utilities" as bulma;
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 999999;
|
||||||
|
background-color: bulma.$blue;
|
||||||
|
transition: width 0.1s, opacity 0.3s;
|
||||||
|
}
|
||||||
|
</style>
|
@ -9,103 +9,91 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
|
import { computed, ref, useTemplateRef } from "vue";
|
||||||
|
|
||||||
export default {
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
props: {
|
|
||||||
starCount: {
|
|
||||||
required: false,
|
|
||||||
type: Number,
|
|
||||||
default: 5
|
|
||||||
},
|
|
||||||
|
|
||||||
readonly: {
|
const props = defineProps({
|
||||||
required: false,
|
starCount: {
|
||||||
type: Boolean,
|
required: false,
|
||||||
default: false
|
type: Number,
|
||||||
},
|
default: 5
|
||||||
|
},
|
||||||
|
|
||||||
step: {
|
readonly: {
|
||||||
required: false,
|
required: false,
|
||||||
type: Number,
|
type: Boolean,
|
||||||
default: 0.5
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
value: {
|
step: {
|
||||||
required: false,
|
required: false,
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0.5
|
||||||
}
|
},
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
modelValue: {
|
||||||
return {
|
required: false,
|
||||||
temporaryValue: null
|
type: Number,
|
||||||
};
|
default: 0
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
ratingPercent() {
|
|
||||||
return ((this.value || 0) / this.starCount) * 100.0;
|
|
||||||
},
|
|
||||||
|
|
||||||
temporaryPercent() {
|
|
||||||
if (this.temporaryValue !== null) {
|
|
||||||
return (this.temporaryValue / this.starCount) * 100.0;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
filledStyle() {
|
|
||||||
const width = this.temporaryPercent || this.ratingPercent;
|
|
||||||
return {
|
|
||||||
width: width + "%"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
handleClick(evt) {
|
|
||||||
if (this.temporaryValue !== null) {
|
|
||||||
this.$emit("input", this.temporaryValue);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleMousemove(evt) {
|
|
||||||
if (this.readonly) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapperBox = this.$refs.wrapper.getBoundingClientRect();
|
|
||||||
const wrapperWidth = wrapperBox.right - wrapperBox.left;
|
|
||||||
const mousePosition = evt.clientX;
|
|
||||||
|
|
||||||
if (mousePosition > wrapperBox.left && mousePosition < wrapperBox.right) {
|
|
||||||
const filledRatio = ((mousePosition - wrapperBox.left) / wrapperWidth);
|
|
||||||
|
|
||||||
const totalSteps = this.starCount / this.step;
|
|
||||||
const filledSteps = Math.round(totalSteps * filledRatio);
|
|
||||||
|
|
||||||
this.temporaryValue = filledSteps * this.step;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleMouseleave(evt) {
|
|
||||||
this.temporaryValue = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const temporaryValue = ref(null);
|
||||||
|
const ratingPercent = computed(() => ((props.modelValue || 0) / props.starCount) * 100.0);
|
||||||
|
const wrapperEl = useTemplateRef("wrapper");
|
||||||
|
|
||||||
|
const temporaryPercent = computed(() => {
|
||||||
|
if (temporaryValue.value !== null) {
|
||||||
|
return (temporaryValue.value / props.starCount) * 100.0;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const filledStyle = computed(() => {
|
||||||
|
const width = temporaryPercent.value || ratingPercent.value;
|
||||||
|
return {
|
||||||
|
width: width + "%"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClick(evt) {
|
||||||
|
if (temporaryValue.value !== null) {
|
||||||
|
emit("update:modelValue", temporaryValue.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMousemove(evt) {
|
||||||
|
if (props.readonly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapperBox = wrapperEl.value.getBoundingClientRect();
|
||||||
|
const wrapperWidth = wrapperBox.right - wrapperBox.left;
|
||||||
|
const mousePosition = evt.clientX;
|
||||||
|
|
||||||
|
if (mousePosition > wrapperBox.left && mousePosition < wrapperBox.right) {
|
||||||
|
const filledRatio = ((mousePosition - wrapperBox.left) / wrapperWidth);
|
||||||
|
|
||||||
|
const totalSteps = props.starCount / props.step;
|
||||||
|
const filledSteps = Math.round(totalSteps * filledRatio);
|
||||||
|
|
||||||
|
temporaryValue.value = filledSteps * props.step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseleave(evt) {
|
||||||
|
temporaryValue.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
@import "../styles/variables";
|
@use "bulma/sass/utilities" as bulma;
|
||||||
|
|
||||||
span.rating {
|
span.rating {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -125,7 +113,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filled-set {
|
.filled-set {
|
||||||
color: $yellow;
|
color: bulma.$yellow;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -6,61 +6,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
|
import { ref } from "vue";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
|
|
||||||
export default {
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
props: {
|
|
||||||
placeholder: {
|
const props = defineProps({
|
||||||
required: false,
|
placeholder: {
|
||||||
type: String,
|
required: false,
|
||||||
default: ""
|
type: String,
|
||||||
|
default: ""
|
||||||
|
},
|
||||||
|
|
||||||
|
modelValue: {
|
||||||
|
required: false,
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = ref(null);
|
||||||
|
|
||||||
|
const triggerInput = debounce(function() {
|
||||||
|
emit("update:modelValue", text.value);
|
||||||
},
|
},
|
||||||
|
250,
|
||||||
|
{ leading: false, trailing: true })
|
||||||
|
|
||||||
value: {
|
function userUpdateText(newText) {
|
||||||
required: false,
|
if (text.value !== newText) {
|
||||||
type: String,
|
text.value = newText;
|
||||||
default: ""
|
triggerInput();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
data() {
|
function propUpdateText(newText) {
|
||||||
return {
|
if (text.value === null && text.value !== newText) {
|
||||||
text: null
|
text.value = newText;
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
triggerInput: debounce(function() {
|
|
||||||
this.$emit("input", this.text);
|
|
||||||
}, 250, {leading: false, trailing: true}),
|
|
||||||
|
|
||||||
userUpdateText(text) {
|
|
||||||
if (this.text !== text) {
|
|
||||||
this.text = text;
|
|
||||||
this.triggerInput();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
propUpdateText(text) {
|
|
||||||
if (this.text === null && this.text !== text) {
|
|
||||||
this.text = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.$watch("value",
|
|
||||||
val => this.propUpdateText(val),
|
|
||||||
{
|
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,84 +2,71 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
export default {
|
import {computed, nextTick, ref, useTemplateRef} from "vue";
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
required: true,
|
|
||||||
type: Array
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
return {
|
|
||||||
hasFocus: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
const props = defineProps({
|
||||||
tagText() {
|
modelValue: {
|
||||||
return this.value.join(" ");
|
required: true,
|
||||||
}
|
type: Array
|
||||||
},
|
}
|
||||||
|
});
|
||||||
|
|
||||||
watch: {
|
const hasFocus = ref(false);
|
||||||
},
|
const tagText = computed(() => props.modelValue.join(" "));
|
||||||
|
const inputElement = useTemplateRef("input");
|
||||||
|
|
||||||
methods: {
|
function inputHandler(el) {
|
||||||
inputHandler(el) {
|
let str = el.target.value;
|
||||||
let str = el.target.value;
|
checkInput(str);
|
||||||
this.checkInput(str);
|
nextTick(() => {
|
||||||
this.$nextTick(() => {
|
el.target.value = str;
|
||||||
el.target.value = str;
|
});
|
||||||
});
|
}
|
||||||
},
|
|
||||||
|
|
||||||
checkInput(str) {
|
function checkInput(str) {
|
||||||
if (this.hasFocus) {
|
if (hasFocus.value) {
|
||||||
const m = str.match(/\S\s+\S*$/);
|
const m = str.match(/\S\s+\S*$/);
|
||||||
|
|
||||||
if (m !== null) {
|
if (m !== null) {
|
||||||
str = str.substring(0, m.index + 1);
|
str = str.substring(0, m.index + 1);
|
||||||
} else {
|
} else {
|
||||||
str = "";
|
str = "";
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newTags = [...new Set(str.toString().split(/\s+/).filter(t => t.length > 0))];
|
|
||||||
|
|
||||||
if (!this.arraysEqual(newTags, this.value)) {
|
|
||||||
this.$emit("input", newTags);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getFocus() {
|
|
||||||
this.hasFocus = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
loseFocus() {
|
|
||||||
this.hasFocus = false;
|
|
||||||
this.checkInput(this.$refs.input.value);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
arraysEqual(arr1, arr2) {
|
|
||||||
if(arr1.length !== arr2.length)
|
|
||||||
return false;
|
|
||||||
for(let i = arr1.length; i--;) {
|
|
||||||
if(arr1[i] !== arr2[i])
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newTags = [...new Set(str.toString().split(/\s+/).filter(t => t.length > 0))];
|
||||||
|
|
||||||
|
if (!arraysEqual(newTags, props.modelValue)) {
|
||||||
|
emit("update:modelValue", newTags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFocus() {
|
||||||
|
hasFocus.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loseFocus() {
|
||||||
|
hasFocus.value = false;
|
||||||
|
checkInput(inputElement.value.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function arraysEqual(arr1, arr2) {
|
||||||
|
if(arr1.length !== arr2.length)
|
||||||
|
return false;
|
||||||
|
for(let i = arr1.length; i--;) {
|
||||||
|
if(arr1[i] !== arr2[i])
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
@ -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" v-model="model" :disabled="disabled"></textarea>
|
||||||
<input v-else :type="type" :class="inputClasses" :value="value" @input="input" :disabled="disabled">
|
<input v-else :type="type" :class="inputClasses" v-model="model" :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">
|
||||||
@ -12,81 +12,67 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
export default {
|
import { computed } from "vue";
|
||||||
props: {
|
|
||||||
label: {
|
|
||||||
required: false,
|
|
||||||
type: String,
|
|
||||||
default: ""
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
required: false,
|
|
||||||
type: [String, Number],
|
|
||||||
default: ""
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
required: false,
|
|
||||||
type: String,
|
|
||||||
default: "text"
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
validationError: {
|
|
||||||
required: false,
|
|
||||||
type: String,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
const props = defineProps({
|
||||||
isTextarea() {
|
label: {
|
||||||
return this.type === "textarea";
|
required: false,
|
||||||
},
|
type: String,
|
||||||
|
default: ""
|
||||||
controlClasses() {
|
},
|
||||||
return [
|
modelValue: {
|
||||||
"control",
|
required: false,
|
||||||
{
|
type: [String, Number],
|
||||||
"has-icons-right": this.validationError !== null
|
default: ""
|
||||||
}
|
},
|
||||||
]
|
type: {
|
||||||
},
|
required: false,
|
||||||
|
type: String,
|
||||||
inputClasses() {
|
default: "text"
|
||||||
return [
|
},
|
||||||
"is-small-mobile",
|
disabled: {
|
||||||
{
|
type: Boolean,
|
||||||
"textarea": this.isTextarea,
|
default: false
|
||||||
"input": !this.isTextarea,
|
},
|
||||||
"is-danger": this.validationError !== null
|
validationError: {
|
||||||
}
|
required: false,
|
||||||
]
|
type: String,
|
||||||
},
|
default: null
|
||||||
|
|
||||||
helpMessage() {
|
|
||||||
return this.validationError;
|
|
||||||
},
|
|
||||||
|
|
||||||
helpClasses() {
|
|
||||||
return [
|
|
||||||
"help",
|
|
||||||
{
|
|
||||||
"is-danger": this.validationError !== null
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
input(evt) {
|
|
||||||
this.$emit("input", evt.target.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const model = defineModel({
|
||||||
|
type: [String, Number],
|
||||||
|
default: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const isTextarea = computed(() => props.type === "textarea");
|
||||||
|
const controlClasses = computed(() => [
|
||||||
|
"control",
|
||||||
|
{
|
||||||
|
"has-icons-right": props.validationError !== null
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const inputClasses = computed(() =>[
|
||||||
|
"is-small-mobile",
|
||||||
|
{
|
||||||
|
"textarea": isTextarea.value,
|
||||||
|
"input": !isTextarea.value,
|
||||||
|
"is-danger": props.validationError !== null
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const helpMessage = computed(() => props.validationError);
|
||||||
|
const helpClasses = computed(() => [
|
||||||
|
"help",
|
||||||
|
{
|
||||||
|
"is-danger": props.validationError !== null
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -6,16 +6,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
props: {
|
errors: {
|
||||||
errors: {
|
required: false,
|
||||||
required: false,
|
type: Object,
|
||||||
type: Object,
|
default: {}
|
||||||
default: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
@ -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>
|
||||||
@ -136,99 +144,82 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
|
import { computed } from "vue";
|
||||||
import api from "../lib/Api";
|
import api from "../lib/Api";
|
||||||
import { mapState } from "vuex";
|
import { mapState } from "pinia";
|
||||||
|
import { useNutrientStore } from "../stores/nutrient";
|
||||||
|
import { useLoadResource } from "../lib/useLoadResource";
|
||||||
|
|
||||||
export default {
|
const nutrientStore = useNutrientStore();
|
||||||
props: {
|
const nutrients = computed(() => nutrientStore.nutrientList);
|
||||||
food: {
|
const { loadResource } = useLoadResource();
|
||||||
required: true,
|
|
||||||
type: Object
|
const props = defineProps({
|
||||||
},
|
food: {
|
||||||
validationErrors: {
|
required: true,
|
||||||
required: false,
|
type: Object
|
||||||
type: Object,
|
|
||||||
default: {}
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
required: false,
|
|
||||||
type: String,
|
|
||||||
default: "Editing"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
validationErrors: {
|
||||||
data() {
|
required: false,
|
||||||
return {
|
type: Object,
|
||||||
|
default: {}
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
action: {
|
||||||
computed: {
|
required: false,
|
||||||
...mapState({
|
type: String,
|
||||||
nutrients: 'nutrientList'
|
default: "Editing"
|
||||||
}),
|
|
||||||
|
|
||||||
visibleFoodUnits() {
|
|
||||||
return this.food.food_units.filter(iu => iu._destroy !== true);
|
|
||||||
},
|
|
||||||
|
|
||||||
hasNdbn() {
|
|
||||||
return this.food.ndbn !== null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
addUnit() {
|
|
||||||
this.food.food_units.push({
|
|
||||||
id: null,
|
|
||||||
name: null,
|
|
||||||
gram_weight: null
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
removeUnit(unit) {
|
|
||||||
if (unit.id) {
|
|
||||||
unit._destroy = true;
|
|
||||||
} else {
|
|
||||||
const idx = this.food.food_units.findIndex(i => i === unit);
|
|
||||||
this.food.food_units.splice(idx, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeNdbn() {
|
|
||||||
this.food.ndbn = null;
|
|
||||||
this.food.usda_food_name = null;
|
|
||||||
this.food.ndbn_units = [];
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSearchItems(text) {
|
|
||||||
return api.getUsdaFoodSearch(text)
|
|
||||||
.then(data => data.map(f => {
|
|
||||||
return {
|
|
||||||
name: f.name,
|
|
||||||
ndbn: f.ndbn,
|
|
||||||
description: ["#", f.ndbn, ", Cal:", f.kcal, ", Carbs:", f.carbohydrates, ", Fat:", f.lipid, ", Protein:", f.protein].join("")
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
searchItemSelected(food) {
|
|
||||||
this.food.ndbn = food.ndbn;
|
|
||||||
this.food.usda_food_name = food.name;
|
|
||||||
this.food.ndbn_units = [];
|
|
||||||
|
|
||||||
this.loadResource(
|
|
||||||
api.postIngredientSelectNdbn(this.food)
|
|
||||||
.then(i => Object.assign(this.food, i))
|
|
||||||
);
|
|
||||||
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleFoodUnits = computed(() => props.food.food_units.filter(iu => iu._destroy !== true));
|
||||||
|
const hasNdbn = computed(() => props.food.ndbn !== null);
|
||||||
|
|
||||||
|
function addUnit() {
|
||||||
|
props.food.food_units.push({
|
||||||
|
id: null,
|
||||||
|
name: null,
|
||||||
|
gram_weight: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeUnit(unit) {
|
||||||
|
if (unit.id) {
|
||||||
|
unit._destroy = true;
|
||||||
|
} else {
|
||||||
|
const idx = props.food.food_units.findIndex(i => i === unit);
|
||||||
|
props.food.food_units.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNdbn() {
|
||||||
|
props.food.ndbn = null;
|
||||||
|
props.food.usda_food_name = null;
|
||||||
|
props.food.ndbn_units = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSearchItems(text) {
|
||||||
|
return api.getUsdaFoodSearch(text)
|
||||||
|
.then(data => data.map(f => {
|
||||||
|
return {
|
||||||
|
name: f.name,
|
||||||
|
ndbn: f.ndbn,
|
||||||
|
description: ["#", f.ndbn, ", Cal:", f.kcal, ", Carbs:", f.carbohydrates, ", Fat:", f.lipid, ", Protein:", f.protein].join("")
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchItemSelected(food) {
|
||||||
|
props.food.ndbn = food.ndbn;
|
||||||
|
props.food.usda_food_name = food.name;
|
||||||
|
props.food.ndbn_units = [];
|
||||||
|
|
||||||
|
loadResource(
|
||||||
|
api.postIngredientSelectNdbn(props.food)
|
||||||
|
.then(i => Object.assign(props.food, i))
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -53,24 +53,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
import { mapState } from "vuex";
|
import { computed } from "vue";
|
||||||
|
import { useNutrientStore } from "../stores/nutrient";
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
props: {
|
food: {
|
||||||
food: {
|
required: true,
|
||||||
required: true,
|
type: Object
|
||||||
type: Object
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapState({
|
|
||||||
nutrients: 'nutrientList'
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
const nutrientStore = useNutrientStore();
|
||||||
|
const nutrients = computed(() => nutrientStore.nutrientList);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -31,27 +31,21 @@
|
|||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
import RecipeEdit from "./RecipeEdit";
|
import RecipeEdit from "./RecipeEdit";
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
props: {
|
log: {
|
||||||
log: {
|
required: true,
|
||||||
required: true,
|
type: Object
|
||||||
type: Object
|
|
||||||
},
|
|
||||||
validationErrors: {
|
|
||||||
required: false,
|
|
||||||
type: Object,
|
|
||||||
default: {}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
validationErrors: {
|
||||||
components: {
|
required: false,
|
||||||
RecipeEdit
|
type: Object,
|
||||||
}
|
default: {}
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
@ -44,22 +44,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
import RecipeShow from "./RecipeShow";
|
import RecipeShow from "./RecipeShow";
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
props: {
|
log: {
|
||||||
log: {
|
required: true,
|
||||||
required: true,
|
type: Object
|
||||||
type: Object
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
RecipeShow
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -19,37 +19,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
export default {
|
import { computed } from "vue";
|
||||||
props: {
|
|
||||||
note: {
|
|
||||||
required: true,
|
|
||||||
type: Object
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
const emit = defineEmits(["save", "cancel"]);
|
||||||
return {
|
const props = defineProps({
|
||||||
};
|
note: {
|
||||||
},
|
required: true,
|
||||||
|
type: Object
|
||||||
computed: {
|
|
||||||
canSave() {
|
|
||||||
return this.note && this.note.content && this.note.content.length;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
save() {
|
|
||||||
this.$emit("save", this.note);
|
|
||||||
},
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.$emit("cancel");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const canSave = computed(() => props.note?.content?.length);
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
emit("save", props.note);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emit("cancel");
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
@ -204,67 +206,50 @@ _underline_
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
import autosize from "autosize";
|
import { computed, ref, useTemplateRef, watch } from "vue";
|
||||||
|
import { useAutosize } from "../lib/useAutosize";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
import api from "../lib/Api";
|
import api from "../lib/Api";
|
||||||
|
|
||||||
import RecipeEditIngredientEditor from "./RecipeEditIngredientEditor";
|
import RecipeEditIngredientEditor from "./RecipeEditIngredientEditor";
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
props: {
|
recipe: {
|
||||||
recipe: {
|
required: true,
|
||||||
required: true,
|
type: Object
|
||||||
type: Object
|
|
||||||
},
|
|
||||||
forLogging: {
|
|
||||||
required: false,
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
forLogging: {
|
||||||
data() {
|
required: false,
|
||||||
return {
|
type: Boolean,
|
||||||
stepPreviewCache: null,
|
default: false
|
||||||
isDescriptionHelpOpen: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
stepPreview() {
|
|
||||||
if (this.stepPreviewCache === null) {
|
|
||||||
return this.recipe.rendered_steps;
|
|
||||||
} else {
|
|
||||||
return this.stepPreviewCache;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
updatePreview: debounce(function() {
|
|
||||||
api.postPreviewSteps(this.recipe.step_text)
|
|
||||||
.then(data => this.stepPreviewCache = data.rendered_steps)
|
|
||||||
.catch(err => this.stepPreviewCache = "?? Error ??");
|
|
||||||
}, 750)
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
'recipe.step_text': function() {
|
|
||||||
this.updatePreview();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
//autosize(this.$refs.step_text_area);
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
RecipeEditIngredientEditor
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
const stepTextArea = useTemplateRef("step_text_area");
|
||||||
|
const stepPreviewCache = ref(null);
|
||||||
|
const isDescriptionHelpOpen = ref(false);
|
||||||
|
|
||||||
|
useAutosize(stepTextArea);
|
||||||
|
|
||||||
|
const stepPreview = computed(() => {
|
||||||
|
if (stepPreviewCache.value === null) {
|
||||||
|
return props.recipe.rendered_steps;
|
||||||
|
} else {
|
||||||
|
return stepPreviewCache.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatePreview = debounce(function() {
|
||||||
|
api.postPreviewSteps(props.recipe.step_text)
|
||||||
|
.then(data => stepPreviewCache.value = data.rendered_steps)
|
||||||
|
.catch(err => stepPreviewCache.value = "?? Error ??");
|
||||||
|
}, 750);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.recipe.step_text,
|
||||||
|
() => updatePreview()
|
||||||
|
);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
@ -39,172 +43,155 @@
|
|||||||
</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 "vuex";
|
const mediaQueryStore = useMediaQueryStore();
|
||||||
|
|
||||||
export default {
|
const props = defineProps({
|
||||||
props: {
|
ingredients: {
|
||||||
ingredients: {
|
required: true,
|
||||||
required: true,
|
type: Array
|
||||||
type: Array
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isBulkEditing: false,
|
|
||||||
bulkEditText: null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapState({
|
|
||||||
isMobile: state => state.mediaQueries.mobile
|
|
||||||
}),
|
|
||||||
bulkIngredientPreview() {
|
|
||||||
if (this.bulkEditText === null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const regex = /^\s*(?:([\d\/.]+(?:\s+[\d\/]+)?)\s+)?(?:([\w-]+)(?:\s+of)?\s+)?([^,|]+?|.+\|)(?:,\s*([^|]*?))?(?:\s*\[(\d+)\]\s*)?$/i;
|
|
||||||
|
|
||||||
const magicFunc = function(str) {
|
|
||||||
if (str === "-") {
|
|
||||||
return "";
|
|
||||||
} else {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const parsed = [];
|
|
||||||
const lines = this.bulkEditText.replace("\r", "").split("\n");
|
|
||||||
|
|
||||||
for (let line of lines) {
|
|
||||||
if (line.length === 0) { continue; }
|
|
||||||
|
|
||||||
const match = line.match(regex);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const matchedName = match[3].replace(/\|\s*$/, "");
|
|
||||||
let item = {quantity: magicFunc(match[1]), units: magicFunc(match[2]), name: magicFunc(matchedName), preparation: magicFunc(match[4]), id: match[5] || null};
|
|
||||||
parsed.push(item);
|
|
||||||
} else {
|
|
||||||
parsed.push(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed;
|
|
||||||
},
|
|
||||||
|
|
||||||
visibleIngredients() {
|
|
||||||
return this.ingredients.filter(i => i._destroy !== true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
createIngredient() {
|
|
||||||
const sort_orders = this.ingredients.map(i => i.sort_order);
|
|
||||||
sort_orders.push(0);
|
|
||||||
const next_sort_order = Math.max(...sort_orders) + 5;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: null,
|
|
||||||
quantity: null,
|
|
||||||
units: null,
|
|
||||||
name: null,
|
|
||||||
preparation: null,
|
|
||||||
ingredient_id: null,
|
|
||||||
sort_order: next_sort_order
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
addIngredient() {
|
|
||||||
this.ingredients.push(this.createIngredient());
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteFood(food) {
|
|
||||||
if (food.id) {
|
|
||||||
food._destroy = true;
|
|
||||||
} else {
|
|
||||||
const idx = this.ingredients.findIndex(i => i === food);
|
|
||||||
this.ingredients.splice(idx, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
bulkEditIngredients() {
|
|
||||||
this.isBulkEditing = true;
|
|
||||||
|
|
||||||
let text = [];
|
|
||||||
|
|
||||||
for (let item of this.visibleIngredients) {
|
|
||||||
text.push(
|
|
||||||
item.quantity + " " +
|
|
||||||
(item.units || "-") + " " +
|
|
||||||
(item.name.indexOf(",") >= 0 ? item.name + "|" : item.name) +
|
|
||||||
(item.preparation ? (", " + item.preparation) : "") +
|
|
||||||
(item.id ? (" [" + item.id + "]") : "")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bulkEditText = text.join("\n");
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelBulkEditing() {
|
|
||||||
this.isBulkEditing = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
saveBulkEditing() {
|
|
||||||
const parsed = this.bulkIngredientPreview.filter(i => i !== null);
|
|
||||||
const existing = [...this.ingredients];
|
|
||||||
const newList = [];
|
|
||||||
|
|
||||||
for (let parsedIngredient of parsed) {
|
|
||||||
let newIngredient = null;
|
|
||||||
|
|
||||||
if (parsedIngredient.id !== null) {
|
|
||||||
let intId = parseInt(parsedIngredient.id);
|
|
||||||
let exIdx = existing.findIndex(i => i.id === intId);
|
|
||||||
if (exIdx >= 0) {
|
|
||||||
let ex = existing[exIdx];
|
|
||||||
if (ex.name === parsedIngredient.name) {
|
|
||||||
newIngredient = ex;
|
|
||||||
existing.splice(exIdx, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newIngredient === null) {
|
|
||||||
newIngredient = this.createIngredient();
|
|
||||||
}
|
|
||||||
|
|
||||||
newIngredient.quantity = parsedIngredient.quantity;
|
|
||||||
newIngredient.units = parsedIngredient.units;
|
|
||||||
newIngredient.name = parsedIngredient.name;
|
|
||||||
newIngredient.preparation = parsedIngredient.preparation;
|
|
||||||
newList.push(newIngredient);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let oldExisting of existing.filter(i => i.id !== null)) {
|
|
||||||
newList.push({id: oldExisting.id, _destroy: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ingredients.splice(0);
|
|
||||||
let sortIdx = 0;
|
|
||||||
for (let n of newList) {
|
|
||||||
n.sort_order = sortIdx++;
|
|
||||||
this.ingredients.push(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isBulkEditing = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
RecipeEditIngredientItem
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isBulkEditing = ref(false);
|
||||||
|
const bulkEditText = ref(null);
|
||||||
|
const isMobile = computed(() => mediaQueryStore.mobile);
|
||||||
|
const visibleIngredients = computed(() => props.ingredients.filter(i => i._destroy !== true));
|
||||||
|
|
||||||
|
const bulkIngredientPreview = computed(() => {
|
||||||
|
if (bulkEditText.value === null || bulkEditText.value === "") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = /^\s*(?:([\d\/.]+(?:\s+[\d\/]+)?)\s+)?(?:([\w-]+)(?:\s+of)?\s+)?([^,|]+?|.+\|)(?:,\s*([^|]*?))?(?:\s*\[(\d+)\]\s*)?$/i;
|
||||||
|
|
||||||
|
const magicFunc = function(str) {
|
||||||
|
if (str === "-") {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsed = [];
|
||||||
|
const lines = bulkEditText.value.replace("\r", "").split("\n");
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
if (line.length === 0) { continue; }
|
||||||
|
|
||||||
|
const match = line.match(regex);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const matchedName = match[3].replace(/\|\s*$/, "");
|
||||||
|
let item = {quantity: magicFunc(match[1]), units: magicFunc(match[2]), name: magicFunc(matchedName), preparation: magicFunc(match[4]), id: match[5] || null};
|
||||||
|
parsed.push(item);
|
||||||
|
} else {
|
||||||
|
parsed.push(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
});
|
||||||
|
|
||||||
|
function createIngredient() {
|
||||||
|
const sort_orders = props.ingredients.map(i => i.sort_order);
|
||||||
|
sort_orders.push(0);
|
||||||
|
const next_sort_order = Math.max(...sort_orders) + 5;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
quantity: null,
|
||||||
|
units: null,
|
||||||
|
name: null,
|
||||||
|
preparation: null,
|
||||||
|
ingredient_id: null,
|
||||||
|
sort_order: next_sort_order
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addIngredient() {
|
||||||
|
props.ingredients.push(createIngredient());
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFood(food) {
|
||||||
|
if (food.id) {
|
||||||
|
food._destroy = true;
|
||||||
|
} else {
|
||||||
|
const idx = props.ingredients.findIndex(i => i === food);
|
||||||
|
props.ingredients.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bulkEditIngredients() {
|
||||||
|
isBulkEditing.value = true;
|
||||||
|
|
||||||
|
let text = [];
|
||||||
|
|
||||||
|
for (let item of visibleIngredients.value) {
|
||||||
|
text.push(
|
||||||
|
item.quantity + " " +
|
||||||
|
(item.units || "-") + " " +
|
||||||
|
(item.name.indexOf(",") >= 0 ? item.name + "|" : item.name) +
|
||||||
|
(item.preparation ? (", " + item.preparation) : "") +
|
||||||
|
(item.id ? (" [" + item.id + "]") : "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkEditText.value = text.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelBulkEditing() {
|
||||||
|
isBulkEditing.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveBulkEditing() {
|
||||||
|
const parsed = bulkIngredientPreview.value.filter(i => i !== null);
|
||||||
|
const existing = [...props.ingredients];
|
||||||
|
const newList = [];
|
||||||
|
|
||||||
|
for (let parsedIngredient of parsed) {
|
||||||
|
let newIngredient = null;
|
||||||
|
|
||||||
|
if (parsedIngredient.id !== null) {
|
||||||
|
let intId = parseInt(parsedIngredient.id);
|
||||||
|
let exIdx = existing.findIndex(i => i.id === intId);
|
||||||
|
if (exIdx >= 0) {
|
||||||
|
let ex = existing[exIdx];
|
||||||
|
if (ex.name === parsedIngredient.name) {
|
||||||
|
newIngredient = ex;
|
||||||
|
existing.splice(exIdx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newIngredient === null) {
|
||||||
|
newIngredient = createIngredient();
|
||||||
|
}
|
||||||
|
|
||||||
|
newIngredient.quantity = parsedIngredient.quantity;
|
||||||
|
newIngredient.units = parsedIngredient.units;
|
||||||
|
newIngredient.name = parsedIngredient.name;
|
||||||
|
newIngredient.preparation = parsedIngredient.preparation;
|
||||||
|
newList.push(newIngredient);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let oldExisting of existing.filter(i => i.id !== null)) {
|
||||||
|
newList.push({id: oldExisting.id, _destroy: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
props.ingredients.splice(0);
|
||||||
|
let sortIdx = 0;
|
||||||
|
for (let n of newList) {
|
||||||
|
n.sort_order = sortIdx++;
|
||||||
|
props.ingredients.push(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
isBulkEditing.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -44,55 +44,50 @@
|
|||||||
</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
|
||||||
},
|
|
||||||
showLabels: {
|
|
||||||
required: false,
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
showLabels: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
methods: {
|
const autocompleteElement = useTemplateRef("autocomplete");
|
||||||
deleteFood(ingredient) {
|
|
||||||
this.$emit("deleteFood", ingredient);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSearchItems(text) {
|
watch(props.ingredient, (val) => {
|
||||||
return api.getSearchIngredients(text);
|
if (props.ingredient.ingredient && props.ingredient.ingredient.name !== val) {
|
||||||
},
|
props.ingredient.ingredient_id = null;
|
||||||
|
props.ingredient.ingredient = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
searchItemSelected(ingredient) {
|
function deleteFood(ingredient) {
|
||||||
this.ingredient.ingredient_id = ingredient.id;
|
emit("deleteFood", ingredient);
|
||||||
this.ingredient.ingredient = ingredient;
|
}
|
||||||
this.ingredient.name = ingredient.name;
|
|
||||||
},
|
|
||||||
|
|
||||||
nameClick() {
|
function updateSearchItems(text) {
|
||||||
if (this.ingredient.ingredient_id === null && this.ingredient.name !== null && this.ingredient.name.length > 2) {
|
return api.getSearchIngredients(text);
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,10 +95,10 @@
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
@import "../styles/variables";
|
@use "bulma/sass/utilities" as bulma;
|
||||||
|
|
||||||
.edit-ingredient-item {
|
.edit-ingredient-item {
|
||||||
border-bottom: solid 1px $grey-light;
|
border-bottom: solid 1px bulma.$grey-light;
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.25rem;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
|
@ -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>
|
||||||
@ -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>
|
||||||
@ -146,154 +150,122 @@
|
|||||||
</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, mapMutations, mapState } from "vuex";
|
import { useTaskStore } from "../stores/task";
|
||||||
|
|
||||||
export default {
|
const taskStore = useTaskStore();
|
||||||
props: {
|
const router = useRouter();
|
||||||
recipe: {
|
|
||||||
required: true,
|
|
||||||
type: Object
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
const props = defineProps({
|
||||||
return {
|
recipe: {
|
||||||
showNutrition: false,
|
required: true,
|
||||||
showConvertDialog: false,
|
type: Object
|
||||||
addToTasksMenuOpen: false,
|
}
|
||||||
|
});
|
||||||
|
|
||||||
scaleValue: '1',
|
const showNutrition = ref(false);
|
||||||
systemConvertValue: "",
|
const showConvertDialog = ref(false);
|
||||||
unitConvertValue: "",
|
const addToTasksMenuOpen = ref(false);
|
||||||
|
|
||||||
scaleOptions: [
|
const scaleValue = ref('1');
|
||||||
'1/4',
|
const systemConvertValue = ref('');
|
||||||
'1/3',
|
const unitConvertValue = ref('');
|
||||||
'1/2',
|
|
||||||
'2/3',
|
|
||||||
'3/4',
|
|
||||||
'1',
|
|
||||||
'1 1/2',
|
|
||||||
'2',
|
|
||||||
'3',
|
|
||||||
'4'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
const scaleOptions = [
|
||||||
...mapState([
|
'1/4',
|
||||||
'taskLists'
|
'1/3',
|
||||||
]),
|
'1/2',
|
||||||
|
'2/3',
|
||||||
timeDisplay() {
|
'3/4',
|
||||||
let a = this.formatMinutes(this.recipe.active_time);
|
'1',
|
||||||
const t = this.formatMinutes(this.recipe.total_time);
|
'1 1/2',
|
||||||
|
'2',
|
||||||
|
'3',
|
||||||
|
'4'
|
||||||
|
];
|
||||||
|
|
||||||
if (a) {
|
const timeDisplay = computed(() => {
|
||||||
a = ` (${a} active)`;
|
let a = formatMinutes(props.recipe.active_time);
|
||||||
}
|
const t = formatMinutes(props.recipe.total_time);
|
||||||
|
|
||||||
return t + a;
|
if (a) {
|
||||||
},
|
a = ` (${a} active)`;
|
||||||
|
}
|
||||||
|
|
||||||
sourceUrl() {
|
return t + a;
|
||||||
try {
|
});
|
||||||
return new URL(this.recipe.source);
|
|
||||||
} catch(err) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isSourceUrl() {
|
const sourceUrl = computed(() => {
|
||||||
return this.sourceUrl !== null;
|
try {
|
||||||
},
|
return new URL(props.recipe.source);
|
||||||
|
} catch(err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
sourceText() {
|
const isSourceUrl = computed(() => sourceUrl.value !== null);
|
||||||
if (this.isSourceUrl) {
|
const sourceText = computed(() => isSourceUrl.value ? sourceUrl.value.host : props.recipe.source);
|
||||||
return this.sourceUrl.host;
|
|
||||||
} else {
|
|
||||||
return this.source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
watch(props.recipe, (r) => {
|
||||||
recipe: {
|
if (r) {
|
||||||
handler: function(r) {
|
scaleValue.value = r.converted_scale || '1';
|
||||||
if (r) {
|
systemConvertValue.value = r.converted_system;
|
||||||
this.scaleValue = r.converted_scale || '1';
|
unitConvertValue.value = r.converted_unit;
|
||||||
this.systemConvertValue = r.converted_system;
|
}
|
||||||
this.unitConvertValue = r.converted_unit;
|
}, { immediate: true });
|
||||||
}
|
|
||||||
},
|
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
onMounted(() => {
|
||||||
...mapActions([
|
taskStore.ensureTaskLists();
|
||||||
'ensureTaskLists'
|
});
|
||||||
]),
|
|
||||||
|
|
||||||
...mapMutations([
|
|
||||||
'setCurrentTaskList'
|
|
||||||
]),
|
|
||||||
|
|
||||||
addRecipeToList(list) {
|
function addRecipeToList(list) {
|
||||||
console.log(list);
|
api.addRecipeToTaskList(list.id, props.recipe.id)
|
||||||
api.addRecipeToTaskList(list.id, this.recipe.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.setCurrentTaskList(list);
|
taskStore.setCurrentTaskList(list);
|
||||||
this.$router.push({name: 'task_lists'})
|
router.push({name: 'task_lists'})
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
convert() {
|
|
||||||
this.showConvertDialog = false;
|
|
||||||
this.$router.push({name: 'recipe', query: { scale: this.scaleValue, system: this.systemConvertValue, unit: this.unitConvertValue }});
|
|
||||||
},
|
|
||||||
|
|
||||||
roundValue(v) {
|
function convert() {
|
||||||
return parseFloat(v).toFixed(2);
|
showConvertDialog.value = false;
|
||||||
},
|
router.push({name: 'recipe', query: { scale: scaleValue.value, system: systemConvertValue.value, unit: unitConvertValue.value }});
|
||||||
|
}
|
||||||
|
|
||||||
formatMinutes(min) {
|
function roundValue(v) {
|
||||||
if (min) {
|
return parseFloat(v).toFixed(2);
|
||||||
const partUnits = [
|
}
|
||||||
{unit: "d", minutes: 60 * 24},
|
|
||||||
{unit: "h", minutes: 60},
|
|
||||||
{unit: "m", minutes: 1}
|
|
||||||
];
|
|
||||||
|
|
||||||
const parts = [];
|
function formatMinutes(min) {
|
||||||
let remaining = min;
|
if (min) {
|
||||||
|
const partUnits = [
|
||||||
|
{unit: "d", minutes: 60 * 24},
|
||||||
|
{unit: "h", minutes: 60},
|
||||||
|
{unit: "m", minutes: 1}
|
||||||
|
];
|
||||||
|
|
||||||
for (let unit of partUnits) {
|
const parts = [];
|
||||||
let val = Math.floor(remaining / unit.minutes);
|
let remaining = min;
|
||||||
remaining = remaining % unit.minutes;
|
|
||||||
|
|
||||||
if (val > 0) {
|
for (let unit of partUnits) {
|
||||||
parts.push(`${val} ${unit.unit}`);
|
let val = Math.floor(remaining / unit.minutes);
|
||||||
}
|
remaining = remaining % unit.minutes;
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join(" ");
|
if (val > 0) {
|
||||||
} else {
|
parts.push(`${val} ${unit.unit}`);
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
return parts.join(" ");
|
||||||
mounted() {
|
} else {
|
||||||
this.ensureTaskLists();
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
</style>
|
</style>
|
||||||
|
@ -24,39 +24,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
export default {
|
import { onMounted, useTemplateRef } from "vue";
|
||||||
props: {
|
|
||||||
taskItem: {
|
|
||||||
required: true,
|
|
||||||
type: Object
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
const emit = defineEmits(["save"]);
|
||||||
inputKeydown(evt) {
|
const props = defineProps({
|
||||||
switch (evt.key) {
|
taskItem: {
|
||||||
case "Enter":
|
required: true,
|
||||||
evt.preventDefault();
|
type: Object
|
||||||
this.save();
|
}
|
||||||
}
|
});
|
||||||
},
|
|
||||||
|
|
||||||
save() {
|
const nameElement = useTemplateRef("nameInput");
|
||||||
this.$emit("save", this.taskItem);
|
|
||||||
},
|
|
||||||
|
|
||||||
focus() {
|
onMounted(() => focus());
|
||||||
this.$refs.nameInput.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
function inputKeydown(evt) {
|
||||||
this.focus();
|
switch (evt.key) {
|
||||||
|
case "Enter":
|
||||||
|
evt.preventDefault();
|
||||||
|
save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
emit("save", props.taskItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
function focus() {
|
||||||
|
nameElement.value.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
focus
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -60,138 +60,106 @@
|
|||||||
</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 "vuex";
|
import { useTaskStore } from "../stores/task";
|
||||||
import cloneDeep from "lodash/cloneDeep";
|
import { useLoadResource } from "../lib/useLoadResource";
|
||||||
|
|
||||||
import TaskItemEdit from "./TaskItemEdit";
|
import TaskItemEdit from "./TaskItemEdit";
|
||||||
|
|
||||||
const newItemTemplate = function(listId) {
|
const { loadResource } = useLoadResource();
|
||||||
|
const taskStore = useTaskStore();
|
||||||
|
const itemEditElement = useTemplateRef("itemEdit");
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
taskList: {
|
||||||
|
required: true,
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 {
|
||||||
task_list_id: listId,
|
task_list_id: null,
|
||||||
name: '',
|
name: '',
|
||||||
quantity: '',
|
quantity: '',
|
||||||
completed: false
|
completed: false
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
export default {
|
function save() {
|
||||||
props: {
|
newItem.value.task_list_id = props.taskList.id;
|
||||||
taskList: {
|
loadResource(
|
||||||
required: true,
|
taskStore.createTaskItem(newItem.value)
|
||||||
type: Object
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showAddItem: false,
|
|
||||||
newItem: null,
|
|
||||||
newItemValidationErrors: {}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
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: {
|
|
||||||
...mapActions([
|
|
||||||
'createTaskItem',
|
|
||||||
'updateTaskItem',
|
|
||||||
'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>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
@import "../styles/variables";
|
|
||||||
|
|
||||||
.columns {
|
.columns {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -14,58 +14,55 @@
|
|||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
export default {
|
import { ref } from "vue";
|
||||||
props: {
|
|
||||||
taskList: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
|
|
||||||
active: {
|
const emit = defineEmits(["select", "delete"]);
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
const props = defineProps({
|
||||||
default: false
|
taskList: {
|
||||||
}
|
type: Object,
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
active: {
|
||||||
return {
|
type: Boolean,
|
||||||
hovering: false,
|
required: false,
|
||||||
confirmingDelete: false
|
default: false
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
selectList() {
|
|
||||||
this.$emit("select", this.taskList);
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteList() {
|
|
||||||
this.confirmingDelete = false;
|
|
||||||
this.$emit("delete", this.taskList);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hovering = ref(false);
|
||||||
|
const confirmingDelete = ref(false);
|
||||||
|
|
||||||
|
function selectList() {
|
||||||
|
emit("select", props.taskList);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteList() {
|
||||||
|
confirmingDelete.value = false;
|
||||||
|
emit("delete", props.taskList);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
@import "~styles/variables";
|
@use "bulma/sass/utilities" 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,34 +14,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
export default {
|
const emit = defineEmits(["save"]);
|
||||||
props: {
|
|
||||||
taskList: {
|
|
||||||
required: true,
|
|
||||||
type: Object
|
|
||||||
},
|
|
||||||
|
|
||||||
validationErrors: {
|
const props = defineProps({
|
||||||
required: false,
|
taskList: {
|
||||||
type: Object,
|
required: true,
|
||||||
default: function() { return {}; }
|
type: Object
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
validationErrors: {
|
||||||
save() {
|
required: false,
|
||||||
this.$emit("save");
|
type: Object,
|
||||||
},
|
default: function() { return {}; }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
nameKeydownHandler(evt) {
|
function save() {
|
||||||
switch (evt.key) {
|
emit("save");
|
||||||
case "Enter":
|
}
|
||||||
evt.preventDefault();
|
|
||||||
this.save();
|
function nameKeydownHandler(evt) {
|
||||||
}
|
switch (evt.key) {
|
||||||
}
|
case "Enter":
|
||||||
|
evt.preventDefault();
|
||||||
|
save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,10 +5,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
export default {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
@ -7,7 +7,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Parsley is released under the MIT License. All code © Dan Elbert 2020.
|
Parsley is released under the MIT License. All code © Dan Elbert 2024.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@ -19,11 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
export default {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -2,11 +2,8 @@
|
|||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
export default {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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 {
|
.then(data => {
|
||||||
return null;
|
output.value = data.output;
|
||||||
}
|
errors.value = data.errors;
|
||||||
}
|
})
|
||||||
},
|
);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
methods: {
|
watch(ingredient_name, function(val) {
|
||||||
updateSearchItems(text) {
|
if (ingredient.value && ingredient.value.name !== val) {
|
||||||
return api.getSearchIngredients(text);
|
ingredient.value = null;
|
||||||
},
|
}
|
||||||
|
});
|
||||||
|
|
||||||
searchItemSelected(ingredient) {
|
watch(
|
||||||
this.ingredient = ingredient || null;
|
[input, outputUnit, density, ingredient],
|
||||||
this.ingredient_name = ingredient.name || null;
|
() => updateOutput()
|
||||||
this.density = ingredient.density || null;
|
);
|
||||||
},
|
|
||||||
|
|
||||||
updateOutput: debounce(function() {
|
function updateSearchItems(text) {
|
||||||
if (this.input && this.input.length > 0) {
|
return api.getSearchIngredients(text);
|
||||||
this.loadResource(api.getCalculate(this.input, this.outputUnit, this.ingredient ? this.ingredient.ingredient_id : null, this.density)
|
}
|
||||||
.then(data => {
|
|
||||||
this.output = data.output;
|
|
||||||
this.errors = data.errors;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 500)
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
function searchItemSelected(ingredient) {
|
||||||
'ingredient_name': function(val) {
|
ingredient.value = ingredient || null;
|
||||||
if (this.ingredient && this.ingredient.name !== val) {
|
ingredient_name.value = ingredient.name || null;
|
||||||
this.ingredient = null;
|
density.value = ingredient.density || null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
function getErrors(type) {
|
||||||
this.$watch(
|
if (errors.value[type] && errors.value[type].length > 0) {
|
||||||
function() {
|
return errors.value[type].join(", ");
|
||||||
return [this.input, this.outputUnit, this.density, this.ingredient];
|
} else {
|
||||||
},
|
return null;
|
||||||
function() {
|
|
||||||
this.updateOutput();
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,41 +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 { mapState } from "vuex";
|
|
||||||
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);
|
||||||
...mapState({
|
const foodId = computed(() => route.params.id);
|
||||||
foodId: state => state.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>
|
||||||
|
|
||||||
|
@ -9,66 +9,59 @@
|
|||||||
</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 { 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 { useLoadResource } from "../lib/useLoadResource";
|
||||||
|
|
||||||
export default {
|
const { loadResource } = useLoadResource();
|
||||||
data() {
|
const router = useRouter();
|
||||||
return {
|
|
||||||
food: {
|
|
||||||
name: null,
|
|
||||||
notes: null,
|
|
||||||
ndbn: null,
|
|
||||||
density: null,
|
|
||||||
water: null,
|
|
||||||
ash: null,
|
|
||||||
protein: null,
|
|
||||||
kcal: null,
|
|
||||||
fiber: null,
|
|
||||||
sugar: null,
|
|
||||||
carbohydrates: null,
|
|
||||||
calcium: null,
|
|
||||||
iron: null,
|
|
||||||
magnesium: null,
|
|
||||||
phosphorus: null,
|
|
||||||
potassium: null,
|
|
||||||
sodium: null,
|
|
||||||
zinc: null,
|
|
||||||
copper: null,
|
|
||||||
manganese: null,
|
|
||||||
vit_c: null,
|
|
||||||
vit_b6: null,
|
|
||||||
vit_b12: null,
|
|
||||||
vit_a: null,
|
|
||||||
vit_e: null,
|
|
||||||
vit_d: null,
|
|
||||||
vit_k: null,
|
|
||||||
cholesterol: null,
|
|
||||||
lipids: null,
|
|
||||||
food_units: []
|
|
||||||
},
|
|
||||||
validationErrors: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
const validationErrors = ref({});
|
||||||
save() {
|
const food = reactive({
|
||||||
this.validationErrors = {}
|
name: null,
|
||||||
this.loadResource(
|
notes: null,
|
||||||
api.postFood(this.food)
|
ndbn: null,
|
||||||
.then(() => this.$router.push('/foods'))
|
density: null,
|
||||||
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
|
water: null,
|
||||||
);
|
ash: null,
|
||||||
}
|
protein: null,
|
||||||
},
|
kcal: null,
|
||||||
|
fiber: null,
|
||||||
|
sugar: null,
|
||||||
|
carbohydrates: null,
|
||||||
|
calcium: null,
|
||||||
|
iron: null,
|
||||||
|
magnesium: null,
|
||||||
|
phosphorus: null,
|
||||||
|
potassium: null,
|
||||||
|
sodium: null,
|
||||||
|
zinc: null,
|
||||||
|
copper: null,
|
||||||
|
manganese: null,
|
||||||
|
vit_c: null,
|
||||||
|
vit_b6: null,
|
||||||
|
vit_b12: null,
|
||||||
|
vit_a: null,
|
||||||
|
vit_e: null,
|
||||||
|
vit_d: null,
|
||||||
|
vit_k: null,
|
||||||
|
cholesterol: null,
|
||||||
|
lipids: null,
|
||||||
|
food_units: []
|
||||||
|
});
|
||||||
|
|
||||||
components: {
|
function save() {
|
||||||
FoodEdit
|
validationErrors.value = {}
|
||||||
}
|
loadResource(
|
||||||
|
api.postFood(food)
|
||||||
|
.then(() => router.push('/foods'))
|
||||||
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -14,48 +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 { 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 { 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);
|
||||||
...mapState({
|
const validationErrors = ref({});
|
||||||
foodId: state => state.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>
|
||||||
|
@ -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">
|
||||||
<app-icon icon="pencil"></app-icon>
|
<router-link class="button" :to="{name: 'edit_food', params: { id: i.id } }">
|
||||||
</router-link>
|
<app-icon icon="pencil"></app-icon>
|
||||||
<button v-if="isLoggedIn" type="button" class="button is-danger" @click="deleteFood(i)">
|
</router-link>
|
||||||
<app-icon icon="x"></app-icon>
|
<button type="button" class="button is-danger" @click="deleteFood(i)">
|
||||||
</button>
|
<app-icon icon="x"></app-icon>
|
||||||
|
</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,
|
|
||||||
foodForDeletion: null,
|
|
||||||
search: {
|
|
||||||
page: 1,
|
|
||||||
per: 25,
|
|
||||||
name: null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
const foodData = ref(null);
|
||||||
foods() {
|
const foodForDeletion = ref(null);
|
||||||
if (this.foodData) {
|
const search = reactive({
|
||||||
return this.foodData.foods;
|
page: 1,
|
||||||
} else {
|
per: 25,
|
||||||
return [];
|
name: null
|
||||||
}
|
});
|
||||||
},
|
|
||||||
|
|
||||||
totalPages() {
|
const foods = computed(() => foodData.value?.foods || []);
|
||||||
if (this.foodData) {
|
const totalPages = computed(() => foodData.value?.total_pages || 0);
|
||||||
return this.foodData.total_pages
|
const currentPage = computed(() => foodData.value?.current_page || 0);
|
||||||
}
|
const showConfirmFoodDelete = computed(() => foodForDeletion.value !== null);
|
||||||
return 0;
|
const confirmFoodDeleteMessage = computed(() => {
|
||||||
},
|
if (foodForDeletion.value !== null) {
|
||||||
|
return `Are you sure you want to delete ${foodForDeletion.value.name}?`;
|
||||||
|
} else {
|
||||||
|
return "??";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
currentPage() {
|
const getList = debounce(function() {
|
||||||
if (this.foodData) {
|
return loadResource(
|
||||||
return this.foodData.current_page
|
api.getFoodList(search.page, search.per, search.name)
|
||||||
}
|
.then(data => foodData.value = data)
|
||||||
return 0;
|
);
|
||||||
},
|
}, 500, {leading: true, trailing: true});
|
||||||
|
|
||||||
showConfirmFoodDelete() {
|
watch(search,
|
||||||
return this.foodForDeletion !== null;
|
() => getList(),
|
||||||
},
|
{
|
||||||
|
deep: true,
|
||||||
confirmFoodDeleteMessage() {
|
immediate: true
|
||||||
if (this.foodForDeletion !== null) {
|
|
||||||
return `Are you sure you want to delete ${this.foodForDeletion.name}?`;
|
|
||||||
} else {
|
|
||||||
return "??";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
);
|
||||||
|
|
||||||
methods: {
|
function changePage(idx) {
|
||||||
changePage(idx) {
|
search.page = idx;
|
||||||
this.search.page = idx;
|
}
|
||||||
},
|
|
||||||
|
|
||||||
getList: debounce(function() {
|
function deleteFood(food) {
|
||||||
return this.loadResource(
|
foodForDeletion.value = food;
|
||||||
api.getFoodList(this.search.page, this.search.per, this.search.name)
|
}
|
||||||
.then(data => this.foodData = data)
|
|
||||||
);
|
|
||||||
}, 500, {leading: true, trailing: true}),
|
|
||||||
|
|
||||||
deleteFood(food) {
|
function foodDeleteCancel() {
|
||||||
this.foodForDeletion = food;
|
foodForDeletion.value = null;
|
||||||
},
|
}
|
||||||
|
|
||||||
foodDeleteCancel() {
|
function foodDeleteConfirm() {
|
||||||
this.foodForDeletion = null;
|
if (foodForDeletion.value !== null) {
|
||||||
},
|
loadResource(
|
||||||
|
api.deleteFood(foodForDeletion.value.id).then(res => {
|
||||||
foodDeleteConfirm() {
|
foodForDeletion.value = null;
|
||||||
if (this.foodForDeletion !== null) {
|
return getList();
|
||||||
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,
|
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,42 +8,34 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_log', params: { id: logId }}">Edit</router-link>
|
<router-link v-if="appConfig.isLoggedIn" class="button" :to="{name: 'edit_log', params: { id: logId }}">Edit</router-link>
|
||||||
<router-link class="button" to="/logs">Back</router-link>
|
<router-link class="button" to="/logs">Back</router-link>
|
||||||
</div>
|
</div>
|
||||||
</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 { mapState } from "vuex";
|
|
||||||
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 route = useRoute();
|
||||||
return {
|
const appConfig = useAppConfigStore();
|
||||||
log: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
const log = ref(null);
|
||||||
...mapState({
|
const logId = computed(() => route.params.id);
|
||||||
logId: state => state.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>
|
||||||
|
|
||||||
|
@ -16,54 +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 { 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 { useLoadResource } from "../lib/useLoadResource";
|
||||||
|
|
||||||
export default {
|
const { loadResource } = useLoadResource();
|
||||||
data() {
|
const route = useRoute();
|
||||||
return {
|
const router = useRouter();
|
||||||
validationErrors: {},
|
|
||||||
log: {
|
|
||||||
date: null,
|
|
||||||
rating: null,
|
|
||||||
notes: null,
|
|
||||||
recipe: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
const validationErrors = ref({});
|
||||||
...mapState({
|
const log = reactive({
|
||||||
recipeId: state => state.route.params.recipeId,
|
date: null,
|
||||||
})
|
rating: null,
|
||||||
},
|
notes: null,
|
||||||
|
recipe: null
|
||||||
|
});
|
||||||
|
|
||||||
methods: {
|
const recipeId = computed(() => route.params.recipeId);
|
||||||
save() {
|
|
||||||
this.log.original_recipe_id = this.recipeId;
|
|
||||||
this.validationErrors = {};
|
|
||||||
|
|
||||||
this.loadResource(
|
onBeforeMount(() => {
|
||||||
api.postLog(this.log)
|
loadResource(
|
||||||
.then(() => this.$router.push('/'))
|
api.getRecipe(recipeId.value, null, null, null, data => log.recipe = data)
|
||||||
.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 = {};
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
loadResource(
|
||||||
LogEdit
|
api.postLog(log)
|
||||||
}
|
.then(() => router.push('/'))
|
||||||
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -16,48 +16,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
import { mapState } from "vuex";
|
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({});
|
||||||
...mapState({
|
const log = ref(null);
|
||||||
logId: state => state.route.params.id,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
const logId = computed(() => route.params.id);
|
||||||
save() {
|
|
||||||
this.validationErrors = {};
|
|
||||||
this.loadResource(
|
|
||||||
api.patchLog(this.log)
|
|
||||||
.then(() => this.$router.push('/'))
|
|
||||||
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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: {
|
function save() {
|
||||||
LogEdit
|
validationErrors.value = {};
|
||||||
}
|
loadResource(
|
||||||
|
api.patchLog(log.value)
|
||||||
|
.then(() => router.push('/'))
|
||||||
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -5,86 +5,64 @@
|
|||||||
</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 :model-value="l.rating" readonly></app-rating></td>
|
||||||
<td>{{l.notes}}</td>
|
<td>{{l.notes}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
</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 {
|
|
||||||
logData: null,
|
|
||||||
search: {
|
|
||||||
page: 1,
|
|
||||||
per: 25
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
const logData = ref(null);
|
||||||
logs() {
|
const search = reactive({
|
||||||
if (this.logData) {
|
page: 1,
|
||||||
return this.logData.logs;
|
per: 25
|
||||||
} else {
|
});
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
totalPages() {
|
const logs = computed(() => logData.value?.logs || []);
|
||||||
if (this.logData) {
|
const totalPages = computed(() => logData.value?.total_pages || 0);
|
||||||
return this.logData.total_pages
|
const currentPage = computed(() => logData.value?.current_page || 0);
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
currentPage() {
|
const getList = debounce(function() {
|
||||||
if (this.logData) {
|
loadResource(
|
||||||
return this.logData.current_page
|
api.getLogList(search.page, search.per)
|
||||||
}
|
.then(data => logData.value = data)
|
||||||
return 0;
|
);
|
||||||
|
}, 500, {leading: true, trailing: true});
|
||||||
|
|
||||||
|
watch(search,
|
||||||
|
() => getList(),
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
}
|
}
|
||||||
},
|
);
|
||||||
|
|
||||||
methods: {
|
function changePage(idx) {
|
||||||
changePage(idx) {
|
search.page = 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})
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.$watch("search",
|
|
||||||
() => this.getList(),
|
|
||||||
{
|
|
||||||
deep: true,
|
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -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,68 +31,60 @@
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</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(
|
});
|
||||||
api.getNoteList()
|
|
||||||
.then(data => this.notes = data)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
addNote() {
|
function refreshList() {
|
||||||
this.editNote = { id: null, content: "" };
|
loadResource(
|
||||||
},
|
api.getNoteList()
|
||||||
|
.then(data => notes.value = data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
saveNote() {
|
function addNote() {
|
||||||
this.loadResource(
|
editNote.value = { id: null, content: "" };
|
||||||
api.postNote(this.editNote)
|
}
|
||||||
|
|
||||||
|
function saveNote() {
|
||||||
|
loadResource(
|
||||||
|
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>
|
||||||
|
@ -17,63 +17,53 @@
|
|||||||
<recipe-show :recipe="recipe"></recipe-show>
|
<recipe-show :recipe="recipe"></recipe-show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_recipe', params: { id: recipeId }}">Edit</router-link>
|
<router-link v-if="appConfig.isLoggedIn" class="button" :to="{name: 'edit_recipe', params: { id: recipeId }}">Edit</router-link>
|
||||||
<router-link class="button" to="/">Back</router-link>
|
<router-link class="button" to="/">Back</router-link>
|
||||||
</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 { mapState } from "vuex";
|
|
||||||
import api from "../lib/Api";
|
import api from "../lib/Api";
|
||||||
|
import { useLoadResource } from "../lib/useLoadResource";
|
||||||
|
import { useAppConfigStore } from "../stores/appConfig";
|
||||||
|
|
||||||
export default {
|
const appConfig = useAppConfigStore();
|
||||||
data: function () {
|
const route = useRoute();
|
||||||
return {
|
const { loadResource } = useLoadResource();
|
||||||
recipe: null,
|
const recipe = ref(null);
|
||||||
showNutrition: false
|
|
||||||
|
const recipeId = computed(() => route.params.id);
|
||||||
|
const scale = computed(() => route.query.scale || null);
|
||||||
|
const system = computed(() => route.query.system || null);
|
||||||
|
const unit = computed(() => route.query.unit || null);
|
||||||
|
const isScaled = computed(() => recipe.value?.converted_scale?.length !== undefined && recipe.value.converted_scale.length > 0 && recipe.value.converted_scale !== "1");
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query,
|
||||||
|
() => refreshData(),
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => recipe.value,
|
||||||
|
(newRecipe) => {
|
||||||
|
if (newRecipe) {
|
||||||
|
document.title = `${newRecipe.name}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
)
|
||||||
|
|
||||||
computed: {
|
function refreshData() {
|
||||||
...mapState({
|
loadResource(
|
||||||
recipeId: state => state.route.params.id,
|
api.getRecipe(recipeId.value, scale.value, system.value, unit.value, data => recipe.value = data)
|
||||||
routeQuery: state => state.route.query,
|
);
|
||||||
scale: state => state.route.query.scale || null,
|
|
||||||
system: state => state.route.query.system || null,
|
|
||||||
unit: state => state.route.query.unit || null
|
|
||||||
}),
|
|
||||||
|
|
||||||
isScaled() {
|
|
||||||
return this.recipe.converted_scale !== null && this.recipe.converted_scale.length > 0 && this.recipe.converted_scale !== "1";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
routeQuery() {
|
|
||||||
this.refreshData();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
refreshData() {
|
|
||||||
this.loadResource(
|
|
||||||
api.getRecipe(this.recipeId, this.scale, this.system, this.unit, data => this.recipe = data)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.refreshData();
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
RecipeShow
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
</style>
|
</style>
|
||||||
|
@ -13,44 +13,38 @@
|
|||||||
</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: {},
|
|
||||||
recipe: {
|
|
||||||
name: null,
|
|
||||||
source: null,
|
|
||||||
description: null,
|
|
||||||
yields: null,
|
|
||||||
total_time: null,
|
|
||||||
active_time: null,
|
|
||||||
step_text: null,
|
|
||||||
tags: [],
|
|
||||||
ingredients: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
const validationErrors = ref({});
|
||||||
save() {
|
const recipe = ref({
|
||||||
this.validationErrors = {};
|
name: null,
|
||||||
this.loadResource(
|
source: null,
|
||||||
api.postRecipe(this.recipe)
|
description: null,
|
||||||
.then(() => this.$router.push('/'))
|
yields: null,
|
||||||
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
|
total_time: null,
|
||||||
);
|
active_time: null,
|
||||||
}
|
step_text: null,
|
||||||
},
|
tags: [],
|
||||||
|
ingredients: []
|
||||||
|
});
|
||||||
|
|
||||||
components: {
|
function save() {
|
||||||
RecipeEdit
|
validationErrors.value = {};
|
||||||
}
|
loadResource(
|
||||||
|
api.postRecipe(recipe.value)
|
||||||
|
.then(() => router.push('/'))
|
||||||
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -18,49 +18,39 @@
|
|||||||
</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 { 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';
|
||||||
|
|
||||||
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);
|
||||||
...mapState({
|
|
||||||
recipeId: state => state.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()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
function save() {
|
||||||
this.loadResource(
|
validationErrors.value = {};
|
||||||
api.getRecipe(this.recipeId, null, null, null, data => { this.recipe = data; return data; })
|
loadResource(
|
||||||
);
|
api.patchRecipe(recipe.value)
|
||||||
},
|
.then(() => router.push({name: 'recipe', params: {id: recipeId.value }}))
|
||||||
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
||||||
components: {
|
);
|
||||||
RecipeEdit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h1 class="title">Recipes</h1>
|
<h1 class="title">Recipes</h1>
|
||||||
|
|
||||||
<router-link v-if="isLoggedIn" :to="{name: 'new_recipe'}" class="button is-primary">Create Recipe</router-link>
|
<router-link v-if="appConfig.isLoggedIn" :to="{name: 'new_recipe'}" class="button is-primary">Create Recipe</router-link>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
@ -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">
|
||||||
@ -25,10 +25,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<app-search-text placeholder="search names" :value="search.name" @input="setSearchName($event)"></app-search-text>
|
<app-search-text placeholder="search names" :value="search.name" @update:modelValue="setSearchName($event)"></app-search-text>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<app-search-text placeholder="search tags" :value="search.tags" @input="setSearchTags($event)"></app-search-text>
|
<app-search-text placeholder="search tags" :value="search.tags" @update:modelValue="setSearchTags($event)"></app-search-text>
|
||||||
</td>
|
</td>
|
||||||
<td colspan="5"></td>
|
<td colspan="5"></td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -42,17 +42,19 @@
|
|||||||
</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>
|
||||||
<td class="recipe-time">{{ formatRecipeTime(r.total_time, r.active_time) }}</td>
|
<td class="recipe-time">{{ formatRecipeTime(r.total_time, r.active_time) }}</td>
|
||||||
<td><app-date-time :date-time="r.created_at" :show-time="false"></app-date-time></td>
|
<td><app-date-time :date-time="r.created_at" :show-time="false"></app-date-time></td>
|
||||||
<td>
|
<td>
|
||||||
<app-dropdown hover v-if="isLoggedIn" class="is-right">
|
<app-dropdown hover v-if="appConfig.isLoggedIn" class="is-right">
|
||||||
<button slot="button" class="button is-small">
|
<template #button>
|
||||||
<app-icon icon="menu"></app-icon>
|
<button class="button is-small">
|
||||||
</button>
|
<app-icon icon="menu"></app-icon>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="dropdown-item">
|
<div class="dropdown-item">
|
||||||
<router-link :to="{name: 'new_log', params: { recipeId: r.id } }" class="button is-primary is-fullwidth">
|
<router-link :to="{name: 'new_log', params: { recipeId: r.id } }" class="button is-primary is-fullwidth">
|
||||||
@ -83,239 +85,217 @@
|
|||||||
</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>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
|
import { computed, reactive, ref, watch } from "vue";
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import api from "../lib/Api";
|
import api from "../lib/Api";
|
||||||
import debounce from "lodash/debounce";
|
|
||||||
import { mapMutations, mapState } from "vuex";
|
|
||||||
import AppLoading from "./AppLoading";
|
import AppLoading from "./AppLoading";
|
||||||
|
import { useAppConfigStore } from "../stores/appConfig";
|
||||||
|
import { useMediaQueryStore } from "../stores/mediaQuery";
|
||||||
|
import { useLoadResource } from "../lib/useLoadResource";
|
||||||
|
|
||||||
export default {
|
const appConfig = useAppConfigStore();
|
||||||
props: {
|
const mediaQueries = useMediaQueryStore();
|
||||||
searchQuery: {
|
const { loadResource, localLoading } = useLoadResource();
|
||||||
type: Object,
|
const router = useRouter();
|
||||||
required: false,
|
|
||||||
default: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
const props = defineProps({
|
||||||
return {
|
searchQuery: {
|
||||||
recipeData: null,
|
type: Object,
|
||||||
recipeForDeletion: null
|
required: false,
|
||||||
};
|
default: {}
|
||||||
},
|
}
|
||||||
|
});
|
||||||
|
|
||||||
computed: {
|
const tableHeader = [
|
||||||
...mapState([
|
{name: 'name', label: 'Name', sort: true},
|
||||||
"mediaQueries"
|
{name: 'tags', label: 'Tags', sort: false},
|
||||||
]),
|
{name: 'rating', label: 'Rating', sort: true},
|
||||||
|
{name: 'yields', label: 'Yields', sort: false},
|
||||||
|
{name: 'total_time', label: 'Time', sort: true},
|
||||||
|
{name: 'created_at', label: 'Created', sort: true}
|
||||||
|
];
|
||||||
|
|
||||||
search() {
|
const recipeData = ref(null);
|
||||||
return {
|
const recipeForDeletion = ref(null);
|
||||||
name: this.searchQuery.name || null,
|
const isTouch = computed(() => mediaQueries.touch);
|
||||||
tags: this.searchQuery.tags || null,
|
|
||||||
column: this.searchQuery.column || "created_at",
|
|
||||||
direction: this.searchQuery.direction || "desc",
|
|
||||||
page: this.searchQuery.page || 1,
|
|
||||||
per: this.searchQuery.per || 25
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
recipes() {
|
const search = computed(() => ({
|
||||||
if (this.recipeData) {
|
name: props.searchQuery.name || null,
|
||||||
return this.recipeData.recipes;
|
tags: props.searchQuery.tags || null,
|
||||||
} else {
|
column: props.searchQuery.column || "created_at",
|
||||||
return [];
|
direction: props.searchQuery.direction || "desc",
|
||||||
}
|
page: props.searchQuery.page || 1,
|
||||||
},
|
per: props.searchQuery.per || 25
|
||||||
|
}));
|
||||||
|
|
||||||
tableHeader() {
|
const recipes = computed(() => {
|
||||||
return [
|
if (recipeData.value) {
|
||||||
{name: 'name', label: 'Name', sort: true},
|
return recipeData.value.recipes;
|
||||||
{name: 'tags', label: 'Tags', sort: false},
|
} else {
|
||||||
{name: 'rating', label: 'Rating', sort: true},
|
return [];
|
||||||
{name: 'yields', label: 'Yields', sort: false},
|
}
|
||||||
{name: 'total_time', label: 'Time', sort: true},
|
});
|
||||||
{name: 'created_at', label: 'Created', sort: true}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
totalPages() {
|
const totalPages = computed(() => {
|
||||||
if (this.recipeData) {
|
if (recipeData.value) {
|
||||||
return this.recipeData.total_pages;
|
return recipeData.value.total_pages;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
},
|
});
|
||||||
|
|
||||||
currentPage() {
|
const currentPage = computed(() => {
|
||||||
if (this.recipeData) {
|
if (recipeData.value) {
|
||||||
return this.recipeData.current_page;
|
return recipeData.value.current_page;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
},
|
});
|
||||||
|
|
||||||
showConfirmRecipeDelete() {
|
const showConfirmRecipeDelete = computed(() => recipeForDeletion.value !== null);
|
||||||
return this.recipeForDeletion !== null;
|
|
||||||
},
|
|
||||||
|
|
||||||
confirmRecipeDeleteMessage() {
|
const confirmRecipeDeleteMessage = computed(() => {
|
||||||
if (this.showConfirmRecipeDelete) {
|
if (showConfirmRecipeDelete.value) {
|
||||||
return `Are you sure you want to delete ${this.recipeForDeletion.name}?`;
|
return `Are you sure you want to delete ${recipeForDeletion.value.name}?`;
|
||||||
} else {
|
} else {
|
||||||
return "??";
|
return "??";
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
watch(search, () => {
|
||||||
...mapMutations([
|
getList().then(() => appConfig.initialLoad = true);
|
||||||
"setInitialLoad"
|
}, {
|
||||||
]),
|
deep: true,
|
||||||
|
immediate: true
|
||||||
buildQueryParams() {
|
});
|
||||||
return {
|
|
||||||
name: this.searchQuery.name,
|
|
||||||
tags: this.searchQuery.tags,
|
|
||||||
column: this.searchQuery.column,
|
|
||||||
direction: this.searchQuery.direction,
|
|
||||||
page: this.searchQuery.page,
|
|
||||||
per: this.searchQuery.per
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
redirectToParams(params) {
|
function getList() {
|
||||||
const rParams = {};
|
return loadResource(
|
||||||
|
api.getRecipeList(search.value.page, search.value.per, search.value.column, search.value.direction, search.value.name, search.value.tags, data => recipeData.value = data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (params.name) {
|
|
||||||
rParams.name = params.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.tags) {
|
function buildQueryParams() {
|
||||||
rParams.tags = params.tags;
|
return {
|
||||||
}
|
name: props.searchQuery.name,
|
||||||
|
tags: props.searchQuery.tags,
|
||||||
if (params.column) {
|
column: props.searchQuery.column,
|
||||||
rParams.column = params.column;
|
direction: props.searchQuery.direction,
|
||||||
}
|
page: props.searchQuery.page,
|
||||||
|
per: props.searchQuery.per
|
||||||
if (params.direction) {
|
|
||||||
rParams.direction = params.direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.page) {
|
|
||||||
rParams.page = params.page;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.per) {
|
|
||||||
rParams.per = params.per;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$router.push({name: 'recipeList', query: rParams});
|
|
||||||
},
|
|
||||||
|
|
||||||
changePage(idx) {
|
|
||||||
const p = this.buildQueryParams();
|
|
||||||
p.page = idx;
|
|
||||||
this.redirectToParams(p);
|
|
||||||
},
|
|
||||||
|
|
||||||
setSort(col) {
|
|
||||||
const p = this.buildQueryParams();
|
|
||||||
|
|
||||||
if (p.column === col) {
|
|
||||||
p.direction = p.direction === "desc" ? "asc" : "desc";
|
|
||||||
} else {
|
|
||||||
p.column = col;
|
|
||||||
p.direction = "asc";
|
|
||||||
}
|
|
||||||
this.redirectToParams(p);
|
|
||||||
},
|
|
||||||
|
|
||||||
setSearchName(name) {
|
|
||||||
const p = this.buildQueryParams();
|
|
||||||
if (name !== p.name) {
|
|
||||||
p.name = name;
|
|
||||||
p.page = null;
|
|
||||||
this.redirectToParams(p);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setSearchTags(tags) {
|
|
||||||
const p = this.buildQueryParams();
|
|
||||||
if (tags !== p.tags) {
|
|
||||||
p.tags = tags;
|
|
||||||
p.page = null;
|
|
||||||
this.redirectToParams(p);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteRecipe(recipe) {
|
|
||||||
this.recipeForDeletion = recipe;
|
|
||||||
},
|
|
||||||
|
|
||||||
recipeDeleteConfirm() {
|
|
||||||
if (this.recipeForDeletion !== null) {
|
|
||||||
this.loadResource(
|
|
||||||
api.deleteRecipe(this.recipeForDeletion.id).then(() => {
|
|
||||||
this.recipeForDeletion = null;
|
|
||||||
return this.getList();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
recipeDeleteCancel() {
|
|
||||||
this.recipeForDeletion = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
getList() {
|
|
||||||
return this.loadResource(
|
|
||||||
api.getRecipeList(this.search.page, this.search.per, this.search.column, this.search.direction, this.search.name, this.search.tags, data => this.recipeData = data)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
formatRecipeTime(total, active) {
|
|
||||||
let str = "";
|
|
||||||
|
|
||||||
if (total && total > 0) {
|
|
||||||
str += total;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (active && active > 0) {
|
|
||||||
if (str.length) {
|
|
||||||
str += " (" + active + ")";
|
|
||||||
} else {
|
|
||||||
str += active;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.$watch("search",
|
|
||||||
() => {
|
|
||||||
this.getList().then(() => this.setInitialLoad(true));
|
|
||||||
},
|
|
||||||
{
|
|
||||||
deep: true,
|
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
AppLoading
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function redirectToParams(params) {
|
||||||
|
const rParams = {};
|
||||||
|
|
||||||
|
if (params.name) {
|
||||||
|
rParams.name = params.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.tags) {
|
||||||
|
rParams.tags = params.tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.column) {
|
||||||
|
rParams.column = params.column;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.direction) {
|
||||||
|
rParams.direction = params.direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.page) {
|
||||||
|
rParams.page = params.page;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.per) {
|
||||||
|
rParams.per = params.per;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({name: 'recipeList', query: rParams});
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePage(idx) {
|
||||||
|
const p = buildQueryParams();
|
||||||
|
p.page = idx;
|
||||||
|
redirectToParams(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSort(col) {
|
||||||
|
const p = buildQueryParams();
|
||||||
|
|
||||||
|
if (p.column === col) {
|
||||||
|
p.direction = p.direction === "desc" ? "asc" : "desc";
|
||||||
|
} else {
|
||||||
|
p.column = col;
|
||||||
|
p.direction = "asc";
|
||||||
|
}
|
||||||
|
redirectToParams(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSearchName(name) {
|
||||||
|
const p = buildQueryParams();
|
||||||
|
if (name !== p.name) {
|
||||||
|
p.name = name;
|
||||||
|
p.page = null;
|
||||||
|
redirectToParams(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSearchTags(tags) {
|
||||||
|
const p = buildQueryParams();
|
||||||
|
if (tags !== p.tags) {
|
||||||
|
p.tags = tags;
|
||||||
|
p.page = null;
|
||||||
|
redirectToParams(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteRecipe(recipe) {
|
||||||
|
recipeForDeletion.value = recipe;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recipeDeleteConfirm() {
|
||||||
|
if (recipeForDeletion.value !== null) {
|
||||||
|
loadResource(
|
||||||
|
api.deleteRecipe(recipeForDeletion.value.id).then(() => {
|
||||||
|
recipeForDeletion.value = null;
|
||||||
|
return getList();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recipeDeleteCancel() {
|
||||||
|
recipeForDeletion.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRecipeTime(total, active) {
|
||||||
|
let str = "";
|
||||||
|
|
||||||
|
if (total && total > 0) {
|
||||||
|
str += total;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active && active > 0) {
|
||||||
|
if (str.length) {
|
||||||
|
str += " (" + active + ")";
|
||||||
|
} else {
|
||||||
|
str += active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -22,15 +22,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
import api from "../lib/Api";
|
import { computed, onBeforeMount, ref } from "vue";
|
||||||
import * as Errors from '../lib/Errors';
|
import * as Errors from '../lib/Errors';
|
||||||
import { mapActions, mapMutations, mapState } from "vuex";
|
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,84 +39,52 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
const { loadResource } = useLoadResource();
|
||||||
data() {
|
const taskStore = useTaskStore();
|
||||||
return {
|
|
||||||
showListDropdown: false,
|
|
||||||
newList: newListTemplate(),
|
|
||||||
newListValidationErrors: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
const showListDropdown = ref(false);
|
||||||
...mapState([
|
const newList = ref(newListTemplate());
|
||||||
'taskLists',
|
const newListValidationErrors = ref({});
|
||||||
'currentTaskList'
|
|
||||||
]),
|
|
||||||
listSelectLabel() {
|
|
||||||
if (this.currentTaskList === null) {
|
|
||||||
return "Select or Create a List";
|
|
||||||
} else {
|
|
||||||
return this.currentTaskList.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
const taskLists = computed(() => taskStore.taskLists);
|
||||||
...mapActions([
|
const currentTaskList = computed(() => taskStore.currentTaskList);
|
||||||
'refreshTaskLists',
|
const listSelectLabel = computed(() => {
|
||||||
'createTaskList',
|
if (currentTaskList.value === null) {
|
||||||
'deleteTaskList',
|
return "Select or Create a List";
|
||||||
'deleteTaskItems',
|
} else {
|
||||||
'completeTaskItems'
|
return currentTaskList.value.name;
|
||||||
]),
|
|
||||||
...mapMutations([
|
|
||||||
'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) {
|
|
||||||
this.loadResource(
|
|
||||||
this.deleteTaskList(list)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
deleteAllItems() {
|
|
||||||
this.loadResource(
|
|
||||||
this.deleteTaskItems({
|
|
||||||
taskList: this.currentTaskList,
|
|
||||||
taskItems: this.currentTaskList.task_items
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.loadResource(
|
|
||||||
this.refreshTaskLists()
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
TaskListMiniForm,
|
|
||||||
TaskListDropdownItem,
|
|
||||||
TaskItemList
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
loadResource(taskStore.refreshTaskLists());
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectList(list) {
|
||||||
|
taskStore.setCurrentTaskList(list);
|
||||||
|
showListDropdown.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveNewList() {
|
||||||
|
loadResource(
|
||||||
|
taskStore.createTaskList(newList.value)
|
||||||
|
.then(() => showListDropdown.value = false)
|
||||||
|
.then(() => { newList.value = newListTemplate(); newListValidationErrors.value = {}; } )
|
||||||
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => newListValidationErrors.value = err.validationErrors()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteList(list) {
|
||||||
|
loadResource(taskStore.deleteTaskList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteAllItems() {
|
||||||
|
loadResource(
|
||||||
|
taskStore.deleteTaskItems({
|
||||||
|
taskList: currentTaskList.value,
|
||||||
|
taskItems: currentTaskList.value.task_items
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -13,41 +13,37 @@
|
|||||||
</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: {
|
|
||||||
username: '',
|
|
||||||
full_name: '',
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
password_confirmation: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
const validationErrors = ref({});
|
||||||
save() {
|
const userObj = ref({
|
||||||
this.validationErrors = {};
|
username: '',
|
||||||
this.loadResource(
|
full_name: '',
|
||||||
api.postUser(this.userObj)
|
email: '',
|
||||||
.then(() => this.checkAuthentication())
|
password: '',
|
||||||
.then(() => this.$router.push('/'))
|
password_confirmation: ''
|
||||||
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
function save() {
|
||||||
UserEdit
|
validationErrors.value = {};
|
||||||
}
|
loadResource(
|
||||||
|
api.postUser(userObj.value)
|
||||||
|
.then(() => checkAuthentication())
|
||||||
|
.then(() => router.push('/'))
|
||||||
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -13,63 +13,56 @@
|
|||||||
</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 {
|
userObj.value = null;
|
||||||
this.userObj = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
save() {
|
|
||||||
this.validationErrors = {};
|
|
||||||
this.loadResource(
|
|
||||||
api.patchUser(this.userObj)
|
|
||||||
.then(() => this.checkAuthentication())
|
|
||||||
.then(() => {
|
|
||||||
this.$router.push('/');
|
|
||||||
})
|
|
||||||
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
UserEdit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
validationErrors.value = {};
|
||||||
|
loadResource(
|
||||||
|
api.patchUser(userObj.value)
|
||||||
|
.then(() => checkAuthentication())
|
||||||
|
.then(() => {
|
||||||
|
router.push('/');
|
||||||
|
})
|
||||||
|
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
@ -45,57 +45,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
|
|
||||||
import api from "../lib/Api";
|
import { computed, nextTick, ref, useTemplateRef } from "vue";
|
||||||
import { mapActions, mapState } from "vuex";
|
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([
|
|
||||||
'loginMessage'
|
|
||||||
]),
|
|
||||||
enableSubmit() {
|
|
||||||
return this.username !== '' && this.password !== '' && !this.isLoading;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
const showLogin = ref(false);
|
||||||
...mapActions([
|
const error = ref('');
|
||||||
'login'
|
const username = ref("");
|
||||||
]),
|
const password = ref("");
|
||||||
|
|
||||||
openDialog() {
|
const loginMessage = computed(() => appConfig.loginMessage);
|
||||||
this.showLogin = true;
|
const enableSubmit = computed(() => username.value !== "" && password.value !== "" && !appConfig.isLoading);
|
||||||
this.$nextTick(() => this.$refs.usernameInput.focus());
|
|
||||||
},
|
|
||||||
|
|
||||||
performLogin() {
|
function openDialog() {
|
||||||
if (this.username !== '' && this.password !== '') {
|
showLogin.value = true;
|
||||||
const params = {username: this.username, password: this.password};
|
nextTick(() => {
|
||||||
|
userNameElement.value.focus();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.loadResource(
|
function performLogin() {
|
||||||
this.login(params)
|
if (username.value !== '' && password.value !== '') {
|
||||||
|
const params = {username: username.value, password: password.value};
|
||||||
|
|
||||||
|
loadResource(
|
||||||
|
appConfig.login(params)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
this.showLogin = false;
|
showLogin.value = false;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
app/javascript/lib/ActionCable.js
Normal file
17
app/javascript/lib/ActionCable.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import * as cable from "@rails/actioncable";
|
||||||
|
|
||||||
|
let consumer = null;
|
||||||
|
|
||||||
|
function createChannel(baseUrl, ...args) {
|
||||||
|
if (consumer === null) {
|
||||||
|
if (baseUrl !== null && baseUrl.toString() !== "") {
|
||||||
|
consumer = cable.createConsumer(baseUrl);
|
||||||
|
} else {
|
||||||
|
consumer = cable.createConsumer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return consumer.subscriptions.create(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createChannel };
|
@ -1,4 +1,4 @@
|
|||||||
import config from '../config';
|
import config from './config';
|
||||||
import * as Errors from './Errors';
|
import * as Errors from './Errors';
|
||||||
|
|
||||||
class Api {
|
class Api {
|
||||||
|
35
app/javascript/lib/ClickStrike.js
Normal file
35
app/javascript/lib/ClickStrike.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
function clickStrikeClick(evt) {
|
||||||
|
const isStrikable = el => el && el.tagName === "LI";
|
||||||
|
const strikeClass = "is-strikethrough";
|
||||||
|
|
||||||
|
let t = evt.target;
|
||||||
|
|
||||||
|
while (t !== null && t !== this && !isStrikable(t)) {
|
||||||
|
t = t.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStrikable(t)) {
|
||||||
|
const classList = t.className.split(" ");
|
||||||
|
const strIdx = classList.findIndex(c => c === strikeClass);
|
||||||
|
if (strIdx >= 0) {
|
||||||
|
classList.splice(strIdx, 1);
|
||||||
|
} else {
|
||||||
|
classList.push(strikeClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
t.className = classList.join(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installClickStrike(app) {
|
||||||
|
app.directive('click-strike', {
|
||||||
|
beforeMount(el) {
|
||||||
|
el.addEventListener("click", clickStrikeClick);
|
||||||
|
},
|
||||||
|
|
||||||
|
unmounted(el) {
|
||||||
|
el.removeEventListener("click", clickStrikeClick);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -1,84 +0,0 @@
|
|||||||
|
|
||||||
import Vue from 'vue';
|
|
||||||
import { mapActions, mapGetters, mapMutations, mapState } from 'vuex';
|
|
||||||
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) {
|
|
||||||
const isStrikable = el => el && el.tagName === "LI";
|
|
||||||
const strikeClass = "is-strikethrough";
|
|
||||||
|
|
||||||
let t = evt.target;
|
|
||||||
|
|
||||||
while (t !== null && t !== this && !isStrikable(t)) {
|
|
||||||
t = t.parentElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStrikable(t)) {
|
|
||||||
const classList = t.className.split(" ");
|
|
||||||
const strIdx = classList.findIndex(c => c === strikeClass);
|
|
||||||
if (strIdx >= 0) {
|
|
||||||
classList.splice(strIdx, 1);
|
|
||||||
} else {
|
|
||||||
classList.push(strikeClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
t.className = classList.join(" ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.directive('click-strike', {
|
|
||||||
bind(el) {
|
|
||||||
el.addEventListener("click", clickStrikeClick);
|
|
||||||
},
|
|
||||||
|
|
||||||
unbind(el) {
|
|
||||||
el.removeEventListener("click", clickStrikeClick);
|
|
||||||
}
|
|
||||||
});
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,49 +0,0 @@
|
|||||||
// Adds a module to a vuex store with a set of media query states
|
|
||||||
|
|
||||||
const defaultOptions = {
|
|
||||||
module: "mediaQueries"
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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)`
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function(store, options) {
|
|
||||||
let opts = Object.assign({}, defaultOptions, options || {});
|
|
||||||
const moduleName = opts.module;
|
|
||||||
|
|
||||||
const initialState = {};
|
|
||||||
|
|
||||||
for (let device in mediaQueries) {
|
|
||||||
const query = window.matchMedia(mediaQueries[device]);
|
|
||||||
query.onchange = (q) => {
|
|
||||||
store.commit(moduleName + "/MEDIA_QUERY_CHANGED", {mediaName: device, value: q.matches});
|
|
||||||
};
|
|
||||||
initialState[device] = query.matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.registerModule(moduleName, {
|
|
||||||
namespaced: true,
|
|
||||||
state: initialState,
|
|
||||||
mutations: {
|
|
||||||
"MEDIA_QUERY_CHANGED" (state, data) {
|
|
||||||
state[data.mediaName] = data.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
9
app/javascript/lib/useAutosize.js
Normal file
9
app/javascript/lib/useAutosize.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { onBeforeUnmount } from "vue";
|
||||||
|
import autosize from 'autosize';
|
||||||
|
|
||||||
|
export function useAutosize(elementRef) {
|
||||||
|
autosize(elementRef.value);
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
autosize.destroy(elementRef.value);
|
||||||
|
});
|
||||||
|
}
|
12
app/javascript/lib/useCheckAuthentication.js
Normal file
12
app/javascript/lib/useCheckAuthentication.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useAppConfigStore } from "../stores/appConfig";
|
||||||
|
|
||||||
|
export function useCheckAuthentication(loadResource) {
|
||||||
|
const appConfig = useAppConfigStore();
|
||||||
|
const checkAuthentication = function() {
|
||||||
|
return loadResource(appConfig.updateCurrentUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
checkAuthentication
|
||||||
|
}
|
||||||
|
}
|
10
app/javascript/lib/useGlobalTweenGroup.js
Normal file
10
app/javascript/lib/useGlobalTweenGroup.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Group } from '@tweenjs/tween.js'
|
||||||
|
|
||||||
|
let group = null;
|
||||||
|
|
||||||
|
export function useGlobalTweenGroup() {
|
||||||
|
if (group === null) {
|
||||||
|
group = new Group();
|
||||||
|
}
|
||||||
|
return group;
|
||||||
|
}
|
27
app/javascript/lib/useLoadResource.js
Normal file
27
app/javascript/lib/useLoadResource.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { useAppConfigStore } from "../stores/appConfig";
|
||||||
|
|
||||||
|
export function useLoadResource() {
|
||||||
|
const appConfig = useAppConfigStore();
|
||||||
|
const localLoadingCount = ref(0);
|
||||||
|
const localLoading = computed(() => localLoadingCount.value > 0);
|
||||||
|
|
||||||
|
const loadResource = async (promise) => {
|
||||||
|
appConfig.setLoading(true);
|
||||||
|
localLoadingCount.value = localLoadingCount.value + 1;
|
||||||
|
try {
|
||||||
|
return await promise;
|
||||||
|
} catch (error) {
|
||||||
|
appConfig.setError(error);
|
||||||
|
} finally {
|
||||||
|
appConfig.setLoading(false);
|
||||||
|
localLoadingCount.value = localLoadingCount.value - 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadResource,
|
||||||
|
localLoading,
|
||||||
|
localLoadingCount
|
||||||
|
};
|
||||||
|
}
|
@ -1,17 +1,10 @@
|
|||||||
import '../styles';
|
import '../styles';
|
||||||
import "vue-resize/dist/vue-resize";
|
import { createApp } from 'vue';
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
import Vue from 'vue'
|
|
||||||
import { sync } from 'vuex-router-sync';
|
|
||||||
import { swInit } from "../lib/ServiceWorker";
|
import { swInit } from "../lib/ServiceWorker";
|
||||||
import responsiveSync from "../lib/VuexResponsiveSync";
|
import config from '../lib/config';
|
||||||
import VueProgressBar from "vue-progressbar";
|
import { installClickStrike } from "../lib/ClickStrike";
|
||||||
import VueResize from "vue-resize";
|
|
||||||
import config from '../config';
|
|
||||||
import store from '../store';
|
|
||||||
import router from '../router';
|
import router from '../router';
|
||||||
import '../lib/GlobalMixins';
|
|
||||||
import App from '../components/App';
|
|
||||||
|
|
||||||
import AppAutocomplete from "../components/AppAutocomplete";
|
import AppAutocomplete from "../components/AppAutocomplete";
|
||||||
import AppConfirm from "../components/AppConfirm";
|
import AppConfirm from "../components/AppConfirm";
|
||||||
@ -30,57 +23,40 @@ import AppTagEditor from "../components/AppTagEditor";
|
|||||||
import AppTextField from "../components/AppTextField";
|
import AppTextField from "../components/AppTextField";
|
||||||
import AppValidationErrors from "../components/AppValidationErrors";
|
import AppValidationErrors from "../components/AppValidationErrors";
|
||||||
|
|
||||||
Vue.component("AppAutocomplete", AppAutocomplete);
|
import App from '../components/App.vue'
|
||||||
Vue.component("AppConfirm", AppConfirm);
|
|
||||||
Vue.component("AppDateTime", AppDateTime);
|
|
||||||
Vue.component("AppDatePicker", AppDatePicker);
|
|
||||||
Vue.component("AppDropdown", AppDropdown);
|
|
||||||
Vue.component("AppExpandTransition", AppExpandTransition);
|
|
||||||
Vue.component("AppIcon", AppIcon);
|
|
||||||
Vue.component("AppIconicIcon", AppIconicIcon);
|
|
||||||
Vue.component("AppModal", AppModal);
|
|
||||||
Vue.component("AppNavbar", AppNavbar);
|
|
||||||
Vue.component("AppPager", AppPager);
|
|
||||||
Vue.component("AppRating", AppRating);
|
|
||||||
Vue.component("AppSearchText", AppSearchText);
|
|
||||||
Vue.component("AppTagEditor", AppTagEditor);
|
|
||||||
Vue.component("AppTextField", AppTextField);
|
|
||||||
Vue.component("AppValidationErrors", AppValidationErrors);
|
|
||||||
|
|
||||||
|
// Resize?
|
||||||
Vue.use(VueProgressBar, {
|
// Progressbar?
|
||||||
// color: '#bffaf3',
|
|
||||||
// failedColor: '#874b4b',
|
|
||||||
// thickness: '5px',
|
|
||||||
// transition: {
|
|
||||||
// speed: '0.2s',
|
|
||||||
// opacity: '0.6s',
|
|
||||||
// termination: 300
|
|
||||||
// },
|
|
||||||
// autoRevert: true,
|
|
||||||
// location: 'left',
|
|
||||||
// inverse: false
|
|
||||||
});
|
|
||||||
|
|
||||||
Vue.use(VueResize);
|
|
||||||
|
|
||||||
sync(store, router);
|
|
||||||
responsiveSync(store);
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const appElement = document.getElementById('app');
|
||||||
|
if (!appElement) { return; }
|
||||||
|
|
||||||
const app = document.getElementById('app');
|
config.baseApiUrl = appElement.dataset.url;
|
||||||
config.baseApiUrl = app.dataset.url;
|
|
||||||
|
|
||||||
window.$vm = new Vue({
|
const app = createApp(App);
|
||||||
el: '#app',
|
const pinia = createPinia();
|
||||||
store,
|
app.use(pinia);
|
||||||
router,
|
app.use(router);
|
||||||
render: createElement => createElement('App'),
|
swInit();
|
||||||
mounted() {
|
installClickStrike(app);
|
||||||
this.$nextTick(() => swInit(store));
|
|
||||||
},
|
|
||||||
components: { 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(appElement);
|
||||||
});
|
});
|
||||||
|
@ -1,49 +1,39 @@
|
|||||||
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';
|
||||||
import TheCalculator from './components/TheCalculator';
|
import TheCalculator from '../components/TheCalculator';
|
||||||
|
|
||||||
import TheLog from './components/TheLog';
|
import TheLog from '../components/TheLog';
|
||||||
import TheLogList from './components/TheLogList';
|
import TheLogList from '../components/TheLogList';
|
||||||
import TheLogCreator from './components/TheLogCreator';
|
import TheLogCreator from '../components/TheLogCreator';
|
||||||
import TheLogEditor from './components/TheLogEditor';
|
import TheLogEditor from '../components/TheLogEditor';
|
||||||
|
|
||||||
import TheFoodList from './components/TheFoodList';
|
import TheFoodList from '../components/TheFoodList';
|
||||||
import TheFood from "./components/TheFood";
|
import TheFood from "../components/TheFood";
|
||||||
import TheFoodEditor from "./components/TheFoodEditor";
|
import TheFoodEditor from "../components/TheFoodEditor";
|
||||||
import TheFoodCreator from "./components/TheFoodCreator";
|
import TheFoodCreator from "../components/TheFoodCreator";
|
||||||
import TheNotesList from './components/TheNotesList';
|
import TheNotesList from '../components/TheNotesList';
|
||||||
import TheRecipe from './components/TheRecipe';
|
import TheRecipe from '../components/TheRecipe';
|
||||||
import TheRecipeEditor from './components/TheRecipeEditor';
|
import TheRecipeEditor from '../components/TheRecipeEditor';
|
||||||
import TheRecipeCreator from './components/TheRecipeCreator';
|
import TheRecipeCreator from '../components/TheRecipeCreator';
|
||||||
import TheRecipeList from './components/TheRecipeList';
|
import TheRecipeList from '../components/TheRecipeList';
|
||||||
|
|
||||||
import TheTaskListList from './components/TheTaskListList';
|
import TheTaskListList from '../components/TheTaskListList';
|
||||||
|
|
||||||
import TheUserCreator from './components/TheUserCreator';
|
import TheUserCreator from '../components/TheUserCreator';
|
||||||
import TheUserEditor from './components/TheUserEditor';
|
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.addRoutes(
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/recipes'
|
redirect: '/recipes'
|
||||||
@ -144,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("/");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -175,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;
|
@ -1,236 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import Vuex from 'vuex'
|
|
||||||
|
|
||||||
import api from '../lib/Api';
|
|
||||||
|
|
||||||
Vue.use(Vuex);
|
|
||||||
|
|
||||||
export default new Vuex.Store({
|
|
||||||
strict: process.env.NODE_ENV !== 'production',
|
|
||||||
state: {
|
|
||||||
initialLoad: false,
|
|
||||||
updateAvailable: false,
|
|
||||||
loadingCount: 0,
|
|
||||||
error: null,
|
|
||||||
authChecked: false,
|
|
||||||
user: null,
|
|
||||||
loginMessage: null,
|
|
||||||
|
|
||||||
taskLists: [],
|
|
||||||
currentTaskList: null,
|
|
||||||
|
|
||||||
nutrientList: {
|
|
||||||
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" }
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getters: {
|
|
||||||
isLoading(state) {
|
|
||||||
return state.loadingCount > 0;
|
|
||||||
},
|
|
||||||
isLoggedIn(state) {
|
|
||||||
return state.user !== null;
|
|
||||||
},
|
|
||||||
isAdmin(state) {
|
|
||||||
return state.user !== null && state.user.admin === true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
setUpdateAvailable(state, value) {
|
|
||||||
state.updateAvailable = value;
|
|
||||||
},
|
|
||||||
|
|
||||||
setInitialLoad(state, value) {
|
|
||||||
state.initialLoad = value;
|
|
||||||
},
|
|
||||||
|
|
||||||
setLoading(state, value) {
|
|
||||||
if (value) {
|
|
||||||
state.loadingCount = state.loadingCount + 1;
|
|
||||||
} else {
|
|
||||||
state.loadingCount = state.loadingCount - 1;
|
|
||||||
}
|
|
||||||
state.loading = state.loadingCount !== 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
setError(state, value) {
|
|
||||||
console.log(value);
|
|
||||||
state.error = value;
|
|
||||||
},
|
|
||||||
|
|
||||||
setUser(state, user) {
|
|
||||||
state.authChecked = true;
|
|
||||||
state.user = user;
|
|
||||||
},
|
|
||||||
|
|
||||||
setLoginMessage(state, msg) {
|
|
||||||
state.loginMessage = msg;
|
|
||||||
},
|
|
||||||
|
|
||||||
setTaskLists(state, lists) {
|
|
||||||
state.taskLists = lists || [];
|
|
||||||
},
|
|
||||||
|
|
||||||
setCurrentTaskList(state, list) {
|
|
||||||
state.currentTaskList = list || null;
|
|
||||||
},
|
|
||||||
|
|
||||||
appendTaskItem(state, item) {
|
|
||||||
const listId = item.task_list_id;
|
|
||||||
const list = state.taskLists.find(l => l.id === listId);
|
|
||||||
if (list) {
|
|
||||||
list.task_items.push(item);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
replaceTaskItem(state, item) {
|
|
||||||
const listId = item.task_list_id;
|
|
||||||
const list = state.taskLists.find(l => l.id === listId);
|
|
||||||
if (list) {
|
|
||||||
const taskIdx = list.task_items.findIndex(i => i.id === item.id);
|
|
||||||
if (taskIdx >= 0) {
|
|
||||||
list.task_items.splice(taskIdx, 1, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeTaskItems(state, payload) {
|
|
||||||
const listId = payload.taskList.id;
|
|
||||||
const list = state.taskLists.find(l => l.id === listId);
|
|
||||||
if (list) {
|
|
||||||
|
|
||||||
list.task_items = list.task_items.filter(item => {
|
|
||||||
return payload.taskItems.findIndex(i => i.id === item.id) === -1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setTaskItemCompletion(state, payload) {
|
|
||||||
const listId = payload.taskList.id;
|
|
||||||
const list = state.taskLists.find(l => l.id === listId);
|
|
||||||
if (list) {
|
|
||||||
list.task_items.forEach(item => {
|
|
||||||
if (payload.taskItems.findIndex(i => i.id === item.id) >= 0) {
|
|
||||||
item.completed = payload.completed;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
updateCurrentUser({commit}) {
|
|
||||||
return api.getCurrentUser()
|
|
||||||
.then(user => {
|
|
||||||
commit("setUser", user);
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
login({commit}, authData) {
|
|
||||||
return api.postLogin(authData.username, authData.password)
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
commit("setUser", data.user);
|
|
||||||
commit("setLoginMessage", null);
|
|
||||||
} else {
|
|
||||||
commit("setUser", null);
|
|
||||||
commit("setLoginMessage", data.message);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
logout({commit}) {
|
|
||||||
return api.getLogout()
|
|
||||||
.then(() => {
|
|
||||||
commit("setUser", null);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
refreshTaskLists({commit, state}) {
|
|
||||||
const cb = function(data) {
|
|
||||||
commit("setTaskLists", data);
|
|
||||||
let ctl = null;
|
|
||||||
|
|
||||||
if (state.currentTaskList) {
|
|
||||||
ctl = data.find(l => l.id === state.currentTaskList.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctl = ctl || data[0] || null;
|
|
||||||
commit("setCurrentTaskList", ctl);
|
|
||||||
};
|
|
||||||
|
|
||||||
return api.getTaskLists(cb)
|
|
||||||
},
|
|
||||||
|
|
||||||
ensureTaskLists({dispatch, state}) {
|
|
||||||
if (state.user && state.taskLists.length === 0) {
|
|
||||||
return dispatch("refreshTaskLists");
|
|
||||||
} else {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
createTaskList({commit, dispatch}, newList) {
|
|
||||||
return api.postTaskList(newList)
|
|
||||||
.then(data => commit("setCurrentTaskList", data))
|
|
||||||
.then(() => dispatch("refreshTaskLists"))
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteTaskList({dispatch}, taskList) {
|
|
||||||
return api.deleteTaskList(taskList)
|
|
||||||
.then(() => dispatch("refreshTaskLists"));
|
|
||||||
},
|
|
||||||
|
|
||||||
createTaskItem({commit, dispatch}, taskItem) {
|
|
||||||
|
|
||||||
return api.postTaskItem(taskItem.task_list_id, taskItem)
|
|
||||||
.then(data => {
|
|
||||||
commit("appendTaskItem", data);
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateTaskItem({commit}, taskItem) {
|
|
||||||
return api.patchTaskItem(taskItem.task_list_id, taskItem)
|
|
||||||
.then(data => {
|
|
||||||
commit("replaceTaskItem", data);
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteTaskItems({commit}, payload) {
|
|
||||||
return api.deleteTaskItems(payload.taskList.id, payload.taskItems)
|
|
||||||
.then(() => commit("removeTaskItems", payload));
|
|
||||||
},
|
|
||||||
|
|
||||||
completeTaskItems({commit}, payload) {
|
|
||||||
return api.completeTaskItems(payload.taskList.id, payload.taskItems, !payload.completed)
|
|
||||||
.then(() => commit("setTaskItemCompletion", payload));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
102
app/javascript/stores/appConfig.js
Normal file
102
app/javascript/stores/appConfig.js
Normal 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
|
||||||
|
};
|
||||||
|
});
|
35
app/javascript/stores/mediaQuery.js
Normal file
35
app/javascript/stores/mediaQuery.js
Normal 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;
|
||||||
|
});
|
36
app/javascript/stores/nutrient.js
Normal file
36
app/javascript/stores/nutrient.js
Normal 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
|
||||||
|
};
|
||||||
|
});
|
114
app/javascript/stores/task.js
Normal file
114
app/javascript/stores/task.js
Normal 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
|
||||||
|
};
|
||||||
|
});
|
@ -1,107 +1,97 @@
|
|||||||
@mixin responsive-button-size($size) {
|
@use "bulma/sass/utilities" as bulma;
|
||||||
&.is-small-#{$size} {
|
@use "bulma/sass/utilities/controls";
|
||||||
@include button-small;
|
@use "bulma/sass/utilities/mixins";
|
||||||
}
|
|
||||||
|
|
||||||
&.is-medium-#{$size} {
|
|
||||||
@include button-medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-large-#{$size} {
|
|
||||||
@include button-large;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin responsive-label-size($size) {
|
@mixin responsive-label-size($size) {
|
||||||
&.is-small-#{$size} {
|
&.is-small-#{$size} {
|
||||||
font-size: $size-small;
|
font-size: bulma.$size-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-medium-#{$size} {
|
&.is-medium-#{$size} {
|
||||||
font-size: $size-medium;
|
font-size: bulma.$size-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-large-#{$size} {
|
&.is-large-#{$size} {
|
||||||
font-size: $size-large;
|
font-size: bulma.$size-large;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin responsive-control-size($size) {
|
@mixin responsive-control-size($size) {
|
||||||
&.is-small-#{$size} {
|
&.is-small-#{$size} {
|
||||||
@include control-small;
|
@include controls.control-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-medium-#{$size} {
|
&.is-medium-#{$size} {
|
||||||
@include control-medium;
|
@include controls.control-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-large-#{$size} {
|
&.is-large-#{$size} {
|
||||||
@include control-large;
|
@include controls.control-large;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@include mobile {
|
@include mixins.mobile {
|
||||||
@include responsive-button-size("mobile");
|
@include responsive-control-size("mobile");
|
||||||
}
|
}
|
||||||
|
|
||||||
@include tablet {
|
@include mixins.tablet {
|
||||||
@include responsive-button-size("tablet");
|
@include responsive-control-size("tablet");
|
||||||
}
|
}
|
||||||
|
|
||||||
@include desktop {
|
@include mixins.desktop {
|
||||||
@include responsive-button-size("desktop");
|
@include responsive-control-size("desktop");
|
||||||
}
|
}
|
||||||
|
|
||||||
@include widescreen {
|
@include mixins.widescreen {
|
||||||
@include responsive-button-size("widescreen");
|
@include responsive-control-size("widescreen");
|
||||||
}
|
}
|
||||||
|
|
||||||
@include fullhd {
|
@include mixins.fullhd {
|
||||||
@include responsive-button-size("fullhd");
|
@include responsive-control-size("fullhd");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
@include mobile {
|
@include mixins.mobile {
|
||||||
@include responsive-label-size("mobile");
|
@include responsive-label-size("mobile");
|
||||||
}
|
}
|
||||||
|
|
||||||
@include tablet {
|
@include mixins.tablet {
|
||||||
@include responsive-label-size("tablet");
|
@include responsive-label-size("tablet");
|
||||||
}
|
}
|
||||||
|
|
||||||
@include desktop {
|
@include mixins.desktop {
|
||||||
@include responsive-label-size("desktop");
|
@include responsive-label-size("desktop");
|
||||||
}
|
}
|
||||||
|
|
||||||
@include widescreen {
|
@include mixins.widescreen {
|
||||||
@include responsive-label-size("widescreen");
|
@include responsive-label-size("widescreen");
|
||||||
}
|
}
|
||||||
|
|
||||||
@include fullhd {
|
@include mixins.fullhd {
|
||||||
@include responsive-label-size("fullhd");
|
@include responsive-label-size("fullhd");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input, .textarea {
|
.input, .textarea {
|
||||||
@include mobile {
|
@include mixins.mobile {
|
||||||
@include responsive-control-size("mobile");
|
@include responsive-control-size("mobile");
|
||||||
}
|
}
|
||||||
|
|
||||||
@include tablet {
|
@include mixins.tablet {
|
||||||
@include responsive-control-size("tablet");
|
@include responsive-control-size("tablet");
|
||||||
}
|
}
|
||||||
|
|
||||||
@include desktop {
|
@include mixins.desktop {
|
||||||
@include responsive-control-size("desktop");
|
@include responsive-control-size("desktop");
|
||||||
}
|
}
|
||||||
|
|
||||||
@include widescreen {
|
@include mixins.widescreen {
|
||||||
@include responsive-control-size("widescreen");
|
@include responsive-control-size("widescreen");
|
||||||
}
|
}
|
||||||
|
|
||||||
@include fullhd {
|
@include mixins.fullhd {
|
||||||
@include responsive-control-size("fullhd");
|
@include responsive-control-size("fullhd");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,35 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
//$family-sans-serif: "Verdana", "Helvetica Neue", "Helvetica", "Arial", sans-serif !default
|
|
||||||
$family-serif: Georgia, "Times New Roman", Times, serif;
|
|
||||||
|
|
||||||
// Bluma default overrides
|
|
||||||
//$green: #79A736;
|
|
||||||
//$red: #d4424e;
|
|
||||||
//$blue: #9c36a7;
|
|
||||||
|
|
||||||
$green: $coolors-green;
|
|
||||||
$blue: $coolors-blue;
|
|
||||||
$red: $coolors-red;
|
|
||||||
$yellow: $coolors-yellow;
|
|
||||||
$dark: $coolors-dark;
|
|
||||||
|
|
||||||
$primary: $green;
|
|
||||||
$family-primary: $family-serif;
|
|
||||||
|
|
||||||
$modal-content-width: 750px;
|
|
||||||
|
|
||||||
|
|
||||||
// Make all Bulma variables and functions available
|
|
||||||
@import "~bulma/sass/utilities/initial-variables";
|
|
||||||
@import "~bulma/sass/utilities/functions";
|
|
||||||
@import "~bulma/sass/utilities/derived-variables";
|
|
||||||
|
|
||||||
//$dropdown-item-hover-background-color: $white-ter;
|
|
@ -1,5 +1,9 @@
|
|||||||
|
|
||||||
@include until($desktop) {
|
@use "bulma/sass/utilities" as bulma;
|
||||||
|
@use "bulma/sass/utilities/mixins";
|
||||||
|
|
||||||
|
|
||||||
|
@include mixins.until(bulma.$desktop) {
|
||||||
.modal.is-wide {
|
.modal.is-wide {
|
||||||
.modal-content, .modal-card {
|
.modal-content, .modal-card {
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
@ -8,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include from($desktop) {
|
@include mixins.from(bulma.$desktop) {
|
||||||
.modal.is-wide {
|
.modal.is-wide {
|
||||||
.modal-content, .modal-card {
|
.modal-content, .modal-card {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
@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";
|
@use "bulma/versions/bulma-no-dark-mode" as bulma with (
|
||||||
@import "~bulma/sass/components/navbar";
|
$family-primary: $family-serif,
|
||||||
@import "~bulma/sass/components/level";
|
$green: $coolors-green,
|
||||||
@import "~bulma/sass/components/message";
|
$blue: $coolors-blue,
|
||||||
@import "~bulma/sass/components/modal";
|
$red: $coolors-red,
|
||||||
@import "~bulma/sass/components/pagination";
|
$yellow: $coolors-yellow,
|
||||||
@import "~bulma/sass/components/panel";
|
$dark: $coolors-dark,
|
||||||
@import "~bulma/sass/elements/_all";
|
$primary: $coolors-green,
|
||||||
@import "~bulma/sass/form/_all";
|
$modal-content-width: 750px
|
||||||
@import "~bulma/sass/grid/columns";
|
);
|
||||||
@import "~bulma/sass/layout/section";
|
|
||||||
|
|
||||||
@import "./responsive_controls";
|
@import "./responsive_controls";
|
||||||
@import "./wide_modal";
|
@import "./wide_modal";
|
||||||
@ -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) {
|
||||||
|
@ -91,15 +91,15 @@ class Food < Ingredient
|
|||||||
:vit_e,
|
:vit_e,
|
||||||
:vit_d,
|
:vit_d,
|
||||||
:vit_k,
|
:vit_k,
|
||||||
:cholesterol
|
:cholesterol,
|
||||||
|
[:lipids, :lipid],
|
||||||
|
[:density, :density_best_guess]
|
||||||
]
|
]
|
||||||
|
|
||||||
copy_fields.each do |f|
|
copy_fields.each do |f|
|
||||||
self.send("#{f}=".to_sym, food.send(f.to_sym))
|
to, from = f.is_a?(Array) ? f : [f, f]
|
||||||
|
self.send("#{to}=".to_sym, food.send(from.to_sym))
|
||||||
end
|
end
|
||||||
|
|
||||||
self.lipids = food.lipid
|
|
||||||
self.density = food.density_best_guess
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
class TaskItem < ApplicationRecord
|
class TaskItem < ApplicationRecord
|
||||||
include DefaultValues
|
include DefaultValues
|
||||||
|
|
||||||
belongs_to :task_list, touch: true
|
belongs_to :task_list, touch: true, inverse_of: :task_items
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
class TaskList < ApplicationRecord
|
class TaskList < ApplicationRecord
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
has_many :task_items, dependent: :delete_all
|
has_many :task_items, dependent: :delete_all, inverse_of: :task_list
|
||||||
|
|
||||||
validates :name,
|
validates :name,
|
||||||
presence: true,
|
presence: true,
|
||||||
@ -13,22 +13,22 @@ class TaskList < ApplicationRecord
|
|||||||
if recurse_depth > 10
|
if recurse_depth > 10
|
||||||
raise "This shouldn't be. Did you make a recipe loop?"
|
raise "This shouldn't be. Did you make a recipe loop?"
|
||||||
end
|
end
|
||||||
|
|
||||||
recipe.recipe_ingredients.each do |ri|
|
recipe.recipe_ingredients.each do |ri|
|
||||||
if ri.ingredient.is_a?(Recipe)
|
if ri.ingredient.is_a?(Recipe)
|
||||||
add_recipe_ingredients(ri.ingredient, recurse_depth + 1)
|
add_recipe_ingredients(ri.ingredient, recurse_depth + 1)
|
||||||
else
|
else
|
||||||
item = self.task_items.detect { |i| i.name.downcase == ri.name.downcase } || TaskItem.new(name: ri.name, task_list: self)
|
item = self.task_items.detect { |i| i.name.downcase == ri.name.downcase } || task_items.build(name: ri.name)
|
||||||
quantity_str = [ri.quantity, ri.units].delete_if { |i| i.blank? }.join(' ')
|
quantity_str = [ri.quantity, ri.units].delete_if { |i| i.blank? }.join(' ')
|
||||||
if item.quantity.blank?
|
if item.quantity.blank?
|
||||||
item.quantity = quantity_str
|
item.quantity = quantity_str
|
||||||
else
|
else
|
||||||
item.quantity += (', ' + quantity_str)
|
item.quantity += (', ' + quantity_str)
|
||||||
end
|
end
|
||||||
|
|
||||||
item.save
|
item.save
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user