Compare commits

..

3 Commits
main ... sw

Author SHA1 Message Date
f4b86e9a6c ui 2018-07-22 15:53:08 -05:00
8a13dfc3d6 Merge branch 'master' into sw 2018-07-15 17:02:53 -05:00
b74e3224bc updating service worker 2018-07-15 17:02:19 -05:00
546 changed files with 16994 additions and 3732179 deletions

18
.babelrc Normal file
View File

@ -0,0 +1,18 @@
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": "> 1%",
"uglify": true
},
"useBuiltIns": true
}]
],
"plugins": [
"syntax-dynamic-import",
"transform-object-rest-spread",
["transform-class-properties", { "spec": true }]
]
}

View File

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

View File

@ -1 +0,0 @@
port: 3000

12
.gitignore vendored
View File

@ -9,7 +9,7 @@
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3*
/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
/log/*
@ -27,13 +27,3 @@
/node_modules
yarn-debug.log*
.yarn-integrity
/dist
/public/packs
/public/packs-test
/node_modules
/yarn-error.log
yarn-debug.log*
.yarn-integrity
.yarn
.pnp.*

7
.postcssrc.yml Normal file
View File

@ -0,0 +1,7 @@
plugins:
postcss-import: {}
postcss-cssnext: {
features: {
customProperties: false
}
}

View File

@ -1 +1 @@
3.3.5
2.4.4

View File

@ -1,40 +1,50 @@
FROM ruby:3.3.5-bookworm
FROM phusion/passenger-ruby24:latest
RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash - && \
apt-get update && apt-get dist-upgrade -y && \
apt-get install -y \
nodejs \
nginx && \
apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
# Use baseimage-docker's init process.
CMD ["/sbin/my_init"]
RUN gem update --no-document --system && gem install bundler --no-document && corepack enable
# Install Node
RUN curl -sL https://deb.nodesource.com/setup_8.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 install -y --no-install-recommends nodejs yarn && \
apt-get clean && rm -rf /var/lib/apt/lists/*
RUN gem update --system && gem install bundler
# Enable Nginx / Passenger
RUN rm -f /etc/service/nginx/down
# Install nginx config files
RUN rm /etc/nginx/sites-enabled/default
ADD docker/nginx_server.conf /etc/nginx/sites-enabled/parsley.conf
ADD docker/nginx_env.conf /etc/nginx/main.d/env.conf
# Add scripts
ADD docker/bin/* /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/nginx_service.sh /usr/local/bin/rails_service.sh
# Add DB Migration Script
RUN mkdir -p /etc/my_init.d
ADD docker/db_migrate.sh /etc/my_init.d/db_migrate.sh
RUN chmod +x /etc/my_init.d/db_migrate.sh
# Set Default RAILS_ENV
ENV RAILS_ENV docker
ENV PASSENGER_APP_ENV docker
# Setup directory and install gems
RUN mkdir -p /parsley
WORKDIR /parsley
RUN mkdir -p /home/app/parsley/log /home/app/parsley/tmp
RUN chown -R app:app /home/app/parsley/
WORKDIR /home/app/parsley/
COPY Gemfile* ./
RUN bundle install
RUN bundle install --deployment --jobs 4 --without development test
COPY package.json yarn.lock ./
RUN yarn install --immutable
RUN yarn install --production=true
COPY . .
# Copy the app into the image
COPY --chown="app" . .
# Compile assets
RUN env RAILS_ENV=production bundle exec rails shakapacker:clobber shakapacker:compile
RUN su app -c "bundle exec rails webpacker:clobber webpacker:compile"
ENV PORT=80
EXPOSE 80
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

39
Gemfile
View File

@ -1,30 +1,37 @@
source 'https://rubygems.org'
gem 'rails', '7.2.1'
gem 'pg', '~> 1.5.8'
gem 'rails', '5.2.0'
gem 'pg', '~> 1.0.0'
gem 'shakapacker', '8.0.2'
gem 'webpacker', '3.5.3'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'oj', '~> 3.16.6'
gem 'csv', '~> 3.3'
gem 'jbuilder', '~> 2.7'
#gem 'jbuilder', git: 'https://github.com/rails/jbuilder', branch: 'master'
gem 'kaminari', '~> 1.2.2'
gem 'unitwise', '~> 2.3.0'
gem 'redcarpet', '~> 3.6.0'
gem 'oj', '~> 3.6.2'
gem 'kaminari', '~> 1.1.1'
gem 'unitwise', '~> 2.2.0'
gem 'redcarpet', '~> 3.4.0'
gem 'dalli', '~> 2.7.6'
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.11'
gem 'dalli', '~> 3.2.8'
gem 'puma', '~> 6.4'
gem 'bcrypt', '~> 3.1.18'
gem 'tzinfo-data'
group :development, :test do
gem 'sqlite3', '~> 2.1.0'
gem 'guard', '~> 2.18.0'
gem 'puma', '~> 3.11'
gem 'sqlite3'
gem 'guard', '~> 2.14.0'
gem 'guard-rspec', require: false
gem 'rspec-rails', '~> 7.0.1'
gem 'rspec-rails', '~> 3.7.2'
gem 'rails-controller-testing'
gem 'factory_bot_rails', '~> 6.4.3'
gem 'database_cleaner', '~> 2.0.2'
gem 'factory_bot_rails', '~> 4.8.2'
gem 'database_cleaner', '~> 1.6.2'
end

View File

@ -1,114 +1,75 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.2.1)
actionpack (= 7.2.1)
activesupport (= 7.2.1)
actioncable (5.2.0)
actionpack (= 5.2.0)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.2.1)
actionpack (= 7.2.1)
activejob (= 7.2.1)
activerecord (= 7.2.1)
activestorage (= 7.2.1)
activesupport (= 7.2.1)
mail (>= 2.8.0)
actionmailer (7.2.1)
actionpack (= 7.2.1)
actionview (= 7.2.1)
activejob (= 7.2.1)
activesupport (= 7.2.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.2.1)
actionview (= 7.2.1)
activesupport (= 7.2.1)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4, < 3.2)
rack-session (>= 1.0.1)
actionmailer (5.2.0)
actionpack (= 5.2.0)
actionview (= 5.2.0)
activejob (= 5.2.0)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.0)
actionview (= 5.2.0)
activesupport (= 5.2.0)
rack (~> 2.0)
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)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.0)
activesupport (= 5.2.0)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.2.1)
activesupport (= 7.2.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (5.2.0)
activesupport (= 5.2.0)
globalid (>= 0.3.6)
activemodel (7.2.1)
activesupport (= 7.2.1)
activerecord (7.2.1)
activemodel (= 7.2.1)
activesupport (= 7.2.1)
timeout (>= 0.4.0)
activestorage (7.2.1)
actionpack (= 7.2.1)
activejob (= 7.2.1)
activerecord (= 7.2.1)
activesupport (= 7.2.1)
marcel (~> 1.0)
activesupport (7.2.1)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
base64 (0.2.0)
bcrypt (3.1.20)
bigdecimal (3.1.8)
bootsnap (1.18.4)
msgpack (~> 1.2)
builder (3.3.0)
coderay (1.1.3)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
crass (1.0.6)
csv (3.3.0)
dalli (3.2.8)
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
database_cleaner-active_record (2.2.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.4)
diff-lcs (1.5.1)
drb (2.2.1)
erubi (1.13.0)
factory_bot (6.5.0)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
ffi (1.17.0)
formatador (1.1.0)
globalid (1.2.1)
activesupport (>= 6.1)
guard (2.18.1)
activemodel (5.2.0)
activesupport (= 5.2.0)
activerecord (5.2.0)
activemodel (= 5.2.0)
activesupport (= 5.2.0)
arel (>= 9.0)
activestorage (5.2.0)
actionpack (= 5.2.0)
activerecord (= 5.2.0)
marcel (~> 0.3.1)
activesupport (5.2.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
arel (9.0.0)
bcrypt (3.1.12)
bootsnap (1.3.0)
msgpack (~> 1.0)
builder (3.2.3)
coderay (1.1.2)
concurrent-ruby (1.0.5)
crass (1.0.4)
dalli (2.7.8)
database_cleaner (1.6.2)
diff-lcs (1.3)
erubi (1.7.1)
factory_bot (4.8.2)
activesupport (>= 3.0.0)
factory_bot_rails (4.8.2)
factory_bot (~> 4.8.2)
railties (>= 3.0.0)
ffi (1.9.25)
formatador (0.2.5)
globalid (0.4.1)
activesupport (>= 4.2.0)
guard (2.14.2)
formatador (>= 0.2.4)
listen (>= 2.7, < 4.0)
lumberjack (>= 1.0.12, < 2.0)
nenv (~> 0.1)
notiffany (~> 0.0)
pry (>= 0.13.0)
pry (>= 0.9.12)
shellany (~> 0.0)
thor (>= 0.18.1)
guard-compat (1.2.1)
@ -116,207 +77,172 @@ GEM
guard (~> 2.1)
guard-compat (~> 1.1)
rspec (>= 2.99.0, < 4.0)
i18n (1.14.6)
i18n (1.0.1)
concurrent-ruby (~> 1.0)
io-console (0.7.2)
irb (1.14.1)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
kaminari (1.2.2)
jbuilder (2.7.0)
activesupport (>= 4.2.0)
multi_json (>= 1.2)
kaminari (1.1.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
kaminari-activerecord (= 1.2.2)
kaminari-core (= 1.2.2)
kaminari-actionview (1.2.2)
kaminari-actionview (= 1.1.1)
kaminari-activerecord (= 1.1.1)
kaminari-core (= 1.1.1)
kaminari-actionview (1.1.1)
actionview
kaminari-core (= 1.2.2)
kaminari-activerecord (1.2.2)
kaminari-core (= 1.1.1)
kaminari-activerecord (1.1.1)
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
kaminari-core (= 1.1.1)
kaminari-core (1.1.1)
liner (0.2.4)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.6.1)
loofah (2.22.0)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
loofah (2.2.2)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
lumberjack (1.2.10)
mail (2.8.1)
nokogiri (>= 1.5.9)
lumberjack (1.0.13)
mail (2.7.0)
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.0.4)
marcel (0.3.2)
mimemagic (~> 0.3.2)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (1.1.0)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.25.1)
msgpack (1.7.2)
method_source (0.9.0)
mimemagic (0.3.2)
mini_mime (1.0.0)
mini_portile2 (2.3.0)
minitest (5.11.3)
msgpack (1.2.4)
multi_json (1.13.1)
nenv (0.3.0)
net-imap (0.4.16)
date
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)
notiffany (0.1.3)
nio4r (2.3.1)
nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
notiffany (0.1.1)
nenv (~> 0.1)
shellany (~> 0.0)
oj (3.16.6)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
ostruct (0.6.0)
package_json (0.1.0)
parslet (2.0.0)
pg (1.5.8)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
psych (5.1.2)
stringio
puma (6.4.3)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.7)
rack-proxy (0.7.7)
oj (3.6.2)
parslet (1.8.2)
pg (1.0.0)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
puma (3.11.4)
rack (2.0.5)
rack-proxy (0.6.4)
rack
rack-session (2.0.0)
rack (>= 3.0.0)
rack-test (2.1.0)
rack (>= 1.3)
rackup (2.1.0)
rack (>= 3)
webrick (~> 1.8)
rails (7.2.1)
actioncable (= 7.2.1)
actionmailbox (= 7.2.1)
actionmailer (= 7.2.1)
actionpack (= 7.2.1)
actiontext (= 7.2.1)
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)
railties (= 7.2.1)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
rack-test (1.0.0)
rack (>= 1.0, < 3)
rails (5.2.0)
actioncable (= 5.2.0)
actionmailer (= 5.2.0)
actionpack (= 5.2.0)
actionview (= 5.2.0)
activejob (= 5.2.0)
activemodel (= 5.2.0)
activerecord (= 5.2.0)
activestorage (= 5.2.0)
activesupport (= 5.2.0)
bundler (>= 1.3.0)
railties (= 5.2.0)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
actionview (~> 5.x, >= 5.0.1)
activesupport (~> 5.x)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.2.1)
actionpack (= 7.2.1)
activesupport (= 7.2.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rake (13.2.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rdoc (6.7.0)
psych (>= 4.0.0)
redcarpet (3.6.0)
reline (0.5.10)
io-console (~> 0.5)
rspec (3.13.0)
rspec-core (~> 3.13.0)
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)
rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2)
railties (5.2.0)
actionpack (= 5.2.0)
activesupport (= 5.2.0)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rake (12.3.1)
rb-fsevent (0.10.3)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
redcarpet (3.4.0)
rspec (3.7.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-core (3.7.1)
rspec-support (~> 3.7.0)
rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.1)
rspec-support (~> 3.7.0)
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.0.1)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.1)
securerandom (0.3.1)
semantic_range (3.0.0)
shakapacker (8.0.2)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
rspec-support (~> 3.7.0)
rspec-rails (3.7.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-support (~> 3.7.0)
rspec-support (3.7.1)
ruby_dep (1.5.0)
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)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2024.2)
rack (> 1, < 3)
sprockets-rails (3.2.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sqlite3 (1.3.13)
thor (0.20.0)
thread_safe (0.3.6)
tzinfo (1.2.5)
thread_safe (~> 0.1)
tzinfo-data (1.2018.5)
tzinfo (>= 1.0.0)
unitwise (2.3.0)
unitwise (2.2.0)
liner (~> 0.2)
memoizable (~> 0.4)
parslet (~> 2.0)
parslet (~> 1.5)
signed_multiset (~> 0.2)
useragent (0.16.10)
webrick (1.8.2)
websocket-driver (0.7.6)
webpacker (3.5.3)
activesupport (>= 4.2)
rack-proxy (>= 0.6.1)
railties (>= 4.2)
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.6.18)
websocket-extensions (0.1.3)
PLATFORMS
ruby
DEPENDENCIES
bcrypt (~> 3.1.18)
bcrypt (~> 3.1.11)
bootsnap (>= 1.1.0)
csv (~> 3.3)
dalli (~> 3.2.8)
database_cleaner (~> 2.0.2)
factory_bot_rails (~> 6.4.3)
guard (~> 2.18.0)
dalli (~> 2.7.6)
database_cleaner (~> 1.6.2)
factory_bot_rails (~> 4.8.2)
guard (~> 2.14.0)
guard-rspec
kaminari (~> 1.2.2)
oj (~> 3.16.6)
pg (~> 1.5.8)
puma (~> 6.4)
rails (= 7.2.1)
jbuilder (~> 2.7)
kaminari (~> 1.1.1)
oj (~> 3.6.2)
pg (~> 1.0.0)
puma (~> 3.11)
rails (= 5.2.0)
rails-controller-testing
redcarpet (~> 3.6.0)
rspec-rails (~> 7.0.1)
shakapacker (= 8.0.2)
sqlite3 (~> 2.1.0)
redcarpet (~> 3.4.0)
rspec-rails (~> 3.7.2)
sqlite3
tzinfo-data
unitwise (~> 2.3.0)
unitwise (~> 2.2.0)
webpacker (= 3.5.3)
BUNDLED WITH
2.5.20
1.16.1

27
Jenkinsfile vendored
View File

@ -1,27 +0,0 @@
library('jenkins_build')
node {
main {
def dockerImageName = "registry.elbert.us/parsley"
def dockerImage
stage("Checkout") {
checkout scm
}
stage("Build") {
dockerImage = docker.build("${dockerImageName}:latest")
}
stage("Publish") {
dockerImage.push()
dockerImage.push(env.JOB_BASE_NAME)
}
if (env.BRANCH_NAME == "production") {
stage("Deploy") {
remote_deploy("azathoth.thenever", "parsley", "./docker-compose-azathoth.yml")
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,4 +0,0 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@ -1,18 +0,0 @@
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

View File

@ -1,12 +0,0 @@
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

View File

@ -6,7 +6,6 @@ module Admin
def index
@users = User.order(:full_name)
render json: UserSerializer.for(@users)
end
def show

View File

@ -4,7 +4,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
def verified_request?
if request.media_type == "application/json"
if request.content_type == "application/json"
true
else
super()
@ -42,18 +42,8 @@ class ApplicationController < ActionController::Base
if owner
yield if block_given?
else
respond_to do |format|
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
flash[:warning] = "Operation Not Permitted"
redirect_to root_path
end
end

View File

@ -7,62 +7,29 @@ class CalculatorController < ApplicationController
def calculate
input = params[:input]
output_unit = params[:output_unit]
ingredient_id = params[:ingredient_id]
ingredient = nil
density = params[:density]
density = nil unless density.present?
if ingredient_id.present?
ingredient = Ingredient.find_by_ingredient_id(ingredient_id)
end
data = {errors: [], output: ''}
data = {errors: Hash.new { |h, k| h[k] = [] }, output: ''}
UnitConversion::with_custom_units(ingredient ? ingredient.custom_units : []) do
density_unit = nil
begin
if density
density_unit = UnitConversion.parse(density)
unless density_unit.density?
data[:errors][:density] << 'not a density unit'
density_unit = nil
end
end
rescue UnitConversion::UnparseableUnitError => e
data[:errors][:density] << 'invalid string'
begin
unit = UnitConversion.parse(input)
if output_unit.present?
unit = unit.convert(output_unit, density)
data[:output] = unit.to_s
else
data[:output] = unit.auto_unit.to_s
end
begin
input_unit = UnitConversion.parse(input)
input_unit.unitwise
rescue UnitConversion::UnparseableUnitError => e
data[:errors][:input] << e.message
input_unit = nil
end
if !input_unit.nil?
if output_unit.present?
begin
input_unit = input_unit.convert(output_unit, density_unit)
rescue UnitConversion::UnparseableUnitError => e
data[:errors][:output_unit] << e.message
end
else
input_unit = input_unit.auto_unit
end
end
if data[:errors].empty?
data[:output] = input_unit.to_s
end
rescue UnitConversion::UnparseableUnitError => e
data[:errors] << e.message
end
render json: data
end
def ingredient_search
@foods = Food.has_density.search(params[:query]).order(:name)
render json: @foods.map { |f| {id: f.id, name: f.name, density: f.density} }
@ingredients = Ingredient.has_density.search(params[:query]).order(:name)
end
end

View File

@ -1,109 +0,0 @@
class FoodsController < ApplicationController
before_action :set_food, only: [:show, :update, :destroy]
before_action :ensure_valid_user, except: [:index, :show]
# GET /foods
# GET /foods.json
def index
@foods = Food.all.order(:name).page(params[:page]).per(params[:per])
if params[:name].present?
@foods = @foods.matches_tokens(:name, params[:name].split.take(4))
end
render json: FoodSummarySerializer.for(@foods, collection_name: 'foods')
end
def show
render json: FoodSerializer.for(@food)
end
# POST /foods
# POST /foods.json
def create
@food = Food.new(food_params)
@food.user = current_user
if @food.ndbn.present?
@food.set_usda_food(UsdaFood.find_by_ndbn(@food.ndbn))
end
respond_to do |format|
if @food.save
format.html { redirect_to foods_path, notice: 'Ingredient was successfully created.' }
format.json { render json: { success: true }, status: :created, location: @food }
else
format.html { render :new }
format.json { render json: @food.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /foods/1
def update
ensure_owner @food do
@food.assign_attributes(food_params)
if @food.ndbn.present?
@food.set_usda_food(UsdaFood.find_by_ndbn(@food.ndbn))
end
respond_to do |format|
if @food.save
format.html { redirect_to foods_path, notice: 'Ingredient was successfully updated.' }
format.json { render json: { success: true }, status: :ok, location: @food }
else
format.html { render :edit }
format.json { render json: @food.errors, status: :unprocessable_entity }
end
end
end
end
# DELETE /foods/1
# DELETE /foods/1.json
def destroy
ensure_owner @food do
@food.destroy
respond_to do |format|
format.html { redirect_to foods_url, notice: 'Ingredient was successfully destroyed.' }
format.json { head :no_content }
end
end
end
def select_ndbn
if params[:id].present?
@food = Food.find(params[:id])
else
@food = Food.new
end
@food.assign_attributes(food_params)
if @food.ndbn.present?
@food.set_usda_food(UsdaFood.find_by_ndbn(@food.ndbn))
end
render json: FoodSerializer.for(@food)
end
def usda_food_search
@foods = UsdaFood.search(params[:query]).limit(250).order(:long_description)
render json: UsdaFoodSerializer.for(@foods)
end
private
# Use callbacks to share common setup or constraints between actions.
def set_food
@food = Food.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def food_params
params.require(:food).permit(:name, :notes, :ndbn, :density, :water, :protein, :lipids, :carbohydrates, :kcal, :fiber, :sugar, :calcium, :sodium, :vit_k, :ash, :iron, :magnesium, :phosphorus, :potassium, :zinc, :copper, :manganese, :vit_c, :vit_b6, :vit_b12, :vit_a, :vit_e, :vit_d, :cholesterol, :food_units_attributes => [:name, :gram_weight, :id, :_destroy])
end
def conversion_params
params.require(:conversion).permit(:input_quantity, :input_units, :scale, :output_units, :ingredient_id)
end
end

View File

@ -1,19 +1,141 @@
class IngredientsController < ApplicationController
def search
@ingredients = Food.search(params[:query]).order(:name).to_a
@ingredients.concat(Recipe.is_ingredient.search_by_name(params[:query]).order(:name).to_a)
@ingredients.sort { |a, b| a.name <=> b.name }
before_action :set_ingredient, only: [:show, :edit, :update, :destroy]
json = @ingredients.map do |i|
{
id: i.ingredient_id,
ingredient_id: i.ingredient_id,
name: i.name,
density: i.density
}
before_action :ensure_valid_user, except: [:index, :show]
# GET /ingredients
# GET /ingredients.json
def index
@ingredients = Ingredient.all.order(:name).page(params[:page]).per(params[:per])
if params[:name].present?
@ingredients = @ingredients.matches_tokens(:name, params[:name].split.take(4))
end
end
def show
end
# GET /ingredients/new
def new
@ingredient = Ingredient.new
end
# GET /ingredients/1/edit
def edit
ensure_owner @ingredient
end
# POST /ingredients
# POST /ingredients.json
def create
@ingredient = Ingredient.new(ingredient_params)
@ingredient.user = current_user
if @ingredient.ndbn.present?
@ingredient.set_usda_food(UsdaFood.find_by_ndbn(@ingredient.ndbn))
end
render json: json
respond_to do |format|
if @ingredient.save
format.html { redirect_to ingredients_path, notice: 'Ingredient was successfully created.' }
format.json { render json: { success: true }, status: :created, location: @ingredient }
else
format.html { render :new }
format.json { render json: @ingredient.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /ingredients/1
def update
ensure_owner @ingredient do
@ingredient.assign_attributes(ingredient_params)
if @ingredient.ndbn.present?
@ingredient.set_usda_food(UsdaFood.find_by_ndbn(@ingredient.ndbn))
end
respond_to do |format|
if @ingredient.save
format.html { redirect_to ingredients_path, notice: 'Ingredient was successfully updated.' }
format.json { render json: { success: true }, status: :ok, location: @ingredient }
else
format.html { render :edit }
format.json { render json: @ingredient.errors, status: :unprocessable_entity }
end
end
end
end
# DELETE /ingredients/1
# DELETE /ingredients/1.json
def destroy
ensure_owner @ingredient do
@ingredient.destroy
respond_to do |format|
format.html { redirect_to ingredients_url, notice: 'Ingredient was successfully destroyed.' }
format.json { head :no_content }
end
end
end
def select_ndbn
if params[:id].present?
@ingredient = Ingredient.find(params[:id])
else
@ingredient = Ingredient.new
end
@ingredient.assign_attributes(ingredient_params)
if @ingredient.ndbn.present?
@ingredient.set_usda_food(UsdaFood.find_by_ndbn(@ingredient.ndbn))
end
render :show
end
def prefetch
@ingredients = Ingredient.all.order(:name)
render :search
end
def search
@ingredients = Ingredient.search(params[:query]).order(:name)
end
def convert
@conversion = Conversion.new(conversion_params)
if @conversion.valid?
@output_quantity = @conversion.output_quantity
@conversion = Conversion.new
else
@output_quantity = ''
end
end
def usda_food_search
@foods = UsdaFood.search(params[:query]).limit(250).order(:long_description)
respond_to do |format|
format.html { render :layout => false }
format.json { }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_ingredient
@ingredient = Ingredient.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def ingredient_params
params.require(:ingredient).permit(:name, :notes, :ndbn, :density, :water, :protein, :lipids, :carbohydrates, :kcal, :fiber, :sugar, :calcium, :sodium, :vit_k, :ash, :iron, :magnesium, :phosphorus, :potassium, :zinc, :copper, :manganese, :vit_c, :vit_b6, :vit_b12, :vit_a, :vit_e, :vit_d, :cholesterol, :ingredient_units_attributes => [:name, :gram_weight, :id, :_destroy])
end
def conversion_params
params.require(:conversion).permit(:input_quantity, :input_units, :scale, :output_units, :ingredient_id)
end
end

View File

@ -8,17 +8,14 @@ class LogsController < ApplicationController
def index
@logs = Log.for_user(current_user).order(date: :desc).page(params[:page]).per(params[:per])
render json: LogSummarySerializer.for(@logs, collection_name: 'logs')
end
def show
ensure_owner(@log)
render json: LogSerializer.for(@log)
end
def edit
ensure_owner(@log)
render json: LogSerializer.for(@log)
end
def update
@ -64,12 +61,12 @@ class LogsController < ApplicationController
private
def set_log
@log = Log.includes({recipe: {recipe_ingredients: {food: :food_units} }}).find(params[:id])
@log = Log.includes({recipe: {recipe_ingredients: {ingredient: :ingredient_units} }}).find(params[:id])
end
def set_recipe
if params[:recipe_id].present?
@recipe = Recipe.includes([{recipe_ingredients: [:food]}]).find(params[:recipe_id])
@recipe = Recipe.includes([{recipe_ingredients: [:ingredient]}]).find(params[:recipe_id])
end
end

View File

@ -1,19 +1,27 @@
class NotesController < ApplicationController
before_action :set_note, only: [:show, :update, :destroy]
before_action :set_note, only: [:show, :edit, :update, :destroy]
before_action :ensure_valid_user
# GET /notes
# GET /notes.json
def index
@notes = Note.for_user(current_user)
render json: NoteSerializer.for(@notes)
end
# GET /notes/1
# GET /notes/1.json
def show
ensure_owner(@note)
render json: NoteSerializer.for(@note)
end
# GET /notes/new
def new
@note = Note.new
end
# GET /notes/1/edit
def edit
ensure_owner(@note)
end
# POST /notes
@ -25,7 +33,7 @@ class NotesController < ApplicationController
respond_to do |format|
if @note.save
format.html { redirect_to notes_path, notice: 'Note was successfully created.' }
format.json { render json: NoteSerializer.for(@note), status: :created, location: @note }
format.json { render :show, status: :created, location: @note }
else
format.html { render :new }
format.json { render json: @note.errors, status: :unprocessable_entity }
@ -40,7 +48,7 @@ class NotesController < ApplicationController
respond_to do |format|
if @note.update(note_params)
format.html { redirect_to notes_path, notice: 'Note was successfully updated.' }
format.json { render json: NoteSerializer.for(@note), status: :ok, location: @note }
format.json { render :show, status: :ok, location: @note }
else
format.html { render :edit }
format.json { render json: @note.errors, status: :unprocessable_entity }

View File

@ -1,14 +1,13 @@
class RecipesController < ApplicationController
before_action :set_recipe, only: [:show, :update, :destroy]
before_action :set_recipe, only: [:show, :edit, :update, :destroy]
before_action :ensure_valid_user, except: [:show, :index]
before_action :ensure_valid_user, except: [:show, :scale, :index]
# GET /recipes
def index
@criteria = ViewModels::RecipeCriteria.new(criteria_params)
@recipes = Recipe.for_criteria(@criteria).includes(:tags)
render json: RecipeSummarySerializer.for(@recipes, collection_name: 'recipes')
end
# GET /recipes/1
@ -39,7 +38,6 @@ class RecipesController < ApplicationController
end
end
render json: RecipeSerializer.for(@recipe)
end
# POST /recipes
@ -57,8 +55,7 @@ class RecipesController < ApplicationController
# PATCH/PUT /recipes/1
def update
ensure_owner(@recipe) do
# Merge in updated_at to force the record to be dirty (in case only tags were changed)
if @recipe.update(recipe_params.merge(updated_at: Time.now))
if @recipe.update(recipe_params)
render json: { success: true }
else
render json: @recipe.errors, status: :unprocessable_entity
@ -84,12 +81,12 @@ class RecipesController < ApplicationController
private
# Use callbacks to share common setup or constraints between actions.
def set_recipe
@recipe = Recipe.includes(recipe_ingredients: [{food: :food_units }, {recipe_as_ingredient: {recipe_ingredients: {food: :food_units }}}]).find(params[:id])
@recipe = Recipe.includes(recipe_ingredients: {ingredient: :ingredient_units }).find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def recipe_params
params.require(:recipe).permit(:name, :description, :source, :yields, :total_time, :active_time, :step_text, :is_ingredient, tag_names: [], recipe_ingredients_attributes: [:name, :ingredient_id, :quantity, :units, :preparation, :sort_order, :id, :_destroy])
params.require(:recipe).permit(:name, :description, :source, :yields, :total_time, :active_time, :step_text, tag_names: [], recipe_ingredients_attributes: [:name, :ingredient_id, :quantity, :units, :preparation, :sort_order, :id, :_destroy])
end
def criteria_params

View File

@ -6,12 +6,11 @@ class TagsController < ApplicationController
def prefetch
@tags = Tag.all.order(:name)
render json: TagSerializer.for(@tags)
render :search
end
def search
@tags = Tag.search(params[:query]).order(:name)
render json: TagSerializer.for(@tags)
end
end

View File

@ -1,68 +0,0 @@
class TaskItemsController < ApplicationController
before_action :ensure_valid_user
before_action :set_task_list
before_action :set_task_item, only: [:update]
def create
@task_item = TaskItem.new(task_item_params)
@task_item.task_list = @task_list
if @task_item.save
TaskChannel.update_task_list(@task_list)
render json: TaskItemSerializer.for(@task_item), status: :created, location: [@task_list, @task_item]
else
render json: @task_item.errors, status: :unprocessable_entity
end
end
def update
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]
else
render json: @task_item.errors, status: :unprocessable_entity
end
end
def destroy
ids = Array.wrap(params[:id]) + Array.wrap(params[:ids])
TaskItem.transaction do
@task_items = @task_list.task_items.find(ids)
@task_items.each { |i| i.destroy }
end
TaskChannel.update_task_list(@task_list)
head :no_content
end
def complete
ids = Array.wrap(params[:id]) + Array.wrap(params[:ids])
new_status = !params[:invert].present?
TaskItem.transaction do
@task_items = @task_list.task_items.find(ids)
@task_items.each { |i| i.update_attribute(:completed, new_status) }
end
TaskChannel.update_task_list(@task_list)
head :no_content
end
private
def task_item_params
params.require(:task_item).permit(:name, :quantity, :completed)
end
def set_task_list
@task_list = TaskList.find(params[:task_list_id])
ensure_owner(@task_list)
end
def set_task_item
@task_item = @task_list.task_items.find(params[:id])
end
end

View File

@ -1,66 +0,0 @@
class TaskListsController < ApplicationController
before_action :ensure_valid_user
before_action :set_task_list, only: [:show, :update, :destroy, :add_recipe]
def index
@task_lists = TaskList.for_user(current_user).includes(:task_items).order(created_at: :desc)
render json: TaskListSerializer.for(@task_lists)
end
def show
ensure_owner(@task_list)
render json: TaskListSerializer.for(@task_list)
end
def create
@task_list = TaskList.new(task_list_params)
@task_list.user = current_user
if @task_list.save
render json: TaskListSerializer.for(@task_list), status: :created, location: @task_list
else
render json: @task_list.errors, status: :unprocessable_entity
end
end
def update
ensure_owner(@task_list) do
if @task_list.update(task_list_params)
TaskChannel.update_task_list(@task_list)
render json: TaskListSerializer.for(@task_list), status: :ok, location: @task_list
else
render json: @task_list.errors, status: :unprocessable_entity
end
end
end
def destroy
ensure_owner(@task_list) do
@task_list.destroy
head :no_content
end
end
def add_recipe
ensure_owner(@task_list) do
recipe = Recipe.find(params[:recipe_id])
@task_list.add_recipe_ingredients(recipe)
TaskChannel.update_task_list(@task_list)
head :no_content
end
end
private
def task_list_params
params.require(:task_list).permit(:name)
end
def set_task_list
@task_list = TaskList.find(params[:id])
end
end

View File

@ -1,14 +1,11 @@
class UsersController < ApplicationController
UserProxy = Struct.new(:user_id)
before_action :ensure_valid_user, except: [:show, :login, :verify_login, :new, :create]
skip_before_action :verify_authenticity_token, only: [:verify_login]
def show
if current_user
render json: UserSerializer.for(current_user)
else
render json: nil
end
end
def login
@ -31,7 +28,7 @@ class UsersController < ApplicationController
if user = User.authenticate(params[:username], params[:password])
set_current_user(user)
format.html { redirect_to root_path, notice: "Welcome, #{user.display_name}" }
format.json { render json: { success: true, user: UserSerializer.for(current_user).serialize } }
format.json { render json: { success: true, user: { id: user.id, name: user.display_name, admin: user.admin? } } }
else
format.html { flash[:error] = "Invalid credentials"; render :login }
format.json { render json: { success: false, message: 'Invalid Credentials', user: nil } }
@ -50,7 +47,7 @@ class UsersController < ApplicationController
if @user.save
set_current_user(@user)
format.html { redirect_to root_path, notice: 'User created.' }
format.json { render json: UserSerializer.for(@user), status: :created, location: @user }
format.json { render :show, status: :created, location: @user }
else
format.html { render :new }
format.json { render json: @user.errors, status: :unprocessable_entity }
@ -68,7 +65,7 @@ class UsersController < ApplicationController
respond_to do |format|
if @user.update(user_params)
format.html { redirect_to root_path, notice: 'User updated.' }
format.json { render json: UserSerializer.for(@user) , status: :created, location: @user }
format.json { render :show, status: :created, location: @user }
else
format.html { render :edit }
format.json { render json: @user.errors, status: :unprocessable_entity }

View File

@ -1,4 +0,0 @@
module FoodsHelper
end

View File

@ -0,0 +1,4 @@
module IngredientsHelper
end

View File

@ -1,67 +1,77 @@
<template>
<div id="app">
<app-progress-bar></app-progress-bar>
<div>
<vue-progress-bar></vue-progress-bar>
<app-navbar></app-navbar>
<section id="main" class="">
<section id="app" class="">
<div class="container">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component v-if="!hasError" :is="Component" />
<div v-else>
<h1>Error!</h1>
<p>{{ appConfig.error }}</p>
</div>
</transition>
</router-view>
<router-view v-if="!hasError"></router-view>
<div v-else>
<h1>Error!</h1>
<p>{{error}}</p>
</div>
</div>
</section>
</div>
</template>
<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";
<script>
const globalTweenGroup = useGlobalTweenGroup();
let animationLoop = true;
import { mapMutations, mapState } from "vuex";
import api from "../lib/Api";
const appConfig = useAppConfigStore();
const hasError = computed(() => appConfig.error !== null);
export default {
data() {
return {
api: api
};
},
computed: {
...mapState({
hasError: state => state.error !== null,
error: state => state.error,
authChecked: state => state.authChecked
})
},
const { loadResource } = useLoadResource();
const { checkAuthentication } = useCheckAuthentication(loadResource);
watch(
() => appConfig.initialLoad,
(val) => {
watch: {
isLoading(val) {
if (val) {
nextTick(() => document.body.classList.remove("loading"));
this.$Progress.start();
} else {
this.$Progress.finish();
}
},
{ immediate: true }
);
onMounted(() => {
// Setup global animation loop
function animate() {
if (animationLoop) {
globalTweenGroup.update();
requestAnimationFrame(animate);
}
}
animate();
},
if (appConfig.user === null && appConfig.authChecked === false) {
checkAuthentication();
}
});
created() {
if (this.user === null && this.authChecked === false) {
this.checkAuthentication();
}
onUnmounted(() => {
animationLoop = false;
});
// Hard coded values taken directly from Bulma css
const mediaQueries = {
mobile: "screen and (max-width: 768px)",
tablet: "screen and (min-width: 769px)",
tabletOnly: "screen and (min-width: 769px) and (max-width: 1023px)",
touch: "screen and (max-width: 1023px)",
desktop: "screen and (min-width: 1024px)",
desktopOnly: "screen and (min-width: 1024px) and (max-width: 1215px)",
widescreen: "screen and (min-width: 1216px)",
widescreenOnly: "screen and (min-width: 1216px) and (max-width: 1407px)",
fullhd: "screen and (min-width: 1408px)"
};
for (let device in mediaQueries) {
const query = window.matchMedia(mediaQueries[device]);
query.onchange = (q) => {
this.$store.commit("setMediaQuery", {mediaName: device, value: q.matches});
};
query.onchange(query);
}
},
components: {
}
}
</script>

View File

@ -27,190 +27,199 @@
</div>
</template>
<script setup>
<script>
import { computed, ref, watch } from "vue";
import debounce from 'lodash/debounce';
const emit = defineEmits(["update:modelValue", "inputClick", "optionSelected"]);
export default {
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
},
const props = defineProps({
modelValue: 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,
labelAttribute: String,
onGetOptions: Function,
searchOptions: Array
},
valueAttribute: String,
labelAttribute: String,
keyAttribute: String,
data() {
return {
options: [],
rawValue: "",
isListOpen: false,
activeListIndex: 0
}
},
onGetOptions: Function,
searchOptions: Array
});
created() {
this.rawValue = this.value;
const options = ref([]);
const rawValue = ref("");
const isListOpen = ref(false);
const activeListIndex = ref(0);
},
const finalInputClass = computed(() => {
let cls = ['input'];
if (props.inputClass === null) {
return cls;
} else if (Array.isArray(props.inputClass)) {
return cls.concat(props.inputClass);
} else {
cls.push(props.inputClass);
return cls;
}
});
watch: {
value(newValue) {
this.rawValue = newValue;
}
},
watch(
() => props.modelValue,
(newValue) => { rawValue.value = newValue; },
{ immediate: true }
);
computed: {
finalInputClass() {
let cls = ['input'];
if (this.inputClass === null) {
return cls;
} else if (Array.isArray(this.inputClass)) {
return cls.concat(this.inputClass);
} else {
cls.push(this.inputClass);
return cls;
}
},
function optionClass(idx) {
return activeListIndex.value === idx ? 'option active' : 'option';
}
debouncedUpdateOptions() {
return debounce(this.updateOptions, this.debounce);
}
},
function optionClick(opt) {
selectOption(opt);
}
methods: {
optionClass(idx) {
return this.activeListIndex === idx ? 'option active' : 'option';
},
function optionKey(opt) {
if (props.keyAttribute) {
return opt[props.keyAttribute]
} else if (props.valueAttribute) {
return opt[props.valueAttribute];
} else {
return opt.toString();
}
}
optionClick(opt) {
this.selectOption(opt);
},
function optionValue(opt) {
if (props.valueAttribute) {
return opt[props.valueAttribute];
} else {
return opt.toString();
}
}
optionKey(opt) {
if (this.valueAttribute) {
return opt[this.valueAttribute];
} else {
return opt.toString();
}
},
function optionLabel(opt) {
if (props.labelAttribute) {
return opt[props.labelAttribute];
} else {
return null;
}
}
optionValue(opt) {
return this.optionKey(opt);
},
function optionMousemove(idx) {
activeListIndex.value = idx;
}
optionLabel(opt) {
if (this.labelAttribute) {
return opt[this.labelAttribute];
} else {
return null;
}
},
function clickHandler(evt) {
emit('inputClick', evt);
}
optionMousemove(idx) {
this.activeListIndex = idx;
},
function blurHandler(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);
}
clickHandler(evt) {
this.$emit("inputClick", evt);
},
function inputHandler(evt) {
const newValue = evt.target.value;
blurHandler(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(() => {
this.isListOpen = false;
},250);
},
if (rawValue.value !== newValue) {
inputHandler(evt) {
const newValue = evt.target.value;
rawValue.value = newValue;
if (this.rawValue !== newValue) {
emit("update:modelValue", newValue);
this.rawValue = newValue;
if (newValue.length >= Math.max(1, props.minLength)) {
updateOptions(newValue);
} else {
isListOpen.value = false;
this.$emit("input", newValue);
if (newValue.length >= Math.max(1, this.minLength)) {
this.debouncedUpdateOptions(newValue);
} 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>
<style lang="scss" scoped>
@use "bulma/sass/utilities" as bulma;
@import "../styles/variables";
$labelLineHeight: 0.8rem;
input.input {
&::placeholder {
color: bulma.$grey-darker;
color: $grey-darker;
}
}
@ -235,7 +244,7 @@
&.active {
color: white;
background-color: bulma.$turquoise;
background-color: $turquoise;
}
.opt_value {

View File

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

View File

@ -1,34 +1,38 @@
<template>
<app-text-field :value="stringValue" @update:modelValue="input" :label="label" type="date"></app-text-field>
<app-text-field :value="stringValue" @input="input" :label="label" type="date"></app-text-field>
</template>
<script setup>
<script>
import { computed } from "vue";
import DateTimeUtils from "../lib/DateTimeUtils";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
modelValue: {
required: false,
type: [Date, String]
export default {
props: {
value: {
required: false,
type: [Date, String]
},
label: {
required: false,
type: String,
default: null
}
},
label: {
required: false,
type: String,
default: null
computed: {
stringValue() {
const d = DateTimeUtils.toDate(this.value);
return DateTimeUtils.formatDateForEdit(d);
}
},
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>

View File

@ -5,49 +5,57 @@
</span>
</template>
<script setup>
<script>
import { computed } from "vue";
import DateTimeUtils from "../lib/DateTimeUtils";
const props = defineProps({
dateTime: {
required: true,
type: [Date, String]
export default {
props: {
dateTime: {
required: true,
type: [Date, String]
},
showDate: {
required: false,
type: Boolean,
default: true
},
showTime: {
required: false,
type: Boolean,
default: true
},
useInput: {
required: false,
type: Boolean,
default: false
}
},
showDate: {
required: false,
type: Boolean,
default: true
},
computed: {
dateObj() {
return DateTimeUtils.toDate(this.dateTime);
},
showTime: {
required: false,
type: Boolean,
default: true
},
friendlyString() {
const parts = [];
if (this.showDate) {
parts.push(DateTimeUtils.formatDate(this.dateObj));
}
if (this.showTime) {
parts.push(DateTimeUtils.formatTime(this.dateObj, true));
}
return parts.join(" ");
},
useInput: {
required: false,
type: Boolean,
default: false
fullString() {
return DateTimeUtils.formatTimestamp(this.dateObj);
}
}
});
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>

View File

@ -1,81 +0,0 @@
<template>
<div ref="dropdown" class="dropdown" :class="{'is-active': open, 'is-hoverable': hover}">
<div class="dropdown-trigger">
<slot name="button">
<button type="button" class="button" :class="buttonClass" @click="toggle">
<span>{{ label }}</span>
<app-icon icon="caret-bottom" size="xs"></app-icon>
</button>
</slot>
</div>
<div class="dropdown-menu">
<div class="dropdown-content">
<slot>
Default Content
</slot>
</div>
</div>
</div>
</template>
<script setup>
import { useTemplateRef } from "vue";
import { onClickOutside } from '@vueuse/core'
const emit = defineEmits(["close", "open"]);
const props = defineProps({
open: {
required: false,
type: Boolean,
default: false
},
hover: {
required: false,
type: Boolean,
default: false
},
label: {
required: false,
type: String,
default: 'Select'
},
buttonClass: {
required: false,
default: ""
}
});
const dropdownElement = useTemplateRef("dropdown");
onClickOutside(dropdownElement, event => handleOutsideClick(event))
function toggle() {
if (props.open) {
triggerClose();
} else {
triggerOpen();
}
}
function triggerOpen() {
emit("open");
}
function triggerClose() {
emit("close");
}
function handleOutsideClick(evt) {
if (props.open) {
if (!dropdownElement.value.contains(evt.target)) {
triggerClose();
}
}
}
</script>

View File

@ -1,104 +0,0 @@
<template>
<transition
name="expand"
@enter="enter"
@leave="leave"
@enter-cancel="cancel"
@leave-cancel="cancel">
<slot></slot>
</transition>
</template>
<script setup>
import TWEEN from '@tweenjs/tween.js';
import { useGlobalTweenGroup } from "../lib/useGlobalTweenGroup";
const props = defineProps({
expandTime: {
type: Number,
default: 250
}
});
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>
<style scoped>
* {
will-change: height;
transform: translateZ(0);
backface-visibility: hidden;
perspective: 1000px;
}
</style>

View File

@ -1,57 +1,49 @@
<template>
<span class="icon" :class="iconClasses" @click="$emit('click', $event)">
<app-iconic-icon :icon="iconicIcon" :size="iconicSize" v-bind="iconicAttributes"></app-iconic-icon>
<span class="icon" :class="sizeClass" @click="$emit('click', $event)">
<svg v-html="svgContent" v-bind="svgAttributes"></svg>
</span>
</template>
<script>
import { computed } from "vue";
class IconData {
constructor(iconicIcon, dataAttributes) {
this.iconicIcon = iconicIcon;
this.dataAttributes = dataAttributes || {};
}
}
class SizeData {
constructor(bulmaIconClass, iconicSize, customClass) {
this.bulmaIconClass = bulmaIconClass;
this.iconicSize = iconicSize || null;
this.customIconClass = customClass || null;
}
}
import CaretBottom from "open-iconic/svg/caret-bottom";
import CaretTop from "open-iconic/svg/caret-top";
import Check from "open-iconic/svg/check";
import CircleCheck from "open-iconic/svg/circle-check.svg";
import LinkBroken from "open-iconic/svg/link-broken";
import LinkIntact from "open-iconic/svg/link-intact";
import LockLocked from "open-iconic/svg/lock-locked";
import LockUnlocked from "open-iconic/svg/lock-unlocked";
import Person from "open-iconic/svg/person";
import Pencil from "open-iconic/svg/pencil";
import Plus from "open-iconic/svg/plus.svg";
import Star from "open-iconic/svg/star";
import X from "open-iconic/svg/x";
const iconMap = {
'caret-bottom': new IconData('caret', {'data-direction': 'bottom'}),
'caret-top': new IconData('caret', {'data-direction': 'top'}),
check: new IconData('check'),
'circle-check': new IconData('circle-check'),
'link-broken': new IconData('link', {'data-state': 'broken'}),
'link-intact': new IconData('link', {'data-state': 'intact'}),
'lock-locked': new IconData('lock', {'data-state': 'locked'}),
'lock-unlocked': new IconData('lock', {'data-state': 'unlocked'}),
menu: new IconData('menu'),
pencil: new IconData('pencil'),
person: new IconData('person'),
'question-mark': new IconData('question-mark'),
star: new IconData('star'),
'star-empty': new IconData('star-empty'),
warning: new IconData('warning'),
x: new IconData('x')
'caret-bottom': CaretBottom,
'caret-top': CaretTop,
check: Check,
'circle-check': CircleCheck,
'link-broken': LinkBroken,
'link-intact': LinkIntact,
'lock-locked': LockLocked,
'lock-unlocked': LockUnlocked,
pencil: Pencil,
person: Person,
plus: Plus,
star: Star,
x: X
};
const sizeMap = {
xs: new SizeData('is-small', 'sm', 'is-xs'),
sm: new SizeData('is-small', 'sm'),
md: new SizeData('', 'sm', 'is-md'),
lg: new SizeData('is-medium', 'md'),
xl: new SizeData('is-large', 'md', 'is-xl')
sm: 'is-small',
md: '' ,
lg: 'is-medium',
xl: 'is-large'
};
export default {
emits: ["click"],
props: {
icon: {
validator: (i) => iconMap[i] !== undefined
@ -61,29 +53,31 @@
type: String,
validator: (s) => sizeMap[s] !== undefined,
default: 'md'
},
padding: {
type: String,
required: false,
default: null
}
},
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);
computed: {
svgObj() {
return iconMap[this.icon];
},
svgAttributes() {
const attrs = {
class: this.size
};
return {
iconClasses,
iconData,
sizeData,
iconicAttributes,
iconicIcon,
iconicSize
for (let a of ['viewBox', 'xmlns']) {
if (this.svgObj.attributes[a]) {
attrs[a] = this.svgObj.attributes[a];
}
}
return attrs;
},
svgContent() {
return this.svgObj.content;
},
sizeClass() {
return sizeMap[this.size];
}
}
}
@ -92,41 +86,38 @@
<style lang="scss" scoped>
span.icon.is-xs {
svg.iconic {
width: 8px;
height: 8px;
.icon {
svg {
width: 100%;
height: 100%;
fill: currentColor;
&.sm {
width: 0.6em;
height: 0.6em;
}
&.md {
width: 1em;
height: 1em;
}
&.lg {
width: 1.33em;
height: 1.33em;
}
&.xl {
width: 2em;
height: 2em;
}
/*&.xl {*/
/*width: 3em;*/
/*height: 3em;*/
/*}*/
}
}
span.icon.is-sm {
svg.iconic {
width: 12px;
height: 12px;
}
}
span.icon.is-md {
svg.iconic {
width: 16px;
height: 16px;
}
}
span.icon.is-lg {
svg.iconic {
width: 32px;
height: 32px;
}
}
span.icon.is-xl {
svg.iconic {
width: 48px;
height: 48px;
}
}
</style>

View File

@ -1,156 +0,0 @@
<template>
<svg ref="svg" v-bind="svgAttributes" v-html="svgContent" :class="calculatedClasses"></svg>
</template>
<script>
import { computed, nextTick, onMounted, onUpdated, useTemplateRef } from "vue";
import Caret from "../iconic/svg/smart/caret";
import Check from "../iconic/svg/smart/check";
import CircleCheck from "../iconic/svg/smart/circle-check";
import Link from "../iconic/svg/smart/link";
import Lock from "../iconic/svg/smart/lock";
import Menu from "../iconic/svg/smart/menu";
import QuestionMark from "../iconic/svg/smart/question-mark.svg"
import Person from "../iconic/svg/smart/person";
import Pencil from "../iconic/svg/smart/pencil";
import Star from "../iconic/svg/smart/star";
import StarEmpty from "../iconic/svg/smart/star-empty";
import Warning from "../iconic/svg/smart/warning";
import X from "../iconic/svg/smart/x";
const APIS = {};
const LOADED_APIS = {};
window._Iconic = {
smartIconApis: APIS
};
let globalIdCounter = 0;
const iconMap = {
caret: Caret,
check: Check,
'circle-check': CircleCheck,
link: Link,
lock: Lock,
menu: Menu,
pencil: Pencil,
person: Person,
'question-mark': QuestionMark,
star: Star,
'star-empty': StarEmpty,
warning: Warning,
x: X
};
export default {
props: {
icon: {
type: String,
required: true,
validator: (i) => iconMap[i] !== undefined
},
size: {
type: String,
default: "lg",
validator: (s) => ["sm", "md", "lg"].indexOf(s) >= 0
},
iconSizeOverride: {
type: String,
validator: (s) => ["sm", "md", "lg", null].indexOf(s) >= 0
},
displaySizeOverride: {
type: String,
validator: (s) => ["sm", "md", "lg", null].indexOf(s) >= 0
}
},
setup(props) {
const svgElement = useTemplateRef("svg");
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);
for (let idRep of svgData.value.idReplacements) {
let newId = `__new_id_${globalIdCounter}`;
globalIdCounter += 1;
content = content.replace(new RegExp(idRep, "g"), newId);
}
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;
}
}
function setupSvgApi(name) {
const apis = APIS;
if (apis && apis[name]) {
const iconApi = apis[name](svgElement.value);
for (let func in iconApi) svgElement.value[func] = iconApi[func]
} else {
svgElement.value.update = function() {}
}
svgElement.value.update();
}
function updateScripts() {
ensureSvgApi(svgName.value, svgData.value.scriptBlocks);
setupSvgApi(svgName.value);
}
onMounted(() => {
updateScripts();
});
onUpdated(() => {
updateScripts();
});
return {
svgData,
svgAttributes,
svgName,
svgContent,
calculatedClasses
};
}
}
</script>

View File

@ -1,17 +0,0 @@
<template>
<div class="app-loading">
Imagine I'm a spinner...
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.app-loading {
position: absolute;
display: none;
}
</style>

View File

@ -1,60 +1,68 @@
<template>
<Teleport to="body">
<div :class="['modal', { 'is-wide': wide, 'is-active': open && error === null }]">
<div ref="container">
<div ref="modal" :class="['popup', 'modal', { 'is-wide': wide, 'is-active': open && error === null }]">
<div class="modal-background" @click="close"></div>
<div class="modal-card">
<header class="modal-card-head">
<slot name="title">
<p class="modal-card-title">{{ title }}</p>
<app-icon class="close-button" icon="x" aria-label="close" @click="close"></app-icon>
<app-icon icon="x" aria-label="close" @click="close"></app-icon>
</slot>
</header>
<section class="modal-card-body">
<slot></slot>
</section>
<footer class="modal-card-foot">
<slot name="footer">
</slot>
</footer>
</div>
</div>
</Teleport>
</div>
</template>
<script setup>
<script>
import { computed } from "vue";
import { useAppConfigStore } from "../stores/appConfig";
import { mapState } from "vuex";
const emit = defineEmits(["dismiss"]);
const props = defineProps({
open: {
type: Boolean,
default: false
export default {
props: {
open: {
type: Boolean,
default: false
},
title: String,
wide: {
type: Boolean,
default: false
}
},
title: String,
wide: {
type: Boolean,
default: false
mounted() {
document.body.appendChild(this.$refs.modal);
},
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>
<style lang="scss" scoped>
.close-button {
cursor: pointer;
}
</style>

View File

@ -15,11 +15,10 @@
<div class="navbar-start">
<a class="navbar-item" v-if="updateAvailable" href="#" @click.prevent="updateApp">UPDATE AVAILABLE!</a>
<router-link to="/" class="navbar-item">Recipes</router-link>
<router-link to="/foods" class="navbar-item">Ingredients</router-link>
<router-link to="/ingredients" class="navbar-item">Ingredients</router-link>
<router-link to="/calculator" class="navbar-item">Calculator</router-link>
<router-link v-if="isLoggedIn" to="/logs" class="navbar-item">Log</router-link>
<router-link v-if="isLoggedIn" to="/notes" class="navbar-item">Notes</router-link>
<router-link v-if="isLoggedIn" to="/tasks" class="navbar-item">Tasks</router-link>
<router-link to="/about" class="navbar-item">About</router-link>
<router-link v-if="isAdmin" to="/admin/users" class="navbar-item">Admin</router-link>
@ -51,27 +50,46 @@
</nav>
</template>
<script setup>
<script>
import { ref, watch } from "vue";
import UserLogin from "./UserLogin";
import { storeToRefs } from "pinia";
import { useAppConfigStore } from "../stores/appConfig";
import { mapState } from "vuex";
import { swUpdate } from "../lib/ServiceWorker";
import { useRoute } from "vue-router";
const appConfig = useAppConfigStore();
const menuActive = ref(false);
const route = useRoute();
const { isAdmin, isLoggedIn, updateAvailable, user } = storeToRefs(appConfig);
export default {
data() {
return {
menuActive: false
};
},
function updateApp() {
swUpdate();
computed: {
...mapState([
'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>

View File

@ -1,7 +1,7 @@
<template>
<nav v-show="totalPages > 1 || showWithSinglePage" class="pagination" role="navigation" :aria-label="pagedItemName + ' page navigation'">
<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': true, 'is-disabled': isLastPage}" :title="isLastPage ? 'This is the last page' : ''" @click.prevent="changePage(currentPage + 1)">Next page</a>
<nav v-show="totalPages > 1" 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-next" :title="isLastPage ? 'This is the last page' : ''" :disabled="isLastPage" @click.prevent="changePage(currentPage + 1)">Next page</a>
<ul class="pagination-list">
<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>
@ -11,81 +11,90 @@
</nav>
</template>
<script setup>
<script>
import { computed } from "vue";
export default {
props: {
pagedItemName: {
required: false,
type: String,
default: ''
},
const emit = defineEmits(["changePage"]);
currentPage: {
required: true,
type: Number
},
const props = defineProps({
pagedItemName: {
required: false,
type: String,
default: ''
},
totalPages: {
required: true,
type: Number
},
currentPage: {
required: true,
type: Number
},
pageWindow: {
required: false,
type: Number,
default: 4
},
totalPages: {
required: true,
type: Number
},
pageOuterWindow: {
required: false,
type: Number,
default: 1
}
},
pageWindow: {
required: false,
type: Number,
default: 4
},
computed: {
pageItems() {
const items = new Set();
pageOuterWindow: {
required: false,
type: Number,
default: 1
},
for (let x = 0; x < this.pageOuterWindow; x++) {
items.add(x + 1);
items.add(this.totalPages - x);
}
showWithSinglePage: {
required: false,
type: Boolean,
default: false
}
});
const start = this.currentPage - Math.ceil(this.pageWindow / 2);
const end = this.currentPage + Math.floor(this.pageWindow / 2);
const pageItems = computed(() => {
const items = new Set();
for (let x = start; x <= end; x++) {
items.add(x);
}
for (let x = 0; x < props.pageOuterWindow; x++) {
items.add(x + 1);
items.add(props.totalPages - x);
}
let emptySpace = -1;
const finalList = [];
const start = props.currentPage - Math.ceil(props.pageWindow / 2);
const end = props.currentPage + Math.floor(props.pageWindow / 2);
[...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--);
}
});
for (let x = start; x <= end; x++) {
items.add(x);
}
return finalList;
},
let emptySpace = -1;
const finalList = [];
isLastPage() {
return this.currentPage === this.totalPages;
},
[...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--);
isFirstPage() {
return this.currentPage === 1;
}
},
methods: {
changePage(idx) {
this.$emit("changePage", idx);
}
}
});
return finalList;
});
const isLastPage = computed(() => props.currentPage === props.totalPages);
const isFirstPage = computed(() => props.currentPage === 1);
function changePage(idx) {
emit("changePage", idx);
}
}
</script>
<style lang="scss" scoped>
ul.pagination {
}
</style>

View File

@ -1,69 +0,0 @@
<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>

View File

@ -1,99 +1,111 @@
<template>
<span ref="wrapper" class="rating" @click="handleClick" @mousemove="handleMousemove" @mouseleave="handleMouseleave">
<span class="set empty-set">
<app-iconic-icon v-for="i in starCount" :key="i" icon="star-empty" size="md"></app-iconic-icon>
<app-icon v-for="i in starCount" :key="i" icon="star"></app-icon>
</span>
<span class="set filled-set" :style="filledStyle">
<app-iconic-icon v-for="i in starCount" :key="i" icon="star" size="md"></app-iconic-icon>
<app-icon v-for="i in starCount" :key="i" icon="star"></app-icon>
</span>
</span>
</template>
<script setup>
<script>
import { computed, ref, useTemplateRef } from "vue";
const emit = defineEmits(["update:modelValue"]);
export default {
props: {
starCount: {
required: false,
type: Number,
default: 5
},
const props = defineProps({
starCount: {
required: false,
type: Number,
default: 5
},
readonly: {
required: false,
type: Boolean,
default: false
},
readonly: {
required: false,
type: Boolean,
default: false
},
step: {
required: false,
type: Number,
default: 0.5
},
step: {
required: false,
type: Number,
default: 0.5
},
value: {
required: false,
type: Number,
default: 0
}
},
modelValue: {
required: false,
type: Number,
default: 0
data() {
return {
temporaryValue: null
};
},
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>
<style lang="scss" scoped>
@use "bulma/sass/utilities" as bulma;
@import "../styles/variables";
span.rating {
position: relative;
@ -101,11 +113,6 @@ function handleMouseleave(evt) {
.set {
white-space: nowrap;
svg.iconic {
width: 1.5em;
height: 1.5em;
}
}
.empty-set {
@ -113,7 +120,7 @@ function handleMouseleave(evt) {
}
.filled-set {
color: bulma.$yellow;
color: $yellow;
position: absolute;
top: 0;
left: 0;

View File

@ -1,51 +0,0 @@
<template>
<div class="field">
<div class="control">
<input type="text" class="input" :placeholder="placeholder" :value="text === null ? '' : text" @input="userUpdateText($event.target.value)">
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import debounce from "lodash/debounce";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
placeholder: {
required: false,
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 })
function userUpdateText(newText) {
if (text.value !== newText) {
text.value = newText;
triggerInput();
}
}
function propUpdateText(newText) {
if (text.value === null && text.value !== newText) {
text.value = newText;
}
}
</script>

View File

@ -2,71 +2,84 @@
<div class="tag-editor control">
<input ref="input" type="text" class="input" :value="tagText" @input="inputHandler" @focus="getFocus" @blur="loseFocus">
<div class="tags">
<span v-for="t in modelValue" :key="t" class="tag">{{t}}</span>
<span v-for="t in value" :key="t" class="tag">{{t}}</span>
</div>
</div>
</template>
<script setup>
<script>
import {computed, nextTick, ref, useTemplateRef} from "vue";
export default {
props: {
value: {
required: true,
type: Array
}
},
const emit = defineEmits(["update:modelValue"]);
data() {
return {
hasFocus: false
};
},
const props = defineProps({
modelValue: {
required: true,
type: Array
}
});
computed: {
tagText() {
return this.value.join(" ");
}
},
const hasFocus = ref(false);
const tagText = computed(() => props.modelValue.join(" "));
const inputElement = useTemplateRef("input");
watch: {
},
function inputHandler(el) {
let str = el.target.value;
checkInput(str);
nextTick(() => {
el.target.value = str;
});
}
methods: {
inputHandler(el) {
let str = el.target.value;
this.checkInput(str);
this.$nextTick(() => {
el.target.value = str;
});
},
function checkInput(str) {
if (hasFocus.value) {
const m = str.match(/\S\s+\S*$/);
checkInput(str) {
if (this.hasFocus) {
const m = str.match(/\S\s+\S*$/);
if (m !== null) {
str = str.substring(0, m.index + 1);
} else {
str = "";
if (m !== null) {
str = str.substring(0, m.index + 1);
} else {
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>

View File

@ -1,78 +1,46 @@
<template>
<div class="field">
<label v-if="label.length" class="label is-small-mobile">{{ label }}</label>
<div :class="controlClasses">
<textarea v-if="isTextarea" :class="inputClasses" v-model="model" :disabled="disabled"></textarea>
<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>
<div class="control">
<textarea v-if="isTextarea" class="textarea is-small-mobile" :value="value" @input="input"></textarea>
<input v-else :type="type" class="input is-small-mobile" :value="value" @input="input">
</div>
<p v-if="helpMessage !== null" :class="helpClasses">
{{ helpMessage }}
</p>
</div>
</template>
<script setup>
<script>
import { computed } from "vue";
export default {
props: {
label: {
required: false,
type: String,
default: ""
},
value: {
required: false,
type: [String, Number],
default: ""
},
type: {
required: false,
type: String,
default: "text"
}
},
const props = defineProps({
label: {
required: false,
type: String,
default: ""
},
modelValue: {
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: {
isTextarea() {
return this.type === "textarea";
}
},
const model = defineModel({
type: [String, Number],
default: ""
});
const isTextarea = computed(() => props.type === "textarea");
const controlClasses = computed(() => [
"control",
{
"has-icons-right": props.validationError !== null
methods: {
input(evt) {
this.$emit("input", evt.target.value);
}
}
]);
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>

View File

@ -6,14 +6,16 @@
</div>
</template>
<script setup>
<script>
const props = defineProps({
errors: {
required: false,
type: Object,
default: {}
export default {
props: {
errors: {
required: false,
type: Object,
default: {}
}
}
}
});
</script>

View File

@ -1,74 +0,0 @@
<template>
<div>
<h3 class="title">
{{food.name}}
</h3>
<div class="message" v-if="food.ndbn">
<div class="message-header">
<span>USDA NDBN #{{ food.ndbn }}</span>
</div>
<div class="message-body">
<a :href="'https://ndb.nal.usda.gov/ndb/foods/show/' + food.ndbn">USDA DB Entry</a>
</div>
</div>
<div class="message">
<div class="message-header">
Custom Units
</div>
<div class="message-body">
<ul>
<li v-for="fu in food.food_units" :key="fu.id">
{{fu.name}}: {{fu.gram_weight}} grams
</li>
</ul>
</div>
</div>
<div class="message">
<div class="message-header">
Nutrition per 100 grams
</div>
<div class="message-body">
<div class="columns is-mobile is-multiline">
<div v-for="(nutrient, name) in nutrients" :key="name" class="column is-half-mobile is-one-third-tablet">
<label class="label is-small-mobile">{{nutrient.label}}</label>
<div class="field has-addons">
<div class="control is-expanded">
<input type="text" class="input is-small-mobile" disabled="true" v-model="food[name]">
</div>
<div class="control">
<button type="button" tabindex="-1" class="unit-label button is-static is-small-mobile">{{nutrient.unit}}</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from "vue";
import { useNutrientStore } from "../stores/nutrient";
const props = defineProps({
food: {
required: true,
type: Object
}
});
const nutrientStore = useNutrientStore();
const nutrients = computed(() => nutrientStore.nutrientList);
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,13 +1,13 @@
<template>
<div>
<h1 class="title">{{action}} {{food.name || "[Unnamed Food]"}}</h1>
<h1 class="title">{{action}} {{ingredient.name || "[Unnamed Ingredient]"}}</h1>
<app-validation-errors :errors="validationErrors"></app-validation-errors>
<div class="field">
<label class="label is-small-mobile">Name</label>
<div class="control">
<input type="text" class="input is-small-mobile" v-model="food.name">
<input type="text" class="input is-small-mobile" v-model="ingredient.name">
</div>
</div>
@ -15,17 +15,16 @@
<label class="label is-small-mobile">Nutrient Databank Number</label>
<div class="field has-addons">
<div class="control">
<button type="button" class="button" :class="{'is-primary': hasNdbn}"><app-icon :icon="hasNdbn ? 'link-intact' : 'link-broken'" size="sm"></app-icon><span>{{food.ndbn}}</span></button>
<button type="button" class="button" :class="{'is-primary': hasNdbn}"><app-icon :icon="hasNdbn ? 'link-intact' : 'link-broken'" size="sm"></app-icon><span>{{ingredient.ndbn}}</span></button>
</div>
<div class="control is-expanded">
<app-autocomplete
:inputClass="'is-small-mobile'"
ref="autocomplete"
v-model="food.usda_food_name"
v-model="ingredient.usda_food_name"
:minLength="2"
valueAttribute="name"
labelAttribute="description"
key-attribute="ndbn"
placeholder=""
@optionSelected="searchItemSelected"
:onGetOptions="updateSearchItems"
@ -40,14 +39,14 @@
<div class="field">
<label class="label is-small-mobile">Density</label>
<div class="control">
<input type="text" class="input is-small-mobile" v-model="food.density">
<input type="text" class="input is-small-mobile" v-model="ingredient.density">
</div>
</div>
<div class="field">
<label class="label is-small-mobile">Notes</label>
<div class="control">
<textarea type="text" class="textarea is-small-mobile" v-model="food.notes"></textarea>
<textarea type="text" class="textarea is-small-mobile" v-model="ingredient.notes"></textarea>
</div>
</div>
@ -62,15 +61,12 @@
<button class="button" type="button" @click="addUnit">Add Unit</button>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Grams</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="unit in visibleFoodUnits" :key="unit.id">
<tr v-for="unit in visibleIngredientUnits" :key="unit.id">
<td>
<div class="control">
<input type="text" class="input is-small-mobile" v-model="unit.name">
@ -85,7 +81,6 @@
<button type="button" class="button is-danger" @click="removeUnit(unit)">X</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
@ -100,18 +95,14 @@
<div class="message-body">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Grams</th>
</tr>
</thead>
<tbody>
<tr v-for="unit in food.ndbn_units">
<tr v-for="unit in ingredient.ndbn_units">
<td>{{unit.description}}</td>
<td>{{unit.gram_weight}}</td>
</tr>
</tbody>
</table>
</div>
@ -130,7 +121,7 @@
<label class="label is-small-mobile">{{nutrient.label}}</label>
<div class="field has-addons">
<div class="control is-expanded">
<input type="text" class="input is-small-mobile" :disabled="hasNdbn" v-model="food[name]">
<input type="text" class="input is-small-mobile" :disabled="hasNdbn" v-model="ingredient[name]">
</div>
<div class="control">
<button type="button" tabindex="-1" class="unit-label button is-static is-small-mobile">{{nutrient.unit}}</button>
@ -144,82 +135,121 @@
</div>
</template>
<script setup>
<script>
import { computed } from "vue";
import api from "../lib/Api";
import { mapState } from "pinia";
import { useNutrientStore } from "../stores/nutrient";
import { useLoadResource } from "../lib/useLoadResource";
const nutrientStore = useNutrientStore();
const nutrients = computed(() => nutrientStore.nutrientList);
const { loadResource } = useLoadResource();
const props = defineProps({
food: {
required: true,
type: Object
export default {
props: {
ingredient: {
required: true,
type: Object
},
validationErrors: {
required: false,
type: Object,
default: {}
},
action: {
required: false,
type: String,
default: "Editing"
}
},
validationErrors: {
required: false,
type: Object,
default: {}
data() {
return {
nutrients: {
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" }
}
};
},
action: {
required: false,
type: String,
default: "Editing"
computed: {
visibleIngredientUnits() {
return this.ingredient.ingredient_units.filter(iu => iu._destroy !== true);
},
hasNdbn() {
return this.ingredient.ndbn !== null;
}
},
methods: {
addUnit() {
this.ingredient.ingredient_units.push({
id: null,
name: null,
gram_weight: null
});
},
removeUnit(unit) {
if (unit.id) {
unit._destroy = true;
} else {
const idx = this.ingredient.ingredient_units.findIndex(i => i === unit);
this.ingredient.ingredient_units.splice(idx, 1);
}
},
removeNdbn() {
this.ingredient.ndbn = null;
this.ingredient.usda_food_name = null;
this.ingredient.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.ingredient.ndbn = food.ndbn;
this.ingredient.usda_food_name = food.name;
this.ingredient.ndbn_units = [];
this.loadResource(
api.postIngredientSelectNdbn(this.ingredient)
.then(i => Object.assign(this.ingredient, 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>

View File

@ -0,0 +1,21 @@
<template>
<div>
{{ingredient.name}}
</div>
</template>
<script>
export default {
props: {
ingredient: {
required: true,
type: Object
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -31,21 +31,27 @@
</template>
<script setup>
<script>
import RecipeEdit from "./RecipeEdit";
const props = defineProps({
log: {
required: true,
type: Object
export default {
props: {
log: {
required: true,
type: Object
},
validationErrors: {
required: false,
type: Object,
default: {}
},
},
validationErrors: {
required: false,
type: Object,
default: {}
},
});
components: {
RecipeEdit
}
}
</script>

View File

@ -21,7 +21,7 @@
</div>
<div class="field-body">
<div class="field">
<app-rating readonly :model-value="log.rating"></app-rating>
<app-rating readonly :value="log.rating"></app-rating>
</div>
</div>
</div>
@ -44,16 +44,22 @@
</div>
</template>
<script setup>
<script>
import RecipeShow from "./RecipeShow";
const props = defineProps({
log: {
required: true,
type: Object
export default {
props: {
log: {
required: true,
type: Object
}
},
components: {
RecipeShow
}
});
}
</script>

View File

@ -19,27 +19,37 @@
</div>
</template>
<script setup>
<script>
import { computed } from "vue";
export default {
props: {
note: {
required: true,
type: Object
}
},
const emit = defineEmits(["save", "cancel"]);
const props = defineProps({
note: {
required: true,
type: Object
data() {
return {
};
},
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>

View File

@ -1,5 +1,5 @@
<template>
<div class="recipe-edit">
<div>
<template v-if="!forLogging">
<div class="columns">
@ -35,11 +35,13 @@
</div>
</div>
<h3 class="title is-4">Ingredients</h3>
<recipe-edit-ingredient-editor :ingredients="recipe.ingredients"></recipe-edit-ingredient-editor>
<br>
<div class="field">
<label class="label title is-4">Directions <button @click="isDescriptionHelpOpen = true" class="button is-small is-link"><app-icon icon="question-mark"></app-icon></button></label>
<label class="label title is-4">Directions</label>
<div class="control columns">
<div class="column">
<textarea ref="step_text_area" class="textarea directions-input" v-model="recipe.step_text"></textarea>
@ -51,214 +53,74 @@
</div>
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox" v-model="recipe.is_ingredient" />
Is Ingredient
</label>
</div>
<app-modal title="Markdown Help" :open="isDescriptionHelpOpen" @dismiss="isDescriptionHelpOpen = false">
<p>
The description editor uses <a href="https://www.markdownguide.org/cheat-sheet/">Markdown</a>. Follow the link for a full
description of the syntax, but below is a quick reference.
</p>
<table class="table">
<thead>
<tr>
<th>Style</th>
<th>Syntax</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>Heading</td>
<td>
<pre>
# Biggest Heading
## Smaller Heading
###### Smallest Heading
</pre>
</td>
<td class="content">
<h1>Biggest Heading</h1>
<h2>Smaller Heading</h2>
<h6>Smallest Heading</h6>
</td>
</tr>
<tr>
<td>Numbered Lists</td>
<td>
<pre>
1. First Item
1. Second Item
1. subitem A
1. subitem B
1. Thrid Item
</pre>
</td>
<td class="content">
<ol>
<li>First Item</li>
<li>Second Item
<ol>
<li>subitem A</li>
<li>subitem B</li>
</ol>
</li>
<li>Thrid Item</li>
</ol>
</td>
</tr>
<tr>
<td>Lists</td>
<td>
<pre>
* First Item
* Second Item
* subitem A
* subitem B
* Third Item
</pre>
</td>
<td class="content">
<ul>
<li>First Item</li>
<li>Second Item
<ul>
<li>subitem A</li>
<li>subitem B</li>
</ul></li>
<li>Third Item</li>
</ul>
</td>
</tr>
<tr>
<td>Basic Styles</td>
<td>
<pre>
*italics*
**bold**
***bold italics***
_underline_
==highlight==
</pre>
</td>
<td class="content">
<p>
<em>italics</em><br>
<strong>bold</strong><br>
<strong><em>bold italics</em></strong><br>
<u>underline</u><br>
<mark>highlight</mark>
</p>
</td>
</tr>
</tbody>
</table>
<h3 class="title is-3">Basic Example</h3>
<h5 class="subtitle is=3">Input</h5>
<pre>
## For the dough
1. Mix dry ingredients
1. Fold in egg whites
1. Sprinkle on sardines
## For the sauce
1. Blend clams ==thoroughly==
1. Melt beef lard and add clam slurry
### Optional (Toppings)
* Raw onion
* Sliced hard boiled eggs
</pre>
<h5 class="subtitle is=3">Output</h5>
<div class="content box">
<h2>For the dough</h2>
<ol>
<li>Mix dry ingredients</li>
<li>Fold in egg whites</li>
<li>Sprinkle on sardines</li>
</ol>
<h2>For the sauce</h2>
<ol>
<li>Blend clams <mark>thoroughly</mark></li>
<li>Melt beef lard and add clam slurry</li>
</ol>
<h3>Optional (Toppings)</h3>
<ul>
<li>Raw onion</li>
<li>Sliced hard boiled eggs</li>
</ul>
</div>
</app-modal>
</div>
</template>
<script setup>
<script>
import { computed, ref, useTemplateRef, watch } from "vue";
import { useAutosize } from "../lib/useAutosize";
import autosize from "autosize";
import debounce from "lodash/debounce";
import api from "../lib/Api";
import RecipeEditIngredientEditor from "./RecipeEditIngredientEditor";
const props = defineProps({
recipe: {
required: true,
type: Object
export default {
props: {
recipe: {
required: true,
type: Object
},
forLogging: {
required: false,
type: Boolean,
default: false
}
},
forLogging: {
required: false,
type: Boolean,
default: false
data() {
return {
stepPreviewCache: null
};
},
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>
<style lang="scss" scoped>
.recipe-edit {
margin-bottom: 1rem;
}
.directions-input {
height: 100%;
}

View File

@ -1,10 +1,6 @@
<template>
<div>
<h3 class="title is-4">
Ingredients
<button type="button" class="button is-primary" @click="bulkEditIngredients">Bulk Edit</button>
</h3>
<button type="button" class="button is-primary" @click="bulkEditIngredients">Bulk Edit</button>
<app-modal wide :open="isBulkEditing" title="Edit Ingredients" @dismiss="cancelBulkEditing">
<div class="columns">
<div class="column is-half bulk-input">
@ -12,22 +8,18 @@
</div>
<div class="column is-half">
<table class="table is-bordered is-narrow is-size-7">
<thead>
<tr>
<th>#</th>
<th>Unit</th>
<th>Name</th>
<th>Prep</th>
</tr>
</thead>
<tbody>
<tr v-for="i in bulkIngredientPreview">
<td>{{i.quantity}}</td>
<td>{{i.units}}</td>
<td>{{i.name}}</td>
<td>{{i.preparation}}</td>
</tr>
</tbody>
</table>
</div>
</div>
@ -36,162 +28,175 @@
</app-modal>
<div>
<recipe-edit-ingredient-item v-for="(i, idx) in visibleIngredients" :key="i.id" :ingredient="i" :show-labels="idx === 0 || isMobile" @deleteFood="deleteFood"></recipe-edit-ingredient-item>
<recipe-edit-ingredient-item v-for="(i, idx) in visibleIngredients" :key="i.id" :ingredient="i" :show-labels="idx === 0 || isMobile" @deleteIngredient="deleteIngredient"></recipe-edit-ingredient-item>
</div>
<button type="button" class="button is-primary" @click="addIngredient">Add Ingredient</button>
</div>
</template>
<script setup>
<script>
import { computed, ref } from "vue";
import { useMediaQueryStore } from "../stores/mediaQuery";
import RecipeEditIngredientItem from "./RecipeEditIngredientItem";
const mediaQueryStore = useMediaQueryStore();
import { mapState } from "vuex";
const props = defineProps({
ingredients: {
required: true,
type: Array
}
});
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;
export default {
props: {
ingredients: {
required: true,
type: Array
}
};
},
const parsed = [];
const lines = bulkEditText.value.replace("\r", "").split("\n");
data() {
return {
isBulkEditing: false,
bulkEditText: null
};
},
for (let line of lines) {
if (line.length === 0) { continue; }
computed: {
...mapState({
isMobile: state => state.mediaQueries.mobile
}),
bulkIngredientPreview() {
if (this.bulkEditText === null) {
return [];
}
const match = line.match(regex);
const regex = /^\s*(?:([\d\/.]+(?:\s+[\d\/]+)?)\s+)?(?:([\w-]+)(?:\s+of)?\s+)?([^,|]+?|.+\|)(?:,\s*([^|]*?))?(?:\s*\[(\d+)\]\s*)?$/i;
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);
}
}
const magicFunc = function(str) {
if (str === "-") {
return "";
} else {
return str;
}
};
return parsed;
});
const parsed = [];
const lines = this.bulkEditText.replace("\r", "").split("\n");
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;
for (let line of lines) {
if (line.length === 0) { continue; }
return {
id: null,
quantity: null,
units: null,
name: null,
preparation: null,
ingredient_id: null,
sort_order: next_sort_order
};
}
const match = line.match(regex);
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 (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);
}
},
if (newIngredient === null) {
newIngredient = createIngredient();
methods: {
createIngredient() {
return {
id: null,
quantity: null,
units: null,
name: null,
preparation: null,
ingredient_id: null,
sort_order: Math.max([0].concat(this.ingredients.map(i => i.sort_order))) + 5
};
},
addIngredient() {
this.ingredients.push(this.createIngredient());
},
deleteIngredient(ingredient) {
if (ingredient.id) {
ingredient._destroy = true;
} else {
const idx = this.ingredients.findIndex(i => i === ingredient);
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;
}
},
newIngredient.quantity = parsedIngredient.quantity;
newIngredient.units = parsedIngredient.units;
newIngredient.name = parsedIngredient.name;
newIngredient.preparation = parsedIngredient.preparation;
newList.push(newIngredient);
components: {
RecipeEditIngredientItem
}
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>

View File

@ -37,57 +37,62 @@
</div>
<div class="column is-narrow">
<span class="label is-small-mobile" v-if="showLabels">&nbsp;</span>
<button type="button" class="button is-danger is-small" @click="deleteFood(ingredient)">
<button type="button" class="button is-danger is-small" @click="deleteIngredient(ingredient)">
<app-icon icon="x" size="md"></app-icon>
</button>
</div>
</div>
</template>
<script setup>
<script>
import { useTemplateRef, watch } from "vue";
import api from "../lib/Api";
const emit = defineEmits(["deleteFood"]);
const props = defineProps({
ingredient: {
required: true,
type: Object
export default {
props: {
ingredient: {
required: true,
type: Object
},
showLabels: {
required: false,
type: Boolean,
default: false
}
},
showLabels: {
required: false,
type: Boolean,
default: false
}
});
const autocompleteElement = useTemplateRef("autocomplete");
methods: {
deleteIngredient(ingredient) {
this.$emit("deleteIngredient", ingredient);
},
watch(props.ingredient, (val) => {
if (props.ingredient.ingredient && props.ingredient.ingredient.name !== val) {
props.ingredient.ingredient_id = null;
props.ingredient.ingredient = null;
}
});
updateSearchItems(text) {
return api.getSearchIngredients(text);
},
function deleteFood(ingredient) {
emit("deleteFood", ingredient);
}
searchItemSelected(ingredient) {
this.ingredient.ingredient_id = ingredient.id;
this.ingredient.ingredient = ingredient;
this.ingredient.name = ingredient.name;
},
function updateSearchItems(text) {
return api.getSearchIngredients(text);
}
nameClick() {
if (this.ingredient.ingredient_id === null && this.ingredient.name !== null && this.ingredient.name.length > 2) {
this.$refs.autocomplete.updateOptions(this.ingredient.name);
}
}
},
function searchItemSelected(ingredient) {
props.ingredient.ingredient_id = ingredient.id;
props.ingredient.ingredient = ingredient;
props.ingredient.name = ingredient.name;
}
watch: {
'ingredient.name': function(val) {
if (this.ingredient.ingredient && this.ingredient.ingredient.name !== val) {
this.ingredient.ingredient_id = null;
this.ingredient.ingredient = null;
}
}
},
function nameClick() {
if (props.ingredient.ingredient_id === null && props.ingredient.name !== null && props.ingredient.name.length > 2) {
autocompleteElement.updateOptions(props.ingredient.name);
components: {
}
}
@ -95,10 +100,10 @@
<style lang="scss" scoped>
@use "bulma/sass/utilities" as bulma;
@import "../styles/variables";
.edit-ingredient-item {
border-bottom: solid 1px bulma.$grey-light;
border-bottom: solid 1px $grey-light;
margin-bottom: 1.25rem;
&:last-child {

View File

@ -24,7 +24,7 @@
<div>
<p class="heading">Source</p>
<p class="title is-6">
<a v-if="isSourceUrl" :href="sourceUrl">{{sourceText}}</a>
<a v-if="isSourceUrl" :href="sourceUrl" target="_blank">{{sourceText}}</a>
<span v-else>{{sourceText}}</span>
</p>
</div>
@ -37,11 +37,6 @@
<div class="message-header">
Ingredients
<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">
<button class="button primary" v-for="tl in taskStore.taskLists" :key="tl.id" @click="addRecipeToList(tl)">
{{tl.name}}
</button>
</app-dropdown>
</div>
<div class="message-body content">
<ul v-if="recipe.ingredients.length > 0" v-click-strike>
@ -65,18 +60,14 @@
<div class="message-header" @click="showNutrition = !showNutrition">Nutrition Data</div>
<div class="message-body" v-show="showNutrition">
<table class="table">
<thead>
<tr>
<th>Item</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr v-for="nutrient in recipe.nutrition_data.nutrients" :key="nutrient.name">
<td>{{nutrient.label}}</td>
<td>{{ roundValue(nutrient.value) }}</td>
<td>{{nutrient.value}}</td>
</tr>
</tbody>
</table>
<h3 class="title is-5">Nutrition Calculation Warnings</h3>
@ -150,118 +141,117 @@
</div>
</template>
<script setup>
<script>
import { computed, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import api from "../lib/Api";
import { useTaskStore } from "../stores/task";
export default {
props: {
recipe: {
required: true,
type: Object
}
},
const taskStore = useTaskStore();
const router = useRouter();
data() {
return {
showNutrition: false,
showConvertDialog: false,
const props = defineProps({
recipe: {
required: true,
type: Object
}
});
scaleValue: '1',
systemConvertValue: "",
unitConvertValue: "",
const showNutrition = ref(false);
const showConvertDialog = ref(false);
const addToTasksMenuOpen = ref(false);
scaleOptions: [
'1/4',
'1/3',
'1/2',
'2/3',
'3/4',
'1',
'1 1/2',
'2',
'3',
'4'
]
};
},
const scaleValue = ref('1');
const systemConvertValue = ref('');
const unitConvertValue = ref('');
computed: {
timeDisplay() {
let a = this.formatMinutes(this.recipe.active_time);
const t = this.formatMinutes(this.recipe.total_time);
const scaleOptions = [
'1/4',
'1/3',
'1/2',
'2/3',
'3/4',
'1',
'1 1/2',
'2',
'3',
'4'
];
if (a) {
a = ` (${a} active)`;
}
const timeDisplay = computed(() => {
let a = formatMinutes(props.recipe.active_time);
const t = formatMinutes(props.recipe.total_time);
return t + a;
},
if (a) {
a = ` (${a} active)`;
}
sourceUrl() {
try {
return new URL(this.recipe.source);
} catch(err) {
return null;
}
},
return t + a;
});
isSourceUrl() {
return this.sourceUrl !== null;
},
const sourceUrl = computed(() => {
try {
return new URL(props.recipe.source);
} catch(err) {
return null;
}
});
const isSourceUrl = computed(() => sourceUrl.value !== null);
const sourceText = computed(() => isSourceUrl.value ? sourceUrl.value.host : props.recipe.source);
watch(props.recipe, (r) => {
if (r) {
scaleValue.value = r.converted_scale || '1';
systemConvertValue.value = r.converted_system;
unitConvertValue.value = r.converted_unit;
}
}, { immediate: true });
onMounted(() => {
taskStore.ensureTaskLists();
});
function addRecipeToList(list) {
api.addRecipeToTaskList(list.id, props.recipe.id)
.then(() => {
taskStore.setCurrentTaskList(list);
router.push({name: 'task_lists'})
});
}
function convert() {
showConvertDialog.value = false;
router.push({name: 'recipe', query: { scale: scaleValue.value, system: systemConvertValue.value, unit: unitConvertValue.value }});
}
function roundValue(v) {
return parseFloat(v).toFixed(2);
}
function formatMinutes(min) {
if (min) {
const partUnits = [
{unit: "d", minutes: 60 * 24},
{unit: "h", minutes: 60},
{unit: "m", minutes: 1}
];
const parts = [];
let remaining = min;
for (let unit of partUnits) {
let val = Math.floor(remaining / unit.minutes);
remaining = remaining % unit.minutes;
if (val > 0) {
parts.push(`${val} ${unit.unit}`);
sourceText() {
if (this.isSourceUrl) {
return this.sourceUrl.host;
} else {
return this.source;
}
}
},
return parts.join(" ");
} else {
return "";
watch: {
recipe: {
handler: function(r) {
if (r) {
this.scaleValue = r.converted_scale || '1';
this.systemConvertValue = r.converted_system;
this.unitConvertValue = r.converted_unit;
}
},
immediate: true
}
},
methods: {
convert() {
this.showConvertDialog = false;
this.$router.push({name: 'recipe', query: { scale: this.scaleValue, system: this.systemConvertValue, unit: this.unitConvertValue }});
},
formatMinutes(min) {
if (min) {
const partUnits = [
{unit: "d", minutes: 60 * 24},
{unit: "h", minutes: 60},
{unit: "m", minutes: 1}
];
const parts = [];
let remaining = min;
for (let unit of partUnits) {
let val = Math.floor(remaining / unit.minutes);
remaining = remaining % unit.minutes;
if (val > 0) {
parts.push(`${val} ${unit.unit}`);
}
}
return parts.join(" ");
} else {
return "";
}
}
}
}

View File

@ -1,71 +0,0 @@
<template>
<div class="columns task-item-edit">
<div class="field column">
<label class="label is-small">Name</label>
<div class="control">
<input class="input is-small" type="text" placeholder="Name" v-model="taskItem.name" @keydown="inputKeydown" ref="nameInput">
</div>
</div>
<div class="field column">
<label class="label is-small">Quantity</label>
<div class="control">
<input class="input is-small" type="text" placeholder="Qty" v-model="taskItem.quantity" @keydown="inputKeydown">
</div>
</div>
<div class="field column">
<div class="control">
<button class="button is-primary" @click="save">Add</button>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, useTemplateRef } from "vue";
const emit = defineEmits(["save"]);
const props = defineProps({
taskItem: {
required: true,
type: Object
}
});
const nameElement = useTemplateRef("nameInput");
onMounted(() => focus());
function inputKeydown(evt) {
switch (evt.key) {
case "Enter":
evt.preventDefault();
save();
}
}
function save() {
emit("save", props.taskItem);
}
function focus() {
nameElement.value.focus();
}
defineExpose({
focus
});
</script>
<style lang="scss" scoped>
.task-item-edit {
}
</style>

View File

@ -1,193 +0,0 @@
<template>
<div>
<div class="panel">
<p class="panel-heading">
{{taskList.name}} ({{completedItemCount}} / {{taskList.task_items.length}})
</p>
<div class="panel-block">
<button class="button is-fullwidth is-primary" @click="toggleShowAddItem">{{ showAddItem ? 'Done' : 'New Item' }}</button>
</div>
<app-expand-transition>
<div class="panel-block" v-if="showAddItem">
<task-item-edit @save="save" :task-item="newItem" ref="itemEdit"></task-item-edit>
</div>
</app-expand-transition>
<transition-group tag="div" name="list-item-move">
<a v-for="i in taskItems" :key="i.id" @click="toggleItem(i)" class="panel-block">
<div class="check">
<app-icon v-if="i.completed" icon="check"></app-icon>
<span class="icon" v-else></span>
</div>
<span>{{ i.quantity }} {{ i.name }}</span>
</a>
</transition-group>
<app-expand-transition>
<div class="panel-block" v-if="uncompletedItemCount > 0">
<button class="button is-fullwidth is-link" @click="completeAllItems">
<span class="check">
<app-icon icon="check"></app-icon>
</span>
<span>Check All</span>
</button>
</div>
</app-expand-transition>
<app-expand-transition>
<div class="panel-block" v-if="completedItemCount > 0">
<button class="button is-fullwidth is-link" @click="unCompleteAllItems">
<span class="check">
<span class="icon"></span>
</span>
<span>Uncheck All</span>
</button>
</div>
</app-expand-transition>
<app-expand-transition>
<div class="panel-block" v-if="completedItemCount > 0">
<button class="button is-fullwidth is-link" @click="deleteCompletedItems">
<app-icon icon="x" class="is-text-danger"></app-icon>
<span>Clear Completed</span>
</button>
</div>
</app-expand-transition>
</div>
</div>
</template>
<script setup>
import { computed, ref, useTemplateRef } from "vue";
import * as Errors from '../lib/Errors';
import { useTaskStore } from "../stores/task";
import { useLoadResource } from "../lib/useLoadResource";
import TaskItemEdit from "./TaskItemEdit";
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 {
task_list_id: null,
name: '',
quantity: '',
completed: false
};
}
function save() {
newItem.value.task_list_id = props.taskList.id;
loadResource(
taskStore.createTaskItem(newItem.value)
.then(() => {
newItem.value = newItemTemplate();
itemEditElement.value.focus();
})
.catch(Errors.onlyFor(Errors.ApiValidationError, err => newItemValidationErrors.value = err.validationErrors()))
)
}
function toggleItem(i) {
loadResource(
taskStore.completeTaskItems({
taskList: props.taskList,
taskItems: [i],
completed: !i.completed
})
);
}
function toggleShowAddItem() {
newItem.value = newItemTemplate();
showAddItem.value = !showAddItem.value;
}
function completeAllItems() {
const toComplete = props.taskList.task_items.filter(i => !i.completed);
loadResource(
taskStore.completeTaskItems({
taskList: props.taskList,
taskItems: toComplete,
completed: true
})
)
}
function unCompleteAllItems() {
const toUnComplete = props.taskList.task_items.filter(i => i.completed);
loadResource(
taskStore.completeTaskItems({
taskList: props.taskList,
taskItems: toUnComplete,
completed: false
})
)
}
function deleteCompletedItems() {
loadResource(
taskStore.deleteTaskItems({
taskList: props.taskList,
taskItems: props.taskList.task_items.filter(i => i.completed)
})
);
}
</script>
<style lang="scss" scoped>
.columns {
margin-top: 0;
margin-bottom: 0;
}
.column {
padding-top: 0;
padding-bottom: 0;
margin-top: 0;
margin-bottom: 0;
}
.check {
display: inline-flex;
margin-right: 1.5rem;
.icon {
position: relative;
&:after {
content: '';
position: absolute;
top: -3px;
left: -3px;
bottom: -3px;
right: -3px;
border: 2px solid currentColor;
}
}
}
</style>

View File

@ -1,69 +0,0 @@
<template>
<div class="dropdown-item" @mouseover="hovering = true" @mouseleave="hovering = false" :class="{hovered: hovering, 'is-active': active}" @click="selectList">
<span>{{taskList.name}} ({{ taskList.task_items.length }})</span>
<button @click.stop="confirmingDelete = true" class="button is-small is-danger is-pulled-right"><app-icon icon="x" size="sm"></app-icon></button>
<div class="is-clearfix"></div>
<app-modal :open="confirmingDelete" :title="'Delete ' + taskList.name + '?'" @dismiss="confirmingDelete = false">
<button class="button is-danger" @click="deleteList">Confirm</button>
<button class="button is-primary" @click="confirmingDelete = false">Cancel</button>
</app-modal>
</div>
</template>
<script setup>
import { ref } from "vue";
const emit = defineEmits(["select", "delete"]);
const props = defineProps({
taskList: {
type: Object,
required: true
},
active: {
type: Boolean,
required: false,
default: false
}
});
const hovering = ref(false);
const confirmingDelete = ref(false);
function selectList() {
emit("select", props.taskList);
}
function deleteList() {
confirmingDelete.value = false;
emit("delete", props.taskList);
}
</script>
<style lang="scss" scoped>
@use "bulma/sass/utilities" as bulma;
@use 'sass:color';
div.dropdown-item {
cursor: pointer;
&.hovered {
color: bulma.$black;
background-color: bulma.$background;
}
&.is-active {
color: color.invert(bulma.$link);
background-color: bulma.$link;
}
}
</style>

View File

@ -1,46 +0,0 @@
<template>
<div>
<app-validation-errors :errors="validationErrors"></app-validation-errors>
<label class="label is-small">Add New List</label>
<div class="field has-addons">
<div class="control">
<input class="input is-small" type="text" v-model="taskList.name" @keydown="nameKeydownHandler">
</div>
<div class="control">
<button type="button" class="button is-primary is-small" @click="save">Add</button>
</div>
</div>
</div>
</template>
<script setup>
const emit = defineEmits(["save"]);
const props = defineProps({
taskList: {
required: true,
type: Object
},
validationErrors: {
required: false,
type: Object,
default: function() { return {}; }
}
});
function save() {
emit("save");
}
function nameKeydownHandler(evt) {
switch (evt.key) {
case "Enter":
evt.preventDefault();
save();
}
}
</script>

View File

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

View File

@ -3,11 +3,11 @@
<h1 class="title">About</h1>
<p>
A Recipe manager. Source available at <a href="https://source.elbert.us/dan/parsley">https://source.elbert.us</a>.
A Recipe manager. Source available from my Git repository at <a href="https://source.elbert.us/dan/parsley">https://source.elbert.us</a>.
</p>
<p>
Parsley is released under the MIT License. All code &copy; Dan Elbert 2024.
Parsley is released under the MIT License. All code &copy; Dan Elbert 2018.
</p>
<p>
@ -19,7 +19,11 @@
</div>
</template>
<script setup>
<script>
export default {
}
</script>

View File

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

View File

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

View File

@ -2,102 +2,121 @@
<div>
<h1 class="title">Calculator</h1>
<div class="box">
<div v-for="err in errors" :key="err" class="notification is-warning">
{{err}}
</div>
<div class="columns">
<app-text-field label="Input" v-model="input" class="column" :validation-error="inputErrors"></app-text-field>
<app-text-field label="Output Unit" v-model="outputUnit" class="column" :validation-error="outputUnitErrors"></app-text-field>
</div>
<div class="columns">
<div class="field column">
<label class="label">Ingredient</label>
<div class="control">
<app-autocomplete
:inputClass="{'is-success': ingredient !== null}"
ref="autocomplete"
v-model="ingredient_name"
:minLength="2"
valueAttribute="name"
labelAttribute="name"
placeholder=""
@optionSelected="searchItemSelected"
:onGetOptions="updateSearchItems"
>
</app-autocomplete>
</div>
<div class="columns">
<div class="field column">
<label class="label">Input</label>
<div class="control">
<input class="input" type="text" placeholder="input" v-model="input">
</div>
</div>
<div class="field column">
<label class="label">Output Unit</label>
<div class="control">
<input class="input" type="text" placeholder="unit" v-model="outputUnit">
</div>
<app-text-field label="Density" v-model="density" class="column" :disabled="ingredient !== null" :validation-error="densityErrors"></app-text-field>
</div>
<app-text-field label="Output" v-model="output" disabled></app-text-field>
</div>
<div class="columns">
<div class="field column">
<label class="label">Ingredient</label>
<div class="control">
<app-autocomplete
:inputClass="{'is-success': ingredient !== null}"
ref="autocomplete"
v-model="ingredient_name"
:minLength="2"
valueAttribute="name"
labelAttribute="name"
placeholder=""
@optionSelected="searchItemSelected"
:onGetOptions="updateSearchItems"
>
</app-autocomplete>
</div>
</div>
<div class="field column">
<label class="label">Density</label>
<div class="control">
<input class="input" type="text" placeholder="8.345 lb/gallon" v-model="density">
</div>
</div>
</div>
<div class="field">
<label class="label">Output</label>
<div class="control">
<input class="input" type="text" disabled="disabled" v-model="output">
</div>
</div>
</div>
</template>
<script setup>
<script>
import { computed, ref, watch } from "vue";
import api from "../lib/Api";
import debounce from "lodash/debounce";
import { useLoadResource } from "../lib/useLoadResource";
const { loadResource } = useLoadResource();
export default {
data() {
return {
input: '',
outputUnit: '',
ingredient_name: '',
ingredient: null,
density: '',
output: '',
errors: []
};
},
const input = ref("");
const outputUnit = ref("");
const ingredient_name = ref("");
const ingredient = ref(null);
const density = ref("");
const output = ref("");
const errors = ref({});
methods: {
updateSearchItems(text) {
return api.getSearchIngredients(text);
},
const inputErrors = computed(() => getErrors("input"));
const outputUnitErrors = computed(() => getErrors("output_unit"));
const densityErrors = computed(() => getErrors("density"));
searchItemSelected(ingredient) {
this.ingredient = ingredient;
this.ingredient_name = ingredient.name;
this.density = ingredient.density;
},
const updateOutput = debounce(function() {
if (input.value && input.value.length > 0) {
loadResource(api.getCalculate(input.value, outputUnit.value, ingredient.value ? ingredient.value.ingredient_id : null, density.value)
updateOutput: debounce(function() {
this.loadResource(api.getCalculate(this.input, this.outputUnit, this.density)
.then(data => {
output.value = data.output;
errors.value = data.errors;
this.output = data.output;
this.errors = data.errors;
})
);
}
}, 500);
);
}, 500)
},
watch(ingredient_name, function(val) {
if (ingredient.value && ingredient.value.name !== val) {
ingredient.value = null;
}
});
watch: {
'ingredient_name': function(val) {
if (this.ingredient && this.ingredient.name !== val) {
this.ingredient = null;
}
}
},
watch(
[input, outputUnit, density, ingredient],
() => updateOutput()
);
created() {
this.$watch(
function() {
return this.input + this.outputUnit + this.density;
},
function() {
this.updateOutput();
}
)
},
function updateSearchItems(text) {
return api.getSearchIngredients(text);
}
function searchItemSelected(ingredient) {
ingredient.value = ingredient || null;
ingredient_name.value = ingredient.name || null;
density.value = ingredient.density || null;
}
function getErrors(type) {
if (errors.value[type] && errors.value[type].length > 0) {
return errors.value[type].join(", ");
} else {
return null;
components: {
}
}

View File

@ -1,41 +0,0 @@
<template>
<div>
<div v-if="food === null">
Loading...
</div>
<div v-else>
<food-show :food="food"></food-show>
</div>
<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>
</div>
</template>
<script setup>
import { computed, onBeforeMount, ref } from "vue";
import { useRoute } from "vue-router";
import FoodShow from "./FoodShow";
import api from "../lib/Api";
import { useLoadResource } from "../lib/useLoadResource";
import { useAppConfigStore } from "../stores/appConfig";
const { loadResource } = useLoadResource();
const appConfig = useAppConfigStore();
const route = useRoute();
const food = ref(null);
const foodId = computed(() => route.params.id);
onBeforeMount(() => {
loadResource(
api.getFood(foodId.value)
.then(data => { food.value = data; return data; })
);
});
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,70 +0,0 @@
<template>
<div>
<food-edit :food="food" :validation-errors="validationErrors" action="Creating"></food-edit>
<button type="button" class="button is-primary" @click="save">Save</button>
<router-link class="button is-secondary" to="/food">Cancel</router-link>
</div>
</template>
<script setup>
import { reactive, ref } from "vue";
import { useRouter } from "vue-router";
import FoodEdit from "./FoodEdit";
import api from "../lib/Api";
import * as Errors from '../lib/Errors';
import { useLoadResource } from "../lib/useLoadResource";
const { loadResource } = useLoadResource();
const router = useRouter();
const validationErrors = ref({});
const food = reactive({
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: []
});
function save() {
validationErrors.value = {}
loadResource(
api.postFood(food)
.then(() => router.push('/foods'))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
);
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,53 +0,0 @@
<template>
<div>
<div v-if="food === null">
Loading...
</div>
<div v-else>
<food-edit :food="food" :validation-errors="validationErrors"></food-edit>
</div>
<button type="button" class="button is-primary" @click="save">Save</button>
<router-link class="button is-secondary" to="/foods">Cancel</router-link>
</div>
</template>
<script setup>
import { computed, onBeforeMount, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import FoodEdit from "./FoodEdit";
import api from "../lib/Api";
import * as Errors from '../lib/Errors';
import { useLoadResource } from "../lib/useLoadResource";
const { loadResource } = useLoadResource();
const router = useRouter();
const route = useRoute();
const food = ref(null);
const validationErrors = ref({});
const foodId = computed(() => route.params.id);
onBeforeMount(() => {
loadResource(
api.getFood(foodId.value)
.then(data => { food.value = data; return data; })
);
});
function save() {
validationErrors.value = {};
loadResource(
api.patchFood(food.value)
.then(() => router.push({name: 'food', params: {id: foodId.value }}))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => validationErrors.value = err.validationErrors()))
);
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,131 +0,0 @@
<template>
<div>
<h1 class="title">Ingredients</h1>
<div class="buttons">
<router-link v-if="appConfig.isLoggedIn" :to="{name: 'new_food'}" class="button is-primary">Create Ingredient</router-link>
</div>
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="food" @changePage="changePage"></app-pager>
<table class="table is-fullwidth is-narrow">
<thead>
<tr>
<th>Name</th>
<th>USDA</th>
<th>KCal per 100g</th>
<th>Density (oz/cup)</th>
<th></th>
</tr>
<tr>
<th>
<div class="field">
<div class="control">
<input type="text" class="input" placeholder="search names" v-model="search.name">
</div>
</div>
</th>
<th colspan="4"></th>
</tr>
</thead>
<transition-group tag="tbody" name="fade" mode="out-in">
<tr v-for="i in foods" :key="i.id">
<td><router-link :to="{name: 'food', params: { id: i.id } }">{{i.name}}</router-link></td>
<td><app-icon v-if="i.usda" icon="check"></app-icon></td>
<td>{{i.kcal}}</td>
<td>{{i.density}}</td>
<td>
<template v-if="appConfig.isLoggedIn">
<router-link class="button" :to="{name: 'edit_food', params: { id: i.id } }">
<app-icon icon="pencil"></app-icon>
</router-link>
<button type="button" class="button is-danger" @click="deleteFood(i)">
<app-icon icon="x"></app-icon>
</button>
</template>
</td>
</tr>
</transition-group>
</table>
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="food" @changePage="changePage"></app-pager>
<div class="buttons">
<router-link v-if="appConfig.isLoggedIn" :to="{name: 'new_food'}" class="button is-primary">Create Ingredient</router-link>
</div>
<app-confirm :open="showConfirmFoodDelete" title="Delete Ingredient?" :message="confirmFoodDeleteMessage" @cancel="foodDeleteCancel" @confirm="foodDeleteConfirm"></app-confirm>
</div>
</template>
<script setup>
import { computed, reactive, ref, watch } from "vue";
import api from "../lib/Api";
import debounce from "lodash/debounce";
import { useAppConfigStore } from "../stores/appConfig";
import { useLoadResource } from "../lib/useLoadResource";
const appConfig = useAppConfigStore();
const { loadResource } = useLoadResource();
const foodData = ref(null);
const foodForDeletion = ref(null);
const search = reactive({
page: 1,
per: 25,
name: null
});
const foods = computed(() => foodData.value?.foods || []);
const totalPages = computed(() => foodData.value?.total_pages || 0);
const currentPage = computed(() => foodData.value?.current_page || 0);
const showConfirmFoodDelete = computed(() => foodForDeletion.value !== null);
const confirmFoodDeleteMessage = computed(() => {
if (foodForDeletion.value !== null) {
return `Are you sure you want to delete ${foodForDeletion.value.name}?`;
} else {
return "??";
}
});
const getList = debounce(function() {
return loadResource(
api.getFoodList(search.page, search.per, search.name)
.then(data => foodData.value = data)
);
}, 500, {leading: true, trailing: true});
watch(search,
() => getList(),
{
deep: true,
immediate: true
}
);
function changePage(idx) {
search.page = idx;
}
function deleteFood(food) {
foodForDeletion.value = food;
}
function foodDeleteCancel() {
foodForDeletion.value = null;
}
function foodDeleteConfirm() {
if (foodForDeletion.value !== null) {
loadResource(
api.deleteFood(foodForDeletion.value.id).then(res => {
foodForDeletion.value = null;
return getList();
})
);
}
}
</script>

View File

@ -0,0 +1,49 @@
<template>
<div>
<div v-if="ingredient === null">
Loading...
</div>
<div v-else>
<ingredient-show :ingredient="ingredient"></ingredient-show>
</div>
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_ingredient', params: { id: ingredientId }}">Edit</router-link>
<router-link class="button" to="/ingredients">Back</router-link>
</div>
</template>
<script>
import IngredientShow from "./IngredientShow";
import { mapState } from "vuex";
import api from "../lib/Api";
export default {
data: function () {
return {
ingredient: null
}
},
computed: {
...mapState({
ingredientId: state => state.route.params.id,
})
},
created() {
this.loadResource(
api.getIngredient(this.ingredientId)
.then(data => { this.ingredient = data; return data; })
);
},
components: {
IngredientShow
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,77 @@
<template>
<div>
<ingredient-edit :ingredient="ingredient" :validation-errors="validationErrors" action="Creating"></ingredient-edit>
<button type="button" class="button is-primary" @click="save">Save</button>
<router-link class="button is-secondary" to="/ingredients">Cancel</router-link>
</div>
</template>
<script>
import IngredientEdit from "./IngredientEdit";
import { mapState } from "vuex";
import api from "../lib/Api";
import * as Errors from '../lib/Errors';
export default {
data() {
return {
ingredient: {
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,
ingredient_units: []
},
validationErrors: {}
}
},
methods: {
save() {
this.validationErrors = {}
this.loadResource(
api.postIngredient(this.ingredient)
.then(() => this.$router.push('/ingredients'))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
);
}
},
components: {
IngredientEdit
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,64 @@
<template>
<div>
<div v-if="ingredient === null">
Loading...
</div>
<div v-else>
<ingredient-edit :ingredient="ingredient" :validation-errors="validationErrors"></ingredient-edit>
</div>
<button type="button" class="button is-primary" @click="save">Save</button>
<router-link class="button is-secondary" to="/ingredients">Cancel</router-link>
</div>
</template>
<script>
import IngredientEdit from "./IngredientEdit";
import { mapState } from "vuex";
import api from "../lib/Api";
import * as Errors from '../lib/Errors';
export default {
data: function () {
return {
ingredient: null,
validationErrors: {}
};
},
computed: {
...mapState({
ingredientId: state => state.route.params.id,
})
},
methods: {
save() {
this.validationErrors = {};
this.loadResource(
api.patchIngredient(this.ingredient)
.then(() => this.$router.push({name: 'ingredient', params: {id: this.ingredientId }}))
.catch(Errors.onlyFor(Errors.ApiValidationError, err => this.validationErrors = err.validationErrors()))
);
}
},
created() {
this.loadResource(
api.getIngredient(this.ingredientId)
.then(data => { this.ingredient = data; return data; })
);
},
components: {
IngredientEdit
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,163 @@
<template>
<div>
<h1 class="title">Ingredients</h1>
<div class="buttons">
<router-link v-if="isLoggedIn" :to="{name: 'new_ingredient'}" class="button is-primary">Create Ingredient</router-link>
</div>
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="ingredient" @changePage="changePage"></app-pager>
<table class="table is-fullwidth is-narrow">
<thead>
<tr>
<th>Name</th>
<th>USDA</th>
<th>KCal per 100g</th>
<th>Density (oz/cup)</th>
<th></th>
</tr>
<tr>
<th>
<div class="field">
<div class="control">
<input type="text" class="input" placeholder="search names" v-model="search.name">
</div>
</div>
</th>
<th colspan="4"></th>
</tr>
</thead>
<tbody>
<tr v-for="i in ingredients" :key="i.id">
<td><router-link :to="{name: 'ingredient', params: { id: i.id } }">{{i.name}}</router-link></td>
<td><app-icon v-if="i.usda" icon="check"></app-icon></td>
<td>{{i.kcal}}</td>
<td>{{i.density}}</td>
<td>
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_ingredient', params: { id: i.id } }">
<app-icon icon="pencil"></app-icon>
</router-link>
<button v-if="isLoggedIn" type="button" class="button is-danger" @click="deleteIngredient(i)">
<app-icon icon="x"></app-icon>
</button>
</td>
</tr>
</tbody>
</table>
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="ingredient" @changePage="changePage"></app-pager>
<div class="buttons">
<router-link v-if="isLoggedIn" :to="{name: 'new_ingredient'}" class="button is-primary">Create Ingredient</router-link>
</div>
<app-confirm :open="showConfirmIngredientDelete" :message="confirmIngredientDeleteMessage" :cancel="ingredientDeleteCancel" :confirm="ingredientDeleteConfirm"></app-confirm>
</div>
</template>
<script>
import api from "../lib/Api";
import debounce from "lodash/debounce";
export default {
data() {
return {
ingredientData: null,
ingredientForDeletion: null,
search: {
page: 1,
per: 25,
name: null
}
};
},
computed: {
ingredients() {
if (this.ingredientData) {
return this.ingredientData.ingredients;
} else {
return [];
}
},
totalPages() {
if (this.ingredientData) {
return this.ingredientData.total_pages
}
return 0;
},
currentPage() {
if (this.ingredientData) {
return this.ingredientData.current_page
}
return 0;
},
showConfirmIngredientDelete() {
return this.ingredientForDeletion !== null;
},
confirmIngredientDeleteMessage() {
if (this.ingredientForDeletion !== null) {
return `Are you sure you want to delete ${this.ingredientForDeletion.name}?`;
} else {
return "??";
}
}
},
methods: {
changePage(idx) {
this.search.page = idx;
},
getList: debounce(function() {
return this.loadResource(
api.getIngredientList(this.search.page, this.search.per, this.search.name)
.then(data => this.ingredientData = data)
);
}, 500, {leading: true, trailing: true}),
deleteIngredient(ingredient) {
this.ingredientForDeletion = ingredient;
},
ingredientDeleteCancel() {
this.ingredientForDeletion = null;
},
ingredientDeleteConfirm() {
if (this.ingredientForDeletion !== null) {
this.loadResource(
api.deleteIngredient(this.ingredientForDeletion.id).then(res => {
this.ingredientForDeletion = null;
return this.getList();
})
);
console.log("This is where the thing happens!!");
this.ingredientForDeletion = null;
}
}
},
created() {
this.$watch("search",
() => this.getList(),
{
deep: true,
immediate: true
}
);
},
components: {
}
}
</script>

View File

@ -8,34 +8,42 @@
</div>
<div class="buttons">
<router-link v-if="appConfig.isLoggedIn" class="button" :to="{name: 'edit_log', params: { id: logId }}">Edit</router-link>
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_log', params: { id: logId }}">Edit</router-link>
<router-link class="button" to="/logs">Back</router-link>
</div>
</div>
</template>
<script setup>
<script>
import { computed, onBeforeMount, ref } from "vue";
import { useRoute } from "vue-router";
import LogShow from "./LogShow";
import { mapState } from "vuex";
import api from "../lib/Api";
import { useLoadResource } from "../lib/useLoadResource";
import { useAppConfigStore } from "../stores/appConfig";
const { loadResource } = useLoadResource();
const route = useRoute();
const appConfig = useAppConfigStore();
export default {
data: function () {
return {
log: null
}
},
const log = ref(null);
const logId = computed(() => route.params.id);
computed: {
...mapState({
logId: state => state.route.params.id,
})
},
onBeforeMount(() => {
loadResource(
api.getLog(logId.value)
.then(data => { log.value = data; return data; })
);
});
created() {
this.loadResource(
api.getLog(this.logId)
.then(data => { this.log = data; return data; })
);
},
components: {
LogShow
}
}
</script>

View File

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

View File

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

View File

@ -5,64 +5,86 @@
</h1>
<table class="table">
<thead>
<tr>
<th>Recipe</th>
<th>Date</th>
<th>Rating</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<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><app-date-time :date-time="l.date" :show-time="false"></app-date-time> </td>
<td><app-rating :model-value="l.rating" readonly></app-rating></td>
<td><app-rating :value="l.rating" readonly></app-rating></td>
<td>{{l.notes}}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
<script>
import { computed, reactive, ref, watch } from "vue";
import api from "../lib/Api";
import debounce from "lodash/debounce";
import { useLoadResource } from "../lib/useLoadResource";
const { loadResource } = useLoadResource();
export default {
data() {
return {
logData: null,
search: {
page: 1,
per: 25
}
};
},
const logData = ref(null);
const search = reactive({
page: 1,
per: 25
});
computed: {
logs() {
if (this.logData) {
return this.logData.logs;
} else {
return [];
}
},
const logs = computed(() => logData.value?.logs || []);
const totalPages = computed(() => logData.value?.total_pages || 0);
const currentPage = computed(() => logData.value?.current_page || 0);
totalPages() {
if (this.logData) {
return this.logData.total_pages
}
return 0;
},
const getList = debounce(function() {
loadResource(
api.getLogList(search.page, search.per)
.then(data => logData.value = data)
);
}, 500, {leading: true, trailing: true});
watch(search,
() => getList(),
{
deep: true,
immediate: true
currentPage() {
if (this.logData) {
return this.logData.current_page
}
return 0;
}
);
},
function changePage(idx) {
search.page = idx;
methods: {
changePage(idx) {
this.search.page = idx;
},
getList: debounce(function() {
this.loadResource(
api.getLogList(this.search.page, this.search.per)
.then(data => this.logData = data)
);
}, 500, {leading: true, trailing: true})
},
created() {
this.$watch("search",
() => this.getList(),
{
deep: true,
immediate: true
}
);
}
}
</script>

View File

@ -9,15 +9,12 @@
</app-modal>
<table class="table">
<thead>
<tr>
<th>Note</th>
<th>Date</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="n in notes" :key="n.id">
<td>
{{ n.content }}
@ -31,60 +28,68 @@
</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
<script>
import { onBeforeMount, ref } from "vue";
import api from "../lib/Api";
import NoteEdit from "./NoteEdit";
import { useLoadResource } from "../lib/useLoadResource";
const { loadResource } = useLoadResource();
const notes = ref([]);
const editNote = ref(null);
export default {
data() {
return {
notes: [],
editNote: null
};
},
onBeforeMount(() => {
refreshList();
});
methods: {
refreshList() {
this.loadResource(
api.getNoteList()
.then(data => this.notes = data)
);
},
function refreshList() {
loadResource(
api.getNoteList()
.then(data => notes.value = data)
);
}
addNote() {
this.editNote = { id: null, content: "" };
},
function addNote() {
editNote.value = { id: null, content: "" };
}
function saveNote() {
loadResource(
api.postNote(editNote.value)
saveNote() {
this.loadResource(
api.postNote(this.editNote)
.then(() => {
editNote.value = null;
return refreshList();
this.editNote = null;
return this.refreshList();
})
);
}
);
},
function cancelNote() {
editNote.value = null;
}
cancelNote() {
this.editNote = null;
},
function deleteNote(n) {
loadResource(
api.deleteNote(n)
deleteNote(n) {
this.loadResource(
api.deleteNote(n)
.then(() => {
return refreshList();
return this.refreshList();
})
);
);
}
},
created() {
this.refreshList();
},
components: {
NoteEdit
}
}
</script>

View File

@ -17,50 +17,60 @@
<recipe-show :recipe="recipe"></recipe-show>
</div>
<router-link v-if="appConfig.isLoggedIn" class="button" :to="{name: 'edit_recipe', params: { id: recipeId }}">Edit</router-link>
<router-link v-if="isLoggedIn" class="button" :to="{name: 'edit_recipe', params: { id: recipeId }}">Edit</router-link>
<router-link class="button" to="/">Back</router-link>
</div>
</template>
<script setup>
<script>
import { computed, ref, watch } from "vue";
import { useRoute } from "vue-router";
import RecipeShow from "./RecipeShow";
import { mapState } from "vuex";
import api from "../lib/Api";
import { useLoadResource } from "../lib/useLoadResource";
import { useAppConfigStore } from "../stores/appConfig";
const appConfig = useAppConfigStore();
const route = useRoute();
const { loadResource } = useLoadResource();
const recipe = ref(null);
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}`;
}
export default {
data: function () {
return {
recipe: null,
showNutrition: false
}
)
},
function refreshData() {
loadResource(
api.getRecipe(recipeId.value, scale.value, system.value, unit.value, data => recipe.value = data)
);
computed: {
...mapState({
recipeId: state => state.route.params.id,
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>

View File

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

View File

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

View File

@ -1,318 +1,233 @@
<template>
<div>
<h1 class="title">Recipes</h1>
<router-link v-if="appConfig.isLoggedIn" :to="{name: 'new_recipe'}" class="button is-primary">Create Recipe</router-link>
<h1 class="title">
Recipes
<router-link v-if="isLoggedIn" :to="{name: 'new_recipe'}" class="button is-primary is-rounded" title="Create new Recipe">
<app-icon icon="plus"></app-icon>
</router-link>
</h1>
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="recipe" @changePage="changePage"></app-pager>
<app-loading v-if="localLoading"></app-loading>
<table class="table is-fullwidth" :class="{ small: isTouch }">
<table class="table is-fullwidth">
<thead>
<tr>
<th v-for="h in tableHeader" :key="h.name">
<a v-if="h.sort" href="#" @click.prevent="setSort(h.name)">
{{h.label}}
<app-icon v-if="search.column === h.name" size="sm" :icon="search.direction === 'asc' ? 'caret-bottom' : 'caret-top'"></app-icon>
</a>
<span v-else>{{h.label}}</span>
</th>
<th></th>
</tr>
<tr>
<td>
<app-search-text placeholder="search names" :value="search.name" @update:modelValue="setSearchName($event)"></app-search-text>
</td>
<td>
<app-search-text placeholder="search tags" :value="search.tags" @update:modelValue="setSearchTags($event)"></app-search-text>
</td>
<td colspan="5"></td>
</tr>
<tr>
<th v-for="h in tableHeader" :key="h.name">
<a v-if="h.sort" href="#" @click.prevent="setSort(h.name)">
{{h.label}}
<app-icon v-if="search.sortColumn === h.name" size="sm" :icon="search.sortDirection === 'asc' ? 'caret-bottom' : 'caret-top'"></app-icon>
</a>
<span v-else>{{h.label}}</span>
</th>
<th></th>
</tr>
<tr>
<td>
<div class="field">
<div class="control">
<input type="text" class="input" placeholder="search names" v-model="search.name">
</div>
</div>
</td>
<td>
<div class="field">
<div class="control">
<input type="text" class="input" placeholder="search tags" v-model="search.tags">
</div>
</div>
</td>
<td colspan="5"></td>
</tr>
</thead>
<transition-group name="fade" tag="tbody">
<tr v-for="r in recipes" :key="r.id">
<td><router-link :to="{name: 'recipe', params: { id: r.id } }">{{r.name}}</router-link></td>
<td>
<div class="tags">
<span class="tag" v-for="tag in r.tags" :key="tag">{{tag}}</span>
</div>
</td>
<td>
<app-rating v-if="r.rating !== null" :model-value="r.rating" readonly></app-rating>
<span v-else>--</span>
</td>
<td>{{ r.yields }}</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-dropdown hover v-if="appConfig.isLoggedIn" class="is-right">
<template #button>
<button class="button is-small">
<app-icon icon="menu"></app-icon>
</button>
</template>
<div class="dropdown-item">
<router-link :to="{name: 'new_log', params: { recipeId: r.id } }" class="button is-primary is-fullwidth">
<app-icon icon="star" size="md"></app-icon> <span>Add Log Entry</span>
</router-link>
<tbody>
<tr v-for="r in recipes" :key="r.id">
<td><router-link :to="{name: 'recipe', params: { id: r.id } }">{{r.name}}</router-link></td>
<td>
<div class="tags">
<span class="tag" v-for="tag in r.tags" :key="tag">{{tag}}</span>
</div>
<div class="dropdown-item">
<router-link :to="{name: 'edit_recipe', params: { id: r.id } }" class="button is-primary is-fullwidth">
<app-icon icon="pencil" size="md"></app-icon> <span>Edit Recipe</span>
</router-link>
</td>
<td>
<app-rating v-if="r.rating !== null" :value="r.rating" readonly></app-rating>
<span v-else>--</span>
</td>
<td>{{ r.yields }}</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>
<div class="field is-grouped">
<div class="control">
<router-link v-if="isLoggedIn" :to="{name: 'new_log', params: { recipeId: r.id } }" class="button is-primary">
<app-icon icon="star" size="md"></app-icon>
</router-link>
</div>
<div class="control">
<router-link v-if="isLoggedIn" :to="{name: 'edit_recipe', params: { id: r.id } }" class="button is-primary">
<app-icon icon="pencil" size="md"></app-icon>
</router-link>
</div>
<div class="control">
<button v-if="isLoggedIn" type="button" class="button is-danger" @click="deleteRecipe(r)">
<app-icon icon="x" size="md"></app-icon>
</button>
</div>
</div>
<div class="dropdown-item">
<button type="button" class="button is-danger is-fullwidth" @click="deleteRecipe(r)">
<app-icon icon="x" size="md"></app-icon> <span>Delete Recipe</span>
</button>
</div>
</app-dropdown>
</td>
</tr>
</transition-group>
</td>
</tr>
</tbody>
</table>
<div v-if="!localLoading && recipes.length === 0">
No Recipes
</div>
<app-pager :current-page="currentPage" :total-pages="totalPages" paged-item-name="recipe" @changePage="changePage"></app-pager>
<app-confirm :open="showConfirmRecipeDelete" title="Delete Recipe?" :message="confirmRecipeDeleteMessage" @cancel="recipeDeleteCancel" @confirm="recipeDeleteConfirm"></app-confirm>
<app-confirm :open="showConfirmRecipeDelete" :message="confirmRecipeDeleteMessage" :cancel="recipeDeleteCancel" :confirm="recipeDeleteConfirm"></app-confirm>
</div>
</template>
<script setup>
<script>
import { computed, reactive, ref, watch } from "vue";
import { useRouter } from 'vue-router'
import api from "../lib/Api";
import AppLoading from "./AppLoading";
import { useAppConfigStore } from "../stores/appConfig";
import { useMediaQueryStore } from "../stores/mediaQuery";
import { useLoadResource } from "../lib/useLoadResource";
import debounce from "lodash/debounce";
const appConfig = useAppConfigStore();
const mediaQueries = useMediaQueryStore();
const { loadResource, localLoading } = useLoadResource();
const router = useRouter();
export default {
data() {
return {
recipeData: null,
recipeForDeletion: null,
search: {
sortColumn: 'created_at',
sortDirection: 'desc',
page: 1,
per: 25,
name: null,
tags: null
}
};
},
const props = defineProps({
searchQuery: {
type: Object,
required: false,
default: {}
}
});
computed: {
recipes() {
if (this.recipeData) {
return this.recipeData.recipes;
} else {
return [];
}
},
const tableHeader = [
{name: 'name', label: 'Name', sort: true},
{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}
];
tableHeader() {
return [
{name: 'name', label: 'Name', sort: true},
{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}
]
},
const recipeData = ref(null);
const recipeForDeletion = ref(null);
const isTouch = computed(() => mediaQueries.touch);
totalPages() {
if (this.recipeData) {
return this.recipeData.total_pages;
}
return 0;
},
const search = computed(() => ({
name: props.searchQuery.name || null,
tags: props.searchQuery.tags || null,
column: props.searchQuery.column || "created_at",
direction: props.searchQuery.direction || "desc",
page: props.searchQuery.page || 1,
per: props.searchQuery.per || 25
}));
currentPage() {
if (this.recipeData) {
return this.recipeData.current_page;
}
return 0;
},
const recipes = computed(() => {
if (recipeData.value) {
return recipeData.value.recipes;
} else {
return [];
}
});
showConfirmRecipeDelete() {
return this.recipeForDeletion !== null;
},
const totalPages = computed(() => {
if (recipeData.value) {
return recipeData.value.total_pages;
}
return 0;
});
const currentPage = computed(() => {
if (recipeData.value) {
return recipeData.value.current_page;
}
return 0;
});
const showConfirmRecipeDelete = computed(() => recipeForDeletion.value !== null);
const confirmRecipeDeleteMessage = computed(() => {
if (showConfirmRecipeDelete.value) {
return `Are you sure you want to delete ${recipeForDeletion.value.name}?`;
} else {
return "??";
}
});
watch(search, () => {
getList().then(() => appConfig.initialLoad = true);
}, {
deep: true,
immediate: true
});
function getList() {
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)
);
}
function buildQueryParams() {
return {
name: props.searchQuery.name,
tags: props.searchQuery.tags,
column: props.searchQuery.column,
direction: props.searchQuery.direction,
page: props.searchQuery.page,
per: props.searchQuery.per
}
}
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;
confirmRecipeDeleteMessage() {
if (this.showConfirmRecipeDelete) {
return `Are you sure you want to delete ${this.recipeForDeletion.name}?`;
} else {
return "??";
}
}
},
methods: {
changePage(idx) {
this.search.page = idx;
},
setSort(col) {
if (this.search.sortColumn === col) {
this.search.sortDirection = this.search.sortDirection === "desc" ? "asc" : "desc";
} else {
this.search.sortColumn = col;
this.search.sortDirection = "asc";
}
},
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: debounce(function() {
return this.loadResource(
api.getRecipeList(this.search.page, this.search.per, this.search.sortColumn, this.search.sortDirection, this.search.name, this.search.tags, data => this.recipeData = data)
);
}, 500, {leading: true, trailing: true}),
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(),
{
deep: true,
immediate: true
}
);
},
components: {
}
return str;
}
</script>
<style lang="scss" scoped>
.recipe-time {
white-space: nowrap;
}
.table th {
white-space: nowrap;
}
.table.small {
td, th {
&:nth-of-type(3), &:nth-of-type(4), &:nth-of-type(5), &:nth-of-type(6) {
display: none;
}
}
}
</style>

View File

@ -1,94 +0,0 @@
<template>
<div>
<h1 class="title is-3">
Tasks
<app-dropdown button-class="is-primary" :open="showListDropdown" :label="listSelectLabel" @open="showListDropdown = true" @close="showListDropdown = false">
<task-list-dropdown-item v-for="l in taskLists" :key="l.id" :task-list="l" :active="currentTaskList !== null && currentTaskList.id === l.id" @select="selectList" @delete="deleteList"></task-list-dropdown-item>
<hr class="dropdown-divider" v-if="taskLists.length > 0">
<div class="dropdown-item">
<task-list-mini-form :task-list="newList" :validation-errors="newListValidationErrors" @save="saveNewList"></task-list-mini-form>
</div>
</app-dropdown>
</h1>
<div class="columns" v-if="currentTaskList !== null">
<div class="column is-6-widescreen is-8-desktop is-10-tablet">
<task-item-list :task-list="currentTaskList"></task-item-list>
</div>
</div>
</div>
</template>
<script setup>
import { computed, onBeforeMount, ref } from "vue";
import * as Errors from '../lib/Errors';
import { useTaskStore } from "../stores/task";
import TaskListMiniForm from "./TaskListMiniForm";
import TaskListDropdownItem from "./TaskListDropdownItem";
import TaskItemList from "./TaskItemList";
import { useLoadResource } from "../lib/useLoadResource";
const newListTemplate = function() {
return {
name: ''
};
};
const { loadResource } = useLoadResource();
const taskStore = useTaskStore();
const showListDropdown = ref(false);
const newList = ref(newListTemplate());
const newListValidationErrors = ref({});
const taskLists = computed(() => taskStore.taskLists);
const currentTaskList = computed(() => taskStore.currentTaskList);
const listSelectLabel = computed(() => {
if (currentTaskList.value === null) {
return "Select or Create a List";
} else {
return currentTaskList.value.name;
}
});
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>
<style lang="scss" scoped>
</style>

View File

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

View File

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

View File

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

View File

@ -7,10 +7,10 @@
<app-modal title="Login" :open="showLogin" @dismiss="showLogin = false">
<div>
<form @submit.prevent="performLogin">
<form @submit.prevent="login">
<div v-if="loginMessage" class="notification is-danger">
{{loginMessage}}
<div v-if="error" class="notification is-danger">
{{error}}
</div>
<div class="field">
@ -34,7 +34,7 @@
<button type="submit" class="button is-primary" :disabled="!enableSubmit">Login</button>
</div>
<div class="control">
<button type="button" class="button is-secondary" @click="showLogin = false">Cancel</button>
<button class="button is-secondary" @click="showLogin = false">Cancel</button>
</div>
</div>
@ -45,44 +45,52 @@
</div>
</template>
<script setup>
<script>
import { computed, nextTick, ref, useTemplateRef } from "vue";
import { useAppConfigStore } from "../stores/appConfig";
import { useLoadResource } from "../lib/useLoadResource";
import api from "../lib/Api";
import { mapMutations } from "vuex";
const appConfig = useAppConfigStore();
const { loadResource } = useLoadResource();
export default {
data() {
return {
showLogin: false,
error: '',
username: '',
password: ''
};
},
const userNameElement = useTemplateRef("usernameInput");
computed: {
enableSubmit() {
return this.username !== '' && this.password !== '' && !this.isLoading;
}
},
const showLogin = ref(false);
const error = ref('');
const username = ref("");
const password = ref("");
methods: {
...mapMutations([
'setUser'
]),
const loginMessage = computed(() => appConfig.loginMessage);
const enableSubmit = computed(() => username.value !== "" && password.value !== "" && !appConfig.isLoading);
openDialog() {
this.showLogin = true;
this.$nextTick(() => this.$refs.usernameInput.focus());
},
function openDialog() {
showLogin.value = true;
nextTick(() => {
userNameElement.value.focus();
})
}
login() {
if (this.username !== '' && this.password !== '') {
this.loadResource(api.postLogin(this.username, this.password).then(data => {
if (data.success) {
this.setUser(data.user);
this.showLogin = false;
} else {
this.error = data.message;
}
}));
}
}
},
function performLogin() {
if (username.value !== '' && password.value !== '') {
const params = {username: username.value, password: password.value};
loadResource(
appConfig.login(params)
.then(data => {
if (data.success) {
showLogin.value = false;
}
})
);
components: {
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,44 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" data-icon="account" width="128" height="128" class="iconic iconic-account" viewBox="0 0 128 128">
<g><title>Sign In &amp; Out</title></g>
<g data-width="128" data-height="128" class="iconic-lg iconic-container" display="inline">
<g class="iconic-account-login">
<path stroke="#000" stroke-width="8" stroke-linecap="square" stroke-miterlimit="10" class="iconic-account-login-entrance iconic-property-accent iconic-property-stroke" d="M46 4h78v120h-78" fill="none"/>
<path stroke="#000" stroke-width="8" stroke-linecap="square" stroke-miterlimit="10" class="iconic-account-login-arrow iconic-account-login-arrow-line iconic-property-stroke" d="M4 64h76" fill="none"/>
<path d="M90.7 63.2l-21.4-12.4c-.7-.4-1.3-.1-1.3.7v25c0 .8.6 1.2 1.3.7l21.4-12.5c.7-.4.7-1 0-1.5z" class="iconic-account-login-arrow iconic-account-login-arrow-head iconic-property-fill"/>
</g>
<g class="iconic-account-logout" display="none">
<path stroke="#000" stroke-width="8" stroke-linecap="square" stroke-miterlimit="10" class="iconic-account-logout-entrance iconic-property-accent iconic-property-stroke" d="M46 4h78v120h-78" fill="none"/>
<path stroke="#000" stroke-width="8" stroke-linecap="square" stroke-miterlimit="10" class="iconic-account-logout-arrow iconic-account-logout-arrow-line iconic-property-stroke" d="M88 64h-76" fill="none"/>
<path d="M1.3 64.8l21.4 12.5c.7.4 1.3.1 1.3-.7v-25c0-.8-.6-1.2-1.3-.7l-21.4 12.3c-.7.5-.7 1.1 0 1.6z" class="iconic-account-logout-arrow iconic-account-logout-arrow-head iconic-property-fill"/>
</g>
</g>
<g data-width="32" data-height="31" class="iconic-md iconic-container" display="none" transform="scale(4)">
<g class="iconic-account-login">
<path stroke="#000" stroke-width="3" stroke-linecap="square" stroke-miterlimit="10" class="iconic-account-login-entrance iconic-property-accent iconic-property-stroke" d="M12.5 1.5h18v28h-18" fill="none"/>
<path stroke="#000" stroke-width="3" stroke-linecap="square" stroke-miterlimit="10" class="iconic-account-login-arrow iconic-account-login-arrow-line iconic-property-stroke" d="M1.5 15.5h14.5" fill="none"/>
<path d="M20.6 15.8l-5.2 3.9c-.2.2-.4.1-.4-.2v-8c0-.3.2-.4.4-.2l5.2 3.9c.2.2.2.4 0 .6z" class="iconic-account-login-arrow iconic-account-login-arrow-head iconic-property-fill"/>
</g>
<g class="iconic-account-logout" display="none">
<path stroke="#000" stroke-width="3" stroke-linecap="square" stroke-miterlimit="10" class="iconic-account-logout-entrance iconic-property-accent iconic-property-stroke" d="M12.5 1.5h18v28h-18" fill="none"/>
<path stroke="#000" stroke-width="3" stroke-linecap="square" stroke-miterlimit="10" class="iconic-account-logout-arrow iconic-account-logout-arrow-line iconic-property-stroke" d="M19.5 15.5h-14.5" fill="none"/>
<path d="M.4 15.2l5.2-3.9c.2-.2.4-.1.4.2v8c0 .3-.2.4-.4.2l-5.2-3.9c-.2-.2-.2-.4 0-.6z" class="iconic-account-logout-arrow iconic-account-logout-arrow-head iconic-property-fill"/>
</g>
</g>
<g data-width="16" data-height="16" class="iconic-sm iconic-container" display="none" transform="scale(8)">
<g class="iconic-account-login">
<path stroke="#000" stroke-width="2" stroke-linecap="square" stroke-miterlimit="10" class="iconic-account-login-entrance iconic-property-accent iconic-property-stroke" d="M7 1h8v14h-8" fill="none"/>
<path stroke="#000" stroke-width="2" stroke-linecap="square" stroke-miterlimit="10" class="iconic-account-login-arrow iconic-account-login-arrow-line iconic-property-stroke" d="M1 8h9" fill="none"/>
<path class="iconic-account-login-arrow iconic-account-login-arrow-head iconic-property-fill" d="M12 8l-4-4v8z"/>
</g>
<g class="iconic-account-logout" display="none">
<path stroke="#000" stroke-width="2" stroke-linecap="square" stroke-miterlimit="10" class="iconic-account-logout-entrance iconic-property-accent iconic-property-stroke" d="M7 1h8v14h-8" fill="none"/>
<path stroke="#000" stroke-width="2" stroke-linecap="square" stroke-miterlimit="10" class="iconic-account-logout-arrow iconic-account-logout-arrow-line iconic-property-stroke" d="M11 8h-9" fill="none"/>
<path class="iconic-account-logout-arrow iconic-account-logout-arrow-head iconic-property-fill" d="M0 8l4 4v-8z"/>
</g>
</g>
<script class="iconic-api" type="application/ecmascript">
//<![CDATA[
var iconicApis=window._Iconic?window._Iconic.smartIconApis:{};iconicApis["account"]=iconicApis["account"]||function(t){var i=function(i){t.setAttribute("data-state",i);var e,n=t.querySelectorAll(".iconic-account-login"),o=t.querySelectorAll(".iconic-account-logout");if("logout"===i){for(e=0;e<n.length;e++)n[e].setAttribute("display","none");for(e=0;e<o.length;e++)o[e].setAttribute("display","inline")}else{for(e=0;e<n.length;e++)n[e].setAttribute("display","inline");for(e=0;e<o.length;e++)o[e].setAttribute("display","none")}},e=function(){var e=t.getAttribute("data-state");e&&i(e)};return{update:e,setState:i}};
//]]>
</script>
</svg>

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,38 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" data-icon="action" width="128" height="128" class="iconic iconic-action" viewBox="0 0 128 128">
<g><title>Action</title></g>
<g data-width="128" data-height="96" class="iconic-lg iconic-container" display="inline" transform="translate(0 16)">
<g class="iconic-action-undo">
<path d="M30 59c0-27.1 21.9-49 49-49s49 21.9 49 49c0-32.6-26.4-59-59-59s-59 26.4-59 59v11h20v-11z" class="iconic-action-undo-stem iconic-property-fill"/>
<path d="M19.2 94.7l-18.4-29.4c-.4-.7-.1-1.3.7-1.3h37c.8 0 1.1.6.7 1.3l-18.4 29.4c-.4.7-1.2.7-1.6 0z" class="iconic-action-undo-arrowhead iconic-property-fill"/>
</g>
<g class="iconic-action-redo" display="none">
<path d="M98 59c0-27.1-21.9-49-49-49s-49 21.9-49 49c0-32.6 26.4-59 59-59s59 26.4 59 59v11h-20v-11z" class="iconic-action-redo-stem iconic-property-fill"/>
<path d="M108.8 94.7l18.4-29.5c.4-.7.1-1.3-.7-1.3h-37c-.8 0-1.1.6-.7 1.3l18.4 29.5c.4.7 1.2.7 1.6 0z" class="iconic-action-redo-arrowhead iconic-property-fill"/>
</g>
</g>
<g data-width="32" data-height="24" class="iconic-md iconic-container" display="none" transform="scale(4) translate(0 4)">
<g class="iconic-action-undo">
<path d="M9 14.5c0-6.4 5.1-11.5 11.5-11.5 6 0 10.9 4.7 11.4 10.5-.6-7.5-6.8-13.5-14.4-13.5-8 0-14.5 6.5-14.5 14.5v2.5h6v-2.5z" class="iconic-action-undo-stem iconic-property-fill"/>
<path d="M6.3 23.6l5.4-7.2c.2-.2.1-.4-.2-.4h-11c-.3 0-.4.2-.2.4l5.4 7.2c.2.2.4.2.6 0z" class="iconic-action-undo-arrowhead iconic-property-fill"/>
</g>
<g class="iconic-action-redo" display="none">
<path d="M23 14.5c0-6.4-5.1-11.5-11.5-11.5-6 0-10.9 4.7-11.5 10.5.6-7.5 6.9-13.5 14.5-13.5 8 0 14.5 6.5 14.5 14.5v2.5h-6v-2.5z" class="iconic-action-redo-stem iconic-property-fill"/>
<path d="M25.7 23.6l-5.4-7.2c-.2-.2-.1-.4.2-.4h11c.3 0 .4.2.2.4l-5.4 7.2c-.2.2-.4.2-.6 0z" class="iconic-action-redo-arrowhead iconic-property-fill"/>
</g>
</g>
<g data-width="16" data-height="12" class="iconic-sm iconic-container" display="none" transform="scale(8) translate(0 2)">
<g class="iconic-action-undo">
<path d="M2 9v-2c0-3.9 3.1-7 7-7s7 3.1 7 7c0-2.8-2.2-5-5-5s-5 2.2-5 5v2h-4z" class="iconic-action-undo-stem iconic-property-fill"/>
<path class="iconic-action-undo-arrowhead iconic-property-fill" d="M0 8l4 4 4-4z"/>
</g>
<g class="iconic-action-redo" display="none">
<path d="M10 9v-2c0-2.8-2.2-5-5-5s-5 2.2-5 5c0-3.9 3.1-7 7-7s7 3.1 7 7v2h-4z" class="iconic-action-redo-stem iconic-property-fill"/>
<path class="iconic-action-redo-arrowhead iconic-property-fill" d="M8 8l4 4 4-4z"/>
</g>
</g>
<script class="iconic-api" type="application/ecmascript">
//<![CDATA[
var iconicApis=window._Iconic?window._Iconic.smartIconApis:{};iconicApis["action"]=iconicApis["action"]||function(t){var i=function(i){t.setAttribute("data-state",i);var n,e,o=t.querySelectorAll(".iconic-action-undo"),c=t.querySelectorAll(".iconic-action-redo");n=e="none","redo"===i?e="inline":n="inline";for(var a=0;a<o.length;a++)o[a].setAttribute("display",n),c[a].setAttribute("display",e)},n=function(){var n=t.getAttribute("data-state");n&&i(n)};return{update:n,setState:i}};
//]]>
</script>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,68 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" data-icon="align" width="128" height="128" class="iconic iconic-align" viewBox="0 0 128 128" preserveAspectRatio="xMidYMid meet">
<g><title>Align</title></g>
<g data-width="128" data-height="86" class="iconic-lg iconic-container" display="inline" transform="translate(0 21)" stroke="#000" stroke-width="8" stroke-linecap="square" stroke-miterlimit="10" fill="none">
<g class="iconic-align-left">
<path class="iconic-align-left-line-4 iconic-align-left-line-short iconic-property-stroke" d="M4 82h82"/>
<path class="iconic-align-left-line-3 iconic-align-left-line-long iconic-property-stroke" d="M4 56h120"/>
<path class="iconic-align-left-line-2 iconic-align-left-line-short iconic-property-stroke" d="M4 30h82"/>
<path class="iconic-align-left-line-1 iconic-align-left-line-long iconic-property-stroke" d="M4 4h120"/>
</g>
<g class="iconic-align-center" display="none">
<path class="iconic-align-center-line-4 iconic-align-center-line-short iconic-property-stroke" d="M23 82h82"/>
<path class="iconic-align-center-line-3 iconic-align-center-line-long iconic-property-stroke" d="M4 56h120"/>
<path class="iconic-align-center-line-2 iconic-align-center-line-short iconic-property-stroke" d="M23 30h82"/>
<path class="iconic-align-center-line-1 iconic-align-center-line-long iconic-property-stroke" d="M4 4h120"/>
</g>
<g class="iconic-align-right" display="none">
<path class="iconic-align-right-line-4 iconic-align-right-line-short iconic-property-stroke" d="M42 82h82"/>
<path class="iconic-align-right-line-3 iconic-align-right-line-long iconic-property-stroke" d="M4 56h120"/>
<path class="iconic-align-right-line-2 iconic-align-right-line-short iconic-property-stroke" d="M42 30h82"/>
<path class="iconic-align-right-line-1 iconic-align-right-line-long iconic-property-stroke" d="M4 4h120"/>
</g>
</g>
<g data-width="32" data-height="27" class="iconic-md iconic-container" display="none" transform="scale(4) translate(0 2)" stroke="#000" stroke-width="3" stroke-linecap="square" stroke-miterlimit="10" fill="none">
<g class="iconic-align-left" display="inline">
<path class="iconic-align-left-line-4 iconic-align-left-line-short iconic-property-stroke" d="M1.5 25.5h21"/>
<path class="iconic-align-left-line-3 iconic-align-left-line-long iconic-property-stroke" d="M1.5 17.5h29"/>
<path class="iconic-align-left-line-2 iconic-align-left-line-short iconic-property-stroke" d="M1.5 9.5h21"/>
<path class="iconic-align-left-line-1 iconic-align-left-line-long iconic-property-stroke" d="M1.5 1.5h29"/>
</g>
<g class="iconic-align-center" display="none">
<path class="iconic-align-center-line-4 iconic-align-center-line-short iconic-property-stroke" d="M5.5 25.5h21"/>
<path class="iconic-align-center-line-3 iconic-align-center-line-long iconic-property-stroke" d="M1.5 17.5h29"/>
<path class="iconic-align-center-line-2 iconic-align-center-line-short iconic-property-stroke" d="M5.5 9.5h21"/>
<path class="iconic-align-center-line-1 iconic-align-center-line-long iconic-property-stroke" d="M1.5 1.5h29"/>
</g>
<g class="iconic-align-right" display="none">
<path class="iconic-align-right-line-4 iconic-align-right-line-short iconic-property-stroke" d="M9.5 25.5h21"/>
<path class="iconic-align-right-line-3 iconic-align-right-line-long iconic-property-stroke" d="M1.5 17.5h29"/>
<path class="iconic-align-right-line-2 iconic-align-right-line-short iconic-property-stroke" d="M9.5 9.5h21"/>
<path class="iconic-align-right-line-1 iconic-align-right-line-long iconic-property-stroke" d="M1.5 1.5h29"/>
</g>
</g>
<g data-width="16" data-height="14" class="iconic-sm iconic-container" display="none" transform="scale(8) translate(0 1)" stroke="#000" stroke-width="2" stroke-linecap="square" stroke-miterlimit="10" fill="none">
<g class="iconic-align-left" display="inline">
<path class="iconic-align-left-line-4 iconic-align-left-line-short iconic-property-stroke" d="M1 13h10"/>
<path class="iconic-align-left-line-3 iconic-align-left-line-long iconic-property-stroke" d="M1 9h14"/>
<path class="iconic-align-left-line-2 iconic-align-left-line-short iconic-property-stroke" d="M1 5h10"/>
<path class="iconic-align-left-line-1 iconic-align-left-line-long iconic-property-stroke" d="M1 1h14"/>
</g>
<g class="iconic-align-center" display="none">
<path class="iconic-align-center-line-4 iconic-align-center-line-short iconic-property-stroke" d="M3 13h10"/>
<path class="iconic-align-center-line-3 iconic-align-center-line-long iconic-property-stroke" d="M1 9h14"/>
<path class="iconic-align-center-line-2 iconic-align-center-line-short iconic-property-stroke" d="M3 5h10"/>
<path class="iconic-align-center-line-1 iconic-align-center-line-long iconic-property-stroke" d="M1 1h14"/>
</g>
<g class="iconic-align-right" display="none">
<path class="iconic-align-right-line-4 iconic-align-right-line-short iconic-property-stroke" d="M5 13h10"/>
<path class="iconic-align-right-line-3 iconic-align-right-line-long iconic-property-stroke" d="M1 9h14"/>
<path class="iconic-align-right-line-2 iconic-align-right-line-short iconic-property-stroke" d="M5 5h10"/>
<path class="iconic-align-right-line-1 iconic-align-right-line-long iconic-property-stroke" d="M1 1h14"/>
</g>
</g>
<script class="iconic-api" type="application/ecmascript">
//<![CDATA[
var iconicApis=window._Iconic?window._Iconic.smartIconApis:{};iconicApis["align"]=iconicApis["align"]||function(t){var i=function(i){t.setAttribute("data-text-alignment",i);var n,e,c,o=t.querySelectorAll(".iconic-align-left"),a=t.querySelectorAll(".iconic-align-center"),r=t.querySelectorAll(".iconic-align-right");switch(n=e=c="none",i){case"center":n="inline";break;case"right":c="inline";break;default:e="inline"}for(var l=0;l<o.length;l++)o[l].setAttribute("display",e),a[l].setAttribute("display",n),r[l].setAttribute("display",c)},n=function(){var n=t.getAttribute("data-text-alignment");n&&i(n)};return{update:n,setTextAlignment:i}};
//]]>
</script>
</svg>

Before

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -1,29 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" class="iconic iconic-aperture" width="128" height="128" viewBox="0 0 128 128">
<g class="iconic-metadata">
<title>Aperture</title>
</g>
<g class="iconic-aperture-lg iconic-container iconic-lg" data-width="128" data-height="128" display="inline">
<path d="M48.989 86l-35.43-61.367c-8.487 10.859-13.559 24.516-13.559 39.367 0 7.73 1.371 15.14 3.882 22h45.107z" class="iconic-aperture-blade iconic-aperture-blade-6 iconic-property-fill" />
<path d="M75.547 88h-70.877c8.17 20.176 26.322 35.227 48.337 39.04l22.54-39.04z" class="iconic-aperture-blade iconic-aperture-blade-5 iconic-property-fill" />
<path d="M90.558 66l-35.434 61.373c2.902.403 5.862.627 8.875.627 19.725 0 37.358-8.929 49.099-22.959l-22.54-39.041z" class="iconic-aperture-blade iconic-aperture-blade-4 iconic-property-fill" />
<path d="M79.011 42l35.43 61.367c8.487-10.859 13.559-24.516 13.559-39.367 0-7.73-1.371-15.14-3.882-22h-45.107z" class="iconic-aperture-blade iconic-aperture-blade-3 iconic-property-fill" />
<path d="M52.453 40h70.877c-8.17-20.176-26.322-35.227-48.337-39.04l-22.54 39.04z" class="iconic-aperture-blade iconic-aperture-blade-2 iconic-property-fill" />
<path d="M37.442 62l35.434-61.373c-2.902-.403-5.862-.627-8.875-.627-19.725 0-37.358 8.929-49.098 22.959l22.54 39.041z" class="iconic-aperture-blade iconic-aperture-blade-1 iconic-property-fill" />
</g>
<g class="iconic-aperture-md iconic-container iconic-md" data-width="32" data-height="32" display="none" transform="scale(4)">
<path d="M11.959 21l-8.563-14.832c-2.12 2.714-3.395 6.121-3.395 9.832 0 1.748.29 3.426.808 5h11.151z" class="iconic-aperture-blade iconic-aperture-blade-6 iconic-property-fill" />
<path d="M18.309 22h-17.136c1.978 4.883 6.289 8.569 11.556 9.663l5.579-9.663z" class="iconic-aperture-blade iconic-aperture-blade-5 iconic-property-fill" />
<path d="M22.351 17l-8.562 14.829c.725.1 1.459.17 2.211.17 4.743 0 8.99-2.075 11.92-5.354l-5.569-9.646z" class="iconic-aperture-blade iconic-aperture-blade-4 iconic-property-fill" />
<path d="M20.041 11l8.563 14.832c2.12-2.714 3.395-6.121 3.395-9.832 0-1.748-.29-3.426-.808-5h-11.151z" class="iconic-aperture-blade iconic-aperture-blade-3 iconic-property-fill" />
<path d="M13.691 10h17.135c-1.978-4.883-6.289-8.569-11.556-9.663l-5.579 9.663z" class="iconic-aperture-blade iconic-aperture-blade-2 iconic-property-fill" />
<path d="M9.649 15l8.562-14.829c-.725-.1-1.459-.17-2.211-.17-4.743 0-8.99 2.075-11.92 5.354l5.569 9.646z" class="iconic-aperture-blade iconic-aperture-blade-1 iconic-property-fill" />
</g>
<g class="iconic-aperture-sm iconic-container iconic-sm" data-width="16" data-height="16" display="none" transform="scale(8)">
<path d="M5.691 10l-3.993-6.916c-1.06 1.357-1.698 3.061-1.698 4.916 0 .692.097 1.36.262 2h5.428z" class="iconic-aperture-blade iconic-aperture-blade-6 iconic-property-fill" />
<path d="M8.577 11h-7.99c.925 2.283 2.872 4.037 5.276 4.702l2.715-4.702z" class="iconic-aperture-blade iconic-aperture-blade-5 iconic-property-fill" />
<path d="M10.887 9l-3.992 6.915c.362.05.729.085 1.105.085 2.183 0 4.16-.877 5.603-2.295l-2.717-4.705z" class="iconic-aperture-blade iconic-aperture-blade-4 iconic-property-fill" />
<path d="M14.302 12.916c1.06-1.357 1.698-3.061 1.698-4.916 0-.692-.097-1.36-.262-2h-5.428l3.993 6.916z" class="iconic-aperture-blade iconic-aperture-blade-3 iconic-property-fill" />
<path d="M7.423 5h7.99c-.925-2.283-2.872-4.037-5.275-4.702l-2.715 4.702z" class="iconic-aperture-blade iconic-aperture-blade-2 iconic-property-fill" />
<path d="M5.113 7l3.992-6.915c-.362-.05-.729-.085-1.105-.085-2.183 0-4.16.877-5.603 2.294l2.717 4.705z" class="iconic-aperture-blade iconic-aperture-blade-1 iconic-property-fill" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1,29 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" data-icon="arrow-right-angle-thick" width="128" height="128" class="iconic iconic-arrow-right-angle-thick" viewBox="0 0 128 128">
<g><title>Arrow 90 Degree Thick</title></g>
<g data-width="128" data-height="128" data-default-max-height="92" class="iconic-lg iconic-container" display="inline" transform="translate(0 16)">
<path stroke="#000" stroke-width="16" stroke-linecap="square" stroke-miterlimit="10" class="iconic-arrow-right-angle-thick-stem iconic-property-stroke" d="M102 28h-94v58" fill="none"/>
<path d="M126.7 27.2l-33.4-20.4c-.7-.4-1.3-.1-1.3.7v41c0 .8.6 1.1 1.3.7l33.4-20.4c.7-.4.7-1.2 0-1.6z" class="iconic-arrow-right-angle-thick-head iconic-arrow-right-angle-thick-head-fill iconic-property-fill"/>
<path d="M126.6 27.4l-45.2-20.8c-.8-.3-1.4 0-1.4.9v41c0 .8.6 1.2 1.4.9l45.3-20.8c.7-.3.7-.9-.1-1.2z" class="iconic-arrow-right-angle-thick-head iconic-arrow-right-angle-thick-head-fill-acute iconic-property-fill" display="none"/>
<path d="M126.6 27.4l-47.2-20.8c-.8-.3-1.1 0-.9.8l7 19.2c.3.8.3 2 0 2.8l-7 19.2c-.3.8.1 1.1.9.8l47.3-20.8c.7-.3.7-.9-.1-1.2z" class="iconic-arrow-right-angle-thick-head iconic-arrow-right-angle-thick-head-fill-angled iconic-property-fill" display="none"/>
<path d="M127.6 28.4c.2-.2.2-.5 0-.7l-25.2-25.3c-.2-.2-.6-.4-.9-.4h-17c-.3 0-.3.2-.1.4l25.3 25.3c.2.2.2.5 0 .7l-25.3 25.2c-.2.2-.1.4.1.4h17c.3 0 .7-.2.9-.4l25.2-25.2z" class="iconic-arrow-right-angle-thick-head iconic-arrow-right-angle-thick-head-line iconic-property-fill" display="none"/>
</g>
<g data-width="32" data-height="32" data-default-max-height="26" class="iconic-md iconic-container" display="none" transform="scale(4) translate(0 2)">
<path stroke="#000" stroke-width="6" stroke-linecap="square" stroke-miterlimit="10" class="iconic-arrow-right-angle-thick-stem iconic-property-stroke" d="M23 9h-20v14" fill="none"/>
<path d="M31.6 9.3l-8.2 6.4c-.2.2-.4.1-.4-.2v-13c0-.3.2-.4.4-.2l8.2 6.4c.2.2.2.4 0 .6z" class="iconic-arrow-right-angle-thick-head iconic-arrow-right-angle-thick-head-fill iconic-property-fill"/>
<path d="M31.6 9.3l-11.1 6.5c-.2.1-.4 0-.4-.2v-13c0-.3.2-.4.4-.2l11.1 6.5c.2 0 .2.2 0 .4z" class="iconic-arrow-right-angle-thick-head iconic-arrow-right-angle-thick-head-fill-acute iconic-property-fill" display="none"/>
<path d="M31.6 9.2l-13.1 6.6c-.2.1-.4 0-.3-.2l2.6-6.1c.1-.3.1-.7 0-.9l-2.6-6.1c-.1-.3 0-.4.3-.2l13.1 6.6c.2 0 .2.2 0 .3z" class="iconic-arrow-right-angle-thick-head iconic-arrow-right-angle-thick-head-fill-angled iconic-property-fill" display="none"/>
<path class="iconic-arrow-right-angle-thick-head iconic-arrow-right-angle-thick-head-line iconic-property-fill" d="M32 9l-9-9h-6l9 9-9 9h6z" display="none"/>
</g>
<g data-width="16" data-height="16" data-default-max-height="14" class="iconic-sm iconic-container" display="none" transform="scale(8) translate(0 1)">
<path stroke="#000" stroke-width="4" stroke-linecap="square" stroke-miterlimit="10" class="iconic-arrow-right-angle-thick-stem iconic-property-stroke" d="M10.5 5h-8.5v7" fill="none"/>
<path class="iconic-arrow-right-angle-thick-head iconic-arrow-right-angle-thick-head-fill iconic-property-fill" d="M16 5l-5 5v-10z"/>
<path class="iconic-arrow-right-angle-thick-head iconic-arrow-right-angle-thick-head-fill-acute iconic-property-fill" d="M16 5l-8 5v-10z" display="none"/>
<path class="iconic-arrow-right-angle-thick-head iconic-arrow-right-angle-thick-head-fill-angled iconic-property-fill" d="M16 5l-9 5 2-5-2-5z" display="none"/>
<path class="iconic-arrow-right-angle-thick-head iconic-arrow-right-angle-thick-head-line iconic-property-fill" d="M16 5l-5-5h-4l5 5-5 5h4z" display="none"/>
</g>
<script class="iconic-api" type="application/ecmascript">
//<![CDATA[
var iconicApis=window._Iconic?window._Iconic.smartIconApis:{};iconicApis["arrow-right-angle-thick"]=iconicApis["arrow-right-angle-thick"]||function(t){function e(e){t.setAttribute("data-head",e);for(var a=t.querySelectorAll(".iconic-arrow-right-angle-thick-head"),r=0;r<a.length;r++)a[r].setAttribute("display","none");var i=".iconic-arrow-right-angle-thick-head-"+e,n=t.querySelectorAll(i);n.length||(n=t.querySelectorAll(".iconic-arrow-right-angle-thick-head-fill"));for(var o=0;o<n.length;o++)n[o].setAttribute("display","inline")}function a(e){t.setAttribute("data-direction",e);var a,o,c;switch(e){case"bottom-left":a=["scale(-1 1)","rotate(0 64 64)","translate(-128 16)"],o=["scale(-4 4)","rotate(0 16 16)","translate(-32 2)"],c=["scale(-8 8)","rotate(0 8 8)","translate(-16 1)"];break;case"top-left":a=["rotate(180 64 64)","translate(0 16)"],o=["scale(4)","rotate(180 16 16)","translate(0 2)"],c=["scale(8)","rotate(180 8 8)","translate(0 1)"];break;case"top-right":a=["scale(-1 1)","rotate(180 64 64)","translate(128 16)"],o=["scale(-4 4)","rotate(180 16 16)","translate(32 2)"],c=["scale(-8 8)","rotate(180 8 8)","translate(16 1)"];break;case"right-top":a=["rotate(270 64 64)","translate(0 16)"],o=["scale(4)","rotate(270 16 16)","translate(0 2)"],c=["scale(8)","rotate(270 8 8)","translate(0 1)"];break;case"right-bottom":a=["scale(-1 1)","rotate(90 64 64)","translate(0 144)"],o=["scale(-4 4)","rotate(90 16 16)","translate(0 34)"],c=["scale(-8 8)","rotate(90 8 8)","translate(0 17)"];break;case"left-top":a=["scale(-1 1)","rotate(270 64 64)","translate(0 -112)"],o=["scale(-4 4)","rotate(270 16 16)","translate(0 -30)"],c=["scale(-8 8)","rotate(270 8 8)","translate(0 -15)"];break;case"left-bottom":a=["rotate(90 64 64)","translate(0 16)"],o=["scale(4)","rotate(90 16 16)","translate(0 3)"],c=["scale(8)","rotate(90 8 8)","translate(0 1)"];break;default:a=["translate(0 16)"],o=["scale(4)","translate(0 2)"],c=["scale(8)"]}r.setAttribute("transform",a.join(" ")),i.setAttribute("transform",o.join(" ")),n.setAttribute("transform",c.join(" "))}var r=t.querySelector(".iconic-lg"),i=t.querySelector(".iconic-md"),n=t.querySelector(".iconic-sm"),o=function(){var r=t.getAttribute("data-head")||"";e(r);var i=t.getAttribute("data-direction")||"";a(i)};return{update:o,setHead:e,setDirection:a}};
//]]>
</script>
</svg>

Before

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -1,29 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" data-icon="arrow-right-angle" width="128" height="128" class="iconic iconic-arrow-right-angle" viewBox="0 0 128 128">
<g><title>Arrow 90 Degree</title></g>
<g data-width="128" data-height="128" data-default-max-height="96" class="iconic-lg iconic-container" display="inline" transform="translate(0 18)">
<path stroke="#000" stroke-width="8" stroke-linecap="square" stroke-miterlimit="10" class="iconic-arrow-right-angle-stem iconic-property-stroke" d="M114 24h-110v68" fill="none"/>
<path d="M126.7 23.2l-21.4-12.5c-.7-.4-1.3-.1-1.3.7v25c0 .8.6 1.2 1.3.7l21.4-12.5c.7-.3.7-.9 0-1.4z" class="iconic-arrow-right-angle-head iconic-arrow-right-angle-head-fill iconic-property-fill"/>
<path d="M126.6 23.4l-29.2-12.8c-.8-.3-1.4.1-1.4.9v25c0 .8.6 1.2 1.4.9l29.3-12.8c.7-.3.7-.9-.1-1.2z" class="iconic-arrow-right-angle-head iconic-arrow-right-angle-head-fill-acute iconic-property-fill" display="none"/>
<path d="M126.6 23.5l-33.2-13c-.8-.3-1.1 0-.7.8l6.5 11.4c.4.7.4 1.9 0 2.6l-6.5 11.4c-.4.7-.1 1.1.7.8l33.2-12.9c.8-.4.8-.8 0-1.1z" class="iconic-arrow-right-angle-head iconic-arrow-right-angle-head-fill-angled iconic-property-fill" display="none"/>
<path class="iconic-arrow-right-angle-head iconic-arrow-right-angle-head-line iconic-property-fill" d="M128 24l-24-24h-10l24 24-24 24h10z" display="none"/>
</g>
<g data-width="32" data-height="32" data-default-max-height="26" class="iconic-md iconic-container" display="none" transform="scale(4) translate(0 3)">
<path stroke="#000" stroke-width="3" stroke-linecap="square" stroke-miterlimit="10" class="iconic-arrow-right-angle-stem iconic-property-stroke" d="M27 6.5h-25.5v18" fill="none"/>
<path d="M31.6 6.8l-5.2 3.9c-.2.2-.4.1-.4-.2v-8c0-.3.2-.4.4-.2l5.2 3.9c.2.2.2.4 0 .6z" class="iconic-arrow-right-angle-head iconic-arrow-right-angle-head-fill iconic-property-fill"/>
<path d="M31.6 6.7l-8.1 4.1c-.2.1-.4 0-.4-.3v-8c0-.3.2-.4.4-.3l8.1 4.1c.2.1.2.3 0 .4z" class="iconic-arrow-right-angle-head iconic-arrow-right-angle-head-fill-acute iconic-property-fill" display="none"/>
<path d="M31.5 6.7l-9.1 4.1c-.3.1-.4 0-.3-.3l1.7-3.5c.1-.3.1-.7 0-.9l-1.6-3.6c-.1-.3 0-.4.3-.3l9.1 4.1c.2.1.2.3-.1.4z" class="iconic-arrow-right-angle-head iconic-arrow-right-angle-head-fill-angled iconic-property-fill" display="none"/>
<path class="iconic-arrow-right-angle-head iconic-arrow-right-angle-head-line iconic-property-fill" d="M32 6.5l-7-6.5h-3.5l7 6.5-7 6.5h3.5z" display="none"/>
</g>
<g data-width="16" data-height="16" data-default-max-height="13" class="iconic-sm iconic-container" display="none" transform="scale(8) translate(0 1)">
<path stroke="#000" stroke-width="2" stroke-linecap="square" stroke-miterlimit="10" class="iconic-arrow-right-angle-stem iconic-property-stroke" d="M13 4h-12v8" fill="none"/>
<path class="iconic-arrow-right-angle-head iconic-arrow-right-angle-head-fill iconic-property-fill" d="M16 4l-3 3v-6z"/>
<path class="iconic-arrow-right-angle-head iconic-arrow-right-angle-head-fill-acute iconic-property-fill" d="M16 4l-5 3v-6z" display="none"/>
<path class="iconic-arrow-right-angle-head iconic-arrow-right-angle-head-fill-angled iconic-property-fill" d="M16 4l-6 3 2-3-2-3z" display="none"/>
<path class="iconic-arrow-right-angle-head iconic-arrow-right-angle-head-line iconic-property-fill" d="M16 4l-4-4h-2l4 4-4 4h2z" display="none"/>
</g>
<script class="iconic-api" type="application/ecmascript">
//<![CDATA[
var iconicApis=window._Iconic?window._Iconic.smartIconApis:{};iconicApis["arrow-right-angle"]=iconicApis["arrow-right-angle"]||function(t){function e(e){t.setAttribute("data-head",e);for(var a=t.querySelectorAll(".iconic-arrow-right-angle-head"),r=0;r<a.length;r++)a[r].setAttribute("display","none");var i=".iconic-arrow-right-angle-head-"+e,n=t.querySelectorAll(i);n.length||(n=t.querySelectorAll(".iconic-arrow-right-angle-head-fill"));for(var o=0;o<n.length;o++)n[o].setAttribute("display","inline")}function a(e){t.setAttribute("data-direction",e);var a,o,c;switch(e){case"bottom-left":a=["scale(-1 1)","rotate(0 64 64)","translate(-128 18)"],o=["scale(-4 4)","rotate(0 16 16)","translate(-32 3)"],c=["scale(-8 8)","rotate(0 8 8)","translate(-16 1)"];break;case"top-left":a=["rotate(180 64 64)","translate(0 18)"],o=["scale(4)","rotate(180 16 16)","translate(0 3)"],c=["scale(8)","rotate(180 8 8)","translate(0 1)"];break;case"top-right":a=["scale(-1 1)","rotate(180 64 64)","translate(128 18)"],o=["scale(-4 4)","rotate(180 16 16)","translate(32 3)"],c=["scale(-8 8)","rotate(180 8 8)","translate(16 1)"];break;case"right-top":a=["rotate(270 64 64)","translate(0 18)"],o=["scale(4)","rotate(270 16 16)","translate(0 3)"],c=["scale(8)","rotate(270 8 8)","translate(0 1)"];break;case"right-bottom":a=["scale(-1 1)","rotate(90 64 64)","translate(0 146)"],o=["scale(-4 4)","rotate(90 16 16)","translate(0 35)"],c=["scale(-8 8)","rotate(90 8 8)","translate(0 17)"];break;case"left-top":a=["scale(-1 1)","rotate(270 64 64)","translate(0 -110)"],o=["scale(-4 4)","rotate(270 16 16)","translate(0 -29)"],c=["scale(-8 8)","rotate(270 8 8)","translate(0 -15)"];break;case"left-bottom":a=["rotate(90 64 64)","translate(0 18)"],o=["scale(4)","rotate(90 16 16)","translate(0 3)"],c=["scale(8)","rotate(90 8 8)","translate(0 1)"];break;default:a=["translate(0 18)"],o=["scale(4)","translate(0 3)"],c=["scale(8)","translate(0 1)"]}r.setAttribute("transform",a.join(" ")),i.setAttribute("transform",o.join(" ")),n.setAttribute("transform",c.join(" "))}var r=t.querySelector(".iconic-lg"),i=t.querySelector(".iconic-md"),n=t.querySelector(".iconic-sm"),o=function(){var r=t.getAttribute("data-head")||"";r&&e(r);var i=t.getAttribute("data-direction")||"";i&&a(i)};return{update:o,setHead:e,setDirection:a}};
//]]>
</script>
</svg>

Before

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -1,59 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" data-icon="arrow-thick" width="128" height="128" class="iconic iconic-arrow-thick" viewBox="0 0 128 128">
<g><title>Arrow Thick</title></g>
<g data-width="128" data-height="128" data-default-max-height="76" class="iconic-lg iconic-container" display="inline" transform="translate(0 26)">
<path stroke="#000" stroke-width="16" stroke-miterlimit="10" class="iconic-arrow-thick-stem iconic-property-stroke" d="M0 38h110" fill="none"/>
<path d="M126.7 37.2l-33.4-20.4c-.7-.4-1.3-.1-1.3.7v41c0 .8.6 1.1 1.3.7l33.4-20.4c.7-.4.7-1.2 0-1.6z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill iconic-property-fill"/>
<path d="M126.6 37.4l-45.2-20.8c-.8-.3-1.4 0-1.4.9v41c0 .8.6 1.2 1.4.9l45.3-20.8c.7-.3.7-.9-.1-1.2z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-acute iconic-property-fill" display="none"/>
<path d="M127 36.8l-18-21.6c-.5-.6-1-.5-1 .3v45c0 .8.4 1 1 .3l18-21.6c.6-.7.6-1.7 0-2.4z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-oblique iconic-property-fill" display="none"/>
<path d="M126.6 37.4l-47.2-20.8c-.8-.3-1.1 0-.9.8l7 19.2c.3.8.3 2 0 2.8l-7 19.2c-.3.8.1 1.1.9.8l47.3-20.8c.7-.3.7-.9-.1-1.2z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-angled iconic-property-fill" display="none"/>
<path d="M126.7 37.2l-43.4-26.4c-.7-.4-1.3-.1-1.3.7v53c0 .8.6 1.2 1.3.7l43.4-26.4c.7-.5.7-1.1 0-1.6z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-large iconic-property-fill" display="none"/>
<path d="M126.6 37.4l-57.2-26.8c-.8-.4-1.4 0-1.4.9v53c0 .8.6 1.2 1.4.9l57.3-26.7c.7-.4.7-1-.1-1.3z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-large-acute iconic-property-fill" display="none"/>
<path d="M126.6 37.4l-61.2-26.8c-.8-.3-1.2 0-1 .8l7.2 25.1c.2.8.2 2.1 0 2.9l-7.2 25.1c-.2.8.2 1.2 1 .8l61.3-26.8c.7-.2.7-.8-.1-1.1z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-large-angled iconic-property-fill" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-large-oblique iconic-property-fill" d="M128 38l-28-34v68z" display="none"/>
<path d="M127.6 38.4c.2-.2.2-.5 0-.7l-25.3-25.3c-.2-.2-.6-.4-.9-.4h-17c-.3 0-.3.2-.1.4l25.3 25.3c.2.2.2.5 0 .7l-25.2 25.2c-.2.2-.1.4.1.4h17c.3 0 .7-.2.9-.4l25.2-25.2z" class="iconic-arrow-thick-head iconic-arrow-thick-head-line iconic-property-fill" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-acute iconic-property-fill" d="M128 38l-34-26h-20l34 26-34 26h20z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-oblique iconic-property-fill" d="M126 38l-18-28h-16l18 28-18 28h16z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-large iconic-property-fill" d="M128 38l-34-34h-18l34 34-34 34h18z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-large-acute iconic-property-fill" d="M128 38l-46-34h-20l46 34-46 34h20z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-large-oblique iconic-property-fill" d="M126 38l-24-38h-16l24 38-24 38h16z" display="none"/>
</g>
<g data-width="32" data-height="32" data-default-max-height="24" class="iconic-md iconic-container" display="none" transform="scale(4) translate(0 4)">
<path stroke="#000" stroke-width="6" stroke-miterlimit="10" class="iconic-arrow-thick-stem iconic-property-stroke" d="M0 12h26" fill="none"/>
<path d="M31.6 12.3l-8.2 6.4c-.2.2-.4.1-.4-.2v-13c0-.3.2-.4.4-.2l8.2 6.4c.2.2.2.4 0 .6z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill iconic-property-fill"/>
<path d="M31.6 12.3l-11.1 6.5c-.2.1-.4 0-.4-.2v-13c0-.3.2-.4.4-.2l11.1 6.5c.2 0 .2.2 0 .4z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-acute iconic-property-fill" display="none"/>
<path d="M31.7 12.4l-5.4 7.2c-.2.2-.3.2-.3-.1v-15c0-.3.1-.3.3-.1l5.4 7.2c.2.2.2.6 0 .8z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-oblique iconic-property-fill" display="none"/>
<path d="M31.6 12.2l-13.1 6.6c-.2.1-.4 0-.3-.2l2.6-6.1c.1-.3.1-.7 0-.9l-2.6-6.1c-.1-.3 0-.4.3-.2l13.1 6.6c.2 0 .2.2 0 .3z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-angled iconic-property-fill" display="none"/>
<path d="M31.6 12.3l-12.2 9.4c-.2.2-.4.1-.4-.2v-19c0-.3.2-.4.4-.2l12.2 9.4c.2.2.2.4 0 .6z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-large iconic-property-fill" display="none"/>
<path d="M31.6 12.3l-16.1 9.5c-.2.1-.4 0-.4-.2v-19c0-.3.2-.4.4-.2l16.1 9.5c.2 0 .2.2 0 .4z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-large-acute iconic-property-fill" display="none"/>
<path d="M31.7 12.4l-7.4 10.2c-.2.2-.3.2-.3-.1v-21c0-.3.1-.3.3-.1l7.4 10.2c.2.2.2.6 0 .8z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-large-oblique iconic-property-fill" display="none"/>
<path d="M31.6 12.2l-19.1 9.6c-.2.1-.4 0-.3-.2l3.6-9.1c.1-.3.1-.7 0-.9l-3.6-9.1c-.1-.3 0-.4.3-.2l19.1 9.6c.2 0 .2.2 0 .3z" class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-large-angled iconic-property-fill" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line iconic-property-fill" d="M32 12l-9-9h-6l9 9-9 9h6z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-acute iconic-property-fill" d="M32 12l-12-9h-7l12 9-12 9h7z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-oblique iconic-property-fill" d="M32 12l-6-10h-6l6 10-6 10h6z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-large-acute iconic-property-fill" d="M32 12l-15-11h-8l15 11-15 11h8z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-large-oblique iconic-property-fill" d="M32 12l-7-12h-6l7 12-7 12h6z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-large iconic-property-fill" d="M32 12l-11-11h-8l11 11-11 11h8z" display="none"/>
</g>
<g data-width="16" data-height="16" data-default-max-height="16" class="iconic-sm iconic-container" display="none" transform="scale(8)">
<path stroke="#000" stroke-width="4" stroke-miterlimit="10" class="iconic-arrow-thick-stem iconic-property-stroke" d="M0 8h12.25" fill="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-fill iconic-property-fill" d="M16 8l-5 5v-10z"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-acute iconic-property-fill" d="M16 8l-8 5v-10z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-oblique iconic-property-fill" d="M16 8l-4 6v-12z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-angled iconic-property-fill" d="M16 8l-9 5 2-5-2-5z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-large iconic-property-fill" d="M16 8l-7 7v-14z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-large-acute iconic-property-fill" d="M16 8l-11 7v-14z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-large-oblique iconic-property-fill" d="M16 8l-5 8v-16z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-fill-large-angled iconic-property-fill" d="M16 8l-12 7 3-7-3-7z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line iconic-property-fill" d="M16 8l-6-6h-4l6 6-6 6h4z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-acute iconic-property-fill" d="M16 8l-8-6h-5l8 6-8 6h5z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-oblique iconic-property-fill" d="M16 8l-4-7h-4l4 7-4 7h4z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-large iconic-property-fill" d="M16 8l-7-7h-4.5l7 7-7 7h4.5z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-large-acute iconic-property-fill" d="M16 8l-8-6h-5l8 6-8 6h5z" display="none"/>
<path class="iconic-arrow-thick-head iconic-arrow-thick-head-line-large-oblique iconic-property-fill" d="M16 8l-5-8h-4l5 8-5 8h4z" display="none"/>
</g>
<script class="iconic-api" type="application/ecmascript">
//<![CDATA[
var iconicApis=window._Iconic?window._Iconic.smartIconApis:{};iconicApis["arrow-thick"]=iconicApis["arrow-thick"]||function(t){function e(e){t.setAttribute("data-head",e);for(var a=t.querySelectorAll(".iconic-arrow-thick-head"),r=0;r<a.length;r++)a[r].setAttribute("display","none");var i=".iconic-arrow-thick-head-"+e,o=t.querySelectorAll(i);o.length||(o=t.querySelectorAll(".iconic-arrow-thick-head-fill"));for(var n=0;n<o.length;n++)o[n].setAttribute("display","inline")}function a(e){t.setAttribute("data-direction",e);var a=0;switch(e){case"top":a=270;break;case"left":a=180;break;case"bottom":a=90;break;case"top-right":a=-45;break;case"top-left":a=-135;break;case"bottom-right":a=45;break;case"bottom-left":a=135;break;default:a=0}r.setAttribute("transform",["rotate("+a+" 64 64)","translate(0 26)"].join(" ")),i.setAttribute("transform",["scale(4)","rotate("+a+" 16 16)","translate(0 4)"].join(" ")),o.setAttribute("transform",["scale(8)","rotate("+a+" 8 8)"].join(" "))}var r=t.querySelector(".iconic-lg"),i=t.querySelector(".iconic-md"),o=t.querySelector(".iconic-sm"),n=function(){var r=t.getAttribute("data-head")||"";e(r);var i=t.getAttribute("data-direction")||"";a(i)};return{update:n,setHead:e,setDirection:a}};
//]]>
</script>
</svg>

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,59 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" data-icon="arrow" width="128" height="128" class="iconic iconic-arrow" viewBox="0 0 128 128">
<g><title>Arrow</title></g>
<g data-width="128" data-height="128" data-default-max-height="64" class="iconic-lg iconic-container" display="inline" transform="translate(0 32)">
<path stroke="#000" stroke-width="8" stroke-miterlimit="10" class="iconic-arrow-stem iconic-property-stroke" d="M0 32h118" fill="none"/>
<path d="M126.7 31.2l-21.4-12.5c-.7-.4-1.3-.1-1.3.7v25c0 .8.6 1.2 1.3.7l21.4-12.5c.7-.3.7-.9 0-1.4z" class="iconic-arrow-head iconic-arrow-head-fill iconic-property-fill"/>
<path d="M126.6 31.4l-29.2-12.8c-.8-.3-1.4.1-1.4.9v25c0 .8.6 1.2 1.4.9l29.3-12.8c.7-.3.7-.9-.1-1.2z" class="iconic-arrow-head iconic-arrow-head-fill-acute iconic-property-fill" display="none"/>
<path d="M126.6 31.5l-33.2-13c-.8-.3-1.1 0-.7.8l6.5 11.4c.4.7.4 1.9 0 2.6l-6.5 11.4c-.4.7-.1 1.1.7.8l33.2-12.9c.8-.4.8-.8 0-1.1z" class="iconic-arrow-head iconic-arrow-head-fill-angled iconic-property-fill" display="none"/>
<path d="M127.1 30.8l-12.2-15.6c-.5-.7-.9-.5-.9.3v33c0 .8.4 1 .9.3l12.2-15.6c.5-.7.5-1.7 0-2.4z" class="iconic-arrow-head iconic-arrow-head-fill-oblique iconic-property-fill" display="none"/>
<path d="M126.7 31.2l-31.4-18.4c-.7-.4-1.3-.1-1.3.7v37c0 .8.6 1.2 1.3.7l31.4-18.5c.7-.4.7-1 0-1.5z" class="iconic-arrow-head iconic-arrow-head-fill-large iconic-property-fill" display="none"/>
<path d="M126.6 31.4l-43.2-18.8c-.8-.3-1.4.1-1.4.9v37c0 .8.6 1.2 1.4.9l43.2-18.8c.8-.3.8-.9 0-1.2z" class="iconic-arrow-head iconic-arrow-head-fill-large-acute iconic-property-fill" display="none"/>
<path d="M127.1 30.8l-16.2-21.6c-.5-.7-.9-.5-.9.3v45c0 .8.4 1 .9.3l16.2-21.6c.5-.7.5-1.7 0-2.4z" class="iconic-arrow-head iconic-arrow-head-fill-large-oblique iconic-property-fill" display="none"/>
<path d="M126.6 31.4l-47.2-18.8c-.8-.3-1 0-.6.7l10.5 17.4c.4.7.4 1.9 0 2.6l-10.5 17.4c-.4.7-.1 1 .6.7l47.2-18.9c.8-.3.8-.7 0-1.1z" class="iconic-arrow-head iconic-arrow-head-fill-large-angled iconic-property-fill" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line iconic-property-fill" d="M128 32l-24-24h-10l24 24-24 24h10z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-acute iconic-property-fill" d="M128 32l-32-24h-11l32 24-32 24h11z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-oblique iconic-property-fill" d="M128 32l-20-26h-10l20 26-20 26h10z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-large iconic-property-fill" d="M128 32l-30-30h-11l30 30-30 30h11z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-large-acute iconic-property-fill" d="M128 32l-40-30h-12l40 30-40 30h12z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-large-oblique iconic-property-fill" d="M128 32l-24-32h-10l24 32-24 32h10z" display="none"/>
</g>
<g data-width="32" data-height="32" data-default-max-height="19" class="iconic-md iconic-container" display="none" transform="scale(4) translate(0 6) ">
<path stroke="#000" stroke-width="3" stroke-miterlimit="10" class="iconic-arrow-stem iconic-property-stroke" d="M0 9.5h29" fill="none"/>
<path d="M31.6 9.8l-5.2 3.9c-.2.2-.4.1-.4-.2v-8c0-.3.2-.4.4-.2l5.2 3.9c.2.2.2.4 0 .6z" class="iconic-arrow-head iconic-arrow-head-fill iconic-property-fill"/>
<path d="M31.6 9.7l-8.1 4.1c-.2.1-.4 0-.4-.3v-8c0-.3.2-.4.4-.3l8.1 4.1c.2.1.2.3 0 .4z" class="iconic-arrow-head iconic-arrow-head-fill-acute iconic-property-fill" display="none"/>
<path d="M31.7 9.9l-4.3 4.8c-.2.2-.3.1-.3-.1v-10c0-.3.2-.3.3-.1l4.3 4.8c.1 0 .1.4 0 .6z" class="iconic-arrow-head iconic-arrow-head-fill-oblique iconic-property-fill" display="none"/>
<path d="M31.5 9.7l-9.1 4.1c-.3.1-.4 0-.3-.3l1.6-3.6c.1-.3.1-.7 0-.9l-1.6-3.6c-.1-.3 0-.4.3-.3l9.1 4.1c.3.2.3.4 0 .5z" class="iconic-arrow-head iconic-arrow-head-fill-angled iconic-property-fill" display="none"/>
<path d="M31.6 9.8l-8.2 5.9c-.2.2-.4.1-.4-.2v-12c0-.3.2-.4.4-.2l8.2 5.9c.2.2.2.4 0 .6z" class="iconic-arrow-head iconic-arrow-head-fill-large iconic-property-fill" display="none"/>
<path d="M31.6 9.7l-11.1 6c-.2.1-.4 0-.4-.3v-12c0-.3.2-.4.4-.3l11.1 6c.2.3.2.5 0 .6z" class="iconic-arrow-head iconic-arrow-head-fill-large-acute iconic-property-fill" display="none"/>
<path d="M31.7 9.9l-6.3 6.8c-.2.2-.3.1-.3-.1v-14c0-.3.2-.3.3-.1l6.3 6.8c.1 0 .1.4 0 .6z" class="iconic-arrow-head iconic-arrow-head-fill-large-oblique iconic-property-fill" display="none"/>
<path d="M31.5 9.7l-13.1 6.1c-.3.1-.4 0-.2-.2l2.6-5.6c.1-.3.1-.7 0-.9l-2.6-5.6c-.1-.3 0-.4.2-.2l13.1 6.1c.3 0 .3.2 0 .3z" class="iconic-arrow-head iconic-arrow-head-fill-large-angled iconic-property-fill" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line iconic-property-fill" d="M32 9.5l-7-6.5h-3.5l7 6.5-7 6.5h3.5z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-acute iconic-property-fill" d="M32 9.5l-9-6.5h-4l9 6.5-9 6.5h4z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-oblique iconic-property-fill" d="M32 9.5l-6-7.5h-3l6 7.5-6 7.5h3z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-large iconic-property-fill" d="M32 9.5l-11-8.5h-4l11 8.5-11 8.5h4z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-large-acute iconic-property-fill" d="M32 9.5l-11-8.5h-4l11 8.5-11 8.5h4z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-large-oblique iconic-property-fill" d="M32 9.5l-7-9.5h-3.5l7 9.5-7 9.5h3.5z" display="none"/>
</g>
<g data-width="16" data-height="16" data-default-max-height="10" class="iconic-sm iconic-container" display="none" transform="scale(8) translate(0 3)">
<path stroke="#000" stroke-width="2" stroke-miterlimit="10" class="iconic-arrow-stem iconic-property-stroke" d="M14 5h-14" fill="none"/>
<path class="iconic-arrow-head iconic-arrow-head-fill iconic-property-fill" d="M16 5l-3 3v-6z"/>
<path class="iconic-arrow-head iconic-arrow-head-fill-acute iconic-property-fill" d="M16 5l-5 3v-6z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-fill-oblique iconic-property-fill" d="M16 5l-3 4v-8z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-fill-angled iconic-property-fill" d="M16 5l-6 3 2-3-2-3z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-fill-large iconic-property-fill" d="M16 5l-4 4v-8z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-fill-large-acute iconic-property-fill" d="M16 5l-7 4v-8z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-fill-large-angled iconic-property-fill" d="M16 5l-8 4 2-4-2-4z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-fill-large-oblique iconic-property-fill" d="M16 5l-4 5v-10z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line iconic-property-fill" d="M16 5l-4-4h-2l4 4-4 4h2z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-acute iconic-property-fill" d="M16 5l-5-4h-2.5l5 4-5 4h2.5z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-oblique iconic-property-fill" d="M16 5l-3-4h-2l3 4-3 4h2z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-large iconic-property-fill" d="M16 5l-5-5h-2.5l5 5-5 5h2.5z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-large-acute iconic-property-fill" d="M16 5l-6-5h-2.5l6 5-6 5h2.5z" display="none"/>
<path class="iconic-arrow-head iconic-arrow-head-line-large-oblique iconic-property-fill" d="M16 5l-4-5h-2l4 5-4 5h2z" display="none"/>
</g>
<script class="iconic-api" type="application/ecmascript">
//<![CDATA[
var iconicApis=window._Iconic?window._Iconic.smartIconApis:{};iconicApis["arrow"]=iconicApis["arrow"]||function(t){function e(e){t.setAttribute("data-head",e);for(var i=t.querySelectorAll(".iconic-arrow-head"),n=0;n<i.length;n++)i[n].setAttribute("display","none");var r=".iconic-arrow-head-"+e,a=t.querySelectorAll(r);a.length||(a=t.querySelectorAll(".iconic-arrow-head-fill"));for(var c=0;c<a.length;c++)a[c].setAttribute("display","inline")}function i(e){t.setAttribute("data-direction",e);var i=0;switch(e){case"top":i=270;break;case"left":i=180;break;case"bottom":i=90;break;case"top-right":i=-45;break;case"top-left":i=-135;break;case"bottom-right":i=45;break;case"bottom-left":i=135;break;default:i=0}n.setAttribute("transform",["rotate("+i+" 64 64)","translate(0 32)"].join(" ")),r.setAttribute("transform",["scale(4)","rotate("+i+" 16 16)","translate(0 8)"].join(" ")),a.setAttribute("transform",["scale(8)","rotate("+i+" 8 8)","translate(0 4)"].join(" "))}var n=t.querySelector(".iconic-lg"),r=t.querySelector(".iconic-md"),a=t.querySelector(".iconic-sm"),c=function(){var n=t.getAttribute("data-head")||"";e(n);var r=t.getAttribute("data-direction")||"";i(r)};return{update:c,setHead:e,setDirection:i}};
//]]>
</script>
</svg>

Before

Width:  |  Height:  |  Size: 8.9 KiB

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