diff --git a/.browserslistrc b/.browserslistrc deleted file mode 100644 index e94f814..0000000 --- a/.browserslistrc +++ /dev/null @@ -1 +0,0 @@ -defaults diff --git a/.env.core.sample b/.env.core.sample index 7f5dd74..7150220 100644 --- a/.env.core.sample +++ b/.env.core.sample @@ -1,10 +1,13 @@ RAILS_ENV=development RAILS_LOG_TO_STDOUT=1 WJR_HOST=localhost:3000 +WJR_CORE_URL=http://localhost:3000 +WJR_GOOGLE_MAPS_API_KEY= +WJR_FLICKR_API_KEY= WJR_PASSWORD_SALT= WJR_SECRET_TOKEN=secret-token WJR_SECRET_BASE=secret-base -WJR_DEVISE_SECRET_KEY=a3ce9067696ac6eb83a065b0298ec025a97af52d84872e84c9e9e622991fd89f5a2d4b9d20d2e391858ae07240d0c4600c3900a15158e7b90ddae4c20e68d92dm +WJR_DEVISE_SECRET_KEY=generate-with-bin-rails-secret-and-keep-out-of-git WJR_FACEBOOK_APP_ID= WJR_FACEBOOK_APP_SECRET= WJR_GOOGLE_APP_ID= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c7d391c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,176 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + +env: + IMAGE_NAME: core + +jobs: + test: + runs-on: ubuntu-latest + + services: + db: + image: mariadb:10.11 + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: whyjustrun-test + ports: + - 3306:3306 + options: >- + --health-cmd="healthcheck.sh --connect --innodb_initialized" + --health-interval=5s + --health-timeout=5s + --health-retries=10 + + env: + RAILS_ENV: test + WJR_DATABASE_NAME: whyjustrun + WJR_DATABASE_TEST_NAME: whyjustrun-test + WJR_DATABASE_HOST: 127.0.0.1 + WJR_DATABASE_USERNAME: root + WJR_DATABASE_PASSWORD: root + WJR_PASSWORD_SALT: test-only-salt + WJR_SECRET_BASE: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + WJR_SECRET_TOKEN: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + WJR_DEVISE_SECRET_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + WJR_RECAPTCHA_SITE_KEY: test + WJR_RECAPTCHA_SECRET_KEY: test + WJR_SMTP_ADDRESS: localhost + WJR_SMTP_DOMAIN: example.com + WJR_SMTP_USER_NAME: test + WJR_SMTP_PASSWORD: test + WJR_DATA_FOLDER: /tmp/whyjustrun-data/ + WJR_DATA_URL: http://localhost/data/ + + steps: + - uses: actions/checkout@v4 + + - name: Install MySQL client headers + run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev imagemagick ghostscript + + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Prepare test database + run: bundle exec rails db:test:prepare + + - name: Run tests + run: bundle exec rails test + + system-test: + runs-on: ubuntu-latest + + services: + db: + image: mariadb:10.11 + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: whyjustrun-test + ports: + - 3306:3306 + options: >- + --health-cmd="healthcheck.sh --connect --innodb_initialized" + --health-interval=5s + --health-timeout=5s + --health-retries=10 + + env: + RAILS_ENV: test + WJR_DATABASE_NAME: whyjustrun + WJR_DATABASE_TEST_NAME: whyjustrun-test + WJR_DATABASE_HOST: 127.0.0.1 + WJR_DATABASE_USERNAME: root + WJR_DATABASE_PASSWORD: root + WJR_PASSWORD_SALT: test-only-salt + WJR_SECRET_BASE: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + WJR_SECRET_TOKEN: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + WJR_DEVISE_SECRET_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + WJR_RECAPTCHA_SITE_KEY: test + WJR_RECAPTCHA_SECRET_KEY: test + WJR_SMTP_ADDRESS: localhost + WJR_SMTP_DOMAIN: example.com + WJR_SMTP_USER_NAME: test + WJR_SMTP_PASSWORD: test + WJR_DATA_FOLDER: /tmp/whyjustrun-data/ + WJR_DATA_URL: http://localhost/data/ + + steps: + - uses: actions/checkout@v4 + + - name: Install MySQL client headers + run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev imagemagick ghostscript + + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Prepare test database + run: bundle exec rails db:test:prepare + + - name: Run system tests + run: bundle exec rails test:system + + - name: Upload failure screenshots and pages + uses: actions/upload-artifact@v4 + if: failure() + with: + name: system-test-failures + path: tmp/screenshots/ + if-no-files-found: ignore + + brakeman: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install MySQL client headers + run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev + + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Run Brakeman + run: bundle exec brakeman --no-pager + + push: + runs-on: ubuntu-latest + needs: [test, system-test, brakeman] + if: github.event_name == 'push' + + steps: + - uses: actions/checkout@v4 + + - name: Build image + run: docker build . --file Dockerfile --tag $IMAGE_NAME + + - name: Log into GitHub Container Registry + run: echo "${{ secrets.CR_PAT }}" | docker login https://ghcr.io -u ${{ secrets.CR_USER }} --password-stdin + + - name: Push image to GitHub Container Registry + run: | + IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME + + # Change all uppercase to lowercase + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + + # Strip git ref prefix from version + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + + # Strip "v" prefix from tag name + [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + + # Use Docker `latest` tag convention + [ "$VERSION" == "master" ] && VERSION=latest + + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + + docker tag $IMAGE_NAME $IMAGE_ID:$VERSION + docker push $IMAGE_ID:$VERSION diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml deleted file mode 100644 index 145e72a..0000000 --- a/.github/workflows/docker-publish.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Docker - -on: - push: - # Publish `master` as Docker `latest` image. - branches: - - master - - # Run tests for any PRs. - pull_request: - -env: - IMAGE_NAME: core - -jobs: - # Push image to GitHub Packages. - # See also https://docs.docker.com/docker-hub/builds/ - push: - runs-on: ubuntu-latest - if: github.event_name == 'push' - - steps: - - uses: actions/checkout@v4 - - - name: Build image - run: docker build . --file Dockerfile --tag $IMAGE_NAME - - - name: Log into GitHub Container Registry - run: echo "${{ secrets.CR_PAT }}" | docker login https://ghcr.io -u ${{ secrets.CR_USER }} --password-stdin - - - name: Push image to GitHub Container Registry - run: | - IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME - - # Change all uppercase to lowercase - IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') - - # Strip git ref prefix from version - VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') - - # Strip "v" prefix from tag name - [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') - - # Use Docker `latest` tag convention - [ "$VERSION" == "master" ] && VERSION=latest - - echo IMAGE_ID=$IMAGE_ID - echo VERSION=$VERSION - - docker tag $IMAGE_NAME $IMAGE_ID:$VERSION - docker push $IMAGE_ID:$VERSION diff --git a/.gitignore b/.gitignore index fc83913..4ecabf7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,6 @@ /.env /.env.core -/public/packs -/public/packs-test /node_modules /yarn-error.log yarn-debug.log* @@ -26,3 +24,4 @@ yarn-debug.log* vendor/ public/assets/ +/public/redactor/ diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..a0891f5 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.4 diff --git a/CLAUDE.md b/CLAUDE.md index 9aebc42..86cd3d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -WhyJustRun Core is a Ruby on Rails application providing authentication, IOF XML APIs, and cross-club pages for [whyjustrun.ca](https://whyjustrun.ca). It works alongside the separate [WhyJustRun Clubsite](https://github.com/WhyJustRun/Clubsite) app. +WhyJustRun Core is a Ruby on Rails application serving [whyjustrun.ca](https://whyjustrun.ca) (authentication, IOF XML APIs, cross-club pages) and every club's website. Club domains are resolved from the Host header into a `Clubsite::` controller namespace (`app/controllers/clubsite/base_controller.rb`); requests on club domains run in the club's timezone and club-scoped data is always accessed through `current_club` associations. The legacy [WhyJustRun Clubsite](https://github.com/WhyJustRun/Clubsite) (CakePHP) is being decommissioned; until cutover it still serves club domains in production against the same database, so cross-app SSO endpoints and permissive CORS must not be removed from master (they are removed on the `rails-cutover` and `post-cutover-cleanup` branches). ## Development Commands @@ -44,5 +44,5 @@ Test users (password: "password"): admin@example.com, webmaster@example.com, exe - **Cross-app sessions**: Shared authentication with the Clubsite app via `CrossAppSession` model - **Privilege system**: Role-based access with numeric levels (0-100) defined in `config/settings.yml` - **Cron jobs**: Managed by the `whenever` gem -- **Frontend**: Bootstrap with CoffeeScript, Webpacker, jQuery, Leaflet for maps +- **Frontend**: Bootstrap with CoffeeScript, Sprockets asset pipeline, jQuery, Leaflet for maps - **Testing**: Minitest with fixtures in `test/fixtures/` diff --git a/Dockerfile b/Dockerfile index 41747c6..c549ea3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,11 @@ RUN apk add --no-cache \ build-base \ autoconf \ nodejs \ - yarn \ mariadb-dev \ + imagemagick \ + ghostscript \ + chromium \ + chromium-chromedriver \ bash # used in crontab generated by whenever RUN mkdir /application @@ -26,12 +29,8 @@ CMD ["rails", "server", "-b", "0.0.0.0"] FROM dev AS prod -COPY package.json yarn.lock /application/ -RUN yarn install --frozen-lockfile - COPY . /application RUN RAILS_ENV=production \ - NODE_OPTIONS=--openssl-legacy-provider \ SECRET_KEY_BASE=placeholder \ WJR_DATABASE_NAME=placeholder \ WJR_DATABASE_TEST_NAME=placeholder \ diff --git a/Gemfile b/Gemfile index 11972f1..5516700 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,5 @@ source 'http://rubygems.org' -gem 'bundle' - gem 'rails', '~> 7.1' gem 'mysql2', '~> 0.5.3' @@ -9,14 +7,12 @@ gem 'mysql2', '~> 0.5.3' gem 'puma', '~> 6.4' # MySQL Session store -gem "activerecord-session_store", "~> 2.1.0" # Asset pipeline gem 'sass-rails', '~> 6.0.0' gem "coffee-script", "~> 2.4.1" gem "coffee-rails", "~> 5.0.0" gem "uglifier", "~> 4.2.0" -gem "webpacker", ">= 5.4.4" gem 'tzinfo-data' # Authentication @@ -56,5 +52,17 @@ gem "exception_notification", "~> 4.5" # Authorization gem "pundit", "~> 2.3.2" +# Image thumbnail generation (requires ImageMagick) +gem 'image_processing', '~> 1.13' + # Geo gem 'haversine', '~> 0.3.2' + +group :development, :test do + gem 'brakeman', require: false +end + +group :test do + gem 'capybara' + gem 'selenium-webdriver' +end diff --git a/Gemfile.lock b/Gemfile.lock index efda3d5..7fda6fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,13 +56,6 @@ GEM activemodel (= 7.2.3.1) activesupport (= 7.2.3.1) timeout (>= 0.4.0) - activerecord-session_store (2.1.0) - actionpack (>= 6.1) - activerecord (>= 6.1) - cgi (>= 0.3.6) - multi_json (~> 1.11, >= 1.11.2) - rack (>= 2.0.8, < 4) - railties (>= 6.1) activestorage (7.2.3.1) actionpack (= 7.2.3.1) activejob (= 7.2.3.1) @@ -81,6 +74,8 @@ GEM minitest (>= 5.1, < 6) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) autoprefixer-rails (10.4.19.0) execjs (~> 2) base64 (0.3.0) @@ -90,9 +85,18 @@ GEM bootstrap-sass (3.4.1) autoprefixer-rails (>= 5.2.1) sassc (>= 2.0.0) + brakeman (8.0.5) + racc builder (3.3.0) - bundle (0.0.1) - bundler + 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) cgi (0.4.2) chronic (0.10.2) coffee-rails (5.0.0) @@ -141,6 +145,9 @@ GEM logger ostruct ice_cube (0.17.0) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) io-console (0.8.2) irb (1.17.0) pp (>= 0.6.0) @@ -167,10 +174,12 @@ GEM net-pop net-smtp marcel (1.1.0) + matrix (0.4.3) + mini_magick (5.3.2) + logger mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.27.0) - multi_json (1.15.0) mysql2 (0.5.6) net-imap (0.4.20) date @@ -198,14 +207,13 @@ GEM psych (5.3.1) date stringio + public_suffix (7.0.5) puma (6.4.3) nio4r (~> 2.0) pundit (2.3.2) activesupport (>= 3.0.0) racc (1.8.1) rack (3.2.6) - rack-proxy (0.7.7) - rack rack-session (2.1.2) base64 (>= 0.1.0) rack (>= 3.0.0) @@ -250,11 +258,17 @@ GEM psych (>= 4.0.0) tsort recaptcha (5.17.0) + regexp_parser (2.12.0) reline (0.6.3) io-console (~> 0.5) responders (3.2.0) actionpack (>= 7.0) railties (>= 7.0) + rexml (3.4.4) + ruby-vips (2.3.0) + ffi (~> 1.12) + logger + rubyzip (3.4.1) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) @@ -266,7 +280,12 @@ GEM sprockets-rails tilt securerandom (0.4.1) - semantic_range (3.0.0) + selenium-webdriver (4.46.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) @@ -288,16 +307,14 @@ GEM useragent (0.16.11) warden (1.2.9) rack (>= 2.0.9) - webpacker (5.4.4) - activesupport (>= 5.2) - rack-proxy (>= 0.6.1) - railties (>= 5.2) - semantic_range (>= 2.3.0) + websocket (1.2.11) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) whenever (1.0.0) chronic (>= 0.6.3) + xpath (3.2.0) + nokogiri (~> 1.8) zeitwerk (2.7.5) PLATFORMS @@ -305,10 +322,10 @@ PLATFORMS x86_64-linux DEPENDENCIES - activerecord-session_store (~> 2.1.0) bcrypt (~> 3.1.22) bootstrap-sass (~> 3.4.1) - bundle + brakeman + capybara coffee-rails (~> 5.0.0) coffee-script (~> 2.4.1) config (~> 5.5.1) @@ -318,6 +335,7 @@ DEPENDENCIES geocoder (~> 1.8.3) haversine (~> 0.3.2) icalendar (~> 2.12.2) + image_processing (~> 1.13) jbuilder (~> 2.12.0) jquery-rails (~> 4.6.0) leaflet-rails (~> 1.9.3) @@ -329,9 +347,9 @@ DEPENDENCIES rails (~> 7.1) recaptcha (~> 5.17.0) sass-rails (~> 6.0.0) + selenium-webdriver tzinfo-data uglifier (~> 4.2.0) - webpacker (>= 5.4.4) whenever (~> 1.0.0) BUNDLED WITH diff --git a/app/assets/images/clubsite/bootstrap-colorpicker/alpha-horizontal.png b/app/assets/images/clubsite/bootstrap-colorpicker/alpha-horizontal.png new file mode 100755 index 0000000..d0a65c0 Binary files /dev/null and b/app/assets/images/clubsite/bootstrap-colorpicker/alpha-horizontal.png differ diff --git a/app/assets/images/clubsite/bootstrap-colorpicker/alpha.png b/app/assets/images/clubsite/bootstrap-colorpicker/alpha.png new file mode 100755 index 0000000..38043f1 Binary files /dev/null and b/app/assets/images/clubsite/bootstrap-colorpicker/alpha.png differ diff --git a/app/assets/images/clubsite/bootstrap-colorpicker/hue-horizontal.png b/app/assets/images/clubsite/bootstrap-colorpicker/hue-horizontal.png new file mode 100755 index 0000000..a0d9add Binary files /dev/null and b/app/assets/images/clubsite/bootstrap-colorpicker/hue-horizontal.png differ diff --git a/app/assets/images/clubsite/bootstrap-colorpicker/hue.png b/app/assets/images/clubsite/bootstrap-colorpicker/hue.png new file mode 100755 index 0000000..d89560e Binary files /dev/null and b/app/assets/images/clubsite/bootstrap-colorpicker/hue.png differ diff --git a/app/assets/images/clubsite/bootstrap-colorpicker/saturation.png b/app/assets/images/clubsite/bootstrap-colorpicker/saturation.png new file mode 100755 index 0000000..594ae50 Binary files /dev/null and b/app/assets/images/clubsite/bootstrap-colorpicker/saturation.png differ diff --git a/app/assets/images/clubsite/fancybox/blank.gif b/app/assets/images/clubsite/fancybox/blank.gif new file mode 100644 index 0000000..35d42e8 Binary files /dev/null and b/app/assets/images/clubsite/fancybox/blank.gif differ diff --git a/app/assets/images/clubsite/fancybox/fancybox_loading.gif b/app/assets/images/clubsite/fancybox/fancybox_loading.gif new file mode 100644 index 0000000..0158617 Binary files /dev/null and b/app/assets/images/clubsite/fancybox/fancybox_loading.gif differ diff --git a/app/assets/images/clubsite/fancybox/fancybox_overlay.png b/app/assets/images/clubsite/fancybox/fancybox_overlay.png new file mode 100644 index 0000000..a439139 Binary files /dev/null and b/app/assets/images/clubsite/fancybox/fancybox_overlay.png differ diff --git a/app/assets/images/clubsite/fancybox/fancybox_sprite.png b/app/assets/images/clubsite/fancybox/fancybox_sprite.png new file mode 100644 index 0000000..fd8d5ca Binary files /dev/null and b/app/assets/images/clubsite/fancybox/fancybox_sprite.png differ diff --git a/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.eot b/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 0000000..423bd5d Binary files /dev/null and b/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.eot differ diff --git a/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.svg b/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 0000000..4469488 --- /dev/null +++ b/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.ttf b/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000..a498ef4 Binary files /dev/null and b/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.ttf differ diff --git a/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.woff b/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 0000000..d83c539 Binary files /dev/null and b/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.woff differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 8d8189c..97b91c5 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -7,5 +7,23 @@ //= require jquery //= require jquery_ujs //= require bootstrap -//= require_tree . +//= require bootstrap_and_overrides +//= require clubs +//= require content_blocks +//= require courses +//= require events +//= require groups +//= require home +//= require map_standards +//= require maps +//= require memberships +//= require organizers +//= require pages +//= require privileges +//= require results +//= require roles +//= require series +//= require tokens +//= require users +//= require whyjustrun diff --git a/app/assets/javascripts/clubsite.js b/app/assets/javascripts/clubsite.js new file mode 100644 index 0000000..8015cc2 --- /dev/null +++ b/app/assets/javascripts/clubsite.js @@ -0,0 +1,38 @@ +// Clubsite bundle: vendored libraries followed by the wjr modules in +// dependency order. Loaded by the clubsite layouts only. +// +//= require clubsite/jquery +//= require clubsite/underscore +//= require clubsite/knockout +//= require clubsite/moment +//= require clubsite/bootstrap +//= require clubsite/fullcalendar +//= require clubsite/jquery.fancybox +//= require clubsite/jquery.jeditable +//= require clubsite/jquery.ketchup +//= require clubsite/ketchup-bootstrap +//= require clubsite/jquery.placeholder +//= require clubsite/bootstrap-datetimepicker +//= require clubsite/bootstrap-colorpicker +//= require clubsite/bootstrap-typeahead +//= require clubsite/spin +//= require clubsite/ladda +//= require clubsite/markerclusterer +//= require clubsite/wjr/wjr +//= require clubsite/wjr/club +//= require clubsite/wjr/utils +//= require clubsite/wjr/binding +//= require clubsite/wjr/forms +//= require clubsite/wjr/iof +//= require clubsite/wjr/editable +//= require clubsite/wjr/map +//= require clubsite/wjr/calendar +//= require clubsite/wjr/event-list +//= require clubsite/wjr/result-list +//= require clubsite/wjr/result-editor +//= require clubsite/wjr/register-others +//= require clubsite/wjr/edit-event-courses +//= require clubsite/wjr/edit-event-organizers +//= require clubsite/wjr/flickr-photos +//= require clubsite/wjr/wysiwyg.redactor +//= require clubsite/wjr/clubsite diff --git a/app/assets/javascripts/clubsite/wjr/binding.js b/app/assets/javascripts/clubsite/wjr/binding.js new file mode 100644 index 0000000..f1955db --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/binding.js @@ -0,0 +1,28 @@ +/*jslint browser: true indent: 2*/ + +// Handy extensions to the knockout data binding framework +window.WJR = window.WJR || {}; +WJR.binding = (function ($, ko) { + 'use strict'; + ko.bindingHandlers.tooltip = { + init: function (element, valueAccessor) { + var local = ko.utils.unwrapObservable(valueAccessor()), + options = { container: 'body' }; + + ko.utils.extend(options, ko.bindingHandlers.tooltip.options); + ko.utils.extend(options, local); + + $(element).tooltip(options); + + ko.utils.domNodeDisposal.addDisposeCallback(element, function () { + $(element).tooltip("destroy"); + }); + }, + options: { + placement: "right", + trigger: "click" + } + }; + + return ko; +}(jQuery, ko)); diff --git a/app/assets/javascripts/clubsite/wjr/calendar.js b/app/assets/javascripts/clubsite/wjr/calendar.js new file mode 100644 index 0000000..e758a2b --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/calendar.js @@ -0,0 +1,56 @@ +/*jslint browser: true indent: 2*/ +window.WJR = window.WJR || {}; +WJR.calendar = (function (wjr, club, $) { + 'use strict'; + var Calendar = {}; + Calendar.Initialize = function (element) { + element = $(element); + var initialLoad = true, + poppingState = false; + + element.fullCalendar({ + eventSources: [wjr.core.domain + '/club/' + club.id + '/events.json?external_significant_events=all&prefix_club_acronym=external_only'], + year: element.attr('data-calendar-year'), + month: element.attr('data-calendar-month'), + date: element.attr('data-calendar-day'), + timeFormat: 'h:mmtt', + firstDay: 1, + // From google code discussion + viewDisplay : function (view) { + var url, options, date; + date = $.fullCalendar.formatDate(view.start, 'dd-MM-yyyy'); + url = "/events/index/" + date; + options = { viewMode: view.name, start: view.start }; + // IE doesn't support replaceState.. + if (window.history.replaceState) { + //I had to do a little bit of juggling to get it to only run items when necessary + //There might be a better way to do this but I couldn't find one + if (initialLoad) { //Replace the current state to set up state variables. URL should be identical + history.replaceState(options, "Event Calendar", url); + window.onpopstate = function (event) { //set up onpopstate handler + if (!initialLoad) { //the browser kept trying to pop the state on intial load + var start = event.state.start; + if (typeof start === 'string') { //even though i stored a date object, it was coming back as a string for some reason + start = $.fullCalendar.parseDate(start); + } + poppingState = true; //don't re-push state + $('#calendar').fullCalendar('gotoDate', start); + poppingState = true; //don't re-push state + $('#calendar').fullCalendar('changeView', event.state.viewMode); + } + initialLoad = false; + }; + } else { + if (!poppingState) { + history.pushState(options, "Event Calendar", url); + } else { + poppingState = false; + } + } + } + } + }); + }; + + return Calendar; +}(WJR.wjr, WJR.club, jQuery)); diff --git a/app/assets/javascripts/clubsite/wjr/club.js b/app/assets/javascripts/clubsite/wjr/club.js new file mode 100644 index 0000000..a543de5 --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/club.js @@ -0,0 +1,8 @@ + /*jslint browser: true indent: 2*/ + + window.WJR = window.WJR || {}; + WJR.club = (function ($) { + var club = {}; + club.id = $('meta[name="wjr.clubsite.club.id"]').attr("content"); + return club; + }(jQuery)); diff --git a/app/assets/javascripts/clubsite/wjr/clubsite.js b/app/assets/javascripts/clubsite/wjr/clubsite.js new file mode 100644 index 0000000..d8dfadd --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/clubsite.js @@ -0,0 +1,156 @@ +/*jslint browser: true, indent: 2, nomen: true*/ + +window.WJR = window.WJR || {}; +WJR.clubsite = (function ($, _, moment, forms, editable) { + 'use strict'; + var Clubsite = {}; + + Clubsite.initialize = function () { + // Components that we initialize conditionally if needed for the current page + var components = [ + { + selector: 'a.lightbox', + load: function (lightboxes) { + lightboxes.fancybox(); + } + }, { + selector: '.date-picker', + load: function (datePickers) { + datePickers.datetimepicker({ + pickTime: false, + format: "YYYY-MM-DD" + }); + } + }, { + selector: '.event-list', + module: WJR['event-list'], + loadEach: function (element, EventList) { + var eventList = new EventList(); + eventList.initialize(element); + } + }, { + selector: '.result-list', + module: WJR['result-list'], + loadEach: function (element, ResultList) { + var resultList = new ResultList(); + resultList.initialize(element); + } + }, { + selector: '.color-picker', + load: function (colorPickers) { + colorPickers.colorpicker(); + } + }, { + selector: '.result-editor', + module: WJR['result-editor'], + loadEach: function (element, ResultEditor) { + var resultEditor = new ResultEditor(); + resultEditor.initialize(element); + } + }, { + selector: '.register-others', + module: WJR['register-others'], + loadEach: function (element, RegisterOthers) { + var registerOthers = new RegisterOthers(); + registerOthers.initialize(element); + } + }, { + selector: '.edit-event-courses', + module: WJR['edit-event-courses'], + loadEach: function (element, EventCoursesEditor) { + var editor = new EventCoursesEditor(); + editor.initialize(element); + } + }, { + selector: '.edit-event-organizers', + module: WJR['edit-event-organizers'], + loadEach: function (element, EventOrganizersEditor) { + var editor = new EventOrganizersEditor(); + editor.initialize(element); + } + }, { + selector: '.simple-marker-map', + module: WJR.map, + loadEach: function (element, map) { + var markerMap = new map.SimpleMarkerMap(); + markerMap.initialize(element); + } + }, { + selector: '.draggable-marker-map', + module: WJR.map, + loadEach: function (element, map) { + var markerMap = new map.DraggableMarkerMap(); + markerMap.initialize(element); + } + }, { + selector: '.multi-marker-map', + module: WJR.map, + loadEach: function (element, map) { + var markerMap = new map.MultiMarkerMap(); + markerMap.initialize(element); + } + }, { + selector: '.simple-person-picker', + module: WJR.forms, + loadEach: function (element, forms) { + var personPicker = new forms.SimplePersonPicker(); + personPicker.initialize(element); + } + } + ]; + + _.each(components, function (component) { + var results = $(component.selector); + if (results.length !== 0) { + if (component.load !== undefined) { + component.load(results, component.module); + } else { + results.each(function () { + component.loadEach(this, component.module); + }); + } + } + }); + + $('time.timeago').each(function () { + var time = moment($(this).attr('datetime')); + $(this).html(time.fromNow()); + }); + + $('input, textarea').placeholder(); + + forms.validation.checkKetchupFormsAreValidOnSubmit(); + + $('.wjr-wysiwyg').each(function () { + var element = this; + WJR.wysiwyg.createRichTextArea(element); + }); + + $("[data-toggle='tooltip']").tooltip(); + + $('.wjr-calendar').each(function () { + var element = this; + WJR.calendar.Initialize(element); + }); + + $('.flickr-photos-container').each(function () { + var element = this; + WJR['flickr-photos'].Initialize(element); + }); + + editable.initialize(); + + // Event editing form + // Fix for CakePHP form security - exclude the knockout inputs + $("#EventEditForm").submit(function () { + $(this).find('[name ^= "ko_unique"]').attr("name", null); + }); + }; + + return Clubsite; +}(jQuery, _, moment, WJR.forms, WJR.editable)); + +jQuery(function () { + 'use strict'; + WJR.clubsite.initialize(); +}); diff --git a/app/assets/javascripts/clubsite/wjr/edit-event-courses.js b/app/assets/javascripts/clubsite/wjr/edit-event-courses.js new file mode 100644 index 0000000..ebbe185 --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/edit-event-courses.js @@ -0,0 +1,55 @@ +/*jslint nomen: true, browser: true indent: 2*/ +/*global confirm*/ + +window.WJR = window.WJR || {}; +WJR['edit-event-courses'] = (function ($, _, ko) { + 'use strict'; + var EventCoursesEditor = function () { + var viewModel, Course; + + viewModel = { + courses: ko.observableArray([]), + addCourse: function (id, name, distance, climb, isScoreO, description) { + var course = new Course(id, name, distance, climb, isScoreO, description); + this.courses.push(course); + }, + addNewCourse: function () { + this.courses.push(new Course(null, null, null, null, false, null)); + } + }; + + Course = function (id, name, distance, climb, isScoreO, description) { + this.id = id; + this.name = ko.observable(name); + this.distance = ko.observable(distance); + this.climb = ko.observable(climb); + this.rankBy = ko.observable(isScoreO ? "points" : "time"); + this.isScoreO = ko.computed(function () { + return (this.rankBy() === "points"); + }, this); + this.description = ko.observable(description); + this.remove = function () { + // Only need to confirm for deletion if the course hasn't been created locally + if (this.id !== null) { + if (confirm("Are you sure you want to delete this course? This will also delete any results associated to the course.")) { + $.ajax('/courses/delete/' + this.id, { method: 'POST' }); + viewModel.courses.remove(this); + } + } else { + viewModel.courses.remove(this); + } + }; + }; + + this.initialize = function (element) { + var courseJson = $(element.getAttribute('data-course-json-element')).val(); + _.each(JSON.parse(courseJson), function (originalCourse) { + viewModel.addCourse(originalCourse.id, originalCourse.name, originalCourse.distance, originalCourse.climb, originalCourse.isScoreO, originalCourse.description); + }); + + ko.applyBindings(viewModel, element); + }; + }; + + return EventCoursesEditor; +}(jQuery, _, ko)); diff --git a/app/assets/javascripts/clubsite/wjr/edit-event-organizers.js b/app/assets/javascripts/clubsite/wjr/edit-event-organizers.js new file mode 100644 index 0000000..e0607ed --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/edit-event-organizers.js @@ -0,0 +1,65 @@ +/*jslint nomen: true, browser: true indent: 2*/ +/*global confirm*/ + +window.WJR = window.WJR || {}; +WJR['edit-event-organizers'] = (function ($, _, ko, forms) { + 'use strict'; + var EventOrganizersEditor = function () { + var availableRoles = [], finishLoadingOrganizers, + roleById, viewModel, Organizer; + + viewModel = { + organizers: ko.observableArray([]), + addOrganizer: function (id, name, roleId) { + this.organizers.push(new Organizer(id, name, roleId)); + } + }; + + Organizer = function (id, name, roleId) { + this.id = id; + this.name = ko.observable(name); + this.availableRoles = ko.observableArray(availableRoles); + this.role = ko.observable(roleById(roleId)); + this.remove = function () { + viewModel.organizers.remove(this); + }; + }; + + this.initialize = function (element) { + $.getJSON('/roles/index.json', function (data) { + _.each(data, function (role) { + var availableRole = {}; + availableRole.id = role.id; + availableRole.name = role.name; + availableRoles.push(availableRole); + }); + + finishLoadingOrganizers(element); + }); + }; + + roleById = function (id) { + return _.find(availableRoles, function (role) { + return (role.id === id); + }); + }; + + finishLoadingOrganizers = function (element) { + var organizerJson, organizerElement; + organizerElement = $(element.getAttribute('data-organizer-input')); + organizerJson = $(element.getAttribute('data-organizer-json-element')).val(); + _.each(JSON.parse(organizerJson), function (originalOrganizer) { + viewModel.addOrganizer(originalOrganizer.id, originalOrganizer.name, originalOrganizer.role.id); + }); + + forms.personPicker(organizerElement, { maintainInput: false, allowFake: false }, function (person) { + if (person !== null) { + organizerElement.val(null); + viewModel.addOrganizer(person.id, person.name); + } + }); + ko.applyBindings(viewModel, element); + }; + }; + return EventOrganizersEditor; +}(jQuery, _, ko, WJR.forms)); diff --git a/app/assets/javascripts/clubsite/wjr/editable.js b/app/assets/javascripts/clubsite/wjr/editable.js new file mode 100644 index 0000000..b68e210 --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/editable.js @@ -0,0 +1,76 @@ +/*jslint browser: true indent: 2*/ + +window.WJR = window.WJR || {}; +WJR.editable = (function ($) { + 'use strict'; + var editable = {}; + editable.initialize = function () { + var editableContent = $('.page-resource.wjr-editable, .page-resource-title.wjr-editable, .content-block.wjr-editable'); + // Only load jeditable if we actually need to. + if (editableContent.length !== 0) { + (function () { + /*jslint unparam: true*/ + $.editable.addInputType('wysiwyg', { + element: function (settings, original) { + var textarea = $(' +

Use comments to share information with other participants and the event organizers. For example, you can use this to find/offer a ride to the event. All information shared will be public.

+ + + <% end %> + + + diff --git a/app/views/clubsite/events/_course_maps.html.erb b/app/views/clubsite/events/_course_maps.html.erb new file mode 100644 index 0000000..61bac2e --- /dev/null +++ b/app/views/clubsite/events/_course_maps.html.erb @@ -0,0 +1,42 @@ +<%# locals: event %> +<% if event.courses.any? %> +

Course Maps

+ <% maps_posted = false %> + <% event.courses.each do |course| %> + <% next unless media_exists?('Course', course.id) %> + <% maps_posted = true %> + <% + media_url_absolute = current_club.clubsite_url(media_url('Course', course.id, 'image')) + description = "#{formatted_event_date(event)} - #{event.name} - #{course.name} course" + %> +

+ <%= course.name %> + + + + + + + +

+ <%= media_linked_image('Course', course.id, '600x600', {}, { style: 'max-width: 100%', 'data-pin-hover' => true }) %> + <% end %> + <% unless maps_posted %> + No course maps posted yet. + <% end %> + + <% content_for :javascript_bottom do %> + + <% end %> +<% end %> diff --git a/app/views/clubsite/events/_course_registration.html.erb b/app/views/clubsite/events/_course_registration.html.erb new file mode 100644 index 0000000..ab38c69 --- /dev/null +++ b/app/views/clubsite/events/_course_registration.html.erb @@ -0,0 +1,60 @@ +<%# locals: event. Course list with registrations; the register/unregister + actions post to routes that are ported separately. %> +
+ <% user_id = current_user&.id || 0 %> + <% event.courses.each do |course| %> +
+
+
+ <% if !@registered_course_ids.include?(course.id) && @registration_open %> +
+ <%= button_to "/courses/register/#{course.id}/#{user_id}", class: 'btn btn-success' do %> + Register + <% end %> +
+ <% end %> +
+ +

<%= course.name %>

+ + <%= sanitize course.description %> +

+ <% if course.distance.present? %> +
Distance: <%= course.distance %>m + <% end %> + <% if course.climb.present? %> +
Climb: <%= course.climb %>m + <% end %> +

+
+
+
+ <%= render 'results_entries', course: course, results: @sorted_results[course.id] %> +
+
+ <% end %> +
+ +<% if @registration_open %> +
+

Register Others

+ <% if !user_signed_in? %> + Sign in / Sign Up + <% else %> +

To register someone else, choose the course to register them on, then type their name, and pick the person from the drop down list.

+

Families: you don't need to register an account for every family member, just type the participating people's names below and an account will automatically be created for each person.

+
+ +
+
+ + +
+ + <% end %> +
+<% end %> diff --git a/app/views/clubsite/events/_header.html.erb b/app/views/clubsite/events/_header.html.erb new file mode 100644 index 0000000..dad0fd7 --- /dev/null +++ b/app/views/clubsite/events/_header.html.erb @@ -0,0 +1,52 @@ +<%# locals: event, can_edit %> + diff --git a/app/views/clubsite/events/_info.html.erb b/app/views/clubsite/events/_info.html.erb new file mode 100644 index 0000000..eaa7400 --- /dev/null +++ b/app/views/clubsite/events/_info.html.erb @@ -0,0 +1,43 @@ +<%# locals: event %> +<% social_text = 'Check out this event on' %> +<% if event.facebook_url.present? %> + + + <%= social_text %> Facebook + +<% end %> +<% if event.attackpoint_url.present? %> + + + <%= social_text %> Attackpoint + +<% end %> +<% if event.facebook_url.present? || event.attackpoint_url.present? %> +
+<% end %> + +<% if event.organizers.any? %> +

<%= event.organizers.length > 1 ? 'Organizers' : 'Organizer' %>: <%= render 'organizers_list', organizers: event.organizers %>

+<% end %> + +<% if event.map.present? %> +

Map: <%= link_to event.map.name, "/maps/view/#{event.map_id}" %>

+<% end %> + +<% if event.description.present? %> + <%= sanitize event.description %> +<% else %> + Check back soon for more information. +<% end %> +
+ +<% if !event.completed? && event.series&.information.present? %> +

<%= event.series.name %>

+ <%= sanitize event.series.information %> +
+<% end %> + +<% if event.lat.present? %> +

Location

+ +<% end %> diff --git a/app/views/clubsite/events/_knockout_box.html.erb b/app/views/clubsite/events/_knockout_box.html.erb new file mode 100644 index 0000000..b220023 --- /dev/null +++ b/app/views/clubsite/events/_knockout_box.html.erb @@ -0,0 +1,20 @@ +<%# locals: template_name %> + diff --git a/app/views/clubsite/events/_knockout_result_list.html.erb b/app/views/clubsite/events/_knockout_result_list.html.erb new file mode 100644 index 0000000..77278cc --- /dev/null +++ b/app/views/clubsite/events/_knockout_result_list.html.erb @@ -0,0 +1,52 @@ +<%# locals: mode ('normal' or 'live') %> +
+ <% if mode == 'live' %> +

Results produced on

+ <% end %> +
+

+
+

No results

+
+
+ + + + + + + + + + + + + + + + + + + + + +
#ParticipantScore PointsTimePoints
+ + + + + + + + + + +
+ +
+
+
+
+
diff --git a/app/views/clubsite/events/_list.html.erb b/app/views/clubsite/events/_list.html.erb new file mode 100644 index 0000000..0e4e069 --- /dev/null +++ b/app/views/clubsite/events/_list.html.erb @@ -0,0 +1,29 @@ +<%# Knockout event list fed by the IOF XML event feed. Self-contained so the + home page can render it too. %> +
+
+ +
+

Showing events after

+
+

Showing events before

+
+ + <%= render 'clubsite/events/knockout_box', template_name: 'event-box-template' %> +
+
diff --git a/app/views/clubsite/events/_list_header.html.erb b/app/views/clubsite/events/_list_header.html.erb new file mode 100644 index 0000000..b73d17d --- /dev/null +++ b/app/views/clubsite/events/_list_header.html.erb @@ -0,0 +1,24 @@ +<%# locals: type ('calendar' or 'list') %> + diff --git a/app/views/clubsite/events/_live_result_list.html.erb b/app/views/clubsite/events/_live_result_list.html.erb new file mode 100644 index 0000000..3e913fb --- /dev/null +++ b/app/views/clubsite/events/_live_result_list.html.erb @@ -0,0 +1,12 @@ +<%# locals: event %> +

+ Live results - these are not finalized! +
+ Results will automatically update, you don't need to refresh your browser. +

+ +
+
+ <%= render 'knockout_result_list', mode: 'live' %> +
+
diff --git a/app/views/clubsite/events/_live_results_visibility_button.html.erb b/app/views/clubsite/events/_live_results_visibility_button.html.erb new file mode 100644 index 0000000..a4f801e --- /dev/null +++ b/app/views/clubsite/events/_live_results_visibility_button.html.erb @@ -0,0 +1,6 @@ +<%# locals: event, result_list. Editors only; the toggle endpoint is ported + separately. %> +<%= button_to result_list.visible ? 'Hide Live Results' : 'Show Live Results', + "/events/toggle_live_results_visibility/#{event.id}/#{result_list.visible ? 'false' : 'true'}", + class: 'btn btn-primary' %> +

diff --git a/app/views/clubsite/events/_open_graph_tags.html.erb b/app/views/clubsite/events/_open_graph_tags.html.erb new file mode 100644 index 0000000..9c4b512 --- /dev/null +++ b/app/views/clubsite/events/_open_graph_tags.html.erb @@ -0,0 +1,18 @@ +<%# locals: event %> +<% + dynamic_text = "Orienteering event taking place: #{formatted_event_date(event)}." + finish_time = event.read_attribute(:finish_date) + + og_tag('og:site_name', 'WhyJustRun') + og_tag('og:type', 'event') + og_tag('og:url', current_club.clubsite_url("/events/view/#{event.id}")) + og_tag('og:description', "#{dynamic_text} Orienteering is an exciting sport for all ages and fitness levels that involves reading a detailed map and using a compass to find checkpoints.") + og_tag('og:title', event.name) + og_tag('og:image', current_club.clubsite_url('/img/orienteering_symbol.png')) + og_tag('event:start_time', event.date.iso8601) + og_tag('event:end_time', finish_time.iso8601) if finish_time + if event.lat.present? + og_tag('event:location:latitude', event.lat) + og_tag('event:location:longitude', event.lng) + end +%> diff --git a/app/views/clubsite/events/_organizers_list.html.erb b/app/views/clubsite/events/_organizers_list.html.erb new file mode 100644 index 0000000..fa0234b --- /dev/null +++ b/app/views/clubsite/events/_organizers_list.html.erb @@ -0,0 +1,2 @@ +<%# locals: organizers %> +<% organizers.each_with_index do |organizer, index| %><%= ', ' if index.positive? %><%= link_to organizer.user.name, profile_url(organizer.user) %><%= " (#{organizer.role.name})" if organizer.role.present? %><% end %> diff --git a/app/views/clubsite/events/_redirect_view.html.erb b/app/views/clubsite/events/_redirect_view.html.erb new file mode 100644 index 0000000..2ea5e0c --- /dev/null +++ b/app/views/clubsite/events/_redirect_view.html.erb @@ -0,0 +1,13 @@ +<%# locals: event, can_edit. Only editors see this page; everybody else was + redirected to the event's custom URL by the controller. %> +<%= render 'header', event: event, can_edit: can_edit %> + +
+ IMPORTANT +

You are seeing this page because you can edit this event. Normal users will be redirected automatically to the external event page. To start showing this WhyJustRun event page to normal users, remove the Redirect URL on the edit event page.

+
+
+

+ Continue to Event + Edit Event +

diff --git a/app/views/clubsite/events/_registration_section.html.erb b/app/views/clubsite/events/_registration_section.html.erb new file mode 100644 index 0000000..2b5e94d --- /dev/null +++ b/app/views/clubsite/events/_registration_section.html.erb @@ -0,0 +1,29 @@ +<%# locals: event, can_edit %> +<% result_list = event.result_list %> +<% if result_list.present? && result_list.status == ResultList::LIVE_STATUS && can_edit %> +

Live Results

+ <%= render 'live_results_visibility_button', event: event, result_list: result_list %> +<% end %> + +<% if @completed %> + <%= render 'course_maps', event: event %> +<% elsif event.registration_url.present? %> +
+

Registration

+
+
+ + Register at <%= URI.parse(event.registration_url).host rescue event.registration_url %> + +
+<% elsif event.courses.any? %> +
+

Course Registration

+ <% if event.registration_deadline.present? %> +

+ <%= @registration_open ? "Deadline #{short_date_with_time(event.registration_deadline)}" : 'Registration is Closed' %> +

+ <% end %> +
+ <%= render 'course_registration', event: event %> +<% end %> diff --git a/app/views/clubsite/events/_results_entries.html.erb b/app/views/clubsite/events/_results_entries.html.erb new file mode 100644 index 0000000..72294c7 --- /dev/null +++ b/app/views/clubsite/events/_results_entries.html.erb @@ -0,0 +1,44 @@ +<%# locals: course, results. The list of registrations for a course. %> +<% if results.present? %> + + + + + + + + <% user_id = current_user&.id %> + <% results.each do |result| %> + <% own_entry = user_id.present? && (result.registrant_id == user_id || result.user_id == user_id) %> + <% modal_id = own_entry ? "change-comment-modal-#{result.id}" : nil %> + + + + <% end %> + +
<%= results.length == 1 ? 'Entries' : "Entries (#{results.length})" %>
+ <%= link_to result.user.name, profile_url(result.user) %> + +
+ <% if own_entry %> +
+ +
+
+ <%= button_to "/courses/unregister/#{course.id}/#{result.user_id}", class: 'btn btn-xs btn-danger' do %> + Unregister + <% end %> +
+ <% end %> + <% if result.registrant_comment.present? %> +
+ +
+ <% end %> +
+
+ <% if modal_id %> + <%= render 'change_comment_modal', result: result, modal_id: modal_id %> + <% end %> +
+<% end %> diff --git a/app/views/clubsite/events/_results_links.html.erb b/app/views/clubsite/events/_results_links.html.erb new file mode 100644 index 0000000..06f5f39 --- /dev/null +++ b/app/views/clubsite/events/_results_links.html.erb @@ -0,0 +1,9 @@ +<%# locals: event %> +<% { results_url: 'Results at ', routegadget_url: 'RouteGadget at ' }.each do |url_key, title_prefix| %> + <% url = event.public_send(url_key) %> + <% next if url.blank? %> +
+ <%= title_prefix %><%= URI.parse(url).host rescue url %> +
+

+<% end %> diff --git a/app/views/clubsite/events/_results_list.html.erb b/app/views/clubsite/events/_results_list.html.erb new file mode 100644 index 0000000..de964d8 --- /dev/null +++ b/app/views/clubsite/events/_results_list.html.erb @@ -0,0 +1,6 @@ +<%# locals: event_id %> +
+
+ <%= render 'knockout_result_list', mode: 'normal' %> +
+
diff --git a/app/views/clubsite/events/_results_section.html.erb b/app/views/clubsite/events/_results_section.html.erb new file mode 100644 index 0000000..4fe96e6 --- /dev/null +++ b/app/views/clubsite/events/_results_section.html.erb @@ -0,0 +1,22 @@ +<%# locals: event, can_edit %> +
+
+

Results

+
+ + <% result_list = event.result_list %> + <% if event.results_posted %> + <%= render 'results_list', event_id: event.id %> + <% elsif result_list.present? && result_list.status == ResultList::LIVE_STATUS %> + <% if can_edit %> + <%= render 'live_results_visibility_button', event: event, result_list: result_list %> + <% end %> + + <% if result_list.visible %> + <%= render 'live_result_list', event: event %> + <% end %> + <% end %> + + <%= render 'results_links', event: event %> + <%= render 'course_maps', event: event %> +
diff --git a/app/views/clubsite/events/_summary_list.html.erb b/app/views/clubsite/events/_summary_list.html.erb new file mode 100644 index 0000000..4bd8140 --- /dev/null +++ b/app/views/clubsite/events/_summary_list.html.erb @@ -0,0 +1,12 @@ +<% + sections = { + 'Ongoing' => current_club.events.list_includes.ongoing.to_a, + 'Upcoming' => current_club.events.list_includes.upcoming.limit(4).to_a, + 'Past' => current_club.events.list_includes.past.limit(2).to_a + } +%> +<% sections.each do |title, events| %> + <% next if events.empty? %> +

<%= title %>

+ <%= render 'clubsite/events/box_list', events: events %> +<% end %> diff --git a/app/views/clubsite/events/_summary_list_by_series.html.erb b/app/views/clubsite/events/_summary_list_by_series.html.erb new file mode 100644 index 0000000..d767b74 --- /dev/null +++ b/app/views/clubsite/events/_summary_list_by_series.html.erb @@ -0,0 +1,34 @@ +<% ongoing = current_club.events.list_includes.ongoing.to_a %> +<% if ongoing.any? %> +

Ongoing

+ <%= render 'clubsite/events/box_list', events: ongoing %> +<% end %> + +<% current_series = current_club.series.where(is_current: true) %> +<% + upcoming = {} + current_series.each do |series| + upcoming[series.name] = current_club.events.list_includes.upcoming.for_series(series.id).limit(2).to_a + end + upcoming['Other'] = current_club.events.list_includes.upcoming.for_series(0).limit(2).to_a +%> +

Upcoming

+<% upcoming.each do |title, events| %> + <% next if events.empty? %> +

<%= title %>

+ <%= render 'clubsite/events/box_list', events: events %> +<% end %> + +<% + past = {} + current_series.each do |series| + past[series.name] = current_club.events.list_includes.past.for_series(series.id).limit(1).to_a + end + past['Other'] = current_club.events.list_includes.past.for_series(0).limit(1).to_a +%> +

Past

+<% past.each do |title, events| %> + <% next if events.empty? %> +

<%= title %>

+ <%= render 'clubsite/events/box_list', events: events %> +<% end %> diff --git a/app/views/clubsite/events/edit.html.erb b/app/views/clubsite/events/edit.html.erb new file mode 100644 index 0000000..4d2ebef --- /dev/null +++ b/app/views/clubsite/events/edit.html.erb @@ -0,0 +1,265 @@ +<% content_for :title, @event.new_record? ? 'Add Event' : 'Edit Event' %> + + +<% + start_time = @event.date + finish_time = @event.read_attribute(:finish_date) + deadline_time = @event.registration_deadline + edit_url = @event.persisted? ? "/events/edit/#{@event.id}" : '/events/edit' +%> +<%= form_with model: @event, scope: :event, url: edit_url, method: :post, + html: { class: 'form-horizontal', 'data-validate' => 'ketchup' } do |f| %> +
+ +
+ <%= f.text_field :name, id: 'EventName', class: 'form-control', required: 'required', + 'data-validate' => 'validate(required)' %> +
+
+ +
+ +
+
+
+ <%= text_field_tag 'event[date]', start_time&.strftime('%Y-%m-%d'), + id: 'EventDate', size: 10, maxlength: 10, placeholder: 'yyyy-mm-dd', + class: 'form-control date-picker', 'data-format' => 'YYYY-MM-DD', + 'data-validate' => 'validate(date, required)' %> +
+
+ <%= text_field_tag 'event[time]', start_time&.strftime('%H:%M'), + id: 'EventTime', size: 5, maxlength: 5, placeholder: 'hh:mm', + class: 'form-control time-picker', 'data-format' => 'HH:mm', + 'data-validate' => 'validate(required, time)' %> +
+ (24 hour format) +
+
+
+ +
+ +
+
+
+ <%= text_field_tag 'event[finish_date]', finish_time&.strftime('%Y-%m-%d'), + id: 'EventFinishDate', size: 10, maxlength: 10, placeholder: 'yyyy-mm-dd', + class: 'form-control date-picker', 'data-format' => 'YYYY-MM-DD', + 'data-validate' => 'validate(date, date_after(EventDate, EventTime, EventFinishDate, EventFinishTime))' %> +
+
+ <%= text_field_tag 'event[finish_time]', finish_time&.strftime('%H:%M'), + id: 'EventFinishTime', size: 5, maxlength: 5, placeholder: 'hh:mm', + class: 'form-control time-picker', 'data-format' => 'HH:mm', + 'data-validate' => 'validate(time, requires(EventFinishDate, finish date))' %> +
+ (optional) +
+
+
+ +
+ +
+
+
+ <%= text_field_tag 'event[deadline_date]', deadline_time&.strftime('%Y-%m-%d'), + id: 'EventDeadlineDate', size: 10, maxlength: 10, placeholder: 'yyyy-mm-dd', + class: 'form-control date-picker', 'data-format' => 'YYYY-MM-DD', + 'data-validate' => 'validate(date, date_before(EventDate, EventTime, EventDeadlineDate, EventDeadlineTime))' %> +
+
+ <%= text_field_tag 'event[deadline_time]', deadline_time&.strftime('%H:%M'), + id: 'EventDeadlineTime', size: 5, maxlength: 5, placeholder: 'hh:mm', + class: 'form-control time-picker', 'data-format' => 'HH:mm', + 'data-validate' => 'validate(time, requires(EventDeadlineDate, deadline date))' %> +
+ (optional) +
+
+
+ +
+ +
+ <%= f.select :event_classification_id, + @event_classifications.map { |classification| ["#{classification.name} (#{classification.description})", classification.id] }, + { include_blank: 'Choose classification' }, + id: 'EventEventClassificationId', class: 'form-control', required: 'required', + 'data-validate' => 'validate(required)' %> +
+
+ + <% url_fields = [ + { name: 'Redirect URL', field: :custom_url, + placeholder: 'Only use if all info is on external website', + help: 'If you want to use a third party website for the event, enter the URL for the event page here. NOTE: People accessing your event may be automatically redirected to the other website, depending on where on WhyJustRun they come from.' }, + { name: 'Registration URL', field: :registration_url, + help: 'If you want to use a third party registration system like Zone4, enter the URL to the registration page here.' }, + { name: 'Results URL', field: :results_url, + help: 'If you post results on a third party website like WinSplits, enter the URL to the results page here after the event.' }, + { name: 'RouteGadget URL', field: :routegadget_url, + help: 'If you have posted a RouteGadget for the event, enter the URL to the RouteGadget page here after the event.' }, + { name: 'Facebook URL', field: :facebook_url, + help: 'If you have a Facebook Event, add the URL here.' }, + { name: 'Attackpoint URL', field: :attackpoint_url, + help: 'If you have an Attackpoint Event, add the URL here.' } + ] %> + <% url_fields.each do |url_field| %> +
+ +
+ <%= f.url_field url_field[:field], size: 80, class: 'form-control', + placeholder: url_field[:placeholder], + 'data-validate' => 'validate(url_or_empty)' %> +
+
+ <% end %> + +
+ +
+ <%= f.text_area :description, id: 'EventDescription', class: 'form-control wjr-wysiwyg', rows: 12 %> +
+
+ +
+ +
+ <%= f.collection_select :series_id, @series_options, :id, :name, + { include_blank: 'Choose the event series' }, + id: 'EventSeriesId', class: 'form-control' %> +
+
+
+ +
+ <%= f.collection_select :map_id, @maps, :id, :name, + { include_blank: 'Choose the event map' }, + id: 'EventMapId', class: 'form-control' %> +
+
+ +
+ <%= f.hidden_field :organizers, value: @organizers_json, id: 'EventOrganizers', + data: { bind: 'value: ko.toJSON(organizers)' } %> + +
+ +
+ +

+ + + + +
NameRole
+
+
+
+ +
+ <%= f.hidden_field :courses, value: @courses_json, id: 'EventCourses', + data: { bind: 'value: ko.toJSON(courses)' } %> + +
+ +
+ +

+ + + + + + + + + + + + +
NameDist. (m)Climb (m)Rank byDescription
+
+
+
+ +
+ +
+
+
+ <%= f.number_field :number_of_participants, id: 'EventNumberOfParticipants', + min: 0, max: 4294967295, class: 'form-control' %> +
+ (Optional: used for insurance purposes, overrides participant count from event results) +
+
+
+ +
+ +
+
+ <%= f.check_box :is_ranked, id: 'EventIsRanked' %> +
+
+
+ +
+ +
+ <%= f.hidden_field :lat, value: @event.lat || current_club.lat, id: 'EventLat' %> + <%= f.hidden_field :lng, value: @event.lng || current_club.lng, id: 'EventLng' %> +
+
+

Drag the marker to the meeting location

+
+
+ +
+
+ <%= f.submit 'Save', class: 'btn btn-primary' %> +
+
+<% end %> diff --git a/app/views/clubsite/events/edit_results.html.erb b/app/views/clubsite/events/edit_results.html.erb new file mode 100644 index 0000000..b385e65 --- /dev/null +++ b/app/views/clubsite/events/edit_results.html.erb @@ -0,0 +1,95 @@ +<% content_for :title, 'Edit Results' %> + + + + +
+ +
+
+
+
+ <%= form_with url: "/events/editResults/#{@event.id}", method: :post, scope: :event do |f| %> + <%= f.hidden_field :courses, id: 'EventCourses', value: '', + data: { bind: 'value: ko.toJSON(courses)' } %> +
+
+ +
+
+ <%= f.submit 'Save', class: 'btn btn-primary' %> + <% end %> +
+ +
+

Add Competitor

+

Choose the course you would like to add a competitor.
Type in the participant's name and choose the matching person. If they are not already in the system, choose the "Create New User" option.

+
+ +
+
+ +
+
+
+
diff --git a/app/views/clubsite/events/index.html.erb b/app/views/clubsite/events/index.html.erb new file mode 100644 index 0000000..5ca2661 --- /dev/null +++ b/app/views/clubsite/events/index.html.erb @@ -0,0 +1,10 @@ +<% content_for :title, 'Calendar' %> +<%= render 'list_header', type: 'calendar' %> +<%= render 'calendar', year: @year, month: @month, day: @day %> +<% if @series.any? %> + +<% end %> diff --git a/app/views/clubsite/events/listing.html.erb b/app/views/clubsite/events/listing.html.erb new file mode 100644 index 0000000..31c6e28 --- /dev/null +++ b/app/views/clubsite/events/listing.html.erb @@ -0,0 +1,2 @@ +<%= render 'list_header', type: 'list' %> +<%= render 'list' %> diff --git a/app/views/clubsite/events/map.html.erb b/app/views/clubsite/events/map.html.erb new file mode 100644 index 0000000..d833541 --- /dev/null +++ b/app/views/clubsite/events/map.html.erb @@ -0,0 +1,13 @@ + + +
+
diff --git a/app/views/clubsite/events/planner.html.erb b/app/views/clubsite/events/planner.html.erb new file mode 100644 index 0000000..f3f2db1 --- /dev/null +++ b/app/views/clubsite/events/planner.html.erb @@ -0,0 +1,62 @@ +<% content_for :title, 'Event Planner' %> + + +
+
+

Which Map?

+ + + + + + + + + <% @maps.each do |map, last_event| %> + + + + + <% end %> + +
NameLast Used
<%= map.name %> + <% if last_event %> + + <%= time_ago_in_words(last_event.date) %> ago + + <% else %> + Never used + <% end %> +
+
+ +
+

Who's Next?

+

A list of people who have attended at least <%= @attendance_threshold %> events since <%= time_ago_in_words(@date_threshold) %> ago, but haven't volunteered as an organizer.

+ + + + + + + + + <% @volunteers.each do |user, attended| %> + + + + + <% end %> + +
NameAttended
+ <%= user.name %> + + <%= attended %> events +
+
+
diff --git a/app/views/clubsite/events/printable_entries.html.erb b/app/views/clubsite/events/printable_entries.html.erb new file mode 100644 index 0000000..d8ba68a --- /dev/null +++ b/app/views/clubsite/events/printable_entries.html.erb @@ -0,0 +1,53 @@ +<% content_for :title, "#{@event.name} Entries" %> +

<%= @event.name %> on <%= formatted_event_date(@event) %>

+<% if @event.courses.any? %> + <% @event.courses.each do |course| %> + <% results = @sorted_results[course.id] %> +

Course: <%= course.name %> (<%= results.length %> participants)

+ + + + + + + + + + + + + + + <% counter = 0 %> + <% results.each do |result| %> + <% counter += 1 %> + + + + + + + + + + + + <% end %> + <% Clubsite::EventsController::NUM_BLANK_ENTRIES.times do |i| %> + + + + + + + + + + + + <% end %> +
 NameMemberSI numberCommentXStart timeFinish timeTotal time
<%= counter %><%= result.user.name %><%= '✓' if result.user.member_of?(current_club, Date.current.year) %><%= result.user.si_number %><%= result.registrant_comment %>    
<%= counter + i + 1 %>        
+ <% end %> +<% else %> + No courses defined +<% end %> diff --git a/app/views/clubsite/events/show.html.erb b/app/views/clubsite/events/show.html.erb new file mode 100644 index 0000000..46821ad --- /dev/null +++ b/app/views/clubsite/events/show.html.erb @@ -0,0 +1,30 @@ +<% content_for :title, @event.name %> +<% if @event.custom_url.blank? %> + <%= render 'open_graph_tags', event: @event %> + <%= render 'header', event: @event, can_edit: @can_edit %> + +
+ <% if !@has_results %> + <%# Pre-event template %> +
+ <%= render 'info', event: @event %> +
+ +
+ <%= render 'registration_section', event: @event, can_edit: @can_edit %> +
+ <% else %> + <%# Post-event template %> +
+ <%= render 'results_section', event: @event, can_edit: @can_edit %> +
+
+ <%= render 'info', event: @event %> +
+ <% end %> +
+ + <%= render 'clubsite/shared/flickr_photos', event_id: @event.id %> +<% else %> + <%= render 'redirect_view', event: @event, can_edit: @can_edit %> +<% end %> diff --git a/app/views/clubsite/events/upload_maps.html.erb b/app/views/clubsite/events/upload_maps.html.erb new file mode 100644 index 0000000..9f65597 --- /dev/null +++ b/app/views/clubsite/events/upload_maps.html.erb @@ -0,0 +1,31 @@ +<% content_for :title, 'Upload Course Maps' %> + + +

Upload one map for each course. The upload process may take some time, +as thumbnails also have to be generated. Uploading will overwrite any existing maps for the particular course. Allowed map formats: jpg/jpeg, gif, png, pdf.

+<% if @courses.any? %> + + + + + <% @courses.each do |course| %> + + + + + + <% end %> +
CourseAdd mapCurrent Map
<%= course.name %> + <%= form_with url: "/courses/uploadMap/#{course.id}", method: :post, scope: :course, + html: { multipart: true } do |f| %> + <%= f.file_field :image, style: 'width: 240px' %> + <%= f.submit 'Upload' %> + <% end %> + + <%= media_image('Course', course.id, '100x150') %> +
+<% else %> +

No courses defined.

+<% end %> diff --git a/app/views/clubsite/map_standards/edit.html.erb b/app/views/clubsite/map_standards/edit.html.erb new file mode 100644 index 0000000..db5e12e --- /dev/null +++ b/app/views/clubsite/map_standards/edit.html.erb @@ -0,0 +1,34 @@ +<% content_for :title, 'Map standard' %> + + +<% edit_url = @map_standard.persisted? ? "/mapStandards/edit/#{@map_standard.id}" : '/mapStandards/edit' %> +<%= form_with model: @map_standard, url: edit_url, html: { class: 'form-horizontal' } do |f| %> +
+ +
+ <%= f.text_field :name, class: 'form-control' %> +
+
+
+ +
+
+ <%= f.text_field :color, value: @map_standard.color.presence || 'rgba(0,0,0,1)', class: 'form-control' %> + +
+
+
+
+ +
+ <%= f.text_field :description, class: 'form-control' %> +
+
+
+
+ <%= f.submit 'Save', class: 'btn btn-primary' %> +
+
+<% end %> diff --git a/app/views/clubsite/map_standards/index.html.erb b/app/views/clubsite/map_standards/index.html.erb new file mode 100644 index 0000000..8c76b5f --- /dev/null +++ b/app/views/clubsite/map_standards/index.html.erb @@ -0,0 +1,41 @@ +<% content_for :title, 'Map Standards' %> + +
+
+ + + + + + + + + + + <% @map_standards.each do |map_standard| %> + + + + + + + <% end %> + +
NameDescription
<%= map_standard.name %><%= map_standard.description %> + + + + + <%= form_with url: "/mapStandards/delete/#{map_standard.id}", html: { class: 'thin-form' } do %> + + <% end %> +
+
+
diff --git a/app/views/clubsite/maps/_events_on_map.html.erb b/app/views/clubsite/maps/_events_on_map.html.erb new file mode 100644 index 0000000..8ffd119 --- /dev/null +++ b/app/views/clubsite/maps/_events_on_map.html.erb @@ -0,0 +1,19 @@ +<% if events.any? %> +

Events on this map

+ + + + + + + + + <% events.each do |event| %> + + + + + <% end %> + +
SeriesDate
<%= event.series&.acronym.present? ? "#{event.name} (#{event.series.acronym})" : event.name %><%= link_to event.local_date.strftime('%b %-d, %Y'), clubsite_event_path(id: event.id) %>
+<% end %> diff --git a/app/views/clubsite/maps/_info.html.erb b/app/views/clubsite/maps/_info.html.erb new file mode 100644 index 0000000..1f97bd4 --- /dev/null +++ b/app/views/clubsite/maps/_info.html.erb @@ -0,0 +1,9 @@ +<% if map.lat.present? %> +

Location

+ <% location = geocode_look_up(map.lat, map.lng) %> + <% location_name = [location['neighbourhood'], location['city']].reject(&:blank?).join(', ') %> + <% if location_name.present? %> +

<%= location_name %>

+ <% end %> + +<% end %> diff --git a/app/views/clubsite/maps/edit.html.erb b/app/views/clubsite/maps/edit.html.erb new file mode 100644 index 0000000..01e3577 --- /dev/null +++ b/app/views/clubsite/maps/edit.html.erb @@ -0,0 +1,83 @@ +<% content_for :title, @map.persisted? ? 'Edit Map' : 'Add Map' %> + + +<% edit_url = @map.persisted? ? "/maps/edit/#{@map.id}" : '/maps/edit' %> +<%= form_with model: @map, url: edit_url, html: { class: 'form-horizontal', multipart: true } do |f| %> +
+ +
+ <%= f.text_field :name, class: 'form-control', required: 'required' %> +
+
+
+ +
+ <%= f.collection_select :map_standard_id, @map_standards, :id, :name, + { include_blank: 'Choose the standard' }, class: 'form-control' %> +
+
+
+ +
+ <%= f.text_field :scale, class: 'form-control', placeholder: '10000' %> +
+
+
+ +
+ <%= f.text_field :file_url, class: 'form-control' %> +
+
+
+ +
+ <%= f.text_area :notes, class: 'form-control wjr-wysiwyg' %> +
+
+
+ +
+ <%= f.file_field :image %> + <% if @map.persisted? && media_exists?('Map', @map.id) %> +
<%= media_image('Map', @map.id, '400x600', width: '400px', class: 'fitting-image') %> + <% end %> +
+
+ <% if @map.persisted? && media_exists?('Map', @map.id) %> +
+ +
+ <%= media_image('Map', @map.id, '60x60', width: '60px') %> +

The banner is regenerated from a random crop of the map image on every upload.

+
+
+ <% end %> +
+ +
+ <%= f.hidden_field :lat, id: 'MapLat', value: @map.lat || current_club.lat %> + <%= f.hidden_field :lng, id: 'MapLng', value: @map.lng || current_club.lng %> +
data-update-url="/maps/update/<%= @map.id %>"<% end %> + data-zoom="10" + style="height: 400px; width: 100%"> +
+ +

Drag the marker to the location of the map

+
+
+
+
+ <%= f.submit 'Save', class: 'btn btn-primary' %> +
+
+<% end %> diff --git a/app/views/clubsite/maps/index.html.erb b/app/views/clubsite/maps/index.html.erb new file mode 100644 index 0000000..5a86c68 --- /dev/null +++ b/app/views/clubsite/maps/index.html.erb @@ -0,0 +1,28 @@ + +
+
+ <%= render_content_blocks 'general_maps_information', nil, '
' %> +
+
+
+
+
+
diff --git a/app/views/clubsite/maps/report.html.erb b/app/views/clubsite/maps/report.html.erb new file mode 100644 index 0000000..e69d4e6 --- /dev/null +++ b/app/views/clubsite/maps/report.html.erb @@ -0,0 +1,36 @@ + + +
+ + + + + + + + + + + + + + + + <% @maps.each_with_index do |map, index| %> + + + + + + + + + + + + <% end %> + +
NameMap standardCreatedModifiedScaleLatitudeLongitudeThumbnail
<%= index + 1 %><%= link_to map.name, clubsite_map_path(id: map.id) %><%= map.map_standard&.name %><%= map.created&.strftime('%Y-%m-%d') %><%= map.modified&.strftime('%Y-%m-%d') %><%= "1:#{number_with_delimiter(map.scale)}" if map.scale.present? && map.scale.nonzero? %><%= format('%.3f', map.lat.to_f) %><%= format('%.3f', map.lng.to_f) %><%= media_image('Map', map.id, '60x60') if media_exists?('Map', map.id, '60x60') %>
+
diff --git a/app/views/clubsite/maps/show.html.erb b/app/views/clubsite/maps/show.html.erb new file mode 100644 index 0000000..6685e10 --- /dev/null +++ b/app/views/clubsite/maps/show.html.erb @@ -0,0 +1,51 @@ +<% content_for :title, @map.name %> + +
+
+

Statistics

+ + <% if @map.scale.present? && @map.scale.nonzero? %> + + <% end %> + + +
Scale1:<%= number_with_delimiter(@map.scale) %>
Map standard<%= @map.map_standard&.name %>
Events on map<%= @events.size %>
+
+ <% if @map.notes.present? %> +

Notes

+ <%= sanitize @map.notes %> +
+ <% end %> +

Map image

+ <% if media_exists?('Map', @map.id) %> + <%= media_linked_image('Map', @map.id, '400x600', {}, class: 'fitting-image') %> + <% else %> + No map image available. + <% end %> +
+ <%= render 'events_on_map', events: @events %> +
+
+ <%= render 'info', map: @map %> +
+
diff --git a/app/views/clubsite/memberships/edit.html.erb b/app/views/clubsite/memberships/edit.html.erb new file mode 100644 index 0000000..cb24f44 --- /dev/null +++ b/app/views/clubsite/memberships/edit.html.erb @@ -0,0 +1,30 @@ +<% content_for :title, 'Membership' %> + +<% edit_url = @membership.persisted? ? "/memberships/edit/#{@membership.id}" : '/memberships/edit' %> +<%= form_with model: @membership, url: edit_url, html: { class: 'form-horizontal' } do |f| %> +
+ <%= f.label :user_id, 'User', class: 'control-label col-sm-2' %> +
+ <%= f.collection_select :user_id, @users, :id, :name, {}, class: 'form-control' %> +
+
+
+ <%= f.label :year, 'Membership year', class: 'control-label col-sm-2' %> +
+ <%= f.text_field :year, class: 'form-control' %> +
+
+
+ <%= f.label :created, 'Created', class: 'control-label col-sm-2' %> +
+ <%= f.text_field :created, value: @membership.created&.strftime('%Y-%m-%d %H:%M:%S'), class: 'form-control' %> +
+
+
+
+ <%= f.submit 'Update', class: 'btn btn-primary' %> +
+
+<% end %> diff --git a/app/views/clubsite/memberships/index.html.erb b/app/views/clubsite/memberships/index.html.erb new file mode 100644 index 0000000..d79c128 --- /dev/null +++ b/app/views/clubsite/memberships/index.html.erb @@ -0,0 +1,49 @@ +<% content_for :title, 'Memberships' %> +
+

Memberships

+
+
+
+ <%= form_with model: @membership, url: '/memberships/edit' do |f| %> +
+ <%= f.label :user_id, 'User' %> + <%= f.collection_select :user_id, @users, :id, :name, {}, class: 'form-control' %> +
+
+ <%= f.label :year, 'Membership year' %> + <%= f.text_field :year, class: 'form-control' %> +
+
+ <%= f.label :created, 'Created' %> + <%= f.text_field :created, value: @membership.created&.strftime('%Y-%m-%d %H:%M:%S'), class: 'form-control' %> +
+ <%= f.submit 'Add membership', class: 'btn btn-primary' %> + <% end %> +
+
+ +
+
+ <% @memberships.group_by(&:year).each do |year, memberships| %> +

Membership year: <%= year %>

+ + + + + + <% memberships.each do |membership| %> + + + + + + <% end %> + +
Name
<%= membership.user&.name %><%= link_to 'Edit', "/memberships/edit/#{membership.id}", class: 'btn btn-xs btn-default' %> + <%= form_with url: "/memberships/delete/#{membership.id}" do %> + <%= submit_tag 'Remove', class: 'btn btn-xs btn-danger' %> + <% end %> +
+ <% end %> +
+
diff --git a/app/views/clubsite/officials/index.html.erb b/app/views/clubsite/officials/index.html.erb new file mode 100644 index 0000000..67e56d1 --- /dev/null +++ b/app/views/clubsite/officials/index.html.erb @@ -0,0 +1,72 @@ +<% content_for :title, 'Officials' %> +
+
+ + Clubs are strongly encouraged to track Officials' certification levels. This information is useful in many ways, including tracking numbers of qualified officials, recording pre-requisites for official certification, and identifying qualified officials for major events. +
+
+

Add official

+ <%= form_with scope: :official, url: '/officials/add', html: { class: 'form-inline' } do |f| %> + <%= f.hidden_field :user_id, id: 'OfficialUserId' %> +
+ +
+
+ <%= f.collection_select :official_classification_id, @official_classifications, :id, :name, {}, class: 'form-control' %> +
+
+ <%= f.text_field :date, class: 'form-control', placeholder: 'Date (YYYY-MM-DD)' %> +
+ + <% end %> + +

Current officials

+ + + + + + + + + + <% @officials.each do |official| %> + + + + + + <% end %> + +
NameClassification/Date certified
<%= official.user.name %> + <%= form_with scope: :official, url: "/officials/edit/#{official.id}", html: { class: 'form-inline' } do |f| %> + <%= f.hidden_field :user_id, value: official.user_id %> +
+ <%= f.collection_select :official_classification_id, @official_classifications, :id, :name, + { selected: official.official_classification_id }, + class: 'form-control input-sm' %> +
+
+ <%= f.text_field :date, value: official.date&.strftime('%Y-%m-%d'), + class: 'form-control input-sm', placeholder: 'Date (YYYY-MM-DD)' %> +
+ <%= f.submit 'Update', class: 'btn btn-default btn-sm' %> + <% end %> +
+ <%= form_with url: "/officials/delete/#{official.id}" do %> + + <% end %> +
+
+
diff --git a/app/views/clubsite/pages/admin.html.erb b/app/views/clubsite/pages/admin.html.erb new file mode 100644 index 0000000..ae9ebae --- /dev/null +++ b/app/views/clubsite/pages/admin.html.erb @@ -0,0 +1,38 @@ +<% content_for :title, 'Admin' %> + + +
+
+

Users

+ Grant privileges

+ <% if @allow_show_duplicates %> + Show duplicates

+ <% end %> +
+
+

Events

+ Define organizer roles +

+ " class="btn btn-default">Event Participation Information (CSV) +
+
+

Officials

+ Officials certification +
+
+

Series

+ Define +
+ +
+

Club

+ Edit club configuration

+ Customize design +
+
+

Maps

+ Define map standards +
+
diff --git a/app/views/clubsite/pages/contact.html.erb b/app/views/clubsite/pages/contact.html.erb new file mode 100644 index 0000000..39d63a7 --- /dev/null +++ b/app/views/clubsite/pages/contact.html.erb @@ -0,0 +1,6 @@ +<% content_for :title, 'Contact' %> + + +<%= render_content_blocks 'contact' %> diff --git a/app/views/clubsite/pages/display.html.erb b/app/views/clubsite/pages/display.html.erb new file mode 100644 index 0000000..11dfd08 --- /dev/null +++ b/app/views/clubsite/pages/display.html.erb @@ -0,0 +1,21 @@ +<% content_for :title, @page.name %> +<% can_edit = user_signed_in? && current_user.has_privilege?(Settings.privileges.page.edit, current_club) %> +<% can_delete = user_signed_in? && current_user.has_privilege?(Settings.privileges.page.delete, current_club) %> + + + +
+ <%= sanitize @page.content %> +
diff --git a/app/views/clubsite/pages/export.html.erb b/app/views/clubsite/pages/export.html.erb new file mode 100644 index 0000000..5a8fdd2 --- /dev/null +++ b/app/views/clubsite/pages/export.html.erb @@ -0,0 +1,39 @@ +<% content_for :title, 'Export' %> + +
+
+
+

Display info on your club's website

+
+ Much of the information in the OCNDB can be accessed and displayed in your club's website. + A number of "widgets" have been developed for displaying information. A little bit of basic web-programming knowledge is required to include them. + +
Calendar
+ The events calendar can be embeded by adding this code to your site: +
+ + <iframe src="<%= current_club.acronym.to_s.downcase %>.whyjustrun.ca/events.embed"></iframe> + + +
Maps
+ Maps can be embeded by adding this code to your site: +
+ + <iframe src="<%= current_club.acronym.to_s.downcase %>.whyjustrun.ca/maps.embed"></iframe> + +
+
+

Roll-your-own widgets

+

A lot of the information in the OCNDB is available via the API, which generally provides data in IOF XML format.

+

Experienced web developers can create widgets to display this information. For more information check out the API.

+
+
+
+

Upgrading to the WhyJustRun system

+
+ In addition to this functionality, each club has the option to use the WhyJustRun (WJR) system for their club. This involves migrating the club website to WJR... +
+
diff --git a/app/views/clubsite/pages/home.html.erb b/app/views/clubsite/pages/home.html.erb new file mode 100644 index 0000000..2630b5e --- /dev/null +++ b/app/views/clubsite/pages/home.html.erb @@ -0,0 +1,22 @@ +
+
+
+

Events

+
+ <%= render 'clubsite/events/list' %> +
+ +
+
+

News

+
+ <% if current_club.juicer_feed_id.blank? && user_signed_in? && current_user.has_privilege?(Settings.privileges.club.edit, current_club) %> + Add news source + <% end %> + <%= render 'clubsite/shared/juicer_feed' %> + <%= render 'clubsite/shared/facebook_like' %> +
+
+ <%= render_content_blocks 'general_information' %> +
+
diff --git a/app/views/clubsite/pages/other.html.erb b/app/views/clubsite/pages/other.html.erb new file mode 100644 index 0000000..8a2b0d0 --- /dev/null +++ b/app/views/clubsite/pages/other.html.erb @@ -0,0 +1,43 @@ + +
+
+

Events

+ You can enter your club's events in the OCNDB quickly and easily. This allows you to: + + +

Maps

+ You can keep track of your club's maps in the OCNDB. You cann define each map, including scale, mapping standard, and attributes such as date created, last updated, etc . You can also + upload images of the map and specify the map location. The map can be linked to events so that competitors can see the image and see where the map is located. There is also an + overview that shows the location of all the club's maps. + +

Officials

+ Clubs are strongly encouraged to track Officials' certification levels in the OCNDB. This information is useful in many ways, including tracking numbers of qualified officials, recording + pre-requisites for official certification, and identifying qualified officials for major events. + +
+
+

Export

+ This page show how you can incorporate the information from the database into your club's website. + +

Reports

+ Here you can see basic reporting on club information such as participation counts and officials lists, along with examples of "widgets" you can include in you club's website (see below + for more info). + +

Admin

+ Use this page to seet up other users who will be allowed to enter and make changes to your club's information. +
+
+

Upcoming events

+ <%= render 'clubsite/events/list' %> +
+
diff --git a/app/views/clubsite/pages/resources.html.erb b/app/views/clubsite/pages/resources.html.erb new file mode 100644 index 0000000..1bf8344 --- /dev/null +++ b/app/views/clubsite/pages/resources.html.erb @@ -0,0 +1,31 @@ +<% content_for :title, 'Resources' %> + + +<% if @pages.empty? %> +

Sorry, no resources are posted yet.

+<% end %> +<% @pages.each do |page| %> +

<%= link_to page.name, "/pages/#{page.id}" %>

+<% end %> + +<% if user_signed_in? && current_user.has_privilege?(Settings.privileges.page.edit, current_club) %> +
+

Add page

+ <%= form_with scope: :page, url: '/pages/add', html: { class: 'form-horizontal' } do |f| %> +
+ <%= f.label :name, 'Name', class: 'control-label col-sm-2' %> +
+ <%= f.text_field :name, class: 'form-control', 'data-validate' => 'validate(required)' %> +
+
+
+ <%= f.label :content, 'Content', class: 'control-label col-sm-2' %> +
+ <%= f.text_area :content, class: 'wjr-wysiwyg', rows: 20, 'data-validate' => 'validate(required)' %> +
+
+ <%= f.submit 'Add', class: 'btn btn-primary' %> + <% end %> +<% end %> diff --git a/app/views/clubsite/privileges/index.html.erb b/app/views/clubsite/privileges/index.html.erb new file mode 100644 index 0000000..6ff0993 --- /dev/null +++ b/app/views/clubsite/privileges/index.html.erb @@ -0,0 +1,57 @@ +<% content_for :title, 'Privileges' %> + + +
+
+

Types of privileges

+
+ <% @groups.each do |group| %> +
<%= group.name %>
+
<%= group.description %>
+ <% end %> +
+ Those users not listed here can only sign up for events and have no other privileges. +
+
+

Add privilege

+ <%= form_with scope: :privilege, url: '/privileges/add', html: { class: 'form-inline' } do |f| %> +
+ <%= f.collection_select :user_id, @users, :id, :name, { include_blank: 'Select a user' }, + class: 'form-control', required: true %> +
+
+ <%= f.collection_select :group_id, @groups, :id, :name, {}, class: 'form-control' %> +
+ + <% end %> + +

Current privileges

+ + + + + + + + + + <% @privileges.each do |privilege| %> + + + + + + <% end %> + +
NameGroup
<%= privilege.user.name %><%= privilege.user_group.name %> + <%= button_to "/privileges/delete/#{privilege.id}", + class: 'btn btn-sm btn-danger', form_class: 'thin-form' do %> + + <% end %> +
+
+
diff --git a/app/views/clubsite/resources/index.html.erb b/app/views/clubsite/resources/index.html.erb new file mode 100644 index 0000000..6b05392 --- /dev/null +++ b/app/views/clubsite/resources/index.html.erb @@ -0,0 +1,67 @@ +<% content_for :title, 'Resources' %> + + +

Customize your club site's design by uploading these resources.

+ +
+ + + + + + + <% if @can_delete %><% end %> + + + + <% Resource::RESOURCE_KEYS.each do |key, config| %> + <% resource = @resources[key] %> + + + + + <% if @can_delete %> + + <% end %> + + <% end %> + +
TypeCurrentUpload
+ <%= config[:name] %> +

<%= config[:description] %>

+
+ <% if resource %> + <% if resource.thumbnailable? %> + <%= link_to image_tag(resource.url('100'), alt: config[:name]), resource.url %> + <% else %> + <%= link_to resource.relative_path, resource.url %> + <% end %> + <% if resource.caption.present? %> +

<%= resource.caption %>

+ <% end %> + <% else %> + Not uploaded + <% end %> +
+ <%= form_with scope: :resource, url: '/resources/add', html: { multipart: true } do |f| %> + <%= f.hidden_field :key, value: key %> +
+ <%= f.file_field :file, required: true %> +
+
+ <%= f.text_field :caption, placeholder: 'Caption (optional)', + class: 'form-control input-sm' %> +
+ <%= f.submit 'Upload', class: 'btn btn-primary btn-sm' %> + <% end %> +
+ <% if resource %> + <%= button_to "/resources/delete/#{resource.id}", + class: 'btn btn-sm btn-danger', form_class: 'thin-form' do %> + Delete + <% end %> + <% end %> +
+
diff --git a/app/views/clubsite/results/index.html.erb b/app/views/clubsite/results/index.html.erb new file mode 100644 index 0000000..00186d9 --- /dev/null +++ b/app/views/clubsite/results/index.html.erb @@ -0,0 +1,44 @@ + + +
+ + + + + + + + + + + + + <% @results.each do |result| %> + <% event = result.course.event %> + + + + + + + + + <% end %> + +
DateEventCourseNameTimeStatus
<%= event.local_date.strftime('%b %-d, %Y') %><%= link_to event.name, clubsite_event_path(id: event.id) %><%= link_to result.course.name, clubsite_course_path(id: result.course.id) %><%= result.user&.name %><%= result.time&.strftime('%-H:%M:%S') %><%= result.readable_status %>
+
+ +<% if @total_pages > 1 %> + +<% end %> diff --git a/app/views/clubsite/roles/edit.html.erb b/app/views/clubsite/roles/edit.html.erb new file mode 100644 index 0000000..f7ca72d --- /dev/null +++ b/app/views/clubsite/roles/edit.html.erb @@ -0,0 +1,25 @@ +<% content_for :title, 'Role' %> + + +<% edit_url = @role.persisted? ? "/roles/edit/#{@role.id}" : '/roles/edit' %> +<%= form_with model: @role, url: edit_url, html: { class: 'form-horizontal' } do |f| %> +
+ +
+ <%= f.text_field :name, class: 'form-control' %> +
+
+
+ +
+ <%= f.text_field :description, class: 'form-control' %> +
+
+
+
+ <%= f.submit 'Save', class: 'btn btn-primary' %> +
+
+<% end %> diff --git a/app/views/clubsite/roles/index.html.erb b/app/views/clubsite/roles/index.html.erb new file mode 100644 index 0000000..0074da7 --- /dev/null +++ b/app/views/clubsite/roles/index.html.erb @@ -0,0 +1,29 @@ +<% content_for :title, 'Roles' %> + + + + + + + + + + + <% @roles.each do |role| %> + + + + + + <% end %> + +
NameDescription
<%= role.name %><%= role.description %> + + + +
diff --git a/app/views/clubsite/series/edit.html.erb b/app/views/clubsite/series/edit.html.erb new file mode 100644 index 0000000..5bcdb04 --- /dev/null +++ b/app/views/clubsite/series/edit.html.erb @@ -0,0 +1,51 @@ +<% content_for :title, 'Edit Series' %> + + +<% edit_url = @series.persisted? ? "/series/edit/#{@series.id}" : '/series/edit' %> +<%= form_with model: @series, url: edit_url, html: { class: 'form-horizontal' } do |f| %> +
+ +
+ <%= f.text_field :acronym, class: 'form-control' %> +
+
+
+ +
+ <%= f.text_field :name, class: 'form-control' %> +
+
+
+ +
+
+ <%= f.text_field :color, value: @series.color.presence || 'rgba(255,0,0,1)', class: 'form-control' %> + +
+
+
+
+ +
+ <%= f.text_area :information, class: 'wjr-wysiwyg' %> +
+

Information specific to the series is shown on event pages.

+
+
+
+
+ +
+
+ <%= f.check_box :is_current %> +
+
+
+
+
+ <%= f.submit 'Save', class: 'btn btn-primary' %> +
+
+<% end %> diff --git a/app/views/clubsite/series/index.html.erb b/app/views/clubsite/series/index.html.erb new file mode 100644 index 0000000..fa7ce45 --- /dev/null +++ b/app/views/clubsite/series/index.html.erb @@ -0,0 +1,29 @@ +<% content_for :title, 'Series' %> + + + + + + + + + + + <% @series.each do |series| %> + + + + + + <% end %> + +
AcronymName
<%= series.acronym %><%= series.name %> + + + +
diff --git a/app/views/clubsite/shared/_facebook_like.html.erb b/app/views/clubsite/shared/_facebook_like.html.erb new file mode 100644 index 0000000..9905c46 --- /dev/null +++ b/app/views/clubsite/shared/_facebook_like.html.erb @@ -0,0 +1,13 @@ +<% if current_club.facebook_page_id.present? %> +
+
+ +
+
+<% end %> diff --git a/app/views/clubsite/shared/_flickr_photos.html.erb b/app/views/clubsite/shared/_flickr_photos.html.erb new file mode 100644 index 0000000..d036d44 --- /dev/null +++ b/app/views/clubsite/shared/_flickr_photos.html.erb @@ -0,0 +1,24 @@ +<%# locals: event_id %> +
,<%= ERB::Util.url_encode("orienteerapp#{event_id}") %>" data-flickr-api-key="<%= Settings.flickrApiKey %>"> +

Photos

+

Photos are from Flickr. To add your photos to this section, tag your Flickr photos with: whyjustrun<%= event_id %> (all one word)

+ +
diff --git a/app/views/clubsite/shared/_juicer_feed.html.erb b/app/views/clubsite/shared/_juicer_feed.html.erb new file mode 100644 index 0000000..807357c --- /dev/null +++ b/app/views/clubsite/shared/_juicer_feed.html.erb @@ -0,0 +1,6 @@ +<% if current_club.juicer_feed_id.present? %> + + + +

Feed powered by Juicer.io

+<% end %> diff --git a/app/views/clubsite/users/show_duplicates.html.erb b/app/views/clubsite/users/show_duplicates.html.erb new file mode 100644 index 0000000..a310cfe --- /dev/null +++ b/app/views/clubsite/users/show_duplicates.html.erb @@ -0,0 +1,91 @@ + + +
+
+ Merge two accounts. Copy all results, organizers, etc from + Duplicate account to Primary account. Then delete + Duplicate account. If Duplicate account has user attributes that are + NULL in Primary account, then use these. The password of + Primary account will be used. +
+
+

Detected duplicates

+ These accounts were determined to be duplicates. Duplicates exist for accounts + that have similar names. The algorithm for determining which of the two + accounts is the primary account is as follows: + +
+ + + + + + + + + + + + + + + + + + + + + <% @duplicates.each do |match| %> + + <% [match[:primary], match[:duplicate]].each do |entry| %> + + + + + <% end %> + + + <% end %> + +
Primary accountDuplicate account
NameClubIDMost recent event (club)NameClubIDMost recent event (club)
+ <%= entry[:user].name %> + <% if entry[:has_password] %> + Real + <% else %> + Fake + <% end %> + <%= entry[:user].club&.acronym %><%= entry[:user].id %> + <% if entry[:most_recent_event] %> + <%= entry[:most_recent_event][:date].to_date %><% if entry[:most_recent_event][:club_acronym].present? %> (<%= entry[:most_recent_event][:club_acronym] %>)<% end %> + <% end %> + + <%= button_to 'Merge', "/users/merge/#{match[:primary][:user].id}/#{match[:duplicate][:user].id}", class: 'btn btn-default' %> +
+
+
+ <% if @can_merge_any_user %> +
+

Manual merge

+ <%= form_tag '/users/showDuplicates' do %> +
+ + <%= select_tag 'user[0][user_id]', options_from_collection_for_select(@users, :id, :name), + id: 'manual-merge-primary', class: 'form-control' %> +
+
+ + <%= select_tag 'user[1][user_id]', options_from_collection_for_select(@users, :id, :name), + id: 'manual-merge-duplicate', class: 'form-control' %> +
+ <%= submit_tag 'Merge', class: 'btn btn-default' %> + <% end %> +
+ <% end %> +
diff --git a/app/views/layouts/clubsite/_flash.html.erb b/app/views/layouts/clubsite/_flash.html.erb new file mode 100644 index 0000000..65c588e --- /dev/null +++ b/app/views/layouts/clubsite/_flash.html.erb @@ -0,0 +1 @@ +<%= bootstrap_flash %> diff --git a/app/views/layouts/clubsite/_head.html.erb b/app/views/layouts/clubsite/_head.html.erb new file mode 100644 index 0000000..13fd974 --- /dev/null +++ b/app/views/layouts/clubsite/_head.html.erb @@ -0,0 +1,16 @@ + + +<%# Empty: the core APIs are same-origin now that club sites are served by this app %> + + + +<%= csrf_meta_tags %> +<%= stylesheet_link_tag 'clubsite', media: 'all' %> +<%= render 'layouts/clubsite/series_css' %> +<% if club_resources['style'].present? %> + <%# Custom club CSS uploaded through the admin interface; must load after the bundle so it wins %> + <%= stylesheet_link_tag club_resources['style'] %> +<% end %> +<%= yield :open_graph %> + + diff --git a/app/views/layouts/clubsite/_series_css.html.erb b/app/views/layouts/clubsite/_series_css.html.erb new file mode 100644 index 0000000..bb7ba0b --- /dev/null +++ b/app/views/layouts/clubsite/_series_css.html.erb @@ -0,0 +1,9 @@ +<% cache ['series_css', current_club], expires_in: 1.hour do %> + +<% end %> diff --git a/app/views/layouts/clubsite/default.html.erb b/app/views/layouts/clubsite/default.html.erb new file mode 100644 index 0000000..4466b23 --- /dev/null +++ b/app/views/layouts/clubsite/default.html.erb @@ -0,0 +1,78 @@ + + + + <%= content_for?(:title) ? yield(:title) : current_club.name %> + <%= render 'layouts/clubsite/head' %> + <%= yield :head %> + + + +
+
+ <%= render 'layouts/clubsite/flash' %> + <%= yield %> +
+
+ + <%= javascript_include_tag 'clubsite' %> + <%= redactor_script_tags %> + <%= yield :javascript_bottom %> + + diff --git a/app/views/layouts/clubsite/embed.html.erb b/app/views/layouts/clubsite/embed.html.erb new file mode 100644 index 0000000..1b64651 --- /dev/null +++ b/app/views/layouts/clubsite/embed.html.erb @@ -0,0 +1,14 @@ + + + + <%= content_for?(:title) ? yield(:title) : current_club.name %> + <%= render 'layouts/clubsite/head' %> + <%= stylesheet_link_tag 'clubsite_embed', media: 'all' %> + <%= yield :head %> + + + <%= yield %> + <%= javascript_include_tag 'clubsite' %> + <%= yield :javascript_bottom %> + + diff --git a/app/views/layouts/clubsite/other.html.erb b/app/views/layouts/clubsite/other.html.erb new file mode 100644 index 0000000..c178bc7 --- /dev/null +++ b/app/views/layouts/clubsite/other.html.erb @@ -0,0 +1,56 @@ + + + + <%= content_for?(:title) ? yield(:title) : current_club.name %> + <%= render 'layouts/clubsite/head' %> + <%= stylesheet_link_tag 'clubsite_other', media: 'all' %> + <%= yield :head %> + + + +
+ <%= render 'layouts/clubsite/flash' %> + <%= yield %> +
+ + <%= javascript_include_tag 'clubsite' %> + <%= redactor_script_tags %> + <%= yield :javascript_bottom %> + + diff --git a/app/views/layouts/clubsite/printable.html.erb b/app/views/layouts/clubsite/printable.html.erb new file mode 100644 index 0000000..3e78adb --- /dev/null +++ b/app/views/layouts/clubsite/printable.html.erb @@ -0,0 +1,15 @@ + + + + <%= content_for?(:title) ? yield(:title) : current_club.name %> + + + <%= stylesheet_link_tag 'clubsite_printable', media: 'all' %> + <%= yield :head %> + + + <%= yield %> + <%= javascript_include_tag 'clubsite' %> + <%= yield :javascript_bottom %> + + diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index 4df1949..0000000 --- a/babel.config.js +++ /dev/null @@ -1,70 +0,0 @@ -module.exports = function(api) { - var validEnv = ['development', 'test', 'production'] - var currentEnv = api.env() - var isDevelopmentEnv = api.env('development') - var isProductionEnv = api.env('production') - var isTestEnv = api.env('test') - - if (!validEnv.includes(currentEnv)) { - throw new Error( - 'Please specify a valid `NODE_ENV` or ' + - '`BABEL_ENV` environment variables. Valid values are "development", ' + - '"test", and "production". Instead, received: ' + - JSON.stringify(currentEnv) + - '.' - ) - } - - return { - presets: [ - isTestEnv && [ - '@babel/preset-env', - { - targets: { - node: 'current' - } - } - ], - (isProductionEnv || isDevelopmentEnv) && [ - '@babel/preset-env', - { - forceAllTransforms: true, - useBuiltIns: 'entry', - corejs: 3, - modules: false, - exclude: ['transform-typeof-symbol'] - } - ] - ].filter(Boolean), - plugins: [ - 'babel-plugin-macros', - '@babel/plugin-syntax-dynamic-import', - isTestEnv && 'babel-plugin-dynamic-import-node', - '@babel/plugin-transform-destructuring', - [ - '@babel/plugin-proposal-class-properties', - { - loose: true - } - ], - [ - '@babel/plugin-proposal-object-rest-spread', - { - useBuiltIns: true - } - ], - [ - '@babel/plugin-transform-runtime', - { - helpers: false - } - ], - [ - '@babel/plugin-transform-regenerator', - { - async: false - } - ] - ].filter(Boolean) - } -} diff --git a/bin/webpack b/bin/webpack deleted file mode 100755 index 1031168..0000000 --- a/bin/webpack +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env ruby - -ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" -ENV["NODE_ENV"] ||= "development" - -require "pathname" -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", - Pathname.new(__FILE__).realpath) - -require "bundler/setup" - -require "webpacker" -require "webpacker/webpack_runner" - -APP_ROOT = File.expand_path("..", __dir__) -Dir.chdir(APP_ROOT) do - Webpacker::WebpackRunner.run(ARGV) -end diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server deleted file mode 100755 index dd96627..0000000 --- a/bin/webpack-dev-server +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env ruby - -ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" -ENV["NODE_ENV"] ||= "development" - -require "pathname" -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", - Pathname.new(__FILE__).realpath) - -require "bundler/setup" - -require "webpacker" -require "webpacker/dev_server_runner" - -APP_ROOT = File.expand_path("..", __dir__) -Dir.chdir(APP_ROOT) do - Webpacker::DevServerRunner.run(ARGV) -end diff --git a/config/brakeman.ignore b/config/brakeman.ignore new file mode 100644 index 0000000..a87c0d9 --- /dev/null +++ b/config/brakeman.ignore @@ -0,0 +1,34 @@ +{ + "ignored_warnings": [ + { + "fingerprint": "5d41c99952d21e80fdb86aeae06ea42039c714d2ac801571a150f4c89f548bd9", + "note": "Intentional cross-domain redirect: sends the signed-in user to their club's own domain for the clubsite session handoff. The target host always comes from the clubs table, not from user input." + }, + { + "fingerprint": "ec2fa90ab59aba7a6890bfdc5c76cf7e4b1b241b6dcd1f3a604735709a94af2e", + "note": "Short links exist to redirect to admin-configured external destinations stored in the short_links table." + }, + { + "fingerprint": "98b26f60d776fd41ee6f088c833725145be9aac2d7c5b33780241c273622db42", + "note": "Rails 7.2 end-of-life is a known issue; the framework upgrade is tracked as follow-up work to the clubsite consolidation." + }, + { + "fingerprint": "dcd469c3af248cfe9ad0bd62ca6d04d009d336c4a057ed702eae420d78ad92f2", + "note": "Intentional 301 from a club's old domain to its current one; both values come from the clubs table, not from user input." + }, + { + "fingerprint": "6f385d255ed0c43e612d0bb77992a41e96ac6c9186d2510ca2616f5ac694982d", + "note": "MediaStore builds the path from the configured data folder, the club id, and an Integer-cast record id; no user-controlled path components." + }, + { + "fingerprint": "8f1cb77ac9d40ad45b479709b5709735fceded583c1e86999bc47a589252535b", + "note": "SSO sign-in redirect to a club domain; the host is resolved from the clubs table (the raw return_host param is never used) and the path is restricted to relative paths." + }, + { + "fingerprint": "7a50d993f0b0314fb42b58ae7b0e3160e1b80116b463b17351b6f9cc43ecd53f", + "note": "SSO sign-out redirect back to a club domain resolved from the clubs table." + } + ], + "updated": "2026-07-30 00:00:00 +0000", + "brakeman_version": "7.1.1" +} diff --git a/config/environments/development.rb b/config/environments/development.rb index 2e419c4..83908d1 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -78,7 +78,11 @@ IPAddr.new("0.0.0.0/0"), # All IPv4 addresses. IPAddr.new("::/0"), # All IPv6 addresses. "localhost", # The localhost reserved domain. + ".localhost", # Development club domains, e.g. demo.localhost:3000 "host.docker.internal:3000", ENV["RAILS_DEVELOPMENT_HOSTS"] # Additional comma-separated hosts for development. ] + + # Club sites tell crawlers to stay away outside production + config.x.clubsite_robots_hidden = true end diff --git a/config/environments/production.rb b/config/environments/production.rb index 4dd71af..035c605 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -84,10 +84,13 @@ # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false - # Enable DNS rebinding protection and other `Host` header attacks. - config.hosts = ["whyjustrun.ca"] - # Skip DNS rebinding protection for the default health check endpoint. - config.host_authorization = { exclude: ->(request) { request.path == "/up" } } + # Club domains are arbitrary customer strings stored in the clubs table, so + # Rails cannot allowlist hosts. nginx is the Host allowlist: its vhosts are + # generated from the clubs table and a default_server rejects unknown hosts. + config.hosts.clear + + # Club sites are visible to crawlers in production (subject to clubs.visible) + config.x.clubsite_robots_hidden = false end Rails.application.config.middleware.use ExceptionNotification::Rack, diff --git a/config/environments/test.rb b/config/environments/test.rb index adbb4a6..1174140 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -11,6 +11,9 @@ # While tests run files are not watched, reloading is not necessary. config.enable_reloading = false + # Let tests exercise the clubs.visible logic in Clubsite::RobotsController + config.x.clubsite_robots_hidden = false + # Eager loading loads your entire application. When running a single test locally, # this is usually not necessary, and can slow down your test suite. However, it's # recommended that you enable it in continuous integration systems to ensure eager diff --git a/config/initializers/0_devise.rb b/config/initializers/0_devise.rb index 2d97c91..aeea712 100644 --- a/config/initializers/0_devise.rb +++ b/config/initializers/0_devise.rb @@ -71,7 +71,8 @@ # It will change confirmation, password recovery and other workflows # to behave the same regardless if the e-mail provided was right or wrong. # Does not affect registerable. - # config.paranoid = true + # Avoid leaking which email addresses have accounts + config.paranoid = true # By default Devise will store the user in session. You can skip storage for # :http_auth and :token_auth by adding those symbols to the array below. @@ -119,7 +120,7 @@ # ==> Configuration for :rememberable # The time the user will be remembered without asking for credentials again. - config.remember_for = 20.years + config.remember_for = 1.year # If true, extends the user's remember period when remembered via cookie. # config.extend_remember_period = false @@ -130,7 +131,7 @@ # ==> Configuration for :validatable # Range for password length. Default is 8..128. - config.password_length = 6..128 + config.password_length = 8..128 # Email regex used to validate email formats. It simply asserts that # an one (and only one) @ exists in the given string. This is mainly diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 19f51aa..042e2e8 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -12,3 +12,4 @@ # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. # Rails.application.config.assets.precompile += %w( admin.js admin.css ) +Rails.application.config.assets.precompile += %w( clubsite.js clubsite.css clubsite_other.css clubsite_embed.css clubsite_printable.css ) diff --git a/config/initializers/geocoder.rb b/config/initializers/geocoder.rb index 6520d76..0ecad52 100644 --- a/config/initializers/geocoder.rb +++ b/config/initializers/geocoder.rb @@ -1,3 +1,8 @@ -Geocoder::Configuration.lookup = :nominatim -Geocoder::Configuration.cache = {} -Geocoder::Configuration.units = :km +Geocoder.configure( + lookup: :nominatim, + units: :km, + timeout: 5, + # Nominatim's usage policy requires an identifying User-Agent + http_headers: { 'User-Agent' => 'WhyJustRun (support@whyjustrun.ca)' }, + cache: Rails.cache +) diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index dc18996..d7d9a6a 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -2,3 +2,7 @@ # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf + +# Club pages rendered bare for ",error:'

The requested content cannot be loaded.
Please try again later.

',closeBtn:'',next:'',prev:''},openEffect:"fade",openSpeed:250,openEasing:"swing",openOpacity:!0, +openMethod:"zoomIn",closeEffect:"fade",closeSpeed:250,closeEasing:"swing",closeOpacity:!0,closeMethod:"zoomOut",nextEffect:"elastic",nextSpeed:250,nextEasing:"swing",nextMethod:"changeIn",prevEffect:"elastic",prevSpeed:250,prevEasing:"swing",prevMethod:"changeOut",helpers:{overlay:!0,title:!0},onCancel:f.noop,beforeLoad:f.noop,afterLoad:f.noop,beforeShow:f.noop,afterShow:f.noop,beforeChange:f.noop,beforeClose:f.noop,afterClose:f.noop},group:{},opts:{},previous:null,coming:null,current:null,isActive:!1, +isOpen:!1,isOpened:!1,wrap:null,skin:null,outer:null,inner:null,player:{timer:null,isActive:!1},ajaxLoad:null,imgPreload:null,transitions:{},helpers:{},open:function(a,d){if(a&&(f.isPlainObject(d)||(d={}),!1!==b.close(!0)))return f.isArray(a)||(a=t(a)?f(a).get():[a]),f.each(a,function(e,c){var k={},g,h,j,m,l;"object"===f.type(c)&&(c.nodeType&&(c=f(c)),t(c)?(k={href:c.data("fancybox-href")||c.attr("href"),title:c.data("fancybox-title")||c.attr("title"),isDom:!0,element:c},f.metadata&&f.extend(!0,k, +c.metadata())):k=c);g=d.href||k.href||(q(c)?c:null);h=d.title!==v?d.title:k.title||"";m=(j=d.content||k.content)?"html":d.type||k.type;!m&&k.isDom&&(m=c.data("fancybox-type"),m||(m=(m=c.prop("class").match(/fancybox\.(\w+)/))?m[1]:null));q(g)&&(m||(b.isImage(g)?m="image":b.isSWF(g)?m="swf":"#"===g.charAt(0)?m="inline":q(c)&&(m="html",j=c)),"ajax"===m&&(l=g.split(/\s+/,2),g=l.shift(),l=l.shift()));j||("inline"===m?g?j=f(q(g)?g.replace(/.*(?=#[^\s]+$)/,""):g):k.isDom&&(j=c):"html"===m?j=g:!m&&(!g&& +k.isDom)&&(m="inline",j=c));f.extend(k,{href:g,type:m,content:j,title:h,selector:l});a[e]=k}),b.opts=f.extend(!0,{},b.defaults,d),d.keys!==v&&(b.opts.keys=d.keys?f.extend({},b.defaults.keys,d.keys):!1),b.group=a,b._start(b.opts.index)},cancel:function(){var a=b.coming;a&&!1!==b.trigger("onCancel")&&(b.hideLoading(),b.ajaxLoad&&b.ajaxLoad.abort(),b.ajaxLoad=null,b.imgPreload&&(b.imgPreload.onload=b.imgPreload.onerror=null),a.wrap&&a.wrap.stop(!0,!0).trigger("onReset").remove(),b.coming=null,b.current|| +b._afterZoomOut(a))},close:function(a){b.cancel();!1!==b.trigger("beforeClose")&&(b.unbindEvents(),b.isActive&&(!b.isOpen||!0===a?(f(".fancybox-wrap").stop(!0).trigger("onReset").remove(),b._afterZoomOut()):(b.isOpen=b.isOpened=!1,b.isClosing=!0,f(".fancybox-item, .fancybox-nav").remove(),b.wrap.stop(!0,!0).removeClass("fancybox-opened"),b.transitions[b.current.closeMethod]())))},play:function(a){var d=function(){clearTimeout(b.player.timer)},e=function(){d();b.current&&b.player.isActive&&(b.player.timer= +setTimeout(b.next,b.current.playSpeed))},c=function(){d();p.unbind(".player");b.player.isActive=!1;b.trigger("onPlayEnd")};if(!0===a||!b.player.isActive&&!1!==a){if(b.current&&(b.current.loop||b.current.index=c.index?"next":"prev"],b.router=e||"jumpto",c.loop&&(0>a&&(a=c.group.length+a%c.group.length),a%=c.group.length),c.group[a]!==v&&(b.cancel(),b._start(a)))},reposition:function(a,d){var e=b.current,c=e?e.wrap:null,k;c&&(k=b._getPosition(d),a&&"scroll"===a.type?(delete k.position,c.stop(!0,!0).animate(k,200)):(c.css(k),e.pos=f.extend({},e.dim,k)))},update:function(a){var d= +a&&a.type,e=!d||"orientationchange"===d;e&&(clearTimeout(B),B=null);b.isOpen&&!B&&(B=setTimeout(function(){var c=b.current;c&&!b.isClosing&&(b.wrap.removeClass("fancybox-tmp"),(e||"load"===d||"resize"===d&&c.autoResize)&&b._setDimension(),"scroll"===d&&c.canShrink||b.reposition(a),b.trigger("onUpdate"),B=null)},e&&!s?0:300))},toggle:function(a){b.isOpen&&(b.current.fitToView="boolean"===f.type(a)?a:!b.current.fitToView,s&&(b.wrap.removeAttr("style").addClass("fancybox-tmp"),b.trigger("onUpdate")), +b.update())},hideLoading:function(){p.unbind(".loading");f("#fancybox-loading").remove()},showLoading:function(){var a,d;b.hideLoading();a=f('
').click(b.cancel).appendTo("body");p.bind("keydown.loading",function(a){if(27===(a.which||a.keyCode))a.preventDefault(),b.cancel()});b.defaults.fixed||(d=b.getViewport(),a.css({position:"absolute",top:0.5*d.h+d.y,left:0.5*d.w+d.x}))},getViewport:function(){var a=b.current&&b.current.locked||!1,d={x:n.scrollLeft(), +y:n.scrollTop()};a?(d.w=a[0].clientWidth,d.h=a[0].clientHeight):(d.w=s&&r.innerWidth?r.innerWidth:n.width(),d.h=s&&r.innerHeight?r.innerHeight:n.height());return d},unbindEvents:function(){b.wrap&&t(b.wrap)&&b.wrap.unbind(".fb");p.unbind(".fb");n.unbind(".fb")},bindEvents:function(){var a=b.current,d;a&&(n.bind("orientationchange.fb"+(s?"":" resize.fb")+(a.autoCenter&&!a.locked?" scroll.fb":""),b.update),(d=a.keys)&&p.bind("keydown.fb",function(e){var c=e.which||e.keyCode,k=e.target||e.srcElement; +if(27===c&&b.coming)return!1;!e.ctrlKey&&(!e.altKey&&!e.shiftKey&&!e.metaKey&&(!k||!k.type&&!f(k).is("[contenteditable]")))&&f.each(d,function(d,k){if(1h[0].clientWidth||h[0].clientHeight&&h[0].scrollHeight>h[0].clientHeight),h=f(h).parent();if(0!==c&&!j&&1g||0>k)b.next(0>g?"up":"right");d.preventDefault()}}))},trigger:function(a,d){var e,c=d||b.coming||b.current;if(c){f.isFunction(c[a])&&(e=c[a].apply(c,Array.prototype.slice.call(arguments,1)));if(!1===e)return!1;c.helpers&&f.each(c.helpers,function(d,e){if(e&&b.helpers[d]&&f.isFunction(b.helpers[d][a]))b.helpers[d][a](f.extend(!0, +{},b.helpers[d].defaults,e),c)});p.trigger(a)}},isImage:function(a){return q(a)&&a.match(/(^data:image\/.*,)|(\.(jp(e|g|eg)|gif|png|bmp|webp|svg)((\?|#).*)?$)/i)},isSWF:function(a){return q(a)&&a.match(/\.(swf)((\?|#).*)?$/i)},_start:function(a){var d={},e,c;a=l(a);e=b.group[a]||null;if(!e)return!1;d=f.extend(!0,{},b.opts,e);e=d.margin;c=d.padding;"number"===f.type(e)&&(d.margin=[e,e,e,e]);"number"===f.type(c)&&(d.padding=[c,c,c,c]);d.modal&&f.extend(!0,d,{closeBtn:!1,closeClick:!1,nextClick:!1,arrows:!1, +mouseWheel:!1,keys:null,helpers:{overlay:{closeClick:!1}}});d.autoSize&&(d.autoWidth=d.autoHeight=!0);"auto"===d.width&&(d.autoWidth=!0);"auto"===d.height&&(d.autoHeight=!0);d.group=b.group;d.index=a;b.coming=d;if(!1===b.trigger("beforeLoad"))b.coming=null;else{c=d.type;e=d.href;if(!c)return b.coming=null,b.current&&b.router&&"jumpto"!==b.router?(b.current.index=a,b[b.router](b.direction)):!1;b.isActive=!0;if("image"===c||"swf"===c)d.autoHeight=d.autoWidth=!1,d.scrolling="visible";"image"===c&&(d.aspectRatio= +!0);"iframe"===c&&s&&(d.scrolling="scroll");d.wrap=f(d.tpl.wrap).addClass("fancybox-"+(s?"mobile":"desktop")+" fancybox-type-"+c+" fancybox-tmp "+d.wrapCSS).appendTo(d.parent||"body");f.extend(d,{skin:f(".fancybox-skin",d.wrap),outer:f(".fancybox-outer",d.wrap),inner:f(".fancybox-inner",d.wrap)});f.each(["Top","Right","Bottom","Left"],function(a,b){d.skin.css("padding"+b,w(d.padding[a]))});b.trigger("onReady");if("inline"===c||"html"===c){if(!d.content||!d.content.length)return b._error("content")}else if(!e)return b._error("href"); +"image"===c?b._loadImage():"ajax"===c?b._loadAjax():"iframe"===c?b._loadIframe():b._afterLoad()}},_error:function(a){f.extend(b.coming,{type:"html",autoWidth:!0,autoHeight:!0,minWidth:0,minHeight:0,scrolling:"no",hasError:a,content:b.coming.tpl.error});b._afterLoad()},_loadImage:function(){var a=b.imgPreload=new Image;a.onload=function(){this.onload=this.onerror=null;b.coming.width=this.width/b.opts.pixelRatio;b.coming.height=this.height/b.opts.pixelRatio;b._afterLoad()};a.onerror=function(){this.onload= +this.onerror=null;b._error("image")};a.src=b.coming.href;!0!==a.complete&&b.showLoading()},_loadAjax:function(){var a=b.coming;b.showLoading();b.ajaxLoad=f.ajax(f.extend({},a.ajax,{url:a.href,error:function(a,e){b.coming&&"abort"!==e?b._error("ajax",a):b.hideLoading()},success:function(d,e){"success"===e&&(a.content=d,b._afterLoad())}}))},_loadIframe:function(){var a=b.coming,d=f(a.tpl.iframe.replace(/\{rnd\}/g,(new Date).getTime())).attr("scrolling",s?"auto":a.iframe.scrolling).attr("src",a.href); +f(a.wrap).bind("onReset",function(){try{f(this).find("iframe").hide().attr("src","//about:blank").end().empty()}catch(a){}});a.iframe.preload&&(b.showLoading(),d.one("load",function(){f(this).data("ready",1);s||f(this).bind("load.fb",b.update);f(this).parents(".fancybox-wrap").width("100%").removeClass("fancybox-tmp").show();b._afterLoad()}));a.content=d.appendTo(a.inner);a.iframe.preload||b._afterLoad()},_preloadImages:function(){var a=b.group,d=b.current,e=a.length,c=d.preload?Math.min(d.preload, +e-1):0,f,g;for(g=1;g<=c;g+=1)f=a[(d.index+g)%e],"image"===f.type&&f.href&&((new Image).src=f.href)},_afterLoad:function(){var a=b.coming,d=b.current,e,c,k,g,h;b.hideLoading();if(a&&!1!==b.isActive)if(!1===b.trigger("afterLoad",a,d))a.wrap.stop(!0).trigger("onReset").remove(),b.coming=null;else{d&&(b.trigger("beforeChange",d),d.wrap.stop(!0).removeClass("fancybox-opened").find(".fancybox-item, .fancybox-nav").remove());b.unbindEvents();e=a.content;c=a.type;k=a.scrolling;f.extend(b,{wrap:a.wrap,skin:a.skin, +outer:a.outer,inner:a.inner,current:a,previous:d});g=a.href;switch(c){case "inline":case "ajax":case "html":a.selector?e=f("
").html(e).find(a.selector):t(e)&&(e.data("fancybox-placeholder")||e.data("fancybox-placeholder",f('
').insertAfter(e).hide()),e=e.show().detach(),a.wrap.bind("onReset",function(){f(this).find(e).length&&e.hide().replaceAll(e.data("fancybox-placeholder")).data("fancybox-placeholder",!1)}));break;case "image":e=a.tpl.image.replace("{href}", +g);break;case "swf":e='',h="",f.each(a.swf,function(a,b){e+='';h+=" "+a+'="'+b+'"'}),e+='"}(!t(e)||!e.parent().is(a.inner))&&a.inner.append(e);b.trigger("beforeShow");a.inner.css("overflow","yes"===k?"scroll": +"no"===k?"hidden":k);b._setDimension();b.reposition();b.isOpen=!1;b.coming=null;b.bindEvents();if(b.isOpened){if(d.prevMethod)b.transitions[d.prevMethod]()}else f(".fancybox-wrap").not(a.wrap).stop(!0).trigger("onReset").remove();b.transitions[b.isOpened?a.nextMethod:a.openMethod]();b._preloadImages()}},_setDimension:function(){var a=b.getViewport(),d=0,e=!1,c=!1,e=b.wrap,k=b.skin,g=b.inner,h=b.current,c=h.width,j=h.height,m=h.minWidth,u=h.minHeight,n=h.maxWidth,p=h.maxHeight,s=h.scrolling,q=h.scrollOutside? +h.scrollbarWidth:0,x=h.margin,y=l(x[1]+x[3]),r=l(x[0]+x[2]),v,z,t,C,A,F,B,D,H;e.add(k).add(g).width("auto").height("auto").removeClass("fancybox-tmp");x=l(k.outerWidth(!0)-k.width());v=l(k.outerHeight(!0)-k.height());z=y+x;t=r+v;C=E(c)?(a.w-z)*l(c)/100:c;A=E(j)?(a.h-t)*l(j)/100:j;if("iframe"===h.type){if(H=h.content,h.autoHeight&&1===H.data("ready"))try{H[0].contentWindow.document.location&&(g.width(C).height(9999),F=H.contents().find("body"),q&&F.css("overflow-x","hidden"),A=F.outerHeight(!0))}catch(G){}}else if(h.autoWidth|| +h.autoHeight)g.addClass("fancybox-tmp"),h.autoWidth||g.width(C),h.autoHeight||g.height(A),h.autoWidth&&(C=g.width()),h.autoHeight&&(A=g.height()),g.removeClass("fancybox-tmp");c=l(C);j=l(A);D=C/A;m=l(E(m)?l(m,"w")-z:m);n=l(E(n)?l(n,"w")-z:n);u=l(E(u)?l(u,"h")-t:u);p=l(E(p)?l(p,"h")-t:p);F=n;B=p;h.fitToView&&(n=Math.min(a.w-z,n),p=Math.min(a.h-t,p));z=a.w-y;r=a.h-r;h.aspectRatio?(c>n&&(c=n,j=l(c/D)),j>p&&(j=p,c=l(j*D)),cz||y>r)&&(c>m&&j>u)&&!(19n&&(c=n,j=l(c/D)),g.width(c).height(j),e.width(c+x),a=e.width(),y=e.height();else c=Math.max(m,Math.min(c,c-(a-z))),j=Math.max(u,Math.min(j,j-(y-r)));q&&("auto"===s&&jz||y>r)&&c>m&&j>u;c=h.aspectRatio?cu&&j
').appendTo(b.coming?b.coming.parent:a.parent);this.fixed=!1;a.fixed&&b.defaults.fixed&&(this.overlay.addClass("fancybox-overlay-fixed"),this.fixed=!0)},open:function(a){var d=this;a=f.extend({},this.defaults,a);this.overlay?this.overlay.unbind(".overlay").width("auto").height("auto"):this.create(a);this.fixed||(n.bind("resize.overlay",f.proxy(this.update,this)),this.update());a.closeClick&&this.overlay.bind("click.overlay",function(a){if(f(a.target).hasClass("fancybox-overlay"))return b.isActive? +b.close():d.close(),!1});this.overlay.css(a.css).show()},close:function(){var a,b;n.unbind("resize.overlay");this.el.hasClass("fancybox-lock")&&(f(".fancybox-margin").removeClass("fancybox-margin"),a=n.scrollTop(),b=n.scrollLeft(),this.el.removeClass("fancybox-lock"),n.scrollTop(a).scrollLeft(b));f(".fancybox-overlay").remove().hide();f.extend(this,{overlay:null,fixed:!1})},update:function(){var a="100%",b;this.overlay.width(a).height("100%");I?(b=Math.max(G.documentElement.offsetWidth,G.body.offsetWidth), +p.width()>b&&(a=p.width())):p.width()>n.width()&&(a=p.width());this.overlay.width(a).height(p.height())},onReady:function(a,b){var e=this.overlay;f(".fancybox-overlay").stop(!0,!0);e||this.create(a);a.locked&&(this.fixed&&b.fixed)&&(e||(this.margin=p.height()>n.height()?f("html").css("margin-right").replace("px",""):!1),b.locked=this.overlay.append(b.wrap),b.fixed=!1);!0===a.showEarly&&this.beforeShow.apply(this,arguments)},beforeShow:function(a,b){var e,c;b.locked&&(!1!==this.margin&&(f("*").filter(function(){return"fixed"=== +f(this).css("position")&&!f(this).hasClass("fancybox-overlay")&&!f(this).hasClass("fancybox-wrap")}).addClass("fancybox-margin"),this.el.addClass("fancybox-margin")),e=n.scrollTop(),c=n.scrollLeft(),this.el.addClass("fancybox-lock"),n.scrollTop(e).scrollLeft(c));this.open(a)},onUpdate:function(){this.fixed||this.update()},afterClose:function(a){this.overlay&&!b.coming&&this.overlay.fadeOut(a.speedOut,f.proxy(this.close,this))}};b.helpers.title={defaults:{type:"float",position:"bottom"},beforeShow:function(a){var d= +b.current,e=d.title,c=a.type;f.isFunction(e)&&(e=e.call(d.element,d));if(q(e)&&""!==f.trim(e)){d=f('
'+e+"
");switch(c){case "inside":c=b.skin;break;case "outside":c=b.wrap;break;case "over":c=b.inner;break;default:c=b.skin,d.appendTo("body"),I&&d.width(d.width()),d.wrapInner(''),b.current.margin[2]+=Math.abs(l(d.css("margin-bottom")))}d["top"===a.position?"prependTo":"appendTo"](c)}}};f.fn.fancybox=function(a){var d, +e=f(this),c=this.selector||"",k=function(g){var h=f(this).blur(),j=d,k,l;!g.ctrlKey&&(!g.altKey&&!g.shiftKey&&!g.metaKey)&&!h.is(".fancybox-wrap")&&(k=a.groupAttr||"data-fancybox-group",l=h.attr(k),l||(k="rel",l=h.get(0)[k]),l&&(""!==l&&"nofollow"!==l)&&(h=c.length?f(c):e,h=h.filter("["+k+'="'+l+'"]'),j=h.index(this)),a.index=j,!1!==b.open(h,a)&&g.preventDefault())};a=a||{};d=a.index||0;!c||!1===a.live?e.unbind("click.fb-start").bind("click.fb-start",k):p.undelegate(c,"click.fb-start").delegate(c+ +":not('.fancybox-item, .fancybox-nav')","click.fb-start",k);this.filter("[data-fancybox-start=1]").trigger("click");return this};p.ready(function(){var a,d;f.scrollbarWidth===v&&(f.scrollbarWidth=function(){var a=f('
').appendTo("body"),b=a.children(),b=b.innerWidth()-b.height(99).innerWidth();a.remove();return b});if(f.support.fixedPosition===v){a=f.support;d=f('
').appendTo("body");var e=20=== +d[0].offsetTop||15===d[0].offsetTop;d.remove();a.fixedPosition=e}f.extend(b.defaults,{scrollbarWidth:f.scrollbarWidth(),fixed:f.support.fixedPosition,parent:f("body")});a=f(r).width();J.addClass("fancybox-lock-test");d=f(r).width();J.removeClass("fancybox-lock-test");f("").appendTo("head")})})(window,document,jQuery); diff --git a/vendor/assets/javascripts/clubsite/jquery.jeditable.js b/vendor/assets/javascripts/clubsite/jquery.jeditable.js new file mode 100644 index 0000000..258cde4 --- /dev/null +++ b/vendor/assets/javascripts/clubsite/jquery.jeditable.js @@ -0,0 +1,38 @@ +// NOTE: Source has been modified from original +(function($){$.fn.editable=function(target,options){if('disable'==target){$(this).data('disabled.editable',true);return;} +if('enable'==target){$(this).data('disabled.editable',false);return;} +if('destroy'==target){$(this).unbind($(this).data('event.editable')).removeData('disabled.editable').removeData('event.editable');return;} +var settings=$.extend({},$.fn.editable.defaults,{target:target},options);var plugin=$.editable.types[settings.type].plugin||function(){};var submit=$.editable.types[settings.type].submit||function(){};var buttons=$.editable.types[settings.type].buttons||$.editable.types['defaults'].buttons;var content=$.editable.types[settings.type].content||$.editable.types['defaults'].content;var element=$.editable.types[settings.type].element||$.editable.types['defaults'].element;var reset=$.editable.types[settings.type].reset||$.editable.types['defaults'].reset;var callback=settings.callback||function(){};var onedit=settings.onedit||function(){};var onsubmit=settings.onsubmit||function(){};var onreset=settings.onreset||function(){};var onerror=settings.onerror||reset;if(settings.tooltip){$(this).attr('title',settings.tooltip);} +settings.autowidth='auto'==settings.width;settings.autoheight='auto'==settings.height;return this.each(function(){var self=this;var savedwidth=$(self).width();var savedheight=$(self).height();$(this).data('event.editable',settings.event);if(!$.trim($(this).html())){$(this).html(settings.placeholder);} +$(this).bind(settings.event,function(e){if(true===$(this).data('disabled.editable')){return;} +if(self.editing){return;} +if(false===onedit.apply(this,[settings,self])){return;} +e.preventDefault();e.stopPropagation();if(settings.tooltip){$(self).removeAttr('title');} +if(0==$(self).width()){settings.width=savedwidth;settings.height=savedheight;}else{if(settings.width!='none'){settings.width=settings.autowidth?$(self).width():settings.width;} +if(settings.height!='none'){settings.height=settings.autoheight?$(self).height():settings.height;}} +if($(this).html().toLowerCase().replace(/(;|")/g,'')==settings.placeholder.toLowerCase().replace(/(;|")/g,'')){$(this).html('');} +self.editing=true;self.revert=$(self).html();$(self).html('');var form=$('
');if(settings.cssclass){if('inherit'==settings.cssclass){form.attr('class',$(self).attr('class'));}else{form.attr('class',settings.cssclass);}} +if(settings.style){if('inherit'==settings.style){form.attr('style',$(self).attr('style'));form.css('display',$(self).css('display'));}else{form.attr('style',settings.style);}} +var input=element.apply(form,[settings,self]);var input_content;if(settings.loadurl){var t=setTimeout(function(){input.disabled=true;content.apply(form,[settings.loadtext,settings,self]);},100);var loaddata={};loaddata[settings.id]=self.id;if($.isFunction(settings.loaddata)){$.extend(loaddata,settings.loaddata.apply(self,[self.revert,settings]));}else{$.extend(loaddata,settings.loaddata);} +$.ajax({type:settings.loadtype,url:settings.loadurl,data:loaddata,async:false,success:function(result){window.clearTimeout(t);input_content=result;input.disabled=false;}});}else if(settings.data){input_content=settings.data;if($.isFunction(settings.data)){input_content=settings.data.apply(self,[self.revert,settings]);}}else{input_content=self.revert;} +content.apply(form,[input_content,settings,self]);input.attr('name',settings.name);buttons.apply(form,[settings,self]);$(self).append(form);plugin.apply(form,[settings,self]);$(':input:visible:enabled:first',form).focus();if(settings.select){input.select();} +input.keydown(function(e){if(e.keyCode==27){e.preventDefault();reset.apply(form,[settings,self]);}});var t;if('cancel'==settings.onblur){input.blur(function(e){t=setTimeout(function(){reset.apply(form,[settings,self]);},500);});}else if('submit'==settings.onblur){input.blur(function(e){t=setTimeout(function(){form.submit();},200);});}else if($.isFunction(settings.onblur)){input.blur(function(e){settings.onblur.apply(self,[input.val(),settings]);});}else{input.blur(function(e){});} +form.submit(function(e){if(t){clearTimeout(t);} +e.preventDefault();if(false!==onsubmit.apply(form,[settings,self])){if(false!==submit.apply(form,[settings,self])){if($.isFunction(settings.target)){var str=settings.target.apply(self,[input.val(),settings]);$(self).html(str);self.editing=false;callback.apply(self,[self.innerHTML,settings]);if(!$.trim($(self).html())){$(self).html(settings.placeholder);}}else{var submitdata={};submitdata[settings.name]=input.val();submitdata[settings.id]=self.id;if($.isFunction(settings.submitdata)){$.extend(submitdata,settings.submitdata.apply(self,[self.revert,settings]));}else{$.extend(submitdata,settings.submitdata);} +if('PUT'==settings.method){submitdata['_method']='put';} +$(self).html(settings.indicator);var ajaxoptions={type:'POST',data:submitdata,dataType:'html',url:settings.target,success:function(result,status){if(ajaxoptions.dataType=='html'){$(self).html(result);} +self.editing=false;callback.apply(self,[result,settings]);if(!$.trim($(self).html())){$(self).html(settings.placeholder);}},error:function(xhr,status,error){onerror.apply(form,[settings,self,xhr]);}};$.extend(ajaxoptions,settings.ajaxoptions);$.ajax(ajaxoptions);}}} +$(self).attr('title',settings.tooltip);return false;});});this.reset=function(form){if(this.editing){if(false!==onreset.apply(form,[settings,self])){$(self).html(self.revert);self.editing=false;if(!$.trim($(self).html())){$(self).html(settings.placeholder);} +if(settings.tooltip){$(self).attr('title',settings.tooltip);}}}};});};$.editable={types:{defaults:{element:function(settings,original){var input=$('');$(this).append(input);return(input);},content:function(string,settings,original){$(':input:first',this).val(string);},reset:function(settings,original){original.reset(this);},buttons:function(settings,original){var form=this;if(settings.submit){if(settings.submit.match(/>$/)){var submit=$(settings.submit).click(function(){if(submit.attr("type")!="submit"){form.submit();}});}else{var submit=$('