diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9612375 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,37 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8dc4323 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b66b15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore all environment files (except templates). +/.env* +!/.env*.erb + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key + +/app/assets/builds/* +!/app/assets/builds/.keep diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..03463f3 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-3.3.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d18898b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,62 @@ +# syntax = docker/dockerfile:1 + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile +ARG RUBY_VERSION=3.3.0 +FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base + +# Rails app lives here +WORKDIR /rails + +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + + +# Throw-away build stage to reduce size of final image +FROM base as build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git libvips pkg-config + +# Install application gems +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + +# Final stage for app image +FROM base + +# Install packages needed for deployment +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libsqlite3-0 libvips && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Copy built artifacts: gems, application +COPY --from=build /usr/local/bundle /usr/local/bundle +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN useradd rails --create-home --shell /bin/bash && \ + chown -R rails:rails db log storage tmp +USER rails:rails + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD ["./bin/rails", "server"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..03b072e --- /dev/null +++ b/Gemfile @@ -0,0 +1,78 @@ +source "https://rubygems.org" + +ruby "3.3.0" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 7.1.3", ">= 7.1.3.2" + +# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] +gem "sprockets-rails" + +# Use sqlite3 as the database for Active Record +gem "sqlite3", "~> 1.4" + +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", ">= 5.0" + +# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] +gem "importmap-rails" + +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem "turbo-rails" + +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem "stimulus-rails" + +# Use Tailwind CSS [https://github.com/rails/tailwindcss-rails] +gem "tailwindcss-rails" + +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder" + +# Use Redis adapter to run Action Cable in production +# gem "redis", ">= 4.0.1" + +# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] +# gem "kredis" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +# gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[ windows jruby ] + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +gem "image_processing", "~> 1.2" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[ mri windows ] +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" + + # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] + # gem "rack-mini-profiler" + + # Speed up commands on slow machines / big apps [https://github.com/rails/spring] + # gem "spring" +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" +end + +gem "devise", "~> 4.9" + +gem "font-awesome-sass", "~> 6.5.2" + +gem "ruby-vips" + +gem "chartkick" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..e47f78b --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,320 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.1.3.2) + actionpack (= 7.1.3.2) + activesupport (= 7.1.3.2) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.1.3.2) + actionpack (= 7.1.3.2) + activejob (= 7.1.3.2) + activerecord (= 7.1.3.2) + activestorage (= 7.1.3.2) + activesupport (= 7.1.3.2) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.1.3.2) + actionpack (= 7.1.3.2) + actionview (= 7.1.3.2) + activejob (= 7.1.3.2) + activesupport (= 7.1.3.2) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.2) + actionpack (7.1.3.2) + actionview (= 7.1.3.2) + activesupport (= 7.1.3.2) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.3.2) + actionpack (= 7.1.3.2) + activerecord (= 7.1.3.2) + activestorage (= 7.1.3.2) + activesupport (= 7.1.3.2) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.1.3.2) + activesupport (= 7.1.3.2) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.1.3.2) + activesupport (= 7.1.3.2) + globalid (>= 0.3.6) + activemodel (7.1.3.2) + activesupport (= 7.1.3.2) + activerecord (7.1.3.2) + activemodel (= 7.1.3.2) + activesupport (= 7.1.3.2) + timeout (>= 0.4.0) + activestorage (7.1.3.2) + actionpack (= 7.1.3.2) + activejob (= 7.1.3.2) + activerecord (= 7.1.3.2) + activesupport (= 7.1.3.2) + marcel (~> 1.0) + activesupport (7.1.3.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + base64 (0.2.0) + bcrypt (3.1.20) + bigdecimal (3.1.7) + bindex (0.8.1) + bootsnap (1.18.3) + msgpack (~> 1.2) + builder (3.2.4) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + chartkick (5.0.6) + concurrent-ruby (1.2.3) + connection_pool (2.4.1) + crass (1.0.6) + date (3.3.4) + debug (1.9.2) + irb (~> 1.10) + reline (>= 0.3.8) + devise (4.9.4) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + drb (2.2.1) + erubi (1.12.0) + ffi (1.16.3) + font-awesome-sass (6.5.2) + sassc (~> 2.0) + globalid (1.2.1) + activesupport (>= 6.1) + i18n (1.14.4) + concurrent-ruby (~> 1.0) + image_processing (1.12.2) + mini_magick (>= 4.9.5, < 5) + ruby-vips (>= 2.0.17, < 3) + importmap-rails (2.0.1) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.7.2) + irb (1.12.0) + rdoc + reline (>= 0.4.2) + jbuilder (2.11.5) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) + loofah (2.22.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + matrix (0.4.2) + mini_magick (4.12.0) + mini_mime (1.1.5) + minitest (5.22.3) + msgpack (1.7.2) + mutex_m (0.2.0) + net-imap (0.4.10) + 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.1) + nokogiri (1.16.4-aarch64-linux) + racc (~> 1.4) + nokogiri (1.16.4-arm-linux) + racc (~> 1.4) + nokogiri (1.16.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.4-x86-linux) + racc (~> 1.4) + nokogiri (1.16.4-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.4-x86_64-linux) + racc (~> 1.4) + orm_adapter (0.5.0) + psych (5.1.2) + stringio + public_suffix (5.0.5) + puma (6.4.2) + nio4r (~> 2.0) + racc (1.7.3) + rack (3.0.10) + 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.1.3.2) + actioncable (= 7.1.3.2) + actionmailbox (= 7.1.3.2) + actionmailer (= 7.1.3.2) + actionpack (= 7.1.3.2) + actiontext (= 7.1.3.2) + actionview (= 7.1.3.2) + activejob (= 7.1.3.2) + activemodel (= 7.1.3.2) + activerecord (= 7.1.3.2) + activestorage (= 7.1.3.2) + activesupport (= 7.1.3.2) + bundler (>= 1.15.0) + railties (= 7.1.3.2) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.3.2) + actionpack (= 7.1.3.2) + activesupport (= 7.1.3.2) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rake (13.2.1) + rdoc (6.6.3.1) + psych (>= 4.0.0) + regexp_parser (2.9.0) + reline (0.5.1) + io-console (~> 0.5) + responders (3.1.1) + actionpack (>= 5.2) + railties (>= 5.2) + rexml (3.2.6) + ruby-vips (2.2.1) + ffi (~> 1.12) + rubyzip (2.3.2) + sassc (2.4.0) + ffi (~> 1.9) + selenium-webdriver (4.19.0) + base64 (~> 0.2) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + sprockets (4.2.1) + concurrent-ruby (~> 1.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + sprockets (>= 3.0.0) + sqlite3 (1.7.3-aarch64-linux) + sqlite3 (1.7.3-arm-linux) + sqlite3 (1.7.3-arm64-darwin) + sqlite3 (1.7.3-x86-linux) + sqlite3 (1.7.3-x86_64-darwin) + sqlite3 (1.7.3-x86_64-linux) + stimulus-rails (1.3.3) + railties (>= 6.0.0) + stringio (3.1.0) + tailwindcss-rails (2.4.0) + railties (>= 6.0.0) + tailwindcss-rails (2.4.0-aarch64-linux) + railties (>= 6.0.0) + tailwindcss-rails (2.4.0-arm-linux) + railties (>= 6.0.0) + tailwindcss-rails (2.4.0-arm64-darwin) + railties (>= 6.0.0) + tailwindcss-rails (2.4.0-x86_64-darwin) + railties (>= 6.0.0) + tailwindcss-rails (2.4.0-x86_64-linux) + railties (>= 6.0.0) + thor (1.3.1) + timeout (0.4.1) + turbo-rails (2.0.5) + actionpack (>= 6.0.0) + activejob (>= 6.0.0) + railties (>= 6.0.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + warden (1.2.9) + rack (>= 2.0.9) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + webrick (1.8.1) + websocket (1.2.10) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.6.13) + +PLATFORMS + aarch64-linux + arm-linux + arm64-darwin + x86-linux + x86_64-darwin + x86_64-linux + +DEPENDENCIES + bootsnap + capybara + chartkick + debug + devise (~> 4.9) + font-awesome-sass (~> 6.5.2) + image_processing (~> 1.2) + importmap-rails + jbuilder + puma (>= 5.0) + rails (~> 7.1.3, >= 7.1.3.2) + ruby-vips + selenium-webdriver + sprockets-rails + sqlite3 (~> 1.4) + stimulus-rails + tailwindcss-rails + turbo-rails + tzinfo-data + web-console + +RUBY VERSION + ruby 3.3.0p0 + +BUNDLED WITH + 2.5.7 diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 0000000..da151fe --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,2 @@ +web: bin/rails server +css: bin/rails tailwindcss:watch diff --git a/README.md b/README.md new file mode 100644 index 0000000..7db80e4 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# README + +This README would normally document whatever steps are necessary to get the +application up and running. + +Things you may want to cover: + +* Ruby version + +* System dependencies + +* Configuration + +* Database creation + +* Database initialization + +* How to run the test suite + +* Services (job queues, cache servers, search engines, etc.) + +* Deployment instructions + +* ... diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/assets/builds/.keep b/app/assets/builds/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000..b06fc42 --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,5 @@ +//= link_tree ../images +//= link_directory ../stylesheets .css +//= link_tree ../../javascript .js +//= link_tree ../../../vendor/javascript .js +//= link_tree ../builds diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss new file mode 100644 index 0000000..222b37e --- /dev/null +++ b/app/assets/stylesheets/application.scss @@ -0,0 +1,17 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's + * vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ + +@import "font-awesome"; \ No newline at end of file diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css new file mode 100644 index 0000000..8666d2f --- /dev/null +++ b/app/assets/stylesheets/application.tailwind.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* + +@layer components { + .btn-primary { + @apply py-2 px-4 bg-blue-200; + } +} + +*/ diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..d672697 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..0ff5442 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/admin/categories_controller.rb b/app/controllers/admin/categories_controller.rb new file mode 100644 index 0000000..eddf705 --- /dev/null +++ b/app/controllers/admin/categories_controller.rb @@ -0,0 +1,73 @@ +class Admin::CategoriesController < AdminController + before_action :set_admin_category, only: %i[ show edit update destroy ] + + puts "Hello from Admin::CategoriesController" + + # GET /admin/categories or /admin/categories.json + def index + @admin_categories = Category.all + end + + # GET /admin/categories/1 or /admin/categories/1.json + def show + end + + # GET /admin/categories/new + def new + @admin_category = Category.new + end + + # GET /admin/categories/1/edit + def edit + + end + + # POST /admin/categories or /admin/categories.json + def create + @admin_category = Category.new(admin_category_params) + + respond_to do |format| + if @admin_category.save + format.html { redirect_to admin_category_url(@admin_category), notice: "Category was successfully created." } + format.json { render :show, status: :created, location: @admin_category } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @admin_category.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /admin/categories/1 or /admin/categories/1.json + def update + respond_to do |format| + if @admin_category.update(admin_category_params) + format.html { redirect_to admin_category_url(@admin_category), notice: "Category was successfully updated." } + format.json { render :show, status: :ok, location: @admin_category } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @admin_category.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /admin/categories/1 or /admin/categories/1.json + def destroy + @admin_category.destroy! + + respond_to do |format| + format.html { redirect_to admin_categories_url, notice: "Category was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_admin_category + @admin_category = Category.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def admin_category_params + params.require(:category).permit(:name, :description, :image) + end +end diff --git a/app/controllers/admin/orders_controller.rb b/app/controllers/admin/orders_controller.rb new file mode 100644 index 0000000..8ca2469 --- /dev/null +++ b/app/controllers/admin/orders_controller.rb @@ -0,0 +1,71 @@ +class Admin::OrdersController < AdminController + before_action :set_admin_order, only: %i[ show edit update destroy ] + + # GET /admin/orders or /admin/orders.json + def index + @not_fulfilled_orders = Order.where(fulfilled: false).order(created_at: :desc) + @fulfilled_orders = Order.where(fulfilled: true).order(created_at: :desc) + end + + # GET /admin/orders/1 or /admin/orders/1.json + def show + end + + # GET /admin/orders/new + def new + @admin_order = Order.new + end + + # GET /admin/orders/1/edit + def edit + end + + # POST /admin/orders or /admin/orders.json + def create + @admin_order = Order.new(admin_order_params) + + respond_to do |format| + if @admin_order.save + format.html { redirect_to admin_order_url(@admin_order), notice: "Order was successfully created." } + format.json { render :show, status: :created, location: @admin_order } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @admin_order.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /admin/orders/1 or /admin/orders/1.json + def update + respond_to do |format| + if @admin_order.update(admin_order_params) + format.html { redirect_to admin_order_url(@admin_order), notice: "Order was successfully updated." } + format.json { render :show, status: :ok, location: @admin_order } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @admin_order.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /admin/orders/1 or /admin/orders/1.json + def destroy + @admin_order.destroy! + + respond_to do |format| + format.html { redirect_to admin_orders_url, notice: "Order was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_admin_order + @admin_order = Order.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def admin_order_params + params.require(:order).permit(:customer_email, :fulfilled, :total, :address) + end +end diff --git a/app/controllers/admin/products_controller.rb b/app/controllers/admin/products_controller.rb new file mode 100644 index 0000000..ddcd40c --- /dev/null +++ b/app/controllers/admin/products_controller.rb @@ -0,0 +1,85 @@ +class Admin::ProductsController < AdminController + before_action :set_admin_product, only: %i[ show edit update destroy ] + + # GET /admin/products or /admin/products.json + def index + @admin_products = Product.all + end + + # GET /admin/products/1 or /admin/products/1.json + def show + end + + # GET /admin/products/new + def new + @admin_product = Product.new + end + + # GET /admin/products/1/edit + def edit + end + + # POST /admin/products or /admin/products.json + def create + @admin_product = Product.new(admin_product_params) + + respond_to do |format| + if @admin_product.save + format.html { redirect_to admin_product_url(@admin_product), notice: "Product was successfully created." } + format.json { render :show, status: :created, location: @admin_product } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @admin_product.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /admin/products/1 or /admin/products/1.json + # the reason why we have to make the update fucntion is because the + # admin_product_params(which is prvate function look at the bottom code) + # has an empty array at the end that overwrites the prevouis code + def update + # fetch the product using the id from the params hash aka the url + @admin_product = Product.find(params[:id]) + # updating all the non images attributes but using reject function to filter out the images attribute using a key value pair + # the key is ["images"] + # so basically this updates all the text fields but not images fields + if @admin_product.update(admin_product_params.reject { |k| k["images"] }) + # check to see if the user did send images + if admin_product_params["images"] + # if the uder did sends images it will loop over all of them and attach them to the admin + # product using the attach method + admin_product_params["images"].each do |image| + @admin_product.images.attach(image) + end + + # redirects to admin product page after a sucessful update + end + redirect_to admin_products_path, notice: "Product updated successfully" + # if the update fails show the edit page with errors + else + render :edit, status: :unprocessable_entity + end + end + + # DELETE /admin/products/1 or /admin/products/1.json + def destroy + @admin_product.destroy! + + respond_to do |format| + format.html { redirect_to admin_products_url, notice: "Product was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_admin_product + @admin_product = Product.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def admin_product_params + params.require(:product).permit(:name, :description, :price, :category_id, :active, images: []) + end +end diff --git a/app/controllers/admin/stocks_controller.rb b/app/controllers/admin/stocks_controller.rb new file mode 100644 index 0000000..8f849df --- /dev/null +++ b/app/controllers/admin/stocks_controller.rb @@ -0,0 +1,73 @@ +class Admin::StocksController < AdminController + before_action :set_admin_stock, only: %i[ show edit update destroy ] + + # GET /admin/stocks or /admin/stocks.json + def index + @admin_stocks = Stock.where(product_id: params[:product_id]) + end + + # GET /admin/stocks/1 or /admin/stocks/1.json + def show + @product = Product.find(params[:product_id]) + end + + # GET /admin/stocks/new + def new + @admin_stock = Stock.new + end + + # GET /admin/stocks/1/edit + def edit + @product = Product.find(params[:product_id]) + end + + # POST /admin/stocks or /admin/stocks.json + def create + @product = Product.find(params[:product_id]) + @admin_stock = @product.stocks.new(admin_stock_params) + + respond_to do |format| + if @admin_stock.save + format.html { redirect_to admin_product_stock_url(@product, @admin_stock), notice: "Stock was successfully created." } + format.json { render :show, status: :created, location: @admin_stock } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @admin_stock.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /admin/stocks/1 or /admin/stocks/1.json + def update + respond_to do |format| + if @admin_stock.update(admin_stock_params) + format.html { redirect_to admin_product_stock_url(@admin_stock.product, @admin_stock), notice: "Stock was successfully updated." } + format.json { render :show, status: :ok, location: @admin_stock } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @admin_stock.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /admin/stocks/1 or /admin/stocks/1.json + def destroy + @admin_stock.destroy! + + respond_to do |format| + format.html { redirect_to admin_product_stocks_url, notice: "Stock was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_admin_stock + @admin_stock = Stock.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def admin_stock_params + params.require(:stock).permit( :amount, :size) + end +end diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb new file mode 100644 index 0000000..5fdb7fa --- /dev/null +++ b/app/controllers/admin_controller.rb @@ -0,0 +1,37 @@ +class AdminController < ApplicationController + layout 'admin' + before_action :authenticate_admin! + + def index + # Fetch recent, unfulfilled orders + @orders = Order.where(fulfilled: false).order(created_at: :desc).take(5) + + # Quick stats for today + @quick_stats = { + sales: Order.where(created_at: Time.now.midnight..Time.now).count, # Order count + revenue: Order.where(created_at: Time.now.midnight..Time.now).sum(:total)&.round(), # Total revenue + avg_sale: Order.where(created_at: Time.now.midnight..Time.now).average(:total)&.round(), # Average order value + # joins the orderPorduct tables and the orders tables and calculates the average number of items sold per order + per_sale: OrderProduct.joins(:order).where(orders: { created_at: Time.now.midnight..Time.now })&.average(:quantity) # Average items per sale + } + + # Orders for the past 7 days + @orders_by_day = Order.where('created_at > ?', Time.now - 7.days).order(:created_at) + @orders_by_day = @orders_by_day.group_by { |order| order.created_at.to_date } + + # Revenue by day (array of [day, revenue] arrays) + @revenue_by_day = @orders_by_day.map { |day, orders| [day.strftime("%A"), orders.sum(&:total)] } + + # Logic to fill missing days with zero revenue + if @revenue_by_day.count < 7 + days_of_week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + data_hash = @revenue_by_day.to_h # Convert to hash for easier lookup + current_day = Date.today.strftime("%A") + current_day_index = days_of_week.index(current_day) + next_day_index = (current_day_index + 1) % days_of_week.length + ordered_days_with_current_last = days_of_week[next_day_index..-1] + days_of_week[0...next_day_index] + complete_ordered_array_with_current_last = ordered_days_with_current_last.map{ |day| [day, data_hash.fetch(day, 0)] } + @revenue_by_day = complete_ordered_array_with_current_last + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..09705d1 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000..ff67ebe --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,4 @@ +class HomeController < ApplicationController +def index +end +end diff --git a/app/helpers/admin/categories_helper.rb b/app/helpers/admin/categories_helper.rb new file mode 100644 index 0000000..f586acd --- /dev/null +++ b/app/helpers/admin/categories_helper.rb @@ -0,0 +1,2 @@ +module Admin::CategoriesHelper +end diff --git a/app/helpers/admin/orders_helper.rb b/app/helpers/admin/orders_helper.rb new file mode 100644 index 0000000..863374f --- /dev/null +++ b/app/helpers/admin/orders_helper.rb @@ -0,0 +1,2 @@ +module Admin::OrdersHelper +end diff --git a/app/helpers/admin/products_helper.rb b/app/helpers/admin/products_helper.rb new file mode 100644 index 0000000..977a242 --- /dev/null +++ b/app/helpers/admin/products_helper.rb @@ -0,0 +1,2 @@ +module Admin::ProductsHelper +end diff --git a/app/helpers/admin/stocks_helper.rb b/app/helpers/admin/stocks_helper.rb new file mode 100644 index 0000000..1904332 --- /dev/null +++ b/app/helpers/admin/stocks_helper.rb @@ -0,0 +1,2 @@ +module Admin::StocksHelper +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000..6e4cf6c --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1,6 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "@hotwired/turbo-rails" +import "controllers" +import 'chartkick'; +import 'Chart.bundle'; + diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 0000000..1213e85 --- /dev/null +++ b/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/app/javascript/controllers/dashboard_controller.js b/app/javascript/controllers/dashboard_controller.js new file mode 100644 index 0000000..b38eb78 --- /dev/null +++ b/app/javascript/controllers/dashboard_controller.js @@ -0,0 +1,8 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="dashboard" +export default class extends Controller { + connect() { + console.log("DashboardController connected!") + } +} diff --git a/app/javascript/controllers/hello_controller.js b/app/javascript/controllers/hello_controller.js new file mode 100644 index 0000000..0138bf8 --- /dev/null +++ b/app/javascript/controllers/hello_controller.js @@ -0,0 +1,7 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + console.log("hello from Stimulus!") + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 0000000..54ad4ca --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,11 @@ +// Import and register all your controllers from the importmap under controllers/* + +import { application } from "controllers/application" + +// Eager load all controllers defined in the import map under controllers/**/*_controller +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" +eagerLoadControllersFrom("controllers", application) + +// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) +// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" +// lazyLoadControllersFrom("controllers", application) diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..3c34c81 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/models/admin.rb b/app/models/admin.rb new file mode 100644 index 0000000..7a7be2f --- /dev/null +++ b/app/models/admin.rb @@ -0,0 +1,6 @@ +class Admin < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :database_authenticatable, :registerable, + :recoverable, :rememberable, :validatable +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..b63caeb --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/category.rb b/app/models/category.rb new file mode 100644 index 0000000..2b82a6c --- /dev/null +++ b/app/models/category.rb @@ -0,0 +1,5 @@ +class Category < ApplicationRecord + has_one_attached :image do |attachable| + attachable.variant :thumb, resize_to_limit: [50, 50] + end +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/models/order.rb b/app/models/order.rb new file mode 100644 index 0000000..5cab8b7 --- /dev/null +++ b/app/models/order.rb @@ -0,0 +1,3 @@ +class Order < ApplicationRecord + has_many :order_products, dependent: :destroy +end diff --git a/app/models/order_product.rb b/app/models/order_product.rb new file mode 100644 index 0000000..3ff23fb --- /dev/null +++ b/app/models/order_product.rb @@ -0,0 +1,4 @@ +class OrderProduct < ApplicationRecord + belongs_to :product + belongs_to :order +end diff --git a/app/models/product.rb b/app/models/product.rb new file mode 100644 index 0000000..3667ae9 --- /dev/null +++ b/app/models/product.rb @@ -0,0 +1,10 @@ +class Product < ApplicationRecord + has_many_attached :images do |attachable| + attachable.variant :thumb, resize_to_limit: [50, 50] + end + + belongs_to :category + has_many :stocks + has_many :order_products + +end diff --git a/app/models/stock.rb b/app/models/stock.rb new file mode 100644 index 0000000..3278a06 --- /dev/null +++ b/app/models/stock.rb @@ -0,0 +1,3 @@ +class Stock < ApplicationRecord + belongs_to :product +end diff --git a/app/views/admin/categories/_admin_category.json.jbuilder b/app/views/admin/categories/_admin_category.json.jbuilder new file mode 100644 index 0000000..1f27b16 --- /dev/null +++ b/app/views/admin/categories/_admin_category.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! admin_category, :id, :name, :description, :created_at, :updated_at +json.url admin_category_url(admin_category, format: :json) diff --git a/app/views/admin/categories/_category.html.erb b/app/views/admin/categories/_category.html.erb new file mode 100644 index 0000000..de7d811 --- /dev/null +++ b/app/views/admin/categories/_category.html.erb @@ -0,0 +1,17 @@ +
+ Name: + <%= category.name %> +
+ ++ Description: + <%= category.description %> +
+ + <% if action_name != "show" %> + <%= link_to "Show this category", admin_category_path(category), class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> + <%= link_to "Edit this category", edit_admin_category_path(category), class: "rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium" %> +<%= notice %>
+ <% end %> + ++ | ++ Name + | ++ Description + | +
---|---|---|
+ <%= c.image.present? ? image_tag(c.image.variant(:thumb)): image_tag("https://via.placeholder.com/50") %> + | ++ <%= link_to c.name, edit_admin_category_path(c) %> + | ++ <%= c.description %> + | +
<%= notice %>
+ <% end %> + + <%= render @admin_category %> + + <%= link_to "Edit this category", edit_admin_category_path(@admin_category), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> ++ $<%= @quick_stats[:revenue] ? (@quick_stats[:revenue]/100.0).to_s : "0" %> +
++ <%= @quick_stats[:sales] ? (@quick_stats[:sales]).to_s : "0" %> +
++ $<%= @quick_stats[:avg_sale] ? (@quick_stats[:avg_sale]/100.0).to_s : "0" %> +
++ <%= @quick_stats[:per_sale].to_i %> +
+Order ID | +Customer | +Date | +Amount | +
---|---|---|---|
+ <%= link_to order.id, admin_order_path(order), class: "underline" %> + | ++ <%= order.customer_email %> + | ++ <%= order.created_at.strftime("%B %d, %Y %H:%M") %> + | ++ <%= (order.total/100.0).to_s %> + | +
+ Customer email: + <%= order.customer_email %> +
+ ++ Fulfilled: + <%= order.fulfilled %> +
+ ++ Total: + <%= order.total %> +
+ ++ Address: + <%= order.address %> +
+ ++ Address: + <%= order.address %> +
+ + <% if order.order_products.any? %> + Order products: + <% order.order_products.each do |op| %> + <%= "Name: " + op.product.name %> +<%= notice %>
+ <% end %> + ++ Id + | ++ Email + | ++ Price + | ++ Address + | ++ Fulfilled + | + +
---|---|---|---|---|
+ <%= o.id %> + | ++ <%= link_to o.customer_email,[:admin, o] %> + | ++ $<%= (o.total/100.00).to_s %> + | ++ <%= o.address %> + | ++ <%= o.fulfilled %> + | +
+ Id + | ++ Email + | ++ Price + | ++ Address + | ++ Fulfilled + | + +
---|---|---|---|---|
+ <%= o.id %> + | ++ <%= link_to o.customer_email,[:admin, o] %> + | ++ $<%= (o.total/100.00).to_s %> + | ++ <%= o.address %> + | ++ <%= o.fulfilled %> + | +
<%= notice %>
+ <% end %> + + <%= render @admin_order %> + + <%= link_to "Edit this order", edit_admin_order_path(@admin_order), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> ++ Name: + <%= product.name %> +
+ ++ Description: + <%= product.description %> +
+ ++ Price: + <%= product.price %> +
+ ++ Category: + <%= product.category_id %> +
+ ++ Active: + <%= product.active %> +
+ ++ <%= link_to "product stocks", admin_product_stocks_path(product) %> +
+ + <% if action_name != "show" %> + <%= link_to "Show this product", product, class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> + <%= link_to "Edit this product", edit_product_path(product), class: "rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium" %> +<%= notice %>
+ <% end %> + ++ | ++ Name + | ++ Description + | ++ price + | ++ Categories + | ++ active + | +
---|---|---|---|---|---|
+ <%= c.images.any? ? image_tag(c.images.first.variant(:thumb)): image_tag("https://via.placeholder.com/50") %> + | ++ <%= link_to c.name, edit_admin_product_path(c) %> + | ++ <%= c.description %> + | ++ $<%= (c.price/100.0).to_s %> + | ++ <%= c.category_id %> + | ++ <%= c.active %> + | +
<%= notice %>
+ <% end %> + + <%= render @admin_product %> + + <%= link_to "Edit this admin_product", edit_admin_product_path(@admin_product), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> ++ Product: + <%= stock.product_id %> +
+ ++ Amount: + <%= stock.amount %> +
+ ++ Size: + <%= stock.size %> +
+ + <% if action_name != "show" %> + <%= link_to "Show this stock", [:admin, stock.product, stock], class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> + <%= link_to "Edit this stock", edit_admin_product_stock_path(stock.product, stock), class: "rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium" %> +<%= notice %>
+ <% end %> + +<%= notice %>
+ <% end %> + + <%= render @admin_stock %> + + <%= link_to "Edit this stock", edit_admin_product_stock_path(@product, @admin_stock), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> +
+
+
+
+ Simple yet flexible JavaScript charting for designers & developers
+
(props: P, final?: boolean): Pick (props: P[], final?: boolean): Partial (props: P, final?: boolean): Pick (props: P[], final?: boolean): Partial (props: P, final?: boolean): Pick (props: P[], final?: boolean): Partialt.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r="_"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:ie},numbers:{type:"number",properties:ee}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function fe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ge(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function be(t,e){return me(t).getPropertyValue(e)}const xe=["top","right","bottom","left"];function _e(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=xe[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}const ye=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function ve(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o="border-box"===n.boxSizing,a=_e(n,"padding"),r=_e(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(ye(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const Me=t=>Math.round(10*t)/10;function we(t,e,i,s){const n=me(t),o=_e(n,"margin"),a=pe(n.maxWidth,t,"clientWidth")||T,r=pe(n.maxHeight,t,"clientHeight")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,"border","width"),l=_e(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,"clientWidth"),n=pe(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=_e(n,"border","width"),e=_e(n,"padding");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=Me(Math.min(h,a,l.maxWidth)),c=Me(Math.min(c,r,l.maxHeight)),h&&!c&&(c=Me(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=Me(Math.floor(c*s))),{width:h,height:c}}function ke(t,e,i){const s=e||1,n=Math.floor(t.height*s),o=Math.floor(t.width*s);t.height=Math.floor(t.height),t.width=Math.floor(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const Se=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};fe()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch(t){}return t}();function Pe(t,e){const i=be(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function De(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Ce(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Oe(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;h!t.skip))),"monotone"===e.cubicInterpolationMode)ri(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ci(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ci(t)?t:di(t,.075,.3),easeOutElastic:t=>ci(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return ci(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5};function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function pi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function mi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=gi(t,n,i),r=gi(n,o,i),l=gi(o,e,i),h=gi(a,r,i),c=gi(r,l,i);return gi(h,c,i)}const bi=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,xi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function _i(t,e){const i=(""+t).match(bi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}const yi=t=>+t||0;function vi(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=yi(a(t));return i}function Mi(t){return vi(t,{top:"y",right:"x",bottom:"y",left:"x"})}function wi(t){return vi(t,["topLeft","topRight","bottomLeft","bottomRight"])}function ki(t){const e=Mi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(""+s).match(xi)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:_i(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:""};return n.string=De(n),n}function Pi(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Ci(t,e){return Object.assign(Object.create(t),e)}function Oi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ai(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function Ti(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Li(t){return"angle"===t?{between:Z,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ei({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Ri(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Li(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Li(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;h{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;ao)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;n({width:r[t]||0,height:l[t]||0});return{first:P(0),last:P(e-1),widest:P(k),highest:P(S),widths:r,heights:l}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Ae(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&t{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;tn?{start:e-i,end:e}:{start:e,end:e+i}}function Do(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.startt,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=ki(So(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/So(this.options))}generateTickLabels(t){bo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Do(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t-1?t.split("\n"):t}function Ca(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Oa(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,b=0,x=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(x+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),x){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(x-g)*l.lineHeight+(x-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){b=Math.max(b,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),b+=p.width,{width:b,height:m}}function Aa(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Ta(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||Aa(t,e,i,s),yAlign:s}}function La(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:J(g,0,s.width-e.width),y:J(p,0,s.height-e.height)}}function Ea(t,e,i){const s=ki(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function Ra(t){return Pa([],Da(t))}function Ia(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const za={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=Ia(i,t);Pa(e.before,Da(Fa(n,"beforeLabel",this,t))),Pa(e.lines,Fa(n,"label",this,t)),Pa(e.after,Da(Fa(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return Ra(Fa(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Fa(i,"beforeFooter",this,t),n=Fa(i,"footer",this,t),o=Fa(i,"afterFooter",this,t);let a=[];return a=Pa(a,Da(s)),a=Pa(a,Da(n)),a=Pa(a,Da(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;a