├── log └── .keep ├── tmp └── .keep ├── vendor └── .keep ├── lib ├── assets │ └── .keep ├── tasks │ └── db.rake └── collation.rb ├── public ├── favicon.ico ├── assets │ └── .keep ├── apple-touch-icon.png ├── apple-touch-icon-120x120.png ├── apple-touch-icon-precomposed.png ├── apple-touch-icon-120x120-precomposed.png ├── images │ ├── icon.png │ ├── logo.png │ ├── icon_64.png │ ├── icon_120.png │ ├── icon_kero.jpg │ ├── icon_large.png │ ├── icon_small.png │ ├── logo_large.png │ ├── logo_white.png │ ├── banner-background.jpg │ └── logo_white_large.png ├── audios │ ├── guitar │ │ ├── e2.mp3 │ │ ├── e3.mp3 │ │ ├── e4.mp3 │ │ └── e5.mp3 │ ├── piano │ │ ├── e2.mp3 │ │ ├── e3.mp3 │ │ ├── e4.mp3 │ │ └── e5.mp3 │ └── strings │ │ ├── e2.mp3 │ │ ├── e3.mp3 │ │ ├── e4.mp3 │ │ └── e5.mp3 ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── .ruby-version ├── app ├── assets │ ├── images │ │ ├── .keep │ │ └── favicon.ico │ ├── config │ │ └── manifest.js │ ├── stylesheets │ │ ├── application.css.sass │ │ └── partials │ │ │ └── fonts.sass │ └── javascripts │ │ └── application.js ├── helpers │ └── application_helper.rb ├── frontend │ ├── styles │ │ ├── rails_admin.scss │ │ ├── partials │ │ │ ├── flash-message.sass │ │ │ ├── footer.sass │ │ │ ├── terms.sass │ │ │ ├── variables.sass │ │ │ ├── chord-colors.sass │ │ │ ├── score-footer.sass │ │ │ ├── fav.sass │ │ │ ├── changelog.sass │ │ │ ├── login.sass │ │ │ ├── loading.sass │ │ │ ├── tabbar.sass │ │ │ ├── shared-buttons.sass │ │ │ ├── navbar.sass │ │ │ ├── user-page.sass │ │ │ ├── notification.sass │ │ │ ├── user-card.sass │ │ │ └── score-header.sass │ │ └── rechord.sass │ ├── packs │ │ ├── rails_admin.js │ │ └── rechord.js │ ├── components │ │ ├── TitleControl │ │ │ ├── validateTypes.js │ │ │ └── index.js │ │ ├── Score │ │ │ ├── ScoreEditor │ │ │ │ ├── validateTypes.js │ │ │ │ └── changeScrollPosition.js │ │ │ ├── validate.js │ │ │ ├── ClearButton │ │ │ │ └── index.js │ │ │ ├── SetSampleButton │ │ │ │ └── index.js │ │ │ ├── LoopControl │ │ │ │ └── index.js │ │ │ ├── ClickControl │ │ │ │ └── index.js │ │ │ ├── InstrumentControl │ │ │ │ └── index.js │ │ │ ├── BpmControl │ │ │ │ └── index.js │ │ │ ├── CapoControl │ │ │ │ └── index.js │ │ │ ├── BeatControl │ │ │ │ └── index.js │ │ │ ├── VolumeControl │ │ │ │ └── index.js │ │ │ ├── KeyControl │ │ │ │ └── index.js │ │ │ └── UndoControl │ │ │ │ └── index.js │ │ ├── commons │ │ │ ├── ErrorMessages.js │ │ │ ├── ErrorBoundary.js │ │ │ ├── HasAddonsField.js │ │ │ ├── Notification │ │ │ │ ├── DefaultNotification.js │ │ │ │ ├── ReleaseNotification.js │ │ │ │ ├── NotificationIcon.js │ │ │ │ ├── index.js │ │ │ │ └── FavNotification.js │ │ │ ├── Field.js │ │ │ ├── Slider.js │ │ │ ├── HorizontalField.js │ │ │ ├── SelectField.js │ │ │ ├── LinkButton.js │ │ │ ├── Button.js │ │ │ ├── Footer │ │ │ │ └── index.js │ │ │ ├── FlashMessage.js │ │ │ ├── LoginModal │ │ │ │ └── index.js │ │ │ └── ModalCard.js │ │ ├── Routes │ │ │ ├── User │ │ │ │ ├── UserScoresList │ │ │ │ │ └── index.js │ │ │ │ ├── EditUser │ │ │ │ │ └── validateTypes.js │ │ │ │ └── DestroyUserModal │ │ │ │ │ └── index.js │ │ │ ├── FavsList │ │ │ │ └── index.js │ │ │ ├── ScoresList │ │ │ │ └── index.js │ │ │ ├── UsersList │ │ │ │ └── index.js │ │ │ ├── Changelog │ │ │ │ ├── Version.js │ │ │ │ └── index.js │ │ │ ├── NewScore │ │ │ │ └── RestoreModal │ │ │ │ │ └── index.js │ │ │ └── ShowScore │ │ │ │ ├── DestroyScoreModal │ │ │ │ └── index.js │ │ │ │ ├── ScoreFooter │ │ │ │ └── index.js │ │ │ │ └── ScoreHeader │ │ │ │ └── Author.js │ │ ├── CardsList │ │ │ ├── UsersResult │ │ │ │ └── index.js │ │ │ ├── SortSelect │ │ │ │ └── index.js │ │ │ ├── OptionCheckbox │ │ │ │ └── index.js │ │ │ ├── ScoresResult │ │ │ │ └── index.js │ │ │ └── cardsListUtils.js │ │ ├── SharedButtons │ │ │ └── ShareIcon │ │ │ │ └── index.js │ │ ├── index.js │ │ └── StatusControl │ │ │ └── index.js │ ├── constants │ │ ├── beats.js │ │ ├── sampleScore.js │ │ ├── index.js │ │ ├── instruments.js │ │ └── regex.js │ ├── api │ │ └── axios.js │ ├── decorators │ │ └── highlighter.js │ ├── utils │ │ ├── metaDescription.js │ │ ├── browser-dependencies.js │ │ ├── draftjsUtils.js │ │ └── localStorageState.js │ └── validator │ │ ├── index.js │ │ ├── FormWithValidate.js │ │ └── translate.js ├── models │ ├── application_record.rb │ ├── ability.rb │ ├── fav.rb │ ├── maintenance_schedule.rb │ ├── notification.rb │ └── concerns │ │ └── oauth_provider_formatter.rb ├── mailers │ └── application_mailer.rb ├── views │ ├── layouts │ │ ├── _gtm_body.html.erb │ │ ├── _google_ads.html.erb │ │ ├── _ga.html.erb │ │ ├── _gtm_head.html.erb │ │ ├── application.html.slim │ │ └── _meta.html.slim │ └── top │ │ ├── not_supported.html.slim │ │ ├── index.html.slim │ │ └── maintenance.html.slim └── controllers │ ├── statuses_controller.rb │ ├── users │ ├── scores_controller.rb │ └── omniauth_callbacks_controller.rb │ ├── application_controller.rb │ ├── top_controller.rb │ ├── concerns │ └── search_params.rb │ └── favs_controller.rb ├── .browserslistrc ├── .node-version ├── Procfile ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .postcssrc.yml ├── config ├── initializers │ ├── i18n.rb │ ├── omniauth.rb │ ├── mime_types.rb │ ├── impression.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── permissions_policy.rb │ ├── carrierwave.rb │ ├── wrap_parameters.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── inflections.rb │ ├── content_security_policy.rb │ └── exception_notification.rb ├── webpack │ ├── environment.js │ ├── test.js │ ├── production.js │ └── development.js ├── spring.rb ├── environment.rb ├── schedule.rb ├── version.yml ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── application.rb ├── locales │ └── en.yml ├── unicorn.rb ├── storage.yml ├── routes.rb ├── newrelic.yml └── puma.rb ├── bin ├── bundle ├── rake ├── rails ├── webpack ├── webpack-dev-server ├── spring ├── yarn ├── fog ├── pry ├── yri ├── haml ├── puma ├── racc ├── sass ├── scss ├── thor ├── tilt ├── yard ├── oauth ├── rackup ├── slimrb ├── yardoc ├── byebug ├── coderay ├── dotenv ├── fission ├── listen ├── pumactl ├── unicorn ├── bootsnap ├── console ├── nokogiri ├── nrdebug ├── whenever ├── newrelic ├── rbvmomish ├── sprockets ├── httpclient ├── restclient ├── sass-convert ├── wheneverize ├── mongrel_rpm ├── newrelic_cmd ├── unicorn_rails ├── generate-api ├── update └── setup ├── config.ru ├── db ├── migrate │ ├── 20180205144032_add_remote_ip_to_score.rb │ ├── 20180119050446_add_counter_cache_to_score.rb │ ├── 20180119052259_add_counter_cache_to_user.rb │ ├── 20171202020544_add_column_to_user.rb │ ├── 20171202163814_rename_column_to_user.rb │ ├── 20180131060629_add_column_to_score.rb │ ├── 20180119043849_add_favs_count_to_scores.rb │ ├── 20171116152249_create_users.rb │ ├── 20180112052813_create_favs.rb │ ├── 20171127012637_add_column_users.rb │ ├── 20190125093520_create_maintenance_schedules.rb │ ├── 20180620083642_create_notifications.rb │ ├── 20171117024531_create_scores.rb │ └── 20180112050744_create_impressions_table.rb └── seeds.rb ├── Rakefile ├── postcss.config.js ├── .docker └── docker-compose.yml ├── spec └── frontend │ └── decorators │ └── scoreEditorDecorator.spec.js ├── README.md ├── .gitignore ├── .env.sample ├── Capfile ├── package.json ├── .eslintrc └── circle.yml /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.1 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.11.1 2 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-120x120-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec rails server 2 | js: bin/webpack-dev-server 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## したいこと 2 | 3 | ## 期待できる結果 4 | 5 | ## 実装メモ 6 | -------------------------------------------------------------------------------- /.postcssrc.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | postcss-import: {} 3 | postcss-cssnext: {} 4 | -------------------------------------------------------------------------------- /config/initializers/i18n.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.i18n.default_locale = :ja 2 | -------------------------------------------------------------------------------- /app/frontend/styles/rails_admin.scss: -------------------------------------------------------------------------------- 1 | @import "rails_admin/src/rails_admin/styles/base"; 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## スクリーンショット 2 | 3 | ## 改善 4 | 5 | ## 修正 6 | 7 | ## その他 8 | -------------------------------------------------------------------------------- /public/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/images/icon.png -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /public/images/icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/images/icon_64.png -------------------------------------------------------------------------------- /public/audios/guitar/e2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/audios/guitar/e2.mp3 -------------------------------------------------------------------------------- /public/audios/guitar/e3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/audios/guitar/e3.mp3 -------------------------------------------------------------------------------- /public/audios/guitar/e4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/audios/guitar/e4.mp3 -------------------------------------------------------------------------------- /public/audios/guitar/e5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/audios/guitar/e5.mp3 -------------------------------------------------------------------------------- /public/audios/piano/e2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/audios/piano/e2.mp3 -------------------------------------------------------------------------------- /public/audios/piano/e3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/audios/piano/e3.mp3 -------------------------------------------------------------------------------- /public/audios/piano/e4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/audios/piano/e4.mp3 -------------------------------------------------------------------------------- /public/audios/piano/e5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/audios/piano/e5.mp3 -------------------------------------------------------------------------------- /public/images/icon_120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/images/icon_120.png -------------------------------------------------------------------------------- /public/images/icon_kero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/images/icon_kero.jpg -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /public/audios/strings/e2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/audios/strings/e2.mp3 -------------------------------------------------------------------------------- /public/audios/strings/e3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/audios/strings/e3.mp3 -------------------------------------------------------------------------------- /public/audios/strings/e4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/audios/strings/e4.mp3 -------------------------------------------------------------------------------- /public/audios/strings/e5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/audios/strings/e5.mp3 -------------------------------------------------------------------------------- /public/images/icon_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/images/icon_large.png -------------------------------------------------------------------------------- /public/images/icon_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/images/icon_small.png -------------------------------------------------------------------------------- /public/images/logo_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/images/logo_large.png -------------------------------------------------------------------------------- /public/images/logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/images/logo_white.png -------------------------------------------------------------------------------- /app/frontend/packs/rails_admin.js: -------------------------------------------------------------------------------- 1 | import "rails_admin/src/rails_admin/base"; 2 | import "../styles/rails_admin.scss"; 3 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /config/webpack/environment.js: -------------------------------------------------------------------------------- 1 | const { environment } = require('@rails/webpacker') 2 | 3 | module.exports = environment 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /public/images/banner-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/images/banner-background.jpg -------------------------------------------------------------------------------- /public/images/logo_white_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comorebi-notes/rechord/HEAD/public/images/logo_white_large.png -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | ".ruby-version", 3 | ".rbenv-vars", 4 | "tmp/restart.txt", 5 | "tmp/caching-dev.txt" 6 | ) 7 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | load File.expand_path("spring", __dir__) 3 | require_relative "../config/boot" 4 | require "rake" 5 | Rake.application.run 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/frontend/components/TitleControl/validateTypes.js: -------------------------------------------------------------------------------- 1 | export const validateTypes = [ 2 | ["required"], 3 | ["maxLength", 40] 4 | ] 5 | 6 | export default validateTypes 7 | -------------------------------------------------------------------------------- /config/schedule.rb: -------------------------------------------------------------------------------- 1 | set :output, "log/crontab.log" 2 | set :environment, :production 3 | 4 | every 1.day, at: "4:00 am", roles: [:db] do 5 | rake "backup:daily" 6 | end 7 | -------------------------------------------------------------------------------- /app/frontend/components/Score/ScoreEditor/validateTypes.js: -------------------------------------------------------------------------------- 1 | export const validateTypes = [ 2 | ["required"], 3 | ["maxLength", 1024] 4 | ] 5 | 6 | export default validateTypes 7 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/flash-message.sass: -------------------------------------------------------------------------------- 1 | .flash-message 2 | margin-bottom: 2rem 3 | .notification 4 | padding: 1rem 1.5rem 5 | > * 6 | vertical-align: middle 7 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/initializers/omniauth.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.middleware.use OmniAuth::Builder do 2 | configure do |config| 3 | config.full_host = ENV['RAILS_FULL_HOST'] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/webpack/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /db/migrate/20180205144032_add_remote_ip_to_score.rb: -------------------------------------------------------------------------------- 1 | class AddRemoteIpToScore < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :scores, :remote_ip, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/footer.sass: -------------------------------------------------------------------------------- 1 | footer.footer 2 | background-color: #532 3 | color: #fff 4 | a 5 | color: #fff 6 | opacity: .75 7 | &:hover, &:active 8 | opacity: .5 9 | -------------------------------------------------------------------------------- /config/webpack/production.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/webpack/development.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.toWebpackConfig() 6 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | load File.expand_path("spring", __dir__) 3 | APP_PATH = File.expand_path('../config/application', __dir__) 4 | require_relative "../config/boot" 5 | require "rails/commands" 6 | -------------------------------------------------------------------------------- /config/version.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | version: v2.0.0 3 | 4 | development: 5 | <<: *default 6 | test: 7 | <<: *default 8 | production: 9 | <<: *default 10 | staging: 11 | <<: *default 12 | -------------------------------------------------------------------------------- /db/migrate/20180119050446_add_counter_cache_to_score.rb: -------------------------------------------------------------------------------- 1 | class AddCounterCacheToScore < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :scores, :views_count, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180119052259_add_counter_cache_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddCounterCacheToUser < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :scores_count, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/frontend/constants/beats.js: -------------------------------------------------------------------------------- 1 | export const beats = { 2 | "2/4": [2, 4], 3 | "3/4": [3, 4], 4 | "4/4": [4, 4], 5 | "5/4": [5, 4], 6 | "6/4": [6, 4], 7 | "7/4": [7, 4] 8 | } 9 | 10 | export default beats 11 | -------------------------------------------------------------------------------- /db/migrate/20171202020544_add_column_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddColumnToUser < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :email, :string 4 | add_column :users, :twitter, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/ability.rb: -------------------------------------------------------------------------------- 1 | class Ability 2 | include CanCan::Ability 3 | 4 | def initialize(user) 5 | if user && user.admin? 6 | can :access, :rails_admin 7 | can :manage, :all 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /db/migrate/20171202163814_rename_column_to_user.rb: -------------------------------------------------------------------------------- 1 | class RenameColumnToUser < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_column :users, :icon_url, :icon 4 | rename_column :users, :site_url, :site 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: rechord_production 11 | -------------------------------------------------------------------------------- /db/migrate/20180131060629_add_column_to_score.rb: -------------------------------------------------------------------------------- 1 | class AddColumnToScore < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :scores, :capo, :integer, default: 0 4 | add_column :scores, :loop, :boolean, default: false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/terms.sass: -------------------------------------------------------------------------------- 1 | .terms 2 | line-height: 1.8 3 | font-size: .85em 4 | ol 5 | margin-bottom: 1em 6 | &.terms-list 7 | > li 8 | margin-top: 2.5em 9 | h1:not(:first-child) 10 | margin-top: 2.5em 11 | -------------------------------------------------------------------------------- /config/initializers/impression.rb: -------------------------------------------------------------------------------- 1 | # Use this hook to configure impressionist parameters 2 | #Impressionist.setup do |config| 3 | # Define ORM. Could be :active_record (default), :mongo_mapper or :mongoid 4 | # config.orm = :active_record 5 | #end 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/tasks/db.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | namespace :collation do 3 | task change: :environment do 4 | Collation.new.change_all 5 | end 6 | task rollback: :environment do 7 | Collation.new.rollback_all 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/layouts/_gtm_body.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /app/views/layouts/_google_ads.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /db/migrate/20180119043849_add_favs_count_to_scores.rb: -------------------------------------------------------------------------------- 1 | class AddFavsCountToScores < ActiveRecord::Migration[5.1] 2 | def self.up 3 | add_column :scores, :favs_count, :integer, null: false, default: 0 4 | end 5 | 6 | def self.down 7 | remove_column :scores, :favs_count 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20171116152249_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :users do |t| 4 | t.string :provider 5 | t.string :uid 6 | t.string :name 7 | t.string :icon_url 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20180112052813_create_favs.rb: -------------------------------------------------------------------------------- 1 | class CreateFavs < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :favs do |t| 4 | t.references :user, index: true, foreign_key: true 5 | t.references :score, index: true, foreign_key: true 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [ 5 | :password, :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 6 | ] 7 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-flexbugs-fixes'), 5 | require('postcss-preset-env')({ 6 | autoprefixer: { 7 | flexbox: 'no-2009' 8 | }, 9 | stage: 3 10 | }), 11 | require('postcss-cssnext') 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /app/frontend/components/commons/ErrorMessages.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react" 2 | 3 | export default class ErrorMessages extends PureComponent { 4 | render() { 5 | const { error } = this.props 6 | return ( 7 |

{error}

8 | ) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/frontend/api/axios.js: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { document } from "../utils/browser-dependencies" 3 | 4 | axios.defaults.headers["X-Requested-With"] = "XMLHttpRequest" 5 | export default axios 6 | 7 | export const config = { 8 | headers: { 9 | "X-CSRF-Token": document.querySelector("head [name=csrf-token]").content 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | image: postgres:10.1 5 | ports: 6 | - 5432:5432 7 | environment: 8 | POSTGRES_USER: postgres 9 | volumes: 10 | - ./postgres/data:/var/lib/postgresql/data 11 | platform: linux/amd64 12 | redis: 13 | image: redis:latest 14 | ports: 15 | - 6379:6379 16 | -------------------------------------------------------------------------------- /app/frontend/constants/sampleScore.js: -------------------------------------------------------------------------------- 1 | const sampleScore = [ 2 | "# 小節を | で区切ってコードを入力します", 3 | "C|G|Am7|Em7", 4 | "F|Em7Am7|Dm7|G7", 5 | "# 行頭に # を入力するとメモが書けます", 6 | "FM7|G7", 7 | "# % で連打、 _ で休符、 = で直前のコードを伸ばします", 8 | "E7%_Am7|=G", 9 | "# / でオンコード(最低音)を指定できます", 10 | "FC/E|Dm7Dm7/G", 11 | "Cadd9" 12 | ].join("\n") 13 | 14 | export default sampleScore 15 | -------------------------------------------------------------------------------- /app/frontend/decorators/highlighter.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Highlighter from "react-highlight-words" 3 | 4 | export const highlighter = (highlightWords) => (text) => ( 5 | 9 | ) 10 | 11 | export default highlighter 12 | -------------------------------------------------------------------------------- /db/migrate/20171127012637_add_column_users.rb: -------------------------------------------------------------------------------- 1 | class AddColumnUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :screen_name, :string 4 | add_column :users, :profile, :text 5 | add_column :users, :site_url, :string 6 | add_column :users, :admin, :boolean, default: false 7 | add_index :users, :name 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/views/layouts/_ga.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /app/frontend/components/Score/validate.js: -------------------------------------------------------------------------------- 1 | export const validate = (score, bpm) => { 2 | if (!score || score.length === 1) return false 3 | if (!bpm || Number(bpm) === 0) return false 4 | 5 | let hasError = false 6 | 7 | score.forEach((scoreItem) => { 8 | const { notes } = scoreItem 9 | if (!notes) hasError = true 10 | }) 11 | return !hasError 12 | } 13 | 14 | export default validate 15 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/variables.sass: -------------------------------------------------------------------------------- 1 | // breakpoints 2 | $gap: 32px 3 | $tablet: 769px 4 | $desktop: 960px + (2 * $gap) 5 | $mobile: 1024px + (2 * $gap) 6 | $widescreen: 1152px + (2 * $gap) 7 | $fullhd: 1344px + (2 * $gap) !default 8 | 9 | // colors 10 | $primary: #a0ca30 11 | $info: #30b4d0 12 | $success: #53d12f 13 | $warning: #e4a020 14 | $link: #177fcb 15 | $danger: #e44020 16 | -------------------------------------------------------------------------------- /db/migrate/20190125093520_create_maintenance_schedules.rb: -------------------------------------------------------------------------------- 1 | class CreateMaintenanceSchedules < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :maintenance_schedules do |t| 4 | t.string :title, null: false 5 | t.text :description, null: false 6 | t.datetime :start_time, null: false 7 | t.datetime :end_time, null: false 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/models/fav.rb: -------------------------------------------------------------------------------- 1 | class Fav < ApplicationRecord 2 | belongs_to :user 3 | belongs_to :score 4 | 5 | counter_culture :score 6 | 7 | validates :user_id, uniqueness: { scope: :score_id } 8 | 9 | after_destroy :destroy_notification_if_zero_fav 10 | 11 | private 12 | 13 | def destroy_notification_if_zero_fav 14 | Notification.destroy_if_exist(title: score.token) if score.favs_count == 1 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/frontend/components/Routes/User/UserScoresList/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import CardsList from "../../../CardsList" 3 | 4 | export default class UserScoresList extends Component { 5 | render() { 6 | return ( 7 | 13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/views/layouts/_gtm_head.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /app/frontend/components/Routes/FavsList/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import CardsList from "../../CardsList" 3 | 4 | export default class FavsList extends Component { 5 | render() { 6 | return ( 7 | 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/views/top/not_supported.html.slim: -------------------------------------------------------------------------------- 1 | section.hero.is-primary.is-fullheight 2 | .hero-body style="min-height: 100vh" 3 | .container.has-text-centered 4 | img src="/images/logo_white.png" width="160" 5 | h1.title style="margin-top: .4em; margin-bottom: 2em" 6 | | sorry... this browser isn't supported. 7 | h2.subtitle.is-6 8 | | 申し訳ありませんが、このブラウザは rechord に対応していません。 9 | 10 | - if ENV["UA_ID"].present? 11 | = render "layouts/ga" 12 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | hsXUs5TXvrKlB2UFHsKjmOX3EjWuyKjTG9gwz5RdyNi2sEYo4GP/qp3vE1YDHZzUEGLtTnR8FkiwZc8ccFOJW6LURTrd0oXSPHZAGK9/57DlWu2W/Qu2FrkfCPhUxFzVoz3kOCd1J/tNtsGGSS9XjsyyvxZe2jhLNYzcNz+8eu6iBwLiPBC/GMFP6LVDP5MW6cj64IwcmvmYNb9pM7J533K95bJzkcEt3pbeVK2dB7Hx/19mhDM6zdaihOLZE7lpI0TpuZD2FdECRRs9Gqaq3a1wlpiLAbhVHRyuaGn+tWAqOeShdOZBWCHgto8/Xgl43/s/rp9c7rLgU0d7PpYZcREKwhsD+zmT9erYa78x4yAg2MrCUgnOmQ4m4ycBTTW3R4bucWBr6GISYlGbpz4Z96qD7TS62rK6xfC5--acdQMJlxav6uiyzZ--oXso9tXsyGQyAwF93tcOvg== -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css.sass: -------------------------------------------------------------------------------- 1 | $primary: #a0ca30 2 | $info: #30b4d0 3 | $success: #53d12f 4 | $warning: #e4a020 5 | $link: #177fcb 6 | // $danger: #e44020 7 | // $dark: $grey-darker 8 | // $text: $grey-dark 9 | 10 | @import "font-awesome" 11 | @import "bulma" 12 | @import "../../../vendor/assets/bulma-slider.sass" 13 | @import "../../../vendor/assets/bulma-switch.sass" 14 | @import "../../../vendor/assets/bulma-checkradio.sass" 15 | @import "partials/*" 16 | -------------------------------------------------------------------------------- /app/frontend/components/commons/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | 3 | export default class ErrorBoundary extends Component { 4 | constructor() { 5 | super() 6 | this.state = { hasError: false } 7 | } 8 | componentDidCatch = (error) => console.log(error) // eslint-disable-line 9 | render () { 10 | const { hasError } = this.state 11 | const { children } = this.props 12 | return hasError ? : children 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/frontend/components/Routes/User/EditUser/validateTypes.js: -------------------------------------------------------------------------------- 1 | export const validateTypes = { 2 | name: [["required"], ["maxLength", 16], ["format", /^[a-z0-9._-]*$/]], 3 | screenName: [["required"], ["maxLength", 32]], 4 | profile: [["maxLength", 256]], 5 | site: [["maxLength", 256], ["format", /^https?:\/\/([\w-]+\.)+[\w-]+((\/[\w- .?%&=]*)?)*$/]], 6 | twitter: [["maxLength", 16], ["format", /^[a-zA-Z0-9_]*$/]] 7 | } 8 | 9 | export default validateTypes 10 | -------------------------------------------------------------------------------- /bin/webpack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/webpack_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::WebpackRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/fonts.sass: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap') 2 | @import url('https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;700&display=swap') 3 | * 4 | font-family: 'Noto Sans JP', sans-serif 5 | #score-editor, .title-logo, .navbar, .tab-bar, .score-controls, .button, input 6 | *:not(.fa) 7 | font-family: 'Ubuntu', sans-serif 8 | #score-editor 9 | .comment 10 | font-family: 'Noto Sans JP', sans-serif 11 | -------------------------------------------------------------------------------- /db/migrate/20180620083642_create_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateNotifications < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :notifications do |t| 4 | t.string :title, index: true, unique: true 5 | t.text :content 6 | t.json :params 7 | t.integer :template, default: 0 8 | t.integer :user_id, index: true 9 | 10 | t.timestamps 11 | end 12 | 13 | add_column :users, :last_read_at, :timestamp, default: -> { "NOW()" } 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/frontend/decorators/scoreEditorDecorator.spec.js: -------------------------------------------------------------------------------- 1 | import * as scoreEditorDecorator from '../../../app/frontend/decorators/scoreEditorDecorator' 2 | 3 | test('parse multi-line chord progression', () => { 4 | expect(scoreEditorDecorator.parseChordProgression('Gm/CF69|Gm/C\nAbGaug7|')) 5 | .toEqual( 6 | [ 7 | [ 8 | [['G', 'm/C'], ['F', '69']], [['G', 'm/C']] 9 | ], 10 | [ 11 | [['Ab', ''], ['G', 'aug7']] 12 | ] 13 | ] 14 | ) 15 | }) 16 | -------------------------------------------------------------------------------- /app/views/top/index.html.slim: -------------------------------------------------------------------------------- 1 | - content_for :pack do 2 | = javascript_pack_tag "rechord" 3 | = stylesheet_pack_tag "rechord" 4 | 5 | #rechord.rechord 6 | 7 | javascript: 8 | var data = { 9 | currentUser: #{raw current_user&.to_json || {}}, 10 | currentVersion: #{raw Rails.configuration.preference["version"].to_json}, 11 | notifications: #{raw @notifications&.to_json}, 12 | flash: #{raw flash&.to_json || []}, 13 | uaId: #{raw (ENV["UA_ID"] || "")&.to_json} 14 | } 15 | -------------------------------------------------------------------------------- /bin/webpack-dev-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" 4 | ENV["NODE_ENV"] ||= "development" 5 | 6 | require "pathname" 7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 8 | Pathname.new(__FILE__).realpath) 9 | 10 | require "bundler/setup" 11 | 12 | require "webpacker" 13 | require "webpacker/dev_server_runner" 14 | 15 | APP_ROOT = File.expand_path("..", __dir__) 16 | Dir.chdir(APP_ROOT) do 17 | Webpacker::DevServerRunner.run(ARGV) 18 | end 19 | -------------------------------------------------------------------------------- /app/models/maintenance_schedule.rb: -------------------------------------------------------------------------------- 1 | class MaintenanceSchedule < ApplicationRecord 2 | validates :title, presence: true 3 | validates :description, presence: true 4 | validates :start_time, presence: true 5 | validates :end_time, presence: true 6 | 7 | validate :validate_times 8 | 9 | scope :active, -> { where('start_time <= ?', Time.zone.now).where('end_time >= ?', Time.zone.now) } 10 | 11 | private 12 | 13 | def validate_times 14 | errors.add('開始時間は終了時間より前にしてください。') if start_time > end_time 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/controllers/statuses_controller.rb: -------------------------------------------------------------------------------- 1 | class StatusesController < ApplicationController 2 | def show 3 | render json: { 4 | currentUser: current_user || {}, 5 | currentVersion: Rails.configuration.preference["version"], 6 | notifications: Notification.list_for_user(current_user&.id), 7 | maintenanceSchedules: MaintenanceSchedule.active 8 | } 9 | end 10 | 11 | private 12 | 13 | def status_params 14 | params.permit(:current_user_id, :current_version) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/frontend/components/commons/HasAddonsField.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react" 2 | import classNames from "classnames" 3 | 4 | export default class HasAddonsField extends PureComponent { 5 | render() { 6 | const { customClass, customStyle, children } = this.props 7 | const fieldClass = classNames("field", "has-addons", { [customClass]: customClass }) 8 | return ( 9 |
10 | {children} 11 |
12 | ) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /config/initializers/carrierwave.rb: -------------------------------------------------------------------------------- 1 | CarrierWave.configure do |config| 2 | config.fog_credentials = { 3 | provider: "OpenStack", 4 | openstack_tenant: ENV["CONOHA_TENANT_NAME"], 5 | openstack_username: ENV["CONOHA_USERNAME"], 6 | openstack_api_key: ENV["CONOHA_API_PASSWORD"], 7 | openstack_auth_url: ENV["CONOHA_API_AUTH_URL"] 8 | } 9 | config.fog_directory = ENV["CONOHA_CONTAINER_NAME"] 10 | config.asset_host = ENV["CONOHA_ASSET_HOST"] + "/" + ENV["CONOHA_CONTAINER_NAME"] 11 | config.storage :fog 12 | end 13 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"]) 3 | gem "bundler" 4 | require "bundler" 5 | 6 | # Load Spring without loading other gems in the Gemfile, for speed. 7 | Bundler.locked_gems&.specs&.find { |spec| spec.name == "spring" }&.tap do |spring| 8 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 9 | gem "spring", spring.version 10 | require "spring/binstub" 11 | rescue Gem::LoadError 12 | # Ignore when Spring is not installed. 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/frontend/components/Routes/ScoresList/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import CardsList from "../../CardsList" 3 | 4 | export default class ScoresList extends Component { 5 | render() { 6 | const options = [ 7 | { key: "guest", label: "ゲストの投稿を含める" } 8 | ] 9 | return ( 10 | 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/frontend/components/Routes/UsersList/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import CardsList from "../../CardsList" 3 | 4 | export default class UsersList extends Component { 5 | render() { 6 | const options = [ 7 | { key: "no_scores", label: "スコアが無いユーザを含める" } 8 | ] 9 | return ( 10 | 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/chord-colors.sass: -------------------------------------------------------------------------------- 1 | .score 2 | .A, .Gss, .Bbb 3 | color: #f65022 4 | .As, .Bb, .Cbb 5 | color: #f37800 6 | .B, .Ass, .Cb 7 | color: #feb140 8 | .C, .Bs, .Dbb 9 | color: #8fc31f 10 | .Cs, .Bss, .Db 11 | color: #49a944 12 | .D, .Css, .Ebb 13 | color: #009eca 14 | .Ds, .Eb, .Fbb 15 | color: #206ada 16 | .E, .Dss, .Fb 17 | color: #5020f7 18 | .F, .Es, .Gbb 19 | color: #8020c8 20 | .Fs, .Ess, .Gb 21 | color: #b21793 22 | .G, .Fss, .Abb 23 | color: #da008f 24 | .Gs, .Ab 25 | color: #ea305a 26 | -------------------------------------------------------------------------------- /app/frontend/utils/metaDescription.js: -------------------------------------------------------------------------------- 1 | import { document } from "./browser-dependencies" 2 | 3 | const MAX_DESCRIPTION_LENGTH = 300 4 | const ORIGINAL_DESCRIPTION = document.querySelector('[name="description"]').content 5 | 6 | export const set = (text) => { 7 | const metatag = document.querySelector('[name="description"]') 8 | if (text && text.length > 0) { 9 | metatag.content = text.replace(/\n/g, "|").slice(0, MAX_DESCRIPTION_LENGTH) 10 | } else { 11 | metatag.content = ORIGINAL_DESCRIPTION 12 | } 13 | } 14 | export const reset = () => set(ORIGINAL_DESCRIPTION) 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rechord 2 | 3 | [https://rechord.cc](https://rechord.cc) 4 | 5 | rechord は実際に演奏もできるコード進行共有サービスです。 6 | 7 | ## components 8 | - [Ruby on Rails](https://github.com/rails/rails) 9 | - [React](https://github.com/facebook/react) 10 | - [Tone.js](https://github.com/Tonejs/Tone.js/) 11 | - [tonal](https://github.com/danigb/tonal) 12 | - [Draft.js](https://github.com/facebook/draft-js) 13 | - [Bulma](https://github.com/jgthms/bulma) 14 | 15 | ## copyright 16 | Copyright © 2017 comorebi notes All Rights Reserved. 17 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 9 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: unicode 4 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 5 | timeout: 5000 6 | database: <%= ENV['DATABASE_NAME'] %> 7 | host: <%= ENV['DATABASE_HOST'] %> 8 | port: <%= ENV['DATABASE_PORT'] %> 9 | username: <%= ENV['DATABASE_USERNAME'] %> 10 | password: <%= ENV['DATABASE_PASSWORD'] %> 11 | 12 | development: 13 | <<: *default 14 | 15 | production: 16 | <<: *default 17 | 18 | staging: 19 | <<: *default 20 | 21 | test: 22 | <<: *default 23 | database: <%= ENV['DATABASE_TEST'] %> 24 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | yarn = ENV["PATH"].split(File::PATH_SEPARATOR). 5 | select { |dir| File.expand_path(dir) != __dir__ }. 6 | product(["yarn", "yarn.cmd", "yarn.ps1"]). 7 | map { |dir, file| File.expand_path(file, dir) }. 8 | find { |file| File.executable?(file) } 9 | 10 | if yarn 11 | exec yarn, *ARGV 12 | else 13 | $stderr.puts "Yarn executable was not detected in the system." 14 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 15 | exit 1 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/frontend/components/Score/ClearButton/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | 3 | import Field from "../../commons/Field" 4 | import Button from "../../commons/Button" 5 | 6 | export default class ClearButton extends Component { 7 | handleClearText = () => this.props.setInputText("") 8 | render() { 9 | const { disabled } = this.props 10 | return ( 11 | 12 | 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/views/top/maintenance.html.slim: -------------------------------------------------------------------------------- 1 | section.hero.is-primary.is-fullheight 2 | .hero-body style="min-height: 100vh" 3 | .container.has-text-centered 4 | = image_tag '/assets/images/logo_white.png', width: 160 5 | h1.title style="margin-top: .4em; margin-bottom: 2em" 6 | | sorry... rechord is under maintenance! 7 | h2.subtitle.is-6 8 | | 申し訳ありませんが、rechord は現在メンテナンス中です。 9 | br 10 | | メンテナンスの予定は以下の通りです。 11 | - @maintenance_schedules.each do |schedule| 12 | table.table.is-bordered.mt-3 style="margin: 3rem auto 0; min-width: 50%" 13 | tr 14 | th 目的 15 | td= schedule.title 16 | tr 17 | th 概要 18 | td= schedule.description.html_safe 19 | tr 20 | th 開始時間 21 | td= schedule.start_time.strftime('%Y年%m月%d日 %H時%M分') 22 | tr 23 | th 終了時間 24 | td= schedule.end_time.strftime('%Y年%m月%d日 %H時%M分') 25 | 26 | - if ENV['UA_ID'].present? 27 | = render 'layouts/ga' 28 | -------------------------------------------------------------------------------- /app/frontend/components/Score/CapoControl/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | 3 | import HorizontalField from "../../commons/HorizontalField" 4 | import { MIN_CAPO, MAX_CAPO } from "../../../constants" 5 | import * as utils from "../../../utils" 6 | 7 | export default class CapoControl extends Component { 8 | handleChangeCapo = (e) => { 9 | this.props.handleSetState({ capo: utils.valueInRange(e.target.value, MIN_CAPO, MAX_CAPO) }) 10 | } 11 | handleBlur = () => { 12 | const { capo } = this.props 13 | if (!capo) this.props.handleSetState({ capo: 0 }) 14 | } 15 | render() { 16 | const { capo } = this.props 17 | return ( 18 | 19 | 28 | 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/frontend/components/Score/BeatControl/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | 3 | import HorizontalField from "../../commons/HorizontalField" 4 | import SelectField from "../../commons/SelectField" 5 | import { beats } from "../../../constants/beats" 6 | 7 | export default class BeatControl extends Component { 8 | handleChangeBeat = (e) => { 9 | this.props.handleSetState({ beat: e.target.value }) 10 | } 11 | render() { 12 | const { beat, disabled } = this.props 13 | return ( 14 | 15 | 16 | 27 | 28 | 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | /data/* 14 | 15 | # Ignore all logfiles and tempfiles. 16 | /log/* 17 | /tmp/* 18 | !/log/.keep 19 | !/tmp/.keep 20 | 21 | /node_modules 22 | /yarn-error.log 23 | /public/packs 24 | /public/packs-test 25 | /public/uploads 26 | /public/assets/* 27 | !/public/assets/.keep 28 | 29 | .byebug_history 30 | 31 | .env 32 | .rspec 33 | .DS_Store 34 | tags 35 | /.idea 36 | /.docker/postgres/* 37 | 38 | # Ignore master key for decrypting credentials and more. 39 | /config/master.key 40 | 41 | /public/packs 42 | /public/packs-test 43 | /node_modules 44 | /yarn-error.log 45 | yarn-debug.log* 46 | .yarn-integrity 47 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/login.sass: -------------------------------------------------------------------------------- 1 | .modal.login 2 | white-space: initial 3 | .modal-content 4 | width: 400px 5 | .button 6 | font-weight: bold 7 | display: block 8 | &:not(:last-child) 9 | margin-bottom: .8em 10 | .icon 11 | margin-right: .3em 12 | &.twitter 13 | background-color: #00aced 14 | &:hover 15 | background-color: darken(#00aced, 5%) 16 | &.facebook 17 | background-color: #3b5998 18 | &:hover 19 | background-color: darken(#3b5998, 5%) 20 | &.google 21 | background-color: #dd4b39 22 | &:hover 23 | background-color: darken(#dd4b39, 5%) 24 | &.tumblr 25 | background-color: #2c4762 26 | &:hover 27 | background-color: darken(#2c4762, 5%) 28 | &.github 29 | background-color: #24292e 30 | &:hover 31 | background-color: darken(#24292e, 5%) 32 | 33 | @media screen and (max-width: $tablet - 1px) 34 | .modal.login 35 | .modal-content 36 | width: calc(100% - 40px) 37 | -------------------------------------------------------------------------------- /config/unicorn.rb: -------------------------------------------------------------------------------- 1 | rails_root = File.expand_path("../../", __FILE__) 2 | # rails_env = ENV["RAILS_ENV"] || "development" 3 | 4 | ENV["BUNDLE_GEMFILE"] = "#{rails_root}/Gemfile" 5 | 6 | worker_processes 2 7 | working_directory rails_root 8 | timeout 30 9 | preload_app true 10 | 11 | stderr_path File.expand_path("#{rails_root}/log/unicorn_stderr.log", __FILE__) 12 | stdout_path File.expand_path("#{rails_root}/log/unicorn_stdout.log", __FILE__) 13 | 14 | pid File.expand_path("#{rails_root}/tmp/pids/unicorn.pid", __FILE__) 15 | 16 | before_fork do |server, worker| 17 | defined?(ActiveRecord::Base) and 18 | ActiveRecord::Base.connection.disconnect! 19 | 20 | old_pid = "#{server.config[:pid]}.oldbin" 21 | if old_pid != server.pid 22 | begin 23 | sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU 24 | Process.kill(sig, File.read(old_pid).to_i) 25 | rescue Errno::ENOENT, Errno::ESRCH 26 | end 27 | end 28 | end 29 | 30 | after_fork do |server, worker| 31 | defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection 32 | end 33 | -------------------------------------------------------------------------------- /app/frontend/components/Score/VolumeControl/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | 3 | import Slider from "../../commons/Slider" 4 | import { MIN_VOLUME, MAX_VOLUME } from "../../../constants" 5 | import * as utils from "../../../utils" 6 | 7 | export default class VolumeControl extends Component { 8 | handleChangeVolume = (e) => { 9 | this.props.handleSetState({ volume: utils.valueInRange(e.target.value, MIN_VOLUME, MAX_VOLUME) }) 10 | } 11 | render() { 12 | const { volume } = this.props 13 | return ( 14 |
15 | 16 | 17 | 18 | 26 | 27 | 28 | 29 |
30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/frontend/constants/regex.js: -------------------------------------------------------------------------------- 1 | export const signature = "[#♯#b♭b]{0,2}" 2 | export const scales = "[CDEFGAB]" 3 | export const note = `${scales}${signature}` 4 | export const specialNotes = ["%", "=", "_", "<", ">", "N\\.C\\."] 5 | export const onChordSeparator = "(\\/|/|on)" 6 | export const chordTypes = "(?![#♯b♭])(?:(?!(on|l))[Ma-z0-9()(),\\-+#♯#b♭b△ΔΦφø])+" 7 | 8 | export const rootChord = new RegExp(`(${note})|${specialNotes.join("|")}`, "g") 9 | export const onChord = new RegExp(`${onChordSeparator}${note}`, "g") 10 | export const chordType = new RegExp(chordTypes, "g") 11 | export const chord = new RegExp(`^((${note}(${chordTypes})?(${onChordSeparator}${note})?)|${specialNotes.join("|")})$`) 12 | export const separator = /[||ll]/g 13 | export const whiteSpaces = /[  ]+/g 14 | export const comment = /^[#♯].*$/g 15 | export const startMarker = /^<.*$/g 16 | export const endMarker = /^>.*$/g 17 | 18 | export const markerLineTop = /[<>]/ 19 | export const commentLineTop = /[#♯<>]/ 20 | 21 | export const joinOnChord = new RegExp(`${onChordSeparator} (${note})`, "g") 22 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/loading.sass: -------------------------------------------------------------------------------- 1 | .loading-wrapper 2 | min-height: 200px 3 | &:before 4 | content: "loading..." 5 | display: flex 6 | justify-content: center 7 | align-items: center 8 | color: #aaa 9 | font-weight: bold 10 | position: absolute 11 | background-color: #fff 12 | width: calc(100% + 2em) 13 | height: calc(100% + 2em) 14 | top: -1em 15 | left: -1em 16 | z-index: 20 17 | opacity: .7 18 | &:after 19 | content: "\f110" 20 | position: absolute 21 | top: calc(50% - 1.4em) 22 | left: calc(50% - .52em) 23 | font: normal normal normal 14px/1 FontAwesome 24 | font-size: 3.17em 25 | color: #aaa 26 | z-index: 25 27 | text-rendering: auto 28 | -webkit-font-smoothing: antialiased 29 | -moz-osx-font-smoothing: grayscale 30 | -webkit-animation: spin 2s infinite linear 31 | animation: spin 1s infinite steps(8) 32 | opacity: .7 33 | 34 | @keyframes spin 35 | 0% 36 | -webkit-transform: rotate(0deg) 37 | transform: rotate(0deg) 38 | 100% 39 | -webkit-transform: rotate(359deg) 40 | transform: rotate(359deg) 41 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/tabbar.sass: -------------------------------------------------------------------------------- 1 | .tab-bar.tabs 2 | margin-bottom: 0 3 | .container 4 | border-bottom: 1px solid #dbdbdb 5 | display: flex 6 | align-items: center 7 | > div:first-child 8 | flex-grow: 1 9 | ul 10 | border: none 11 | margin-right: .5em 12 | a, span 13 | padding: .85em 1em 14 | a:hover 15 | background-color: #fafafa 16 | .disabled 17 | opacity: .5 18 | .icon 19 | margin: 0 .2em 0 0 20 | padding-left: .5em 21 | font-size: 1.1em 22 | .tab-label 23 | margin: 0 24 | padding: 0 25 | .field 26 | .input, .button, .icon 27 | font-size: .95rem 28 | 29 | @media screen and (max-width: $mobile - 1px) 30 | .tab-bar.tabs 31 | .container 32 | width: 100% 33 | .field 34 | margin-right: 1.5rem 35 | 36 | @media screen and (max-width: $tablet - 1px) 37 | .tab-bar.tabs 38 | .container 39 | ul 40 | .icon 41 | margin: 0 42 | padding: 0 43 | .tab-label 44 | display: none 45 | .field 46 | margin-right: .75rem 47 | -------------------------------------------------------------------------------- /app/frontend/components/commons/Notification/NotificationIcon.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import classNames from "classnames" 3 | 4 | export default class NotificationIcon extends Component { 5 | render() { 6 | const { notifications, handleToggleNotification, customClass } = this.props 7 | const navbarItemClass = classNames("navbar-item", "notification-icon", { [customClass]: customClass }) 8 | 9 | return notifications.length > 0 ? ( 10 | 11 | 12 | 13 | 14 | {notifications.length} 15 | 16 | 17 | 18 | ) : ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | RAILS_FULL_HOST = 'http://localhost:3000' 2 | 3 | REDIS_URL = redis://127.0.0.1:6379 4 | 5 | BASIC_AUTHENTICATION_USERNAME = 'username' 6 | BASIC_AUTHENTICATION_PASSWORD = 'password' 7 | 8 | ### 普段はコメントアウト 9 | # NewRelic 10 | # NEWRELIC_LICENSE_KEY = 11 | # WEBHOOK_URL = 12 | 13 | # Gooogle Analytics 14 | # UA_ID = 15 | 16 | # 開発用DB 17 | DATABASE_NAME = rechord_development 18 | DATABASE_TEST = rechord_test 19 | DATABASE_HOST = localhost 20 | DATABASE_PORT = 5432 21 | DATABASE_USERNAME = postgres 22 | DATABASE_PASSWORD = 23 | 24 | # ConoHa Object Storage 25 | CONOHA_API_AUTH_URL = 26 | CONOHA_API_PASSWORD = 27 | CONOHA_ASSET_HOST = 28 | CONOHA_CONTAINER_NAME = 29 | CONOHA_TENANT_NAME = 30 | CONOHA_USERNAME = 31 | 32 | # omniauth 33 | TWITTER_KEY = 34 | TWITTER_SECRET = 35 | FACEBOOK_KEY = 36 | FACEBOOK_SECRET = 37 | GITHUB_KEY = 38 | GITHUB_SECRET = 39 | GOOGLE_CLIENT_ID = 40 | GOOGLE_CLIENT_SECRET = 41 | TUMBLR_KEY = 42 | TUMBLR_SECRET = 43 | 44 | # backup 45 | PG_DUMP_PATH = 46 | DROPBOX_ACCESS_TOKEN = 47 | BACKUP_FILES_PATH = 48 | 49 | # 本番用 50 | # RAILS_MASTER_KEY = 51 | -------------------------------------------------------------------------------- /app/models/notification.rb: -------------------------------------------------------------------------------- 1 | class Notification < ApplicationRecord 2 | validates :title, length: { maximum: 128 }, uniqueness: true 3 | validates :content, length: { maximum: 2048 } 4 | 5 | scope :list_for_user, -> (user_id) { 6 | return if user_id.blank? 7 | 8 | user = User.find(user_id) 9 | return if user.blank? 10 | 11 | notifications = where(user_id: [user_id, nil]) 12 | notifications = notifications.where("updated_at > ?", user.last_read_at) if user.last_read_at.present? 13 | notifications.order(updated_at: :desc).limit(20) 14 | } 15 | 16 | enum template: { 17 | default: 0, 18 | release: 1, 19 | fav: 2 20 | } 21 | 22 | class << self 23 | def create_or_update_by_fav(fav) 24 | notification = find_by(title: fav.score.token) 25 | if notification.present? 26 | notification.touch # updated_at だけ更新 27 | else 28 | notification = create(template: :fav, title: fav.score.token, user_id: fav.score.user_id) 29 | end 30 | end 31 | 32 | def destroy_if_exist(params) 33 | notification = find_by(params) 34 | notification.destroy! if notification.present? 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/frontend/components/Routes/ShowScore/DestroyScoreModal/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import ModalCard from "../../../commons/ModalCard" 3 | import * as api from "../../../../api" 4 | import * as path from "../../../../utils/path" 5 | 6 | export default class DestroyScoreModal extends Component { 7 | handleDestroyScore = () => { 8 | const { history, token, user: { name } } = this.props 9 | api.destroyScore( 10 | { token }, 11 | () => history.push(path.user.show(name), { flash: ["success", "削除に成功しました。"] }), 12 | () => history.push(path.current, { flash: ["error", "削除に失敗しました。"] }) 13 | ) 14 | } 15 | hideModal = () => this.props.handleToggleDestroyModal() 16 | render() { 17 | const { active } = this.props 18 | return ( 19 | 28 | 一度削除したスコアは復元できません。
29 | 本当にスコアを削除しますか? 30 |
31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | 4 | rescue_from CanCan::AccessDenied do |exception| 5 | redirect_to main_app.root_path 6 | end 7 | 8 | before_action :no_cache 9 | before_action :basic_authenticate if Rails.env.staging? 10 | 11 | private 12 | 13 | def authenticate_user! 14 | session[:user_return_to] = request.path_info 15 | redirect_to root_path unless user_signed_in? 16 | end 17 | 18 | def no_cache 19 | # ブラウザバックで JSON が表示されるのを防止 20 | if request.format.json? 21 | response.headers["Cache-Control"] = "no-cache, no-store" 22 | response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" 23 | response.headers["Pragma"] = "no-cache" 24 | end 25 | end 26 | 27 | def basic_authenticate 28 | authenticate_or_request_with_http_basic do |username, password| 29 | username == ENV['BASIC_AUTHENTICATION_USERNAME'] && password == ENV['BASIC_AUTHENTICATION_PASSWORD'] 30 | end 31 | end 32 | 33 | def after_sign_out_path_for(resource) 34 | root_path 35 | end 36 | 37 | def new_session_path(scope) 38 | root_path 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/frontend/validator/translate.js: -------------------------------------------------------------------------------- 1 | const keys = { 2 | score: { 3 | title: "タイトル", 4 | content: "スコア" 5 | }, 6 | user: { 7 | name: "ID", 8 | screenName: "名前", 9 | profile: "プロフィール", 10 | icon: "アイコン", 11 | site: "サイトURL", 12 | twitter: "Twitter ID " 13 | } 14 | } 15 | 16 | const errorKey = (error) => { 17 | switch (true) { 18 | case (/You are not allowed to upload .* files, allowed types: jpg, jpeg, gif, png/).test(error): 19 | return "wrong_extention" 20 | case (/.*maybe it is not an image\?/).test(error): 21 | return "not_image" 22 | default: 23 | return error 24 | } 25 | } 26 | 27 | const errors = { 28 | blank: (key) => `${key}は必須項目です。`, 29 | too_long: (key) => `${key}が長すぎます。`, 30 | taken: (key) => `その${key}は既に使用されています。`, 31 | over_size: (key) => `${key}のファイルサイズは2MBが上限です。`, 32 | not_image: () => "画像ファイルではありません。", 33 | invalid_format: (key) => `${key}のフォーマットが正しくありません。`, 34 | wrong_extention: (key) => `${key}は .png, .jpg, .jpeg, .gif のみが使用可能です。`, 35 | } 36 | 37 | export const translate = (target, key, error) => errors[errorKey(error)](keys[target][key]) 38 | 39 | export default translate 40 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies 21 | system! 'bin/yarn' 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:prepare' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /app/frontend/components/Routes/User/DestroyUserModal/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import ModalCard from "../../../commons/ModalCard" 3 | import * as api from "../../../../api" 4 | import * as path from "../../../../utils/path" 5 | import { window } from "../../../../utils/browser-dependencies" 6 | 7 | export default class DestroyUserModal extends Component { 8 | handleDestroyUser = () => ( 9 | api.destoryUser( 10 | { name: this.props.user.name }, 11 | () => { 12 | window.location.href = path.root 13 | }, 14 | () => this.props.history.push(path.current, { flash: ["error", "削除に失敗しました。"] }) 15 | ) 16 | ) 17 | hideModal = () => this.props.handleToggleDestroyModal() 18 | render() { 19 | const { active } = this.props 20 | return ( 21 | 30 | ユーザを削除すると、そのユーザで登録されたスコアもすべて削除されます。
31 | 本当にユーザを削除しますか? 32 |
33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/views/layouts/_meta.html.slim: -------------------------------------------------------------------------------- 1 | - description = "rechord はコード進行を入力するだけで誰でも簡単に演奏/共有することができる Web サービスです。" 2 | - base_url = "https://rechord.cc" 3 | - logo_url = "#{base_url}/images/logo.png" 4 | 5 | meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0" 6 | meta name="format-detection" content="telephone=no" 7 | 8 | meta name="title" content=title 9 | meta name="keywords" content="" 10 | meta name="description" content=description 11 | meta name="author" content="comorebi notes" 12 | link rel="canonical" href=base_url 13 | 14 | meta property="og:site_name" content="rechord" 15 | meta property="og:title" content=title 16 | meta property="og:description" content=description 17 | meta property="og:type" content="website" 18 | meta property="og:url" content=request.url 19 | meta property="og:image" content=logo_url 20 | meta property="fb:app_id" content="325988221218874" 21 | 22 | meta name="twitter:card" content="summary" 23 | meta name="twitter:site" content="@rechord_cc" 24 | meta name="twitter:image" content=logo_url 25 | 26 | meta name="google-site-verification" content="Or2JzFxknGLTrrPfPDpFP9jVQFoMmZt15N5lAeLT1_0" 27 | 28 | = favicon_link_tag 29 | -------------------------------------------------------------------------------- /app/frontend/components/commons/Footer/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react" 2 | import { Link } from "react-router-dom" 3 | import { Follow } from "react-twitter-widgets" 4 | import ShareButtons from "../../SharedButtons" 5 | import * as path from "../../../utils/path" 6 | import { location } from "../../../utils/browser-dependencies" 7 | 8 | export default class Footer extends PureComponent { 9 | render() { 10 | return ( 11 |
12 |
13 |
14 | 15 | 16 |

17 | 18 | 19 | 利用規約・プライバシーポリシー 20 | 21 | 22 |
23 | 24 | Copyright © 2017 comorebi notes All Rights Reserved. 25 | 26 |

27 |
28 |
29 |
30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/frontend/components/Score/KeyControl/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | 3 | import HasAddonsField from "../../commons/HasAddonsField" 4 | import Button from "../../commons/Button" 5 | import * as decorator from "../../../decorators/scoreEditorDecorator" 6 | 7 | export default class KeyControl extends Component { 8 | handleKeyChange = (operation) => { 9 | const { inputText, setInputText } = this.props 10 | setInputText(decorator.keyChange(inputText, operation)) 11 | } 12 | handleKeyUp = () => this.handleKeyChange("up") 13 | handleKeyDown = () => this.handleKeyChange("down") 14 | 15 | render() { 16 | const { disabled } = this.props 17 | return ( 18 | 19 |
20 |
28 |
29 |
37 |
38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/frontend/components/Score/UndoControl/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { EditorState } from "draft-js" 3 | 4 | import HasAddonsField from "../../commons/HasAddonsField" 5 | import Button from "../../commons/Button" 6 | 7 | export default class UndoControl extends Component { 8 | handleUndo = () => { 9 | const prevEditorState = EditorState.undo(this.props.editorState) 10 | this.props.handleChangeEditorState(prevEditorState) 11 | } 12 | handleRedo = () => { 13 | const nextEditorState = EditorState.redo(this.props.editorState) 14 | this.props.handleChangeEditorState(nextEditorState) 15 | } 16 | render() { 17 | const { editorState, disabled } = this.props 18 | return ( 19 | 20 |
21 |
27 |
28 |
34 |
35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/collation.rb: -------------------------------------------------------------------------------- 1 | # http://319ring.net/blog/archives/2645/ 2 | 3 | class Collation 4 | def initialize 5 | @connection = ActiveRecord::Base.connection 6 | end 7 | 8 | def change_all 9 | migration_base do |table, column| 10 | # 配列型に対応 11 | column_type = column.array ? "#{column.sql_type}[]" : column.sql_type 12 | @connection.execute "ALTER TABLE #{table} ALTER COLUMN #{column.name} TYPE #{column_type} COLLATE \"C\"" 13 | end 14 | end 15 | 16 | def rollback_all 17 | migration_base do |table, column| 18 | # 配列型に対応 19 | column_type = column.array ? "#{column.sql_type}[]" : column.sql_type 20 | @connection.execute "ALTER TABLE #{table} ALTER COLUMN #{column.name} TYPE #{column_type}" 21 | end 22 | end 23 | 24 | private 25 | def migration_base 26 | @connection.tables.each do |table| 27 | begin 28 | model = Module.const_get(table.classify) 29 | rescue 30 | next 31 | end 32 | # 1からマイグレーションすると、schema cacheのせいで定義が古いまま。 33 | # 削除したり、リネームしたカラムを扱おうとして落ちるので、リセットする。 34 | model.connection.schema_cache.clear! 35 | model.reset_column_information 36 | model.columns.select {|column| column.type == :string || column.type == :text }.each do |column| 37 | yield(table, column) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/frontend/components/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { Router } from "react-router-dom" 3 | import { createBrowserHistory } from "history" 4 | import ReactGA from "react-ga" 5 | 6 | import Routes from "./Routes" 7 | import { window } from "../utils/browser-dependencies" 8 | 9 | export default class Rechord extends Component { 10 | constructor() { 11 | super() 12 | const { uaId } = window.data 13 | const history = createBrowserHistory() 14 | history.pushPageView = () => {} 15 | 16 | if (uaId.length > 0) { 17 | ReactGA.initialize(uaId) 18 | ReactGA.pageview(window.location.pathname) 19 | // history.listen(location => { 20 | // ReactGA.set({ page: location.pathname }) 21 | // ReactGA.pageview(location.pathname) 22 | // }) 23 | 24 | // ページタイトルはロード後に設定されるため、 25 | // 各 Component の componentDidMount で個別に設定 26 | history.pushPageView = () => { 27 | ReactGA.set({ page: window.location.pathname }) 28 | ReactGA.pageview(window.location.pathname) 29 | } 30 | } 31 | this.state = { history } 32 | } 33 | render() { 34 | const { history } = this.state 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/frontend/components/Score/ScoreEditor/changeScrollPosition.js: -------------------------------------------------------------------------------- 1 | import { document } from "../../../utils/browser-dependencies" 2 | 3 | let currentScrollWidth 4 | let currentClientHeight 5 | let isRightEnd 6 | 7 | export const setCurrentScrollPosition = () => { 8 | const editor = document.getElementById("score-editor") 9 | const editorContent = document.getElementsByClassName("public-DraftEditor-content")[0] 10 | 11 | const { scrollWidth, clientWidth, scrollLeft } = editor 12 | const { clientHeight } = editorContent.children[0] 13 | 14 | currentScrollWidth = scrollWidth 15 | currentClientHeight = clientHeight 16 | isRightEnd = scrollWidth === clientWidth + scrollLeft 17 | } 18 | 19 | export const changeScrollPosition = () => { 20 | const editor = document.getElementById("score-editor") 21 | const editorContent = document.getElementsByClassName("public-DraftEditor-content")[0] 22 | 23 | const { scrollWidth, clientWidth } = editor 24 | const { clientHeight } = editorContent.children[0] 25 | 26 | if (isRightEnd && scrollWidth > clientWidth && scrollWidth > currentScrollWidth) { 27 | // 右端での文字入力時にエディタが overscroll したらエディタを右端にスクロール 28 | editor.scrollLeft = scrollWidth - clientWidth 29 | } else if (clientHeight !== currentClientHeight) { 30 | // 文字入力時に高さが変わったらエディタを左端にスクロール 31 | editor.scrollLeft = 0 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | class OnlyAjaxRequest 2 | def matches?(request) 3 | request.xhr? 4 | end 5 | end 6 | 7 | Rails.application.routes.draw do 8 | mount RailsAdmin::Engine => '/admin', as: 'rails_admin' 9 | 10 | devise_for :users, controllers: { omniauth_callbacks: "users/omniauth_callbacks" } 11 | devise_scope :user do 12 | get "/users/logout", to: "devise/sessions#destroy", as: :destroy_user_session 13 | end 14 | 15 | resource :status, only: [:show], constraints: OnlyAjaxRequest.new 16 | 17 | resources :scores, { 18 | only: [:index, :show, :edit, :update, :create, :destroy], 19 | param: :token, 20 | constraints: OnlyAjaxRequest.new 21 | } 22 | 23 | resources :users, { 24 | only: [:index, :show, :update, :destroy], 25 | param: :name, 26 | constraints: OnlyAjaxRequest.new 27 | } do 28 | post :valid_name 29 | put :update_icon 30 | delete :remove_icon 31 | put :read 32 | resources :scores, only: [:index], controller: "users/scores" 33 | end 34 | 35 | resources :favs, only: [:index, :create, :destroy], constraints: OnlyAjaxRequest.new 36 | 37 | scope format: true, constraints: { format: /jpg|png|gif/ } do 38 | get "/*anything", to: proc { [404, {}, [""]] } 39 | end 40 | 41 | get "not_supported" => "top#not_supported" 42 | get "*path" => "top#index" 43 | root "top#index" 44 | end 45 | -------------------------------------------------------------------------------- /app/controllers/top_controller.rb: -------------------------------------------------------------------------------- 1 | class TopController < ApplicationController 2 | before_action :set_title 3 | before_action :set_notifications 4 | before_action :check_browser_support, only: [:index] 5 | before_action :check_maintenance 6 | 7 | def index 8 | end 9 | 10 | def not_supported 11 | end 12 | 13 | def maintenance 14 | end 15 | 16 | private 17 | 18 | def set_title 19 | case request.fullpath 20 | when /\/users\/([^\/]*)(\/[^\/]*)*/ 21 | user = User.friendly.find_by(name: $1) 22 | @title = user&.screen_name 23 | when /\/([^\/]{11})(\/[^\/]*)*/ 24 | score = Score.friendly.find_by(token: $1) 25 | if score&.browsable?(current_user&.id) || score&.owner?(current_user&.id) 26 | @title = score&.title 27 | end 28 | end 29 | end 30 | 31 | def set_notifications 32 | @notifications = Notification.list_for_user(current_user&.id) 33 | end 34 | 35 | # IE11 非対応処理 36 | def check_browser_support 37 | ua = request.user_agent&.downcase 38 | if ua.present? && ((ua.include?('msie') && ua.exclude?('opera')) || ua.include?('trident/7')) 39 | redirect_to controller: :top, action: :not_supported 40 | end 41 | end 42 | 43 | def check_maintenance 44 | @maintenance_schedules = MaintenanceSchedule.active 45 | render :maintenance if @maintenance_schedules.present? 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/frontend/components/commons/Notification/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import ReleaseNotification from "./ReleaseNotification" 3 | import FavNotification from "./FavNotification" 4 | import DefaultNotification from "./DefaultNotification" 5 | import ModalCard from "../ModalCard" 6 | 7 | export default class Notification extends Component { 8 | render() { 9 | const { notifications, isActive, handleToggleNotification, handleClearNotification } = this.props 10 | const notificationTemplate = (notification) => { 11 | const params = { data: notification, key: notification.id, handleToggleNotification } 12 | switch (notification.template) { 13 | case "release": return 14 | case "fav": return 15 | default: return 16 | } 17 | } 18 | return ( 19 | 29 | {isActive && notifications.map(notification => notificationTemplate(notification))} 30 | 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/controllers/concerns/search_params.rb: -------------------------------------------------------------------------------- 1 | module SearchParams 2 | extend ActiveSupport::Concern 3 | 4 | def scores_list_params 5 | { words: words, sort: scores_list_sort_option, order: order, options: scores_list_options } 6 | end 7 | 8 | def users_list_params 9 | { words: words, sort: users_list_sort_option, order: order, options: users_list_options } 10 | end 11 | 12 | def users_favs_list_params 13 | users_list_params 14 | end 15 | 16 | def user_scores_list_params 17 | scores_list_params.merge( 18 | owner: params[:user_name] == current_user&.name 19 | ) 20 | end 21 | 22 | private 23 | 24 | def words 25 | params[:word]&.split(" ") 26 | end 27 | 28 | def order 29 | params[:sort]&.slice(/(asc|desc)$/) || "desc" 30 | end 31 | 32 | def sanitize_sort_option(options, default_option = "id") 33 | option = params[:sort]&.gsub(/_(asc|desc)$/, "") 34 | options.include?(option) ? option : default_option 35 | end 36 | 37 | def scores_list_sort_option 38 | sanitize_sort_option(%w(updated_at title views_count favs_count)) 39 | end 40 | 41 | def users_list_sort_option 42 | sanitize_sort_option(%w(updated_at screen_name scores_count)) 43 | end 44 | 45 | def scores_list_options 46 | { guest: params[:guest] == "true" } 47 | end 48 | 49 | def users_list_options 50 | { no_scores: params[:no_scores] == "true" } 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app/controllers/favs_controller.rb: -------------------------------------------------------------------------------- 1 | class FavsController < ApplicationController 2 | include SearchParams 3 | 4 | before_action :authenticate! 5 | 6 | def index 7 | scores = current_user.favs_list(users_favs_list_params) 8 | total_count = scores.count 9 | scores = scores.page(params[:page] || 1) 10 | 11 | render json: { 12 | result: scores.as_json(include: :user), 13 | total_count: total_count, 14 | current_page: scores.current_page, 15 | total_pages: scores.total_pages 16 | } 17 | end 18 | 19 | def create 20 | fav = Fav.new(fav_params) 21 | if fav.save 22 | if fav.score.user_id.present? && current_user&.id != fav.score.user_id 23 | Notification.create_or_update_by_fav(fav) 24 | end 25 | render json: fav 26 | else 27 | render json: fav.errors.full_messages, status: :unprocessable_entity 28 | end 29 | end 30 | 31 | def destroy 32 | fav = Fav.find(params[:id]) 33 | if fav.destroy 34 | head :ok 35 | else 36 | render json: fav.errors.full_messages, status: :unprocessable_entity 37 | end 38 | end 39 | 40 | private 41 | 42 | def fav_params 43 | params.require(:fav).permit(:user_id, :score_id) 44 | end 45 | 46 | def authenticate! 47 | unless user_signed_in? 48 | render json: "現在ログイン中のユーザは、この操作に対する権限がありません。", status: :unprocessable_entity 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/frontend/components/Routes/ShowScore/ScoreFooter/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { Link } from "react-router-dom" 3 | import * as path from "../../../../utils/path" 4 | import * as localStorageState from "../../../../utils/localStorageState" 5 | 6 | export default class ScoreFooter extends Component { 7 | handleClick = () => localStorageState.set(this.props.currentState, "editScore") 8 | render() { 9 | const { token, handleToggleDestroyModal } = this.props 10 | const editPath = path.score.edit(token) 11 | return ( 12 |
13 |
14 |

15 | 16 | 17 | 18 | 19 | Edit 20 | 21 |

22 |

23 | 29 |

30 |
31 |
32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/shared-buttons.sass: -------------------------------------------------------------------------------- 1 | .shared-url 2 | a 3 | vertical-align: middle 4 | margin-right: .8em 5 | .shared-buttons 6 | display: flex 7 | justify-content: center 8 | padding-top: .3em 9 | .shared-button 10 | margin: .8rem 11 | margin-top: 0 12 | &:first-child 13 | margin-left: 0 14 | &:last-child 15 | margin-right: 0 16 | > div 17 | outline: 0 18 | cursor: pointer 19 | &:hover, &:active 20 | opacity: .8 21 | .fa-stack-1x 22 | font-size: .95em 23 | .fa-google-plus 24 | font-size: .9em 25 | .fa-envelope 26 | font-size: .85em 27 | &.as-show 28 | font-size: .85em 29 | .shared-button 30 | margin: 0 .75em 1.2em 0 31 | &:first-child 32 | margin-left: 0 33 | &:last-child 34 | margin-right: 0 35 | &.as-footer 36 | .shared-button 37 | .fa-stack 38 | .fa 39 | &:first-child 40 | color: #fff !important 41 | opacity: .7 42 | &:last-child 43 | color: #532 !important 44 | 45 | @media screen and (max-width: $tablet - 1px) 46 | .shared-url 47 | display: flex 48 | font-size: 1.1em 49 | flex-direction: column 50 | a 51 | margin-right: 0 52 | margin-bottom: .65em 53 | button 54 | align-self: center 55 | font-size: .85em 56 | .shared-buttons 57 | &.as-show 58 | justify-content: center 59 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | # Load DSL and set up stages 2 | require 'capistrano/setup' 3 | 4 | # Include default deployment tasks 5 | require 'capistrano/deploy' 6 | 7 | # Load the SCM plugin appropriate to your project: 8 | # 9 | # require 'capistrano/scm/hg' 10 | # install_plugin Capistrano::SCM::Hg 11 | # or 12 | # require 'capistrano/scm/svn' 13 | # install_plugin Capistrano::SCM::Svn 14 | # or 15 | require 'capistrano/scm/git' 16 | install_plugin Capistrano::SCM::Git 17 | 18 | # Include tasks from other gems included in your Gemfile 19 | # 20 | # For documentation on these, see for example: 21 | # 22 | # https://github.com/capistrano/rvm 23 | # https://github.com/capistrano/rbenv 24 | # https://github.com/capistrano/chruby 25 | # https://github.com/capistrano/bundler 26 | # https://github.com/capistrano/rails 27 | # https://github.com/capistrano/passenger 28 | # 29 | # require 'capistrano/rvm' 30 | # require 'capistrano/rbenv' 31 | # require 'capistrano/chruby' 32 | # require 'capistrano/bundler' 33 | # require 'capistrano/rails/assets' 34 | # require 'capistrano/rails/migrations' 35 | # require 'capistrano/passenger' 36 | 37 | require 'capistrano/rails' 38 | require 'capistrano/rbenv' 39 | require 'capistrano/bundler' 40 | require 'capistrano/puma' 41 | require 'whenever/capistrano' 42 | install_plugin Capistrano::Puma 43 | 44 | # Load custom tasks from `lib/capistrano/tasks` if you have any defined 45 | Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } 46 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | # # If you are using webpack-dev-server then specify webpack-dev-server host 15 | # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? 16 | 17 | # # Specify URI for violation reports 18 | # # policy.report_uri "/csp-violation-report-endpoint" 19 | # end 20 | 21 | # If you are using UJS then enable automatic nonce generation 22 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 23 | 24 | # Set the nonce only to specific directives 25 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 26 | 27 | # Report CSP violations to a specified URI 28 | # For further information see the following documentation: 29 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 30 | # Rails.application.config.content_security_policy_report_only = true 31 | -------------------------------------------------------------------------------- /app/frontend/components/TitleControl/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import Field from "../commons/Field" 3 | import { validateTypes } from "./validateTypes" 4 | import { validator } from "../../validator" 5 | import FormWithValidate from "../../validator/FormWithValidate" 6 | 7 | export default class TitleControl extends Component { 8 | constructor() { 9 | super() 10 | this.state = { touch: false } 11 | } 12 | validate = (value) => ( 13 | validator({ 14 | key: "title", 15 | types: validateTypes, 16 | setState: this.props.handleSetState, 17 | errors: this.props.errors, 18 | value, 19 | }) 20 | ) 21 | handleTouch = () => { 22 | this.setState({ touch: true }) 23 | this.validate(this.props.title) 24 | } 25 | handleSetTitle = (e) => { 26 | const { touch } = this.state 27 | const { handleSetState } = this.props 28 | handleSetState({ title: e.target.value }) 29 | if (touch) this.validate(e.target.value) 30 | } 31 | render() { 32 | const { title, errors } = this.props 33 | return ( 34 | 35 | 36 | 44 | 45 | 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/frontend/components/CardsList/cardsListUtils.js: -------------------------------------------------------------------------------- 1 | export const sortOptions = (type) => { 2 | const scoresOptions = [ 3 | { value: "", label: "新着順" }, 4 | { value: "updated_at", label: "更新順" }, 5 | { value: "title_asc", label: "タイトル(昇順)" }, 6 | { value: "title_desc", label: "タイトル(降順)" }, 7 | { value: "views_count", label: "閲覧回数順" }, 8 | { value: "favs_count", label: "いいね数順" } 9 | ] 10 | const usersOptions = [ 11 | { value: "", label: "新着順" }, 12 | { value: "updated_at", label: "更新順" }, 13 | { value: "screen_name_asc", label: "名前(昇順)" }, 14 | { value: "screen_name_desc", label: "名前(降順)" }, 15 | { value: "scores_count", label: "スコア数順" } 16 | ] 17 | switch (type) { 18 | case "scores": return scoresOptions 19 | case "favs": return scoresOptions 20 | case "userScores": return scoresOptions 21 | case "users": return usersOptions 22 | default: return [] 23 | } 24 | } 25 | 26 | export const setDefault = (_query, type) => { 27 | const query = _query 28 | switch (type) { 29 | case "scores": 30 | if (!query.word) query.word = "" 31 | if (!query.guest) query.guest = "true" 32 | return query 33 | case "favs": 34 | if (!query.word) query.word = "" 35 | return query 36 | case "userScores": 37 | if (!query.word) query.word = "" 38 | return query 39 | case "users": 40 | if (!query.word) query.word = "" 41 | if (!query.no_scores) query.no_scores = "true" 42 | return query 43 | default: 44 | return query 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/frontend/components/Routes/ShowScore/ScoreHeader/Author.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { Link } from "react-router-dom" 3 | import * as path from "../../../../utils/path" 4 | import * as utils from "../../../../utils" 5 | 6 | export default class Author extends Component { 7 | render() { 8 | const { author, createdAt, updatedAt } = this.props 9 | const existAuthor = author && Object.keys(author).length > 0 10 | const authorPath = existAuthor && path.user.show(author.name) 11 | const iconBlock = ( 12 |
13 | guest user 14 |
15 | ) 16 | return ( 17 |
18 | {existAuthor ? ( 19 | {iconBlock} 20 | ) : ( 21 | iconBlock 22 | )} 23 |
24 |

25 | {existAuthor ? ( 26 | 27 | {author.screen_name} 28 | 29 | ) : ( 30 | Guest User 31 | )} 32 |

33 | 36 | {createdAt !== updatedAt && ( 37 | 40 | )} 41 |
42 |
43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/frontend/components/StatusControl/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | 3 | export default class StatusControl extends Component { 4 | handleChangeStatus = (e) => { 5 | this.props.handleSetState({ status: e.target.value }) 6 | } 7 | render() { 8 | const { status } = this.props 9 | const radioParams = [ 10 | { 11 | label: "Public", 12 | description:

保存されたスコアは誰でも閲覧可能になります。

, 13 | value: "published" 14 | }, { 15 | label: "Private", 16 | description: ( 17 |

18 | 保存されたスコアは非公開になります。
19 | あなた以外には見えず、検索にも表示されません。 20 |

21 | ), 22 | value: "closed" 23 | } 24 | ] 25 | const renderRadioComponent = (param) => ( 26 | 27 | 36 | 37 | 38 | ) 39 | const currentParam = radioParams.find(param => param.value === status) || {} 40 | return ( 41 |
42 |
43 | {radioParams.map(renderRadioComponent)} 44 |
45 |
46 | {currentParam.description} 47 |
48 |
49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/frontend/components/commons/FlashMessage.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react" 2 | import classNames from "classnames" 3 | 4 | export default class FlashMessage extends PureComponent { 5 | constructor() { 6 | super() 7 | this.state = { show: true } 8 | } 9 | componentWillReceiveProps() { 10 | this.setState({ show: true }) 11 | } 12 | handleDelete = () => this.setState({ show: false }) 13 | render() { 14 | const { show } = this.state 15 | const { flash } = this.props 16 | const [type, message] = flash 17 | 18 | const flashStyle = { display: (show ? "block" : "none") } 19 | const notificationClass = classNames("notification", { 20 | "is-success": (type === "success" || type === "notice"), 21 | "is-warning": type === "warning", 22 | "is-danger": (type === "error" || type === "alert") 23 | }) 24 | const icon = { 25 | success: "fa-check-circle", 26 | notice: "fa-check-circle", 27 | warning: "fa-exclamation-circle", 28 | error: "fa-exclamation-triangle", 29 | alert: "fa-exclamation-triangle" 30 | } 31 | const iconClass = classNames("fa", "fa-lg", icon[type]) 32 | return ( 33 |
34 |
35 |
41 |
42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/frontend/utils/localStorageState.js: -------------------------------------------------------------------------------- 1 | import { localStorage } from "./browser-dependencies" 2 | 3 | const localStorageKey = "rechordState" 4 | const commonKey = "rechord" 5 | 6 | export const get = (key) => { 7 | const state = localStorage.getItem(key || localStorageKey) 8 | if (!state) return false 9 | return JSON.parse(state || "{}") 10 | } 11 | 12 | export const set = (state, key) => { 13 | const { update, title, inputText, enabledClick, beat, bpm, capo, volume, loop, instrumentType, status } = state 14 | if (update) return false 15 | 16 | return localStorage.setItem(key || localStorageKey, JSON.stringify({ 17 | title, inputText, enabledClick, beat, bpm, capo, volume, loop, instrumentType, status 18 | })) 19 | } 20 | 21 | export const remove = (key) => localStorage.removeItem(key || localStorageKey) 22 | 23 | export const visit = () => { 24 | const state = JSON.parse(localStorage.getItem(commonKey) || "{}") 25 | const newState = Object.assign(state, { isVisited: true }) 26 | return localStorage.setItem(commonKey, JSON.stringify(newState)) 27 | } 28 | export const isVisited = () => { 29 | const state = JSON.parse(localStorage.getItem(commonKey) || "{}") 30 | return state.isVisited 31 | } 32 | 33 | export const setCurrentVersion = (version) => { 34 | const state = JSON.parse(localStorage.getItem(commonKey) || "{}") 35 | const newState = Object.assign(state, { currentVersion: version }) 36 | return localStorage.setItem(commonKey, JSON.stringify(newState)) 37 | } 38 | export const getCurrentVersion = () => { 39 | const state = JSON.parse(localStorage.getItem(commonKey) || "{}") 40 | return state.currentVersion 41 | } 42 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/navbar.sass: -------------------------------------------------------------------------------- 1 | .navbar 2 | .navbar-brand 3 | .title-logo 4 | margin-left: -1rem 5 | .title 6 | color: #fff 7 | padding-bottom: 3px 8 | span 9 | margin-left: .5rem 10 | span, img 11 | vertical-align: middle 12 | span 13 | &:first-child 14 | margin-right: 8px 15 | opacity: .85 16 | .navbar-menu 17 | .current-user 18 | span 19 | margin-right: .8em 20 | font-size: .9em 21 | img 22 | max-height: 32px 23 | .navbar-end 24 | .navbar-item:last-child 25 | margin-right: -1rem 26 | 27 | @media screen and (max-width: $mobile - 1) 28 | .navbar 29 | .navbar-brand 30 | .title-logo 31 | margin-left: 0 32 | 33 | @media screen and (max-width: $desktop - 1) 34 | .navbar 35 | .button.is-primary.login-button 36 | width: 100% 37 | background-color: $primary 38 | border-color: transparent 39 | color: #fff 40 | .navbar-menu 41 | &.is-active 42 | position: absolute 43 | z-index: 30 44 | width: 100% 45 | .navbar-end 46 | text-align: center 47 | margin-bottom: .5em 48 | .current-user 49 | display: flex 50 | flex-direction: row-reverse 51 | align-items: center 52 | justify-content: center 53 | span 54 | margin-left: .8em 55 | .navbar-item:last-child 56 | margin-right: 0 57 | 58 | @media screen and (min-width: $desktop) 59 | .navbar 60 | .container 61 | .navbar-brand 62 | margin-left: 0 63 | .navbar-menu 64 | margin-right: 0 65 | -------------------------------------------------------------------------------- /app/models/concerns/oauth_provider_formatter.rb: -------------------------------------------------------------------------------- 1 | class OauthProviderFormatter 2 | class << self 3 | def params_for_create_user(auth) 4 | case auth[:provider] 5 | when "twitter" 6 | { 7 | name: auth[:info][:nickname], 8 | screen_name: auth[:info][:name], 9 | profile: auth[:info][:description], 10 | icon: auth[:info][:image], 11 | site: auth[:info][:urls][:Website], 12 | twitter: auth[:info][:nickname] 13 | } 14 | when "facebook" 15 | { 16 | name: SecureRandom.urlsafe_base64(8), 17 | screen_name: auth[:info][:name], 18 | icon: auth[:info][:image], 19 | site: auth[:extra][:raw_info][:link], 20 | email: auth[:info][:email] 21 | } 22 | when "google_oauth2" 23 | { 24 | name: SecureRandom.urlsafe_base64(8), 25 | screen_name: auth[:info][:name], 26 | icon: auth[:info][:image], 27 | site: auth[:info][:urls]&.fetch(:google), 28 | email: auth[:info][:email] 29 | } 30 | when "tumblr" 31 | { 32 | name: auth[:info][:nickname], 33 | screen_name: auth[:info][:name], 34 | icon: auth[:info][:avatar] 35 | } 36 | when "github" 37 | { 38 | name: auth[:info][:nickname], 39 | screen_name: auth[:info][:nickname], 40 | icon: auth[:info][:image], 41 | site: auth[:info][:urls][:GitHub], 42 | email: auth[:info][:email] 43 | } 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /config/newrelic.yml: -------------------------------------------------------------------------------- 1 | # 2 | # This file configures the New Relic Agent. New Relic monitors Ruby, Java, 3 | # .NET, PHP, Python, Node, and Go applications with deep visibility and low 4 | # overhead. For more information, visit www.newrelic.com. 5 | # 6 | # Generated December 20, 2017 7 | # 8 | # This configuration file is custom generated for comorebi notes 9 | # 10 | # For full documentation of agent configuration options, please refer to 11 | # https://docs.newrelic.com/docs/agents/ruby-agent/installation-configuration/ruby-agent-configuration 12 | 13 | common: &default_settings 14 | # Required license key associated with your New Relic account. 15 | license_key: <%= ENV['NEWRELIC_LICENSE_KEY'] %> 16 | 17 | # Your application name. Renaming here affects where data displays in New 18 | # Relic. For more details, see https://docs.newrelic.com/docs/apm/new-relic-apm/maintenance/renaming-applications 19 | app_name: rechord 20 | 21 | # To disable the agent regardless of other settings, uncomment the following: 22 | # agent_enabled: false 23 | 24 | # Logging level for log/newrelic_agent.log 25 | log_level: info 26 | 27 | 28 | # Environment-specific settings are in this section. 29 | # RAILS_ENV or RACK_ENV (as appropriate) is used to determine the environment. 30 | # If your application has other named environments, configure them here. 31 | development: 32 | <<: *default_settings 33 | app_name: rechord (Development) 34 | 35 | test: 36 | <<: *default_settings 37 | # It doesn't make sense to report to New Relic from automated test runs. 38 | monitor_mode: false 39 | 40 | staging: 41 | <<: *default_settings 42 | app_name: rechord (Staging) 43 | 44 | production: 45 | <<: *default_settings 46 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/user-page.sass: -------------------------------------------------------------------------------- 1 | .show-user 2 | .card.user-page 3 | box-shadow: none 4 | margin-bottom: 2em 5 | &.edit 6 | .card-content 7 | padding: 1.5rem .5rem 0 8 | input, textarea 9 | font-size: .9em 10 | .card-image 11 | padding: 0 1.5rem 12 | .icon-buttons 13 | display: flex 14 | margin-top: 1.5em 15 | > * 16 | flex-grow: 1 17 | > *:not(:last-child) 18 | margin-right: .5em 19 | &:last-child 20 | margin-bottom: 1em 21 | .file 22 | display: block 23 | .file-cta 24 | border-radius: 2px 25 | width: 100% 26 | justify-content: center 27 | .dummy-icon 28 | background-color: #ccc 29 | position: absolute 30 | top: 0 31 | left: 0 32 | right: 0 33 | bottom: 0 34 | border-radius: 50% 35 | .content 36 | font-size: .9em 37 | .url 38 | word-break: break-all 39 | .icon 40 | margin-right: .2em 41 | &:not(:last-of-type) 42 | margin-bottom: .4em 43 | .profile 44 | line-height: 1.7 45 | margin-bottom: 2em 46 | p 47 | margin-bottom: 0 48 | .scores 49 | .score-card 50 | .box 51 | .score-title 52 | white-space: normal 53 | 54 | @media screen and (max-width: $tablet - 1px) 55 | .show-user 56 | .card.user-page 57 | .card-image 58 | padding: 0 59 | .icon-buttons 60 | .file, .button 61 | &.is-small 62 | font-size: .85em 63 | .card-content 64 | padding: 1.5em 0 65 | &.edit 66 | .card-content 67 | padding: 0 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rechord", 3 | "private": true, 4 | "dependencies": { 5 | "@babel/preset-env": "^7.21.5", 6 | "@babel/preset-react": "^7.18.6", 7 | "@rails/webpacker": "5.4.4", 8 | "axios": "^0.17.1", 9 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 10 | "babel-polyfill": "^6.26.0", 11 | "chord-translator": "^0.1.0", 12 | "classnames": "^2.2.5", 13 | "draft-js": "^0.10.4", 14 | "history": "^4.7.2", 15 | "moji": "^0.5.1", 16 | "postcss-cssnext": "^3.1.1", 17 | "prop-types": "^15.8.1", 18 | "qs": "^6.5.1", 19 | "rails_admin": "3.1.2", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "react-ga": "^2.3.5", 23 | "react-highlight-words": "^0.10.0", 24 | "react-router-dom": "^4.2.2", 25 | "react-share": "^1.17.0", 26 | "react-twitter-widgets": "^1.7.1", 27 | "tonal": "2.1.2", 28 | "tone": "0.11.12", 29 | "webpack": "^4.46.0", 30 | "webpack-cli": "^3.3.12" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.2.2", 34 | "babel-eslint": "^10.0.1", 35 | "babel-jest": "^24.0.0", 36 | "eslint": "^5.9.0", 37 | "eslint-config-airbnb": "^17.1.0", 38 | "eslint-plugin-import": "^2.14.0", 39 | "eslint-plugin-jsx-a11y": "^6.1.2", 40 | "eslint-plugin-react": "^7.11.1", 41 | "jest": "23.6.0", 42 | "webpack-dev-server": "^3" 43 | }, 44 | "scripts": { 45 | "lint": "eslint app/frontend --ext .js --ext .jsx", 46 | "test": "jest --no-cache ./spec/frontend" 47 | }, 48 | "jest": { 49 | "testPathIgnorePatterns": [ 50 | "/node_modules/", 51 | "/config/" 52 | ] 53 | }, 54 | "resolutions": { 55 | "rails_admin/@fortawesome/fontawesome-free": "^5.15.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/notification.sass: -------------------------------------------------------------------------------- 1 | .notification.content 2 | time 3 | font-size: .9em 4 | color: #888 5 | margin-bottom: .8em 6 | & + p 7 | margin-top: .2em 8 | h3 9 | margin-top: .3em 10 | margin-bottom: .4em 11 | ul 12 | margin-top: .8em 13 | a 14 | color: $link 15 | strong 16 | margin: 0 .2em 17 | &:first-child 18 | margin-left: 0 19 | &:last-child 20 | margin-right: 0 21 | .loading-wrapper 22 | min-height: 50px 23 | &:before 24 | content: none 25 | &:after 26 | top: calc(50% - .5em) 27 | 28 | .notification-icon 29 | .icon 30 | width: 1.5rem 31 | .fa-layers 32 | display: inline-block 33 | position: relative 34 | height: 1em 35 | left: -.2em 36 | text-align: center 37 | vertical-align: -.125em 38 | .fa 39 | position: absolute 40 | top: 0 41 | right: 0 42 | left: 0 43 | bottom: 0 44 | margin: auto 45 | .fa-layers-counter 46 | display: inline-block 47 | box-sizing: border-box 48 | max-width: 5em 49 | min-width: 1.5em 50 | height: 1.5em 51 | line-height: 1 52 | position: absolute 53 | top: -0.75em 54 | left: .65em 55 | font-weight: bold 56 | padding: .25em 57 | border-radius: 1em 58 | text-align: center 59 | background-color: $danger 60 | color: #fff 61 | overflow: hidden 62 | text-overflow: ellipsis 63 | -webkit-transform: scale(.8) 64 | transform: scale(.8) 65 | -webkit-transform-origin: top right 66 | transform-origin: top right 67 | &.is-only-mobile 68 | position: absolute 69 | top: 1px 70 | right: 3.25rem 71 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/user-card.sass: -------------------------------------------------------------------------------- 1 | .users 2 | .user-card 3 | display: block 4 | .box 5 | display: flex 6 | justify-content: space-between 7 | transition: all .1s ease-in-out 8 | background-color: #fff 9 | .media 10 | width: 100% 11 | .screen-name 12 | color: $link 13 | font-weight: bold 14 | margin-bottom: .4em 15 | p 16 | margin-bottom: 0 17 | small 18 | font-weight: normal 19 | font-size: .7em 20 | .screen-name, .user-attributes 21 | transition: all .1s ease-in-out 22 | .user-attributes 23 | line-height: 1.5 24 | font-size: .9em 25 | strong 26 | font-size: 1.2em 27 | margin-left: .25em 28 | p 29 | margin-bottom: .8em 30 | .profile 31 | margin-bottom: 1em 32 | p 33 | font-size: .85em 34 | margin-bottom: 0 35 | nav 36 | align-items: center 37 | .control 38 | font-size: .9rem 39 | margin-top: .2em 40 | &:not(:last-child) 41 | margin-right: 1.2rem 42 | .icon 43 | margin-top: .1em 44 | margin-right: .2em 45 | &:hover, &:active 46 | .box 47 | background-color: #fafafa 48 | &.closed 49 | background-color: #e0e0e0 50 | &:not(:last-child) 51 | margin-bottom: 1.5rem 52 | 53 | .box.no-user 54 | box-shadow: none 55 | background-color: #f4f4f4 56 | padding: 2.4em 57 | .media-content 58 | text-align: center 59 | color: #888 60 | font-weight: bold 61 | 62 | @media screen and (max-width: $tablet - 1px) 63 | .users 64 | .user-card 65 | .box 66 | .image 67 | width: 64px 68 | -------------------------------------------------------------------------------- /db/migrate/20180112050744_create_impressions_table.rb: -------------------------------------------------------------------------------- 1 | class CreateImpressionsTable < ActiveRecord::Migration[5.1] 2 | def self.up 3 | create_table :impressions, :force => true do |t| 4 | t.string :impressionable_type 5 | t.integer :impressionable_id 6 | t.integer :user_id 7 | t.string :controller_name 8 | t.string :action_name 9 | t.string :view_name 10 | t.string :request_hash 11 | t.string :ip_address 12 | t.string :session_hash 13 | t.text :message 14 | t.text :referrer 15 | t.text :params 16 | t.timestamps 17 | end 18 | add_index :impressions, [:impressionable_type, :message, :impressionable_id], :name => "impressionable_type_message_index", :unique => false, :length => {:message => 255 } 19 | add_index :impressions, [:impressionable_type, :impressionable_id, :request_hash], :name => "poly_request_index", :unique => false 20 | add_index :impressions, [:impressionable_type, :impressionable_id, :ip_address], :name => "poly_ip_index", :unique => false 21 | add_index :impressions, [:impressionable_type, :impressionable_id, :session_hash], :name => "poly_session_index", :unique => false 22 | add_index :impressions, [:controller_name,:action_name,:request_hash], :name => "controlleraction_request_index", :unique => false 23 | add_index :impressions, [:controller_name,:action_name,:ip_address], :name => "controlleraction_ip_index", :unique => false 24 | add_index :impressions, [:controller_name,:action_name,:session_hash], :name => "controlleraction_session_index", :unique => false 25 | add_index :impressions, [:impressionable_type, :impressionable_id, :params], :name => "poly_params_request_index", :unique => false, :length => {:params => 255 } 26 | add_index :impressions, :user_id 27 | end 28 | 29 | def self.down 30 | drop_table :impressions 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 2017, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true 8 | } 9 | }, 10 | "extends": "airbnb", 11 | "plugins": [ 12 | "react" 13 | ], 14 | "rules": { 15 | "arrow-parens": 0, 16 | "camelcase": 0, 17 | "class-methods-use-this": 0, 18 | "comma-dangle": 0, 19 | "default-case": 0, 20 | "func-names": 0, 21 | "indent": [2, 2, { "SwitchCase": 1 }], 22 | "jsx-a11y/anchor-has-content": 0, 23 | "jsx-a11y/anchor-is-valid": 0, 24 | "jsx-a11y/click-events-have-key-events": 0, 25 | "jsx-a11y/label-has-associated-control": 0, 26 | "jsx-a11y/label-has-for": 0, 27 | "jsx-a11y/no-static-element-interactions": 0, 28 | "jsx-quotes": 0, 29 | "key-spacing": 0, 30 | "lines-between-class-members": 0, 31 | "max-len": ["error", { "code": 140, "comments": 140 }], 32 | "no-class-assign": 0, 33 | "no-else-return": 0, 34 | "no-fallthrough": 0, 35 | "no-irregular-whitespace": ["error", { "skipRegExps": true }], 36 | "no-multi-spaces": 0, 37 | "no-var": 1, 38 | "object-curly-newline": 0, 39 | "prefer-destructuring": ["error", { "object": true, "array": false }], 40 | "quotes": 0, 41 | "react/destructuring-assignment": 0, 42 | "react/jsx-filename-extension": 0, 43 | "react/jsx-one-expression-per-line": 0, 44 | "react/jsx-props-no-multi-spaces": 0, 45 | "react/no-array-index-key": 0, 46 | "react/no-multi-comp": 0, 47 | "react/prefer-stateless-function": 0, 48 | "react/prop-types": 0, 49 | "react/self-closing-comp": 0, 50 | "semi": 0, 51 | "space-before-function-paren": 0 52 | }, 53 | "globals": { 54 | "window": true, 55 | "$": true, 56 | "before": true, 57 | "describe": true, 58 | "expect": true, 59 | "it": true 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/ruby:2.6.5-node 7 | environment: 8 | BUNDLE_JOBS: 3 9 | BUNDLE_RETRY: 3 10 | BUNDLE_PATH: vendor/bundle 11 | PGHOST: 127.0.0.1 12 | PGUSER: rechord 13 | RAILS_ENV: test 14 | - image: circleci/postgres:10.1-alpine 15 | environment: 16 | POSTGRES_USER: rechord 17 | POSTGRES_DB: rechord_test 18 | POSTGRES_PASSWORD: "" 19 | 20 | working_directory: ~/rechord 21 | 22 | steps: 23 | - checkout 24 | 25 | # bundle 26 | - run: 27 | name: Which bundler? 28 | command: bundle -v 29 | - restore_cache: 30 | keys: 31 | - rails-demo-bundle-v3-{{ checksum "Gemfile.lock" }} 32 | - run: 33 | name: Bundle Install 34 | command: bundle check || bundle install 35 | - save_cache: 36 | key: rails-demo-bundle-v3-{{ checksum "Gemfile.lock" }} 37 | paths: 38 | - vendor/bundle 39 | 40 | # yarn 41 | - restore_cache: 42 | keys: 43 | - rails-demo-yarn-v2-{{ checksum "yarn.lock" }} 44 | - rails-demo-yarn-v2- 45 | - run: 46 | name: yarn install 47 | command: curl -o- -L https://yarnpkg.com/install.sh; yarn install 48 | - save_cache: 49 | key: rails-demo-yarn-v2-{{ checksum "yarn.lock" }} 50 | paths: 51 | - ~/.cache/yarn 52 | 53 | # database 54 | - run: 55 | name: Database setup 56 | command: | 57 | bin/rails db:create 58 | bin/rails db:structure:load 59 | 60 | # test 61 | - run: 62 | name: check lint 63 | command: yarn lint 64 | - run: 65 | name: jest 66 | command: yarn test 67 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /app/frontend/components/commons/LoginModal/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import classNames from "classnames" 3 | 4 | import * as path from "../../../utils/path" 5 | 6 | export default class LoginModal extends Component { 7 | render() { 8 | const { active, hideModal } = this.props 9 | const modalClass = classNames("modal", "login", { "is-active": active }) 10 | const renderButton = ({ service, label }) => ( 11 | 16 | 17 | 18 | 19 | {label} 20 | 21 | ) 22 | const services = [ 23 | { service: "twitter", label: "Twitter" }, 24 | { service: "facebook", label: "Facebook" }, 25 | { service: "google", label: "Google" }, 26 | { service: "tumblr", label: "tumblr" }, 27 | { service: "github", label: "GitHub" } 28 | ] 29 | return ( 30 |
31 |
32 |
33 |
34 |

35 | Login / Register 36 |

37 | {services.map(renderButton)} 38 |

39 | ユーザ登録をすることで、投稿したスコアを管理したり、他人のスコアにいいねを付けたりすることができます。 40 |

41 |

42 | あなたが入力した外部サービスのパスワード等は、管理人を含め第三者に見られることはありません。 43 |

44 |
45 |
46 |
48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 12 | # terminating a worker in development environments. 13 | # 14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 15 | 16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 17 | # 18 | port ENV.fetch("PORT") { 3000 } 19 | 20 | # Specifies the `environment` that Puma will run in. 21 | # 22 | environment ENV.fetch("RAILS_ENV") { "development" } 23 | 24 | # Specifies the `pidfile` that Puma will use. 25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 26 | 27 | # Specifies the number of `workers` to boot in clustered mode. 28 | # Workers are forked web server processes. If using threads and workers together 29 | # the concurrency of the application would be max `threads` * `workers`. 30 | # Workers do not work on JRuby or Windows (both of which do not support 31 | # processes). 32 | # 33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 34 | 35 | # Use the `preload_app!` method when specifying a `workers` number. 36 | # This directive tells Puma to first boot the application and load code 37 | # before forking the application. This takes advantage of Copy On Write 38 | # process behavior so workers use less memory. 39 | # 40 | # preload_app! 41 | 42 | # Allow puma to be restarted by `rails restart` command. 43 | plugin :tmp_restart 44 | -------------------------------------------------------------------------------- /app/frontend/styles/rechord.sass: -------------------------------------------------------------------------------- 1 | @import "partials/variables.sass" 2 | @import "partials/chord-colors.sass" 3 | 4 | @import "partials/loading.sass" 5 | @import "partials/fav.sass" 6 | @import "partials/flash-message.sass" 7 | @import "partials/navbar.sass" 8 | @import "partials/tabbar.sass" 9 | @import "partials/footer.sass" 10 | @import "partials/login.sass" 11 | @import "partials/notification.sass" 12 | @import "partials/shared-buttons.sass" 13 | @import "partials/about.sass" 14 | @import "partials/terms.sass" 15 | @import "partials/changelog.sass" 16 | @import "partials/search.sass" 17 | @import "partials/score-editor.sass" 18 | @import "partials/score-controls.sass" 19 | @import "partials/score-header.sass" 20 | @import "partials/score-footer.sass" 21 | @import "partials/score-card.sass" 22 | @import "partials/user-card.sass" 23 | @import "partials/user-page.sass" 24 | 25 | .user-icon 26 | border-radius: 50% 27 | &.has-border 28 | border: 8px solid #fff 29 | box-shadow: 0 0 0 2px #eaeaea 30 | 31 | .wide-button 32 | display: block 33 | width: 100% 34 | 35 | .animate-button 36 | transition: opacity .3s ease-in-out 37 | 38 | .message-body 39 | border-radius: 0 40 | &.bordered 41 | border: 1px solid #a0ca30 42 | border-radius: 4px 43 | &.has-icon 44 | display: flex 45 | align-items: center 46 | .icon 47 | opacity: .5 48 | margin-right: .6rem 49 | 50 | .twitter-tl 51 | margin: 3em auto 0 52 | width: max-content 53 | 54 | .can-click 55 | cursor: pointer 56 | pointer-events: all !important 57 | 58 | section.section 59 | padding-top: 2rem 60 | margin-bottom: 1em 61 | 62 | @media screen and (min-width: $tablet) 63 | .section.root-section 64 | display: flex 65 | flex-direction: column 66 | 67 | @media screen and (max-width: $mobile - 1) 68 | .section.root-section 69 | .container 70 | width: 100% 71 | 72 | @media screen and (max-width: $tablet - 1) 73 | .twitter-tl 74 | width: auto 75 | -------------------------------------------------------------------------------- /config/initializers/exception_notification.rb: -------------------------------------------------------------------------------- 1 | require 'exception_notification/rails' 2 | 3 | ExceptionNotification.configure do |config| 4 | # Ignore additional exception types. 5 | # ActiveRecord::RecordNotFound, Mongoid::Errors::DocumentNotFound, AbstractController::ActionNotFound and ActionController::RoutingError are already added. 6 | # config.ignored_exceptions += %w{ActionView::TemplateError CustomError} 7 | 8 | # Adds a condition to decide when an exception must be ignored or not. 9 | # The ignore_if method can be invoked multiple times to add extra conditions. 10 | # config.ignore_if do |exception, options| 11 | # not Rails.env.production? 12 | # end 13 | config.ignore_if do |exception, options| 14 | not Rails.env.production? 15 | end 16 | 17 | # Notifiers ================================================================= 18 | 19 | config.add_notifier :slack, webhook_url: ENV['WEBHOOK_URL'], channel: '#rechord_notification' 20 | 21 | # Email notifier sends notifications by email. 22 | # config.add_notifier :email, { 23 | # :email_prefix => "[ERROR] ", 24 | # :sender_address => %{"Notifier" }, 25 | # :exception_recipients => %w{exceptions@example.com} 26 | # } 27 | 28 | # Campfire notifier sends notifications to your Campfire room. Requires 'tinder' gem. 29 | # config.add_notifier :campfire, { 30 | # :subdomain => 'my_subdomain', 31 | # :token => 'my_token', 32 | # :room_name => 'my_room' 33 | # } 34 | 35 | # HipChat notifier sends notifications to your HipChat room. Requires 'hipchat' gem. 36 | # config.add_notifier :hipchat, { 37 | # :api_token => 'my_token', 38 | # :room_name => 'my_room' 39 | # } 40 | 41 | # Webhook notifier sends notifications over HTTP protocol. Requires 'httparty' gem. 42 | # config.add_notifier :webhook, { 43 | # :url => 'http://example.com:5555/hubot/path', 44 | # :http_method => :post 45 | # } 46 | 47 | end 48 | -------------------------------------------------------------------------------- /app/frontend/components/commons/Notification/FavNotification.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { Link } from "react-router-dom" 3 | import * as api from "../../../api" 4 | import * as path from "../../../utils/path" 5 | import * as utils from "../../../utils" 6 | 7 | export default class FavNotification extends Component { 8 | constructor() { 9 | super() 10 | this.state = { score: null, loading: true } 11 | } 12 | componentDidMount() { 13 | const deletedScore = { title: "削除済のスコア" } 14 | api.showScore( 15 | { token: this.props.data.title }, 16 | ({ data: { score } }) => { 17 | this.setState({ score: score.status === "deleted" ? deletedScore : score, loading: false }) 18 | }, 19 | () => this.setState({ score: deletedScore, loading: false }) 20 | ) 21 | } 22 | render() { 23 | const { score, loading } = this.state 24 | const { data: { updated_at: updatedAt }, handleToggleNotification } = this.props 25 | const isActive = loading || (score && score.favs_count > 0) 26 | return isActive && ( 27 |
28 | 29 | {loading ? ( 30 |
31 | ) : ( 32 |

33 | 34 | 35 | 36 | 37 | {score.token ? ( 38 | {score.title} 39 | ) : ( 40 | {score.title} 41 | )} 42 | 43 | が通算 44 | {score.favs_count}回 45 | いいねをされました。 46 |

47 | )} 48 |
49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/frontend/components/commons/ModalCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import classNames from "classnames" 3 | 4 | export default class ModalCard extends Component { 5 | render() { 6 | const { 7 | isActive, title, icon, children, 8 | hasButtons, yesButtonOnly, buttonColor, yesButtonLabel, noButtonLabel, handleClick, hideModal 9 | } = this.props 10 | const modalClass = classNames("modal", { "is-active": isActive }) 11 | const iconClass = classNames("fa", `fa-${icon}`) 12 | const buttonClass = classNames("button", `is-${buttonColor || "success"}`) 13 | 14 | return ( 15 |
16 |
17 |
18 | {title && ( 19 |
20 |

21 | 22 | 23 | 24 | {title} 25 |

26 |
28 | )} 29 |
30 | {children} 31 |
32 | {hasButtons && ( 33 |
34 | 37 | {!yesButtonOnly && ( 38 | 41 | )} 42 |
43 | )} 44 |
45 |
47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /app/frontend/styles/partials/score-header.sass: -------------------------------------------------------------------------------- 1 | .score-header 2 | margin-bottom: .4rem 3 | .private-description 4 | font-size: .9em 5 | opacity: .5 6 | .icon 7 | margin-right: .5em 8 | .title 9 | font-size: 1.8em 10 | margin-bottom: 1.2rem 11 | .button 12 | display: block 13 | margin-bottom: 1rem 14 | 15 | .author 16 | display: flex 17 | align-items: center 18 | margin-bottom: .75em 19 | line-height: 1.3 20 | a 21 | color: #444 22 | &:hover 23 | color: #666 24 | figure 25 | margin-right: .8rem 26 | .created-at, .updated-at 27 | font-size: .8em 28 | color: #888 29 | .updated-at 30 | margin-left: .6em 31 | &:before 32 | content: "(" 33 | &:after 34 | content: ")" 35 | 36 | .others 37 | display: flex 38 | align-items: center 39 | margin: .5em 0 40 | color: #666 41 | height: 2.25rem 42 | .separator 43 | color: #eee 44 | margin: 0 1.2em 45 | &:last-of-type 46 | margin-right: .4em 47 | .icon 48 | color: #666 49 | margin-right: .4em 50 | .button 51 | margin-bottom: 0 52 | margin-left: .4em 53 | color: #666 54 | &:active 55 | border-color: $link 56 | &:disabled 57 | &, .icon 58 | color: #666 !important 59 | 60 | @media screen and (max-width: $tablet - 1px) 61 | .score-header 62 | flex-direction: column 63 | .shared-buttons 64 | margin: 1.5em 0 0 65 | .shared-button 66 | font-size: 1.5em 67 | margin-bottom: 1em 68 | .fa-stack 69 | .fa:last-child 70 | left: .1em 71 | &.as-show 72 | .shared-button 73 | margin-top: .5em 74 | &:not(:last-child) 75 | margin-right: 1.2em 76 | .author 77 | figure.image 78 | width: 52px 79 | height: 52px 80 | p 81 | margin-bottom: .2em 82 | .created-at, .updated-at 83 | display: block 84 | .updated-at 85 | margin-left: 0 86 | &:before 87 | content: "" 88 | &:after 89 | content: "" 90 | --------------------------------------------------------------------------------