├── .controlplane ├── Dockerfile ├── controlplane.yml ├── entrypoint.sh ├── readme.md ├── release_script.sh ├── shakacode-team.md └── templates │ ├── app.yml │ ├── daily-task.yml │ ├── maintenance.yml │ ├── org.yml │ ├── postgres.yml │ ├── rails.yml │ └── redis.yml ├── .coveralls.yml ├── .dockerignore ├── .eslintignore ├── .eslintrc ├── .github ├── actions │ ├── build-docker-image │ │ └── action.yml │ ├── delete-control-plane-app │ │ ├── action.yml │ │ └── delete-app.sh │ └── setup-environment │ │ └── action.yml ├── readme.md └── workflows │ ├── delete-review-app.yml │ ├── deploy-to-control-plane-review-app.yml │ ├── deploy-to-control-plane-staging.yml │ ├── help-command.yml │ ├── js_test.yml │ ├── lint_test.yml │ ├── nightly-remove-stale-review-apps.yml │ ├── promote-staging-to-production.yml │ ├── review-app-help.yml │ └── rspec_test.yml ├── .gitignore ├── .nvmrc ├── .overcommit.yml ├── .prettierignore ├── .prettierrc.yml ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── .scss-lint.yml ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── Procfile ├── Procfile.dev ├── Procfile.dev-static ├── README.md ├── Rakefile ├── app.json ├── app ├── assets │ └── config │ │ └── manifest.js ├── channels │ ├── application_cable │ │ ├── channel.rb │ │ └── connection.rb │ └── comments_channel.rb ├── controllers │ ├── application_controller.rb │ ├── comments_controller.rb │ ├── concerns │ │ └── .keep │ └── pages_controller.rb ├── helpers │ ├── application_helper.rb │ ├── comments_helper.rb │ └── pages_helper.rb ├── jobs │ ├── application_job.rb │ └── comment_relay_job.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── application_record.rb │ ├── comment.rb │ ├── concerns │ │ └── .keep │ └── git_commit_sha.rb └── views │ ├── comments │ ├── _comment.json.jbuilder │ ├── _form.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── index.json.jbuilder │ ├── new.html.erb │ ├── show.html.erb │ ├── show.json.jbuilder │ ├── stimulus.html.erb │ └── turbo │ │ ├── _comment_list.html.erb │ │ ├── _error_notice.html.erb │ │ ├── _horizontal_form.html.erb │ │ ├── _inline_form.html.erb │ │ ├── _stacked_form.html.erb │ │ └── _tabs.html.erb │ ├── layouts │ ├── application.html.erb │ └── stimulus_layout.html.erb │ └── pages │ ├── _header.html.erb │ ├── index.html.erb │ ├── no_router.html.erb │ ├── rescript.html.erb │ └── simple.html.erb ├── babel.config.js ├── bin ├── bundle ├── dev ├── dev-static ├── rails ├── rake ├── rspec ├── rubocop ├── setup ├── shakapacker ├── shakapacker-dev-server ├── thrust ├── update └── yarn ├── bsconfig.json ├── client ├── README.md └── app │ ├── assets │ ├── fonts │ │ ├── OpenSans-Bold.eot │ │ ├── OpenSans-Bold.svg │ │ ├── OpenSans-Bold.ttf │ │ ├── OpenSans-Bold.woff │ │ ├── OpenSans-Light.eot │ │ ├── OpenSans-Light.svg │ │ ├── OpenSans-Light.ttf │ │ └── OpenSans-Light.woff │ ├── images │ │ ├── railsonmaui.png │ │ └── twitter_64.png │ └── styles │ │ ├── app-variables.scss │ │ ├── application.css │ │ └── stimulus.scss │ ├── bundles │ └── comments │ │ ├── actions │ │ └── commentsActionCreators.js │ │ ├── components │ │ ├── CommentBox │ │ │ ├── CommentBox.jsx │ │ │ ├── CommentBox.module.scss │ │ │ ├── CommentForm │ │ │ │ └── CommentForm.jsx │ │ │ └── CommentList │ │ │ │ ├── Comment │ │ │ │ ├── Comment.jsx │ │ │ │ └── Comment.spec.jsx │ │ │ │ ├── CommentList.jsx │ │ │ │ └── CommentList.spec.jsx │ │ ├── CommentScreen │ │ │ └── CommentScreen.jsx │ │ ├── Footer │ │ │ └── Footer.jsx │ │ ├── NavigationBar │ │ │ ├── CommentsCount.jsx │ │ │ └── NavigationBar.jsx │ │ ├── SimpleCommentScreen │ │ │ ├── SimpleCommentScreen.jsx │ │ │ └── SimpleCommentScreen.module.scss │ │ ├── TestReactRouter │ │ │ └── TestReactRouter.jsx │ │ └── TestReactRouterRedirect │ │ │ └── TestReactRouterRedirect.jsx │ │ ├── constants │ │ ├── commentsConstants.js │ │ └── paths.js │ │ ├── containers │ │ ├── NavigationBarContainer.jsx │ │ ├── NonRouterCommentsContainer.jsx │ │ └── RouterCommentsContainer.jsx │ │ ├── layout │ │ └── Layout.jsx │ │ ├── reducers │ │ ├── commentsReducer.js │ │ ├── index.js │ │ └── railsContextReducer.js │ │ ├── rescript │ │ ├── Actions │ │ │ └── Actions.res │ │ ├── CommentForm │ │ │ ├── CommentForm.res │ │ │ └── forms │ │ │ │ ├── HorizontalForm.res │ │ │ │ ├── InlineForm.res │ │ │ │ └── StackedForm.res │ │ ├── CommentList │ │ │ ├── AlertError │ │ │ │ └── AlertError.res │ │ │ ├── Comment │ │ │ │ └── Comment.res │ │ │ └── CommentList.res │ │ ├── Header │ │ │ └── Header.res │ │ ├── ReScriptShow.module.scss │ │ ├── ReScriptShow.res │ │ └── bindings │ │ │ ├── ActionCable.res │ │ │ ├── Axios.res │ │ │ ├── Marked.res │ │ │ └── ReactTransitionGroup.res │ │ ├── routes │ │ └── routes.jsx │ │ ├── startup │ │ ├── App.jsx │ │ ├── ClientRouterApp.jsx │ │ ├── ClientRouterAppExpress.jsx │ │ ├── NavigationBarApp.jsx │ │ ├── ServerRouterApp.jsx │ │ └── serverRegistration.jsx │ │ └── store │ │ ├── commentsStore.js │ │ └── routerCommentsStore.js │ ├── controllers │ ├── comments_controller.js │ └── index.js │ ├── libs │ ├── components │ │ └── BaseComponent.jsx │ ├── i18n │ │ └── selectLanguage.jsx │ ├── middlewares │ │ └── loggerMiddleware.js │ ├── requestsManager.js │ └── testHelper.js │ └── packs │ ├── client-bundle.js │ ├── server-bundle.js │ └── stimulus-bundle.js ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── cors.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── per_form_csrf_tokens.rb │ ├── permissions_policy.rb │ ├── react_on_rails.rb │ ├── request_forgery_protection.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── locales │ ├── de.yml │ ├── en.yml │ ├── ja.yml │ ├── zh-CN.yml │ └── zh-TW.yml ├── puma.rb ├── rails_best_practices.yml ├── routes.rb ├── secrets.yml ├── shakapacker.yml ├── storage.yml └── webpack │ ├── DEBUGGING.md │ ├── alias.js │ ├── client.js │ ├── clientWebpackConfig.js │ ├── commonWebpackConfig.js │ ├── development.js │ ├── production.js │ ├── server.js │ ├── serverWebpackConfig.js │ ├── test.js │ ├── webpack.config.js │ └── webpackConfig.js ├── db ├── .rubocop.yml ├── migrate │ ├── 20140823052830_create_comments.rb │ ├── 20151015160035_set_default_for_author_and_text.rb │ ├── 20151015160334_change_author_and_text_to_not_null.rb │ ├── 20220103221124_create_active_storage_tables.active_storage.rb │ ├── 20230119143747_add_service_name_to_active_storage_blobs.active_storage.rb │ ├── 20230119143748_create_active_storage_variant_records.active_storage.rb │ └── 20230119162216_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb ├── schema.rb └── seeds.rb ├── docs ├── bourbon-integration.md ├── codeship-ci.md ├── contributors.md ├── heroku.md ├── integration-test-notes.md ├── jbuilder.md ├── jquery-with-react-on-rails.md ├── linters.md ├── testing-deployment.md └── tests-and-ci.md ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ ├── ci.rake │ ├── daily.rake │ ├── linters.rake │ └── rails_best_practices.rake ├── log └── .keep ├── package.json ├── postcss.config.js ├── public ├── 400.html ├── 404.html ├── 406-unsupported-browser.html ├── 422.html ├── 500.html ├── favicon.ico ├── icon.png ├── icon.svg └── robots.txt ├── scripts ├── lint └── start-production-server ├── spec ├── factories.rb ├── rails_helper.rb ├── requests │ └── server_render_check_spec.rb ├── rescript │ └── rescript_spec.rb ├── spec_helper.rb ├── stimulus │ └── turbo_spec.rb ├── support │ ├── driver_registration.rb │ └── phantomjs-disable-animations.js └── system │ ├── add_new_comment_spec.rb │ ├── destroy_comment_spec.rb │ ├── edit_comment_spec.rb │ ├── pages_spec.rb │ ├── react_router_demo_spec.rb │ └── shared │ └── contexts.rb ├── tailwind.config.js ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep └── yarn.lock /.controlplane/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # Runs before the main command 3 | # This script is unique to this demo project as it ensures the database and Redis are ready 4 | # before running the rails server or other services. 5 | # You can ignore this sort of "wait" if using external services, like AWS RDS or AWS Aurora. 6 | 7 | wait_for_service() 8 | { 9 | until curl -I -sS $1 2>&1 | grep -q "Empty reply from server"; do 10 | echo " -- $1 is unavailable, sleeping..." 11 | sleep 1 12 | done 13 | echo " -- $1 is available" 14 | } 15 | 16 | echo " -- Starting entrypoint.sh" 17 | 18 | echo " -- Waiting for services" 19 | 20 | # Strip out the host and the port for curl and to keep full resource URL secret 21 | wait_for_service $(echo $DATABASE_URL | sed -e 's|^.*@||' -e 's|/.*$||') 22 | wait_for_service $(echo $REDIS_URL | sed -e 's|redis://||' -e 's|/.*$||') 23 | 24 | echo " -- Finishing entrypoint.sh, executing '$@'" 25 | 26 | # Run the main command 27 | exec "$@" 28 | -------------------------------------------------------------------------------- /.controlplane/release_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | 4 | log() { 5 | echo "[`date +%Y-%m-%d:%H:%M:%S`]: $1" 6 | } 7 | 8 | error_exit() { 9 | log "$1" 1>&2 10 | exit 1 11 | } 12 | 13 | log 'Running release_script.sh per controlplane.yml' 14 | 15 | if [ -x ./bin/rails ]; then 16 | log 'Run DB migrations' 17 | ./bin/rails db:prepare || error_exit "Failed to run DB migrations" 18 | else 19 | error_exit "./bin/rails does not exist or is not executable" 20 | fi 21 | 22 | log 'Completed release_script.sh per controlplane.yml' 23 | -------------------------------------------------------------------------------- /.controlplane/shakacode-team.md: -------------------------------------------------------------------------------- 1 | # Internal Notes to the Shakacode Team 2 | 3 | ## Links 4 | 5 | - [Control Plane Org for Staging and Review Apps](https://console.cpln.io/console/org/shakacode-open-source-examples-staging/-info) 6 | - [Control Plane Org for Deployed App](https://console.cpln.io/console/org/shakacode-open-source-examples/-info) 7 | -------------------------------------------------------------------------------- /.controlplane/templates/app.yml: -------------------------------------------------------------------------------- 1 | # Template setup of the GVC, roughly corresponding to a Heroku app 2 | kind: gvc 3 | name: {{APP_NAME}} 4 | spec: 5 | # For using templates for test apps, put ENV values here, stored in git repo. 6 | # Production apps will have values configured manually after app creation. 7 | env: 8 | - name: DATABASE_URL 9 | # Password does not matter because host postgres.{{APP_NAME}}.cpln.local can only be accessed 10 | # locally within CPLN GVC, and postgres running on a CPLN workload is something only for a 11 | # test app that lacks persistence. 12 | value: 'postgres://the_user:the_password@postgres.{{APP_NAME}}.cpln.local:5432/{{APP_NAME}}' 13 | - name: RAILS_ENV 14 | value: production 15 | - name: NODE_ENV 16 | value: production 17 | - name: RAILS_SERVE_STATIC_FILES 18 | value: 'true' 19 | - name: REDIS_URL 20 | # No password for GVC local Redis. See comment above for postgres. 21 | value: 'redis://redis.{{APP_NAME}}.cpln.local:6379' 22 | # Part of standard configuration 23 | staticPlacement: 24 | locationLinks: 25 | - {{APP_LOCATION_LINK}} 26 | 27 | --- 28 | # Identity is needed to access secrets 29 | kind: identity 30 | name: {{APP_IDENTITY}} 31 | -------------------------------------------------------------------------------- /.controlplane/templates/daily-task.yml: -------------------------------------------------------------------------------- 1 | kind: workload 2 | name: daily-task 3 | spec: 4 | # https://docs.controlplane.com/reference/workload#cron-configuration 5 | type: cron 6 | job: 7 | activeDeadlineSeconds: 3600 8 | concurrencyPolicy: Forbid 9 | historyLimit: 5 10 | restartPolicy: Never 11 | # daily. See cron docs 12 | schedule: 0 0 * * * 13 | containers: 14 | - name: daily-task 15 | cpu: 50m 16 | memory: 256Mi 17 | args: 18 | - bundle 19 | - exec 20 | - rake 21 | - daily 22 | inheritEnv: true 23 | image: {{APP_IMAGE_LINK}} 24 | defaultOptions: 25 | autoscaling: 26 | minScale: 1 27 | maxScale: 1 28 | capacityAI: false 29 | firewallConfig: 30 | external: 31 | outboundAllowCIDR: 32 | - 0.0.0.0/0 33 | # Identity is used for binding workload to secrets 34 | identityLink: {{APP_IDENTITY_LINK}} 35 | -------------------------------------------------------------------------------- /.controlplane/templates/maintenance.yml: -------------------------------------------------------------------------------- 1 | kind: workload 2 | name: maintenance 3 | spec: 4 | type: standard 5 | containers: 6 | - name: maintenance 7 | env: 8 | - name: PORT 9 | value: "3000" 10 | - name: PAGE_URL 11 | value: "" 12 | image: "shakacode/maintenance-mode" 13 | ports: 14 | - number: 3000 15 | protocol: http 16 | defaultOptions: 17 | autoscaling: 18 | minScale: 1 19 | maxScale: 1 20 | capacityAI: false 21 | timeoutSeconds: 60 22 | firewallConfig: 23 | external: 24 | inboundAllowCIDR: 25 | - 0.0.0.0/0 26 | -------------------------------------------------------------------------------- /.controlplane/templates/org.yml: -------------------------------------------------------------------------------- 1 | # Org level secrets are used to store sensitive information that is 2 | # shared across multiple apps in the same organization. This is 3 | # useful for storing things like API keys, database credentials, and 4 | # other sensitive information that is shared across multiple apps 5 | # in the same organization. 6 | 7 | # This is how you apply this once (not during CI) 8 | # cpl apply-template secrets -a qa-react-webpack-rails-tutorial --org shakacode-open-source-examples-staging 9 | 10 | kind: secret 11 | name: {{APP_SECRETS}} 12 | type: dictionary 13 | data: 14 | SOME_ENV: "123456" 15 | 16 | --- 17 | 18 | # Policy is needed to allow identities to access secrets 19 | kind: policy 20 | name: {{APP_SECRETS_POLICY}} 21 | targetKind: secret 22 | targetLinks: 23 | - //secret/{{APP_SECRETS}} 24 | -------------------------------------------------------------------------------- /.controlplane/templates/rails.yml: -------------------------------------------------------------------------------- 1 | # Template setup of Rails server workload, roughly corresponding to Heroku dyno 2 | # type within Procfile. 3 | kind: workload 4 | name: rails 5 | spec: 6 | type: standard 7 | containers: 8 | - name: rails 9 | # 300m is a good starting place for a test app. You can experiment with CPU configuration 10 | # once your app is running. 11 | cpu: 300m 12 | env: 13 | - name: LOG_LEVEL 14 | value: debug 15 | # Inherit other ENV values from GVC 16 | inheritEnv: true 17 | image: {{APP_IMAGE_LINK}} 18 | # 512 corresponds to a standard 1x dyno type 19 | memory: 512Mi 20 | ports: 21 | - number: 3000 22 | protocol: http 23 | defaultOptions: 24 | # Start out like this for "test apps" 25 | autoscaling: 26 | # Max of 1 effectively disables autoscaling, so a like a Heroku dyno count of 1 27 | maxScale: 1 28 | capacityAI: false 29 | firewallConfig: 30 | external: 31 | # Default to allow public access to Rails server 32 | inboundAllowCIDR: 33 | - 0.0.0.0/0 34 | # Could configure outbound for more security 35 | outboundAllowCIDR: 36 | - 0.0.0.0/0 37 | # Identity is used for binding workload to secrets 38 | identityLink: {{APP_IDENTITY_LINK}} 39 | -------------------------------------------------------------------------------- /.controlplane/templates/redis.yml: -------------------------------------------------------------------------------- 1 | kind: workload 2 | name: redis 3 | spec: 4 | type: standard 5 | containers: 6 | - name: redis 7 | image: 'redis:6.2-alpine' 8 | ports: 9 | - number: 6379 10 | protocol: tcp 11 | defaultOptions: 12 | autoscaling: 13 | maxScale: 1 14 | capacityAI: false 15 | # This firewall configuration corresponds to using no password for Redis in the gvc.yml template. 16 | firewallConfig: 17 | internal: 18 | inboundAllowType: same-gvc 19 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: codeship 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # From .gitignore 2 | 3 | # Ignore bundler config. 4 | /.bundle 5 | 6 | # Ignore the default SQLite database. 7 | /db/*.sqlite3 8 | /db/*.sqlite3-journal 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/*.log 12 | /tmp 13 | /public/assets 14 | .env 15 | node_modules 16 | npm-debug.log* 17 | /coverage 18 | dump.rdb 19 | .DS_Store 20 | 21 | # Ignore bundle dependencies 22 | vendor/bundle 23 | 24 | # Ignore GitHub Actions and workflows 25 | .github/ 26 | 27 | # RVM gemset 28 | .ruby-gemset 29 | 30 | # Generated js bundles 31 | /public/packs 32 | /public/packs-test 33 | 34 | # Rubymine/IntelliJ 35 | .idea 36 | 37 | # Redis generated file 38 | dump.rdb 39 | 40 | # Ignore i18n-js 41 | client/app/libs/i18n/translations.js 42 | client/app/libs/i18n/default.js 43 | 44 | /yarn-error.log 45 | yarn-debug.log* 46 | .yarn-integrity 47 | 48 | ################################################### 49 | # Specific to .dockerignore 50 | .git/ 51 | spec/ 52 | scripts/ 53 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tmp/ 3 | public/ 4 | client/app/libs/i18n/translations.js 5 | client/app/libs/i18n/default.js 6 | postcss.config.js 7 | client/app/bundles/comments/rescript/ 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | extends: 2 | - eslint-config-shakacode 3 | - plugin:react/recommended 4 | - plugin:prettier/recommended 5 | - plugin:jsx-a11y/recommended 6 | - prettier 7 | 8 | plugins: 9 | - react 10 | - jsx-a11y 11 | - jest 12 | - prettier 13 | 14 | globals: 15 | __DEBUG_SERVER_ERRORS__: true 16 | __SERVER_ERRORS__: true 17 | 18 | env: 19 | browser: true 20 | node: true 21 | jest: true 22 | 23 | rules: 24 | no-shadow: 0 25 | no-console: 0 26 | function-paren-newline: 0 27 | object-curly-newline: 0 28 | no-restricted-syntax: ["error", "SequenceExpression"] 29 | # https://stackoverflow.com/a/59268871/5241481 30 | import/extensions: 0 31 | 32 | # https://github.com/benmosher/eslint-plugin-import/issues/340 33 | import/no-extraneous-dependencies: 0 34 | 35 | prettier/prettier: "error" 36 | 37 | # currently deprecated https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-onchange.md 38 | jsx-a11y/no-onchange: 0 39 | 40 | settings: 41 | import/core-modules: 42 | - react-redux 43 | import/resolver: 44 | node: 45 | extensions: [".js"] 46 | webpack: 47 | config: 'config/webpack/development.js' 48 | -------------------------------------------------------------------------------- /.github/actions/build-docker-image/action.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker Image 2 | description: 'Builds a Docker image for the application' 3 | 4 | inputs: 5 | app_name: 6 | description: 'Name of the application' 7 | required: true 8 | org: 9 | description: 'Organization name' 10 | required: true 11 | commit: 12 | description: 'Commit SHA to tag the image with' 13 | required: true 14 | PR_NUMBER: 15 | description: 'PR number' 16 | required: false 17 | 18 | runs: 19 | using: "composite" 20 | steps: 21 | - name: Build Docker Image 22 | id: build 23 | shell: bash 24 | run: | 25 | PR_INFO="" 26 | if [ -n "${PR_NUMBER}" ]; then 27 | PR_INFO=" for PR #${PR_NUMBER}" 28 | fi 29 | 30 | echo "🏗️ Building Docker image${PR_INFO} (commit ${{ inputs.commit }})..." 31 | 32 | if cpflow build-image -a "${{ inputs.app_name }}" --commit="${{ inputs.commit }}" --org="${{ inputs.org }}"; then 33 | image_tag="${{ inputs.org }}/${{ inputs.app_name }}:${{ inputs.commit }}" 34 | echo "image_tag=${image_tag}" >> $GITHUB_OUTPUT 35 | echo "✅ Docker image build successful${PR_INFO} (commit ${{ inputs.commit }})" 36 | else 37 | echo "❌ Docker image build failed${PR_INFO} (commit ${{ inputs.commit }})" 38 | exit 1 39 | fi 40 | -------------------------------------------------------------------------------- /.github/actions/delete-control-plane-app/action.yml: -------------------------------------------------------------------------------- 1 | name: Delete Control Plane App 2 | description: 'Deletes a Control Plane application and all its resources' 3 | 4 | inputs: 5 | app_name: 6 | description: 'Name of the application to delete' 7 | required: true 8 | cpln_org: 9 | description: 'Organization name' 10 | required: true 11 | 12 | runs: 13 | using: "composite" 14 | steps: 15 | - name: Delete Application 16 | shell: bash 17 | run: ${{ github.action_path }}/delete-app.sh 18 | env: 19 | APP_NAME: ${{ inputs.app_name }} 20 | CPLN_ORG: ${{ inputs.cpln_org }} 21 | -------------------------------------------------------------------------------- /.github/actions/delete-control-plane-app/delete-app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to delete a Control Plane application 4 | # Required environment variables: 5 | # - APP_NAME: Name of the application to delete 6 | # - CPLN_ORG: Organization name 7 | 8 | set -e 9 | 10 | # Validate required environment variables 11 | : "${APP_NAME:?APP_NAME environment variable is required}" 12 | : "${CPLN_ORG:?CPLN_ORG environment variable is required}" 13 | 14 | # Safety check: prevent deletion of production or staging apps 15 | if echo "$APP_NAME" | grep -iqE '(production|staging)'; then 16 | echo "❌ ERROR: Cannot delete apps containing 'production' or 'staging' in their name" >&2 17 | echo "🛑 This is a safety measure to prevent accidental deletion of production or staging environments" >&2 18 | echo " App name: $APP_NAME" >&2 19 | exit 1 20 | fi 21 | 22 | # Check if app exists before attempting to delete 23 | echo "🔍 Checking if application exists: $APP_NAME" 24 | if ! cpflow exists -a "$APP_NAME" --org "$CPLN_ORG"; then 25 | echo "⚠️ Application does not exist: $APP_NAME" 26 | exit 0 27 | fi 28 | 29 | # Delete the application 30 | echo "🗑️ Deleting application: $APP_NAME" 31 | if ! cpflow delete -a "$APP_NAME" --org "$CPLN_ORG" --yes; then 32 | echo "❌ Failed to delete application: $APP_NAME" >&2 33 | exit 1 34 | fi 35 | 36 | echo "✅ Successfully deleted application: $APP_NAME" 37 | -------------------------------------------------------------------------------- /.github/actions/setup-environment/action.yml: -------------------------------------------------------------------------------- 1 | # Control Plane GitHub Action 2 | 3 | name: 'Setup Environment' 4 | description: 'Sets up Ruby, installs Control Plane CLI, cpflow gem, and sets up the default profile' 5 | 6 | inputs: 7 | token: 8 | description: 'Control Plane token' 9 | required: true 10 | org: 11 | description: 'Control Plane organization' 12 | required: true 13 | 14 | runs: 15 | using: 'composite' 16 | steps: 17 | - name: Set up Ruby 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: '3.3.4' 21 | 22 | - name: Install Control Plane CLI and cpflow gem 23 | shell: bash 24 | run: | 25 | sudo npm install -g @controlplane/cli@3.3.1 26 | cpln --version 27 | gem install cpflow -v 4.1.1 28 | cpflow --version 29 | 30 | - name: Setup Control Plane Profile 31 | shell: bash 32 | run: | 33 | TOKEN="${{ inputs.token }}" 34 | ORG="${{ inputs.org }}" 35 | 36 | if [ -z "$TOKEN" ]; then 37 | echo " Error: Control Plane token not provided" 38 | exit 1 39 | fi 40 | 41 | if [ -z "$ORG" ]; then 42 | echo " Error: Control Plane organization not provided" 43 | exit 1 44 | fi 45 | 46 | echo "Setting up Control Plane profile..." 47 | echo "Organization: $ORG" 48 | cpln profile update default --org "$ORG" --token "$TOKEN" 49 | 50 | echo "Setting up Docker login for Control Plane registry..." 51 | cpln image docker-login --org "$ORG" 52 | -------------------------------------------------------------------------------- /.github/workflows/js_test.yml: -------------------------------------------------------------------------------- 1 | name: "JS CI" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node: [22.x] 17 | ruby: [3.3.4] 18 | 19 | env: 20 | RAILS_ENV: test 21 | NODE_ENV: test 22 | USE_COVERALLS: true 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v3 27 | 28 | - uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | bundler-cache: true 32 | 33 | - name: Use Node.js ${{ matrix.node }} 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: ${{ matrix.node }} 37 | cache: yarn 38 | 39 | - name: Install dependencies 40 | run: | 41 | bundle install 42 | yarn install --frozen-lockfile --non-interactive --prefer-offline 43 | 44 | - name: Build i18n libraries 45 | run: bundle exec rake react_on_rails:locale 46 | 47 | - name: Run js tests 48 | run: bundle exec rake ci:js 49 | -------------------------------------------------------------------------------- /.github/workflows/lint_test.yml: -------------------------------------------------------------------------------- 1 | name: "Lint CI" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node: [22.x] 17 | ruby: [3.3.4] 18 | 19 | env: 20 | RAILS_ENV: test 21 | NODE_ENV: test 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v3 26 | 27 | - uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ${{ matrix.ruby }} 30 | bundler-cache: true 31 | 32 | - name: Use Node.js ${{ matrix.node }} 33 | uses: actions/setup-node@v3 34 | with: 35 | node-version: ${{ matrix.node }} 36 | cache: yarn 37 | 38 | - name: Install dependencies 39 | run: | 40 | bundle install 41 | yarn install --frozen-lockfile --non-interactive --prefer-offline 42 | 43 | - name: Build i18n libraries 44 | run: bundle exec rake react_on_rails:locale 45 | 46 | - name: Run lint 47 | run: bundle exec rake lint 48 | -------------------------------------------------------------------------------- /.github/workflows/nightly-remove-stale-review-apps.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Remove Stale Review Apps and Images 2 | 3 | on: 4 | # Allows you to run this workflow manually from the Actions tab 5 | workflow_dispatch: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | remove-stale-review-apps: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Environment 18 | uses: ./.github/actions/setup-environment 19 | with: 20 | token: ${{ secrets.CPLN_TOKEN_STAGING }} 21 | org: ${{ vars.CPLN_ORG_STAGING }} 22 | 23 | - name: Delete Stale Review Apps 24 | run: cpflow cleanup-stale-apps -a "qa-react-webpack-rails-tutorial" --yes 25 | -------------------------------------------------------------------------------- /.github/workflows/promote-staging-to-production.yml: -------------------------------------------------------------------------------- 1 | name: Promote Staging to Production 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | confirm_promotion: 7 | description: 'Type "promote" to confirm promotion of staging to production' 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | promote-to-production: 13 | runs-on: ubuntu-latest 14 | if: github.event.inputs.confirm_promotion == 'promote' 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Environment 21 | uses: ./.github/actions/setup-environment 22 | with: 23 | token: ${{ secrets.CPLN_TOKEN_PRODUCTION }} 24 | org: ${{ vars.CPLN_ORG_PRODUCTION }} 25 | 26 | - name: Copy Image from Staging 27 | run: cpflow copy-image-from-upstream -a ${{ vars.PRODUCTION_APP_NAME }} -t ${{ secrets.CPLN_TOKEN_STAGING }} 28 | 29 | - name: Deploy Image to Production 30 | run: cpflow deploy-image -a ${{ vars.PRODUCTION_APP_NAME }} --run-release-phase --org ${{ vars.CPLN_ORG_PRODUCTION }} 31 | 32 | - name: Create GitHub Release 33 | if: success() 34 | env: 35 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | run: | 37 | # Get the current date in YYYY-MM-DD format 38 | RELEASE_DATE=$(date '+%Y-%m-%d') 39 | TIMESTAMP=$(date '+%H%M') 40 | 41 | # Create a release tag 42 | RELEASE_TAG="production-${RELEASE_DATE}-${TIMESTAMP}" 43 | 44 | # Create GitHub release 45 | gh release create "${RELEASE_TAG}" \ 46 | --title "Production Release ${RELEASE_DATE} ${TIMESTAMP}" \ 47 | --notes "🚀 Production deployment on ${RELEASE_DATE} at ${TIMESTAMP}" 48 | -------------------------------------------------------------------------------- /.github/workflows/review-app-help.yml: -------------------------------------------------------------------------------- 1 | name: Show Quick Help on PR Creation 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | 7 | permissions: 8 | issues: write 9 | pull-requests: write 10 | 11 | jobs: 12 | show-quick-help: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Show Quick Reference 16 | uses: actions/github-script@v7 17 | with: 18 | script: | 19 | try { 20 | console.log('Creating quick reference message...'); 21 | const helpMessage = [ 22 | '# 🚀 Quick Review App Commands', 23 | '', 24 | 'Welcome! Here are the commands you can use in this PR:', 25 | '', 26 | '### `/deploy-review-app`', 27 | 'Deploy your PR branch for testing', 28 | '', 29 | '### `/delete-review-app`', 30 | 'Remove the review app when done', 31 | '', 32 | '### `/help`', 33 | 'Show detailed instructions, environment setup, and configuration options.', 34 | '', 35 | '---' 36 | ].join('\n'); 37 | 38 | await github.rest.issues.createComment({ 39 | owner: context.repo.owner, 40 | repo: context.repo.repo, 41 | issue_number: context.issue.number, 42 | body: helpMessage 43 | }); 44 | 45 | console.log('Quick reference posted successfully'); 46 | } catch (error) { 47 | console.error('Error posting quick reference:', error); 48 | core.setFailed(`Failed to post quick reference: ${error.message}`); 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/rspec_test.yml: -------------------------------------------------------------------------------- 1 | name: "Rspec CI" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node: [22.x] 17 | ruby: [3.3.4] 18 | 19 | services: 20 | postgres: 21 | image: postgres:11-alpine 22 | ports: 23 | - "5432:5432" 24 | env: 25 | POSTGRES_DB: rails_test 26 | POSTGRES_USER: rails 27 | POSTGRES_PASSWORD: password 28 | 29 | env: 30 | RAILS_ENV: test 31 | NODE_ENV: test 32 | DATABASE_URL: "postgres://rails:password@localhost:5432/rails_test" 33 | DRIVER: selenium_chrome 34 | CHROME_BIN: /usr/bin/google-chrome 35 | USE_COVERALLS: true 36 | 37 | steps: 38 | - name: Install Chrome 39 | uses: browser-actions/setup-chrome@latest 40 | 41 | - name: Check Chrome version 42 | run: chrome --version 43 | 44 | - name: Check Chrome version 45 | run: google-chrome --version 46 | 47 | - name: Set Display environment variable 48 | run: "export DISPLAY=:99" 49 | 50 | - name: Checkout code 51 | uses: actions/checkout@v3 52 | 53 | - name: Use Ruby ${{ matrix.ruby }} 54 | uses: ruby/setup-ruby@v1 55 | with: 56 | ruby-version: ${{ matrix.ruby }} 57 | bundler-cache: true 58 | 59 | - name: Use Node.js ${{ matrix.node }} 60 | uses: actions/setup-node@v3 61 | with: 62 | node-version: ${{ matrix.node }} 63 | cache: yarn 64 | 65 | - name: Install dependencies 66 | run: | 67 | bundle install 68 | yarn install --frozen-lockfile --non-interactive --prefer-offline 69 | 70 | - name: Set up database schema 71 | run: bin/rails db:schema:load 72 | 73 | - name: Build i18n libraries 74 | run: bundle exec rake react_on_rails:locale 75 | 76 | - name: Build Rescript components 77 | run: yarn res:build 78 | 79 | - name: Build shakapacker chunks 80 | run: NODE_ENV=development bundle exec bin/shakapacker 81 | 82 | - name: Run rspec with xvfb 83 | uses: coactions/setup-xvfb@v1 84 | with: 85 | run: bundle exec rake ci:rspec 86 | working-directory: ./ #optional 87 | options: ":99 -ac -screen scn 1600x1200x16" 88 | -------------------------------------------------------------------------------- /.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 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/*.log 16 | /tmp 17 | /public/assets 18 | .env 19 | node_modules 20 | npm-debug.log* 21 | /coverage 22 | dump.rdb 23 | .DS_Store 24 | 25 | # Ignore bundle dependencies 26 | vendor/bundle 27 | 28 | # RVM gemset 29 | .ruby-gemset 30 | 31 | # Generated js bundles 32 | /public/packs 33 | /public/packs-test 34 | 35 | # Rubymine/IntelliJ 36 | .idea 37 | 38 | spec/examples.txt 39 | 40 | # Redis generated file 41 | dump.rdb 42 | 43 | # Ignore i18n-js 44 | client/app/libs/i18n/translations.js 45 | client/app/libs/i18n/default.js 46 | 47 | /yarn-error.log 48 | yarn-debug.log* 49 | .yarn-integrity 50 | 51 | lib/bs 52 | /lib/ocaml 53 | 54 | client/app/bundles/comments/rescript/**/*.bs.js 55 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.12.0 2 | -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | # Use this file to configure the Overcommit hooks you wish to use. This will 2 | # extend the default configuration defined in: 3 | # https://github.com/sds/overcommit/blob/master/config/default.yml 4 | # 5 | # At the topmost level of this YAML file is a key representing type of hook 6 | # being run (e.g. pre-commit, commit-msg, etc.). Within each type you can 7 | # customize each hook, such as whether to only run it on certain files (via 8 | # `include`), whether to only display output if it fails (via `quiet`), etc. 9 | # 10 | # For a complete list of hooks, see: 11 | # https://github.com/sds/overcommit/tree/master/lib/overcommit/hook 12 | # 13 | # For a complete list of options that you can use to customize hooks, see: 14 | # https://github.com/sds/overcommit#configuration 15 | # 16 | # Uncomment the following lines to make the configuration take effect. 17 | 18 | #PreCommit: 19 | # RuboCop: 20 | # enabled: true 21 | # on_warn: fail # Treat all warnings as failures 22 | # 23 | # TrailingWhitespace: 24 | # enabled: true 25 | # exclude: 26 | # - '**/db/structure.sql' # Ignore trailing whitespace in generated files 27 | # 28 | #PostCheckout: 29 | # ALL: # Special hook name that customizes all hooks of this type 30 | # quiet: true # Change all post-checkout hooks to only display output on failure 31 | # 32 | # IndexTags: 33 | # enabled: true # Generate a tags file with `ctags` each time HEAD changes 34 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package.json 3 | tmp/ 4 | public/ 5 | coverage/ 6 | spec/support/ 7 | client/app/libs/i18n/translations.js 8 | client/app/libs/i18n/default.js 9 | vendor/bundle 10 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | printWidth: 110 2 | tabWidth: 2 3 | useTabs: false 4 | semi: true 5 | singleQuote: true 6 | trailingComma: all 7 | bracketSpacing: true 8 | 9 | overrides: 10 | - files: "*.@(css|scss)" 11 | options: 12 | parser: css 13 | singleQuote: false 14 | printWidth: 120 15 | - files: "*.@(json)" 16 | options: 17 | parser: json 18 | printWidth: 100 19 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --format documentation 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # This is the configuration used to check the rubocop source code. 2 | # Check out: https://github.com/bbatsov/rubocop 3 | 4 | require: 5 | - rubocop-performance 6 | - rubocop-rspec 7 | - rubocop-rails 8 | 9 | AllCops: 10 | NewCops: enable 11 | DisplayCopNames: true 12 | TargetRubyVersion: 2.7 13 | Include: 14 | - '**/Rakefile' 15 | - '**/config.ru' 16 | - 'Gemfile' 17 | - '**/*.rb' 18 | - '**/*.rake' 19 | Exclude: 20 | - 'vendor/**/*' 21 | - 'spec/fixtures/**/*' 22 | - 'node_modules/**/*' 23 | - 'db/**/*' 24 | - 'db/schema.rb' 25 | - 'db/seeds.rb' 26 | - 'bin/**/*' 27 | - 'Procfile.*' 28 | 29 | Layout/LineLength: 30 | Max: 120 31 | 32 | Style/StringLiterals: 33 | EnforcedStyle: double_quotes 34 | 35 | Style/Documentation: 36 | Enabled: false 37 | 38 | Style/HashEachMethods: 39 | Enabled: true 40 | 41 | Style/HashTransformKeys: 42 | Enabled: true 43 | 44 | Style/HashTransformValues: 45 | Enabled: true 46 | 47 | Lint/AssignmentInCondition: 48 | Exclude: 49 | - 'bin/spring' 50 | 51 | Lint/SuppressedException: 52 | Exclude: 53 | - 'bin/rails' 54 | - 'bin/rake' 55 | 56 | Metrics/AbcSize: 57 | Max: 28 58 | 59 | Metrics/CyclomaticComplexity: 60 | Max: 7 61 | 62 | Metrics/PerceivedComplexity: 63 | Max: 10 64 | 65 | Metrics/ClassLength: 66 | Max: 150 67 | 68 | Metrics/BlockLength: 69 | Exclude: 70 | - 'config/environments/development.rb' 71 | - 'config/environments/production.rb' 72 | - 'lib/tasks/linters.rake' 73 | - 'spec/rails_helper.rb' 74 | - 'spec/system/add_new_comment_spec.rb' 75 | - 'spec/system/react_router_demo_spec.rb' 76 | - 'lib/tasks/ci.rake' 77 | 78 | Metrics/ParameterLists: 79 | Max: 5 80 | CountKeywordArgs: false 81 | 82 | Metrics/MethodLength: 83 | Max: 41 84 | 85 | Metrics/ModuleLength: 86 | Max: 180 87 | 88 | Naming/RescuedExceptionsVariableName: 89 | Enabled: false 90 | 91 | RSpec/DescribeClass: 92 | Enabled: false 93 | 94 | RSpec/ExampleLength: 95 | Enabled: false 96 | 97 | RSpec/MessageSpies: 98 | Enabled: false 99 | 100 | RSpec/NestedGroups: 101 | Max: 4 102 | 103 | RSpec/MessageChain: 104 | Enabled: false 105 | 106 | RSpec/MultipleExpectations: 107 | Enabled: false 108 | 109 | RSpec/MultipleMemoizedHelpers: 110 | Max: 12 111 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.4 2 | -------------------------------------------------------------------------------- /.scss-lint.yml: -------------------------------------------------------------------------------- 1 | # See http://sass-guidelin.es/#zeros 2 | 3 | scss_files: 4 | - 'client/app/**/*.scss' 5 | 6 | exclude: 'client/node_modules/**' 7 | 8 | linters: 9 | ColorKeyword: 10 | enabled: false 11 | ColorVariable: 12 | enabled: false 13 | HexLength: 14 | enabled: true 15 | style: long 16 | HexNotation: 17 | enabled: true 18 | style: uppercase 19 | IdSelector: 20 | enabled: true 21 | LeadingZero: 22 | enabled: true 23 | style: include_zero 24 | SelectorFormat: 25 | enabled: false 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 2.7.1 5 | 6 | services: 7 | - docker 8 | - postgresql 9 | 10 | notifications: 11 | slack: shakacode:YvfXbuFMcFAHt6ZjABIs0KET 12 | 13 | cache: 14 | bundler: true 15 | directories: 16 | - node_modules # NPM packages 17 | yarn: true 18 | 19 | gemfile: Gemfile 20 | 21 | env: 22 | global: 23 | - RAILS_ENV=test 24 | - NODE_ENV=test 25 | - DRIVER=selenium_chrome 26 | - CHROME_BIN=/usr/bin/google-chrome 27 | - USE_COVERALLS=TRUE 28 | 29 | before_install: 30 | - sudo apt-get update 31 | - sudo apt-get install -y xvfb libappindicator1 fonts-liberation 32 | - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 33 | - sudo dpkg -i google-chrome*.deb 34 | - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen scn 1600x1200x16" 35 | 36 | install: 37 | - travis_retry gem install bundler -v '<2' # Ruby 2.2 and Rails 3.2 & 4.2 depend on bundler 1.x. 38 | - travis_retry nvm install 12 39 | - node -v 40 | - travis_retry npm i -g yarn 41 | - travis_retry bundle install 42 | - travis_retry yarn 43 | - bundle 44 | - yarn 45 | - google-chrome --version 46 | - rake db:setup 47 | - bundle exec rake react_on_rails:locale 48 | 49 | before_script: 50 | - "export DISPLAY=:99" 51 | 52 | script: 53 | - bundle exec rake db:schema:load 54 | - bundle exec rake 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | See: [merged pull requests](https://github.com/shakacode/react-webpack-rails-tutorial/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Amerged). 5 | 6 | ## 2025-01-22 7 | Improvements to control-plane-flow implementation. 8 | 9 | 10 | 11 | 12 | ## [2.1.0] - 2016-03-06 13 | ### Updated 14 | - Changed properties for comments to have a top level name property comments. 15 | 16 | ## 2.0.0 - 2016-03-05 17 | Baseline for Changelog 18 | 19 | [2.1.0]: https://github.com/shakacode/react_on_rails/compare/2.1.0...2.0.0 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Justin Gordon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma -C config/puma.rb 2 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | # Procfile for development using HMR 2 | # You can run these commands in separate shells 3 | rescript: yarn res:dev 4 | redis: redis-server 5 | rails: bundle exec rails s -p 3000 6 | wp-client: HMR=true RAILS_ENV=development NODE_ENV=development bin/shakapacker-dev-server 7 | wp-server: bundle exec rake react_on_rails:locale && HMR=true SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch 8 | -------------------------------------------------------------------------------- /Procfile.dev-static: -------------------------------------------------------------------------------- 1 | # You can run these commands in separate shells 2 | web: rails s -p 3000 3 | redis: redis-server 4 | 5 | # Next line runs a watch process with webpack to compile the changed files. 6 | # When making frequent changes to client side assets, you will prefer building webpack assets 7 | # upon saving rather than when you refresh your browser page. 8 | # Note, if using React on Rails localization you will need to run 9 | # `bundle exec rake react_on_rails:locale` before you run bin/shakapacker 10 | webpack: sh -c 'bundle exec rake react_on_rails:locale && rm -rf public/packs/* || true && bin/shakapacker -w' 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require File.expand_path("config/application", __dir__) 7 | 8 | Rails.application.load_tasks 9 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "NODE_MODULES_CACHE": "false", 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-empty,no-lone-blocks 2 | { 3 | } 4 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Channel < ActionCable::Channel::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Connection < ActionCable::Connection::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/channels/comments_channel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CommentsChannel < ApplicationCable::Channel 4 | def subscribed 5 | stream_from "comments" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | # Prevent CSRF attacks by raising an exception. 5 | # For APIs, you may want to use :null_session instead. 6 | protect_from_forgery with: :exception, 7 | if: proc { request.headers["X-Auth"] != "tutorial_secret" } 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PagesController < ApplicationController 4 | include ReactOnRails::Controller 5 | before_action :set_comments 6 | 7 | def index 8 | # NOTE: The below notes apply if you want to set the value of the props in the controller, as 9 | # compared to the view. However, it's more convenient to use Jbuilder from the view. See 10 | # app/views/pages/index.html.erb:20 11 | # 12 | # <%= react_component('App', props: render(template: "/comments/index.json.jbuilder"), 13 | # prerender: true) %> 14 | # 15 | # 16 | # NOTE: this could be an alternate syntax if you wanted to pass comments as a variable to a partial 17 | # @comments_json_sting = render_to_string(partial: "/comments/comments.json.jbuilder", 18 | # locals: { comments: Comment.all }, format: :json) 19 | # NOTE: @comments is used by the render_to_string call 20 | # @comments_json_string = render_to_string("/comments/index.json.jbuilder") 21 | # NOTE: It's CRITICAL to call respond_to after calling render_to_string, or else Rails will 22 | # not render the HTML version of the index page properly. (not a problem if you do this in the view) 23 | # respond_to do |format| 24 | # format.html 25 | # end 26 | 27 | redux_store("routerCommentsStore", props: comments_json_string) 28 | render_html 29 | end 30 | 31 | # Declaring no_router and simple to indicate we have views for them 32 | def no_router 33 | redux_store("commentsStore", props: comments_json_string) 34 | render_html 35 | end 36 | 37 | def simple; end 38 | 39 | def rescript; end 40 | 41 | private 42 | 43 | def set_comments 44 | @comments = Comment.all.order("id DESC") 45 | end 46 | 47 | def comments_json_string 48 | render_to_string(template: "/comments/index", 49 | locals: { comments: Comment.all }, formats: :json) 50 | end 51 | 52 | def render_html 53 | respond_to(&:html) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/comments_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CommentsHelper 4 | MarkdownToHtmlParser = Redcarpet::Markdown.new(Redcarpet::Render::HTML) 5 | 6 | def markdown_to_html(content) 7 | return "" if content.blank? 8 | 9 | sanitize(MarkdownToHtmlParser.render(content)) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/helpers/pages_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PagesHelper 4 | def git_commit_sha 5 | GitCommitSha.current_sha 6 | end 7 | 8 | def git_commit_sha_short 9 | full_sha = git_commit_sha 10 | full_sha[...7] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationJob < ActiveJob::Base 4 | end 5 | -------------------------------------------------------------------------------- /app/jobs/comment_relay_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CommentRelayJob < ApplicationJob 4 | def perform(comment) 5 | ActionCable.server.broadcast "comments", comment unless comment.destroyed? 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/app/mailers/.keep -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/app/models/.keep -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | end 6 | -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Comment < ApplicationRecord 4 | validates :author, :text, presence: true 5 | after_commit { CommentRelayJob.perform_later(self) } 6 | end 7 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/git_commit_sha.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Retrieves the current git commit SHA of the project 4 | class GitCommitSha 5 | attr_writer :current_sha 6 | 7 | def self.current_sha 8 | @current_sha ||= ENV["GIT_COMMIT_SHA"].presence || 9 | retrieve_sha_from_file.presence || 10 | retrieve_sha_from_git 11 | end 12 | 13 | def self.reset_current_sha 14 | @current_sha = nil 15 | end 16 | 17 | # Assumes the git CLI is available. This is not the case in production on Heroku. 18 | def self.retrieve_sha_from_git 19 | `git rev-parse HEAD 2>/dev/null`.to_s.strip 20 | end 21 | 22 | # Assumes a .source_version file with SHA inside. A special Heroku buildpack creates this for us in production. 23 | def self.retrieve_sha_from_file 24 | expected_filepath = Rails.root.join(".source_version") 25 | File.exist?(expected_filepath) ? File.read(expected_filepath).to_s.strip : nil 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/views/comments/_comment.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! comment, :id, :author, :text, :created_at, :updated_at 2 | -------------------------------------------------------------------------------- /app/views/comments/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(comment, html: { class: "flex flex-col gap-4" }) do |f| %> 2 | <% if comment.errors.any? %> 3 |
4 |

<%= pluralize(comment.errors.count, "error") %> prohibited this comment from being saved:

5 | 6 | 11 |
12 | <% end %> 13 | 14 |
15 | <%= f.label :author, 'Your Name' %>
16 | <%= f.text_field :author, class: "px-3 py-1 leading-4 border border-gray-300 rounded" %> 17 |
18 |
19 | <%= f.label :text, 'Say something using markdown...' %>
20 | <%= f.text_area :text, class: "px-3 py-1 leading-4 border border-gray-300 rounded" %> 21 |
22 |
23 | <%= f.submit 'Post', class: "self-start px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800 cursor-pointer" %> 24 |
25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/views/comments/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Editing Comment

2 | 3 | <%= render 'form', comment: @comment %> 4 | 5 | <%= link_to 'Show', @comment %> | 6 | <%= link_to 'Back', comments_path %> 7 | -------------------------------------------------------------------------------- /app/views/comments/index.html.erb: -------------------------------------------------------------------------------- 1 |

2 | Using Classic Rails 4.2 "generate scaffold" 3 |

4 | 5 |
6 | <% if notice %> 7 |

<%= notice %>

8 | <% end %> 9 | 10 |

Listing Comments

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | <% @comments.each do |comment| %> 23 | 24 | 25 | 26 | 31 | 32 | <% end %> 33 | 34 |
AuthorText
<%= comment.author %><%= markdown_to_html(comment.text) %> 27 | <%= link_to 'Show', comment %> 28 | <%= link_to 'Edit', edit_comment_path(comment) %> 29 | <%= button_to 'Destroy', comment, method: :delete, data: { turbo_confirm: 'Are you sure?' }, class: "text-red-500 cursor-pointer" %> 30 |
35 | 36 | <%= link_to 'New Comment', new_comment_path, class: "not-prose px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800" %> 37 |
38 | -------------------------------------------------------------------------------- /app/views/comments/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | # Specify the partial, as well as the name of the variable used in the partial 2 | json.comments(@comments, partial: "comments/comment", as: :comment) 3 | -------------------------------------------------------------------------------- /app/views/comments/new.html.erb: -------------------------------------------------------------------------------- 1 |

New Comment

2 | 3 | <%= render 'form', comment: @comment %> 4 | 5 | <%= link_to 'Back', comments_path %> 6 | -------------------------------------------------------------------------------- /app/views/comments/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% if notice %> 3 |

<%= notice %>

4 | <% end %> 5 | 6 |

7 | Author: 8 | <%= @comment.author %> 9 |

10 | 11 |

12 | Text: 13 | <%= @comment.text %> 14 |

15 |
16 | 17 | <%= link_to 'Edit', edit_comment_path(@comment) %> | 18 | <%= link_to 'Back', comments_path %> 19 | -------------------------------------------------------------------------------- /app/views/comments/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "comment", comment: @comment 2 | -------------------------------------------------------------------------------- /app/views/comments/stimulus.html.erb: -------------------------------------------------------------------------------- 1 |

Stimulus + Rails Backend (with react_on_rails gem)

2 | 3 | <%= render "pages/header" %> 4 | 5 |
6 |

Comments

7 | 8 | 15 | 16 |
17 | <%= render "comments/turbo/horizontal_form" %> 18 | <%= render "comments/turbo/comment_list" %> 19 |
20 |
21 | -------------------------------------------------------------------------------- /app/views/comments/turbo/_comment_list.html.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_frame_tag "comment_list", class: "comment_list", data: { turbo: true } do %> 2 | 3 | Hidden Refresh Button 4 |
5 | <% @comments.each do |comment| %> 6 |
7 |

<%= comment.author %>

8 | <%= markdown_to_html(comment.text) %> 9 |
10 | <% end %> 11 |
12 | <% end %> 13 | -------------------------------------------------------------------------------- /app/views/comments/turbo/_error_notice.html.erb: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/views/comments/turbo/_horizontal_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_frame_tag "form_tabs", data: { turbo: true } do %> 2 | <%= render "comments/turbo/error_notice" %> 3 | <%= render "comments/turbo/tabs" %> 4 | 5 |
6 | 7 | <%= form_with(model: @comment, data: { action: "turbo:submit-end->comments#resetText" }, class: "form-horizontal flex flex-col gap-4") do |f| %> 8 |
9 | <%= f.label "Name", class: "w-full lg:w-2/12 lg:text-end shrink-0" %> 10 | <%= f.text_field :author, data: { comments_target: "commentAuthor" }, class: "px-3 py-1 leading-4 border border-gray-300 rounded w-full", placeholder: "Your Name" %> 11 |
12 | 13 |
14 | <%= f.label :text, class: "w-full lg:w-2/12 lg:text-end shrink-0" %> 15 | <%= f.text_field :text, data: { comments_target: "commentText" }, class: "px-3 py-1 leading-4 border border-gray-300 rounded w-full", placeholder: "Say something using markdown..." %> 16 |
17 | 18 |
19 | 20 | <%= f.submit "Post", class: "self-start px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800" %> 21 |
22 | <% end %> 23 | <% end %> 24 | -------------------------------------------------------------------------------- /app/views/comments/turbo/_inline_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_frame_tag "form_tabs", data: { turbo: true } do %> 2 | <%= render "comments/turbo/error_notice" %> 3 | <%= render "comments/turbo/tabs" %> 4 | 5 |
6 | 7 | <%= form_with(model: @comment, data: { action: "turbo:submit-end->comments#resetText" }, class: "form-inline flex flex-col lg:flex-row flex-wrap gap-4") do |f| %> 8 |
9 | <%= f.label "Name", class: "form-label mr-15" %> 10 | <%= f.text_field :author, data: { comments_target: "commentAuthor" }, class: "px-3 py-1 leading-4 border border-gray-300 rounded", placeholder: "Your Name" %> 11 |
12 | 13 |
14 | <%= f.label :text, class: "form-label mr-15" %> 15 | <%= f.text_field :text, data: { comments_target: "commentText" }, class: "px-3 py-1 leading-4 border border-gray-300 rounded", placeholder: "Say something using markdown..." %> 16 |
17 | 18 |
19 | <%= f.submit "Post", class: "self-start px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800" %> 20 |
21 | <% end %> 22 | <% end %> 23 | -------------------------------------------------------------------------------- /app/views/comments/turbo/_stacked_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_frame_tag "form_tabs", data: { turbo: true } do %> 2 | <%= render "comments/turbo/error_notice" %> 3 | <%= render "comments/turbo/tabs" %> 4 | 5 |
6 | 7 | <%= form_with(model: @comment, data: { action: "turbo:submit-end->comments#resetText" }, class: "flex flex-col gap-4") do |f| %> 8 |
9 | <%= f.label "Name", class: "w-full" %> 10 | <%= f.text_field :author, data: { comments_target: "commentAuthor" }, class: "px-3 py-1 leading-4 border border-gray-300 rounded w-full", placeholder: "Your Name" %> 11 |
12 | 13 |
14 | <%= f.label :text, class: "w-full" %> 15 | <%= f.text_field :text, data: { comments_target: "commentText" }, class: "px-3 py-1 leading-4 border border-gray-300 rounded w-full", placeholder: "Say something using markdown..." %> 16 |
17 | 18 |
19 | <%= f.submit "Post", class: "self-start px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800" %> 20 |
21 | <% end %> 22 | <% end %> 23 | -------------------------------------------------------------------------------- /app/views/comments/turbo/_tabs.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= link_to "Horizontal Form", horizontal_form_path, class: "px-6 py-2 font-semibold border-0 rounded #{@form_type == "horizontal" ? "text-sky-50 bg-sky-600" : "text-sky-600 hover:bg-gray-100"}" %> 3 | <%= link_to "Stacked Form", stacked_form_path, class: "px-6 py-2 font-semibold border-0 rounded #{@form_type == "stacked" ? "text-sky-50 bg-sky-600" : "text-sky-600 hover:bg-gray-100" }" %> 4 | <%= link_to "Inline Form", inline_form_path, class: "px-6 py-2 font-semibold border-0 rounded #{@form_type == "inline" ? "text-sky-50 bg-sky-600" : "text-sky-600 hover:bg-gray-100"}" %> 5 |
6 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RailsReactTutorial 7 | 8 | <%= stylesheet_pack_tag('client-bundle', 9 | media: 'all', 10 | 'data-turbolinks-track': true) %> 11 | 12 | <%= javascript_pack_tag('client-bundle', 13 | 'data-turbolinks-track': true, 14 | defer: true) %> 15 | 16 | <%= csrf_meta_tags %> 17 | 18 | 19 | <%= react_component "NavigationBarApp" %> 20 | 21 |
22 | <%= yield %> 23 |
24 | 25 | <%= react_component "Footer" %> 26 | 27 | 29 | <%= redux_store_hydration_data %> 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/views/layouts/stimulus_layout.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RailsReactTutorial 7 | 8 | <%= stylesheet_pack_tag('client-bundle', 9 | media: 'all', 10 | 'data-turbolinks-track': true) %> 11 | 12 | <%= javascript_pack_tag('stimulus-bundle', 13 | 'data-turbolinks-track': true, 14 | defer: true) %> 15 | 16 | <%= csrf_meta_tags %> 17 | 18 | 19 | <%= react_component "NavigationBarApp" %> 20 | 21 |
22 | <%= yield %> 23 |
24 | 25 | <%= react_component "Footer" %> 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/views/pages/_header.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Current Commit: 3 | <%= link_to git_commit_sha_short, 4 | "https://github.com/shakacode/react-webpack-rails-tutorial/commit/#{git_commit_sha}", 5 | id: "git-commit-sha" %> 6 |

7 | 8 | 45 |
46 | 47 |
48 | -------------------------------------------------------------------------------- /app/views/pages/index.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <%= link_to "Open Source example", 3 | "https://github.com/shakacode/react-webpack-rails-tutorial/" %> of using the <%= link_to "React on Rails gem", "https://github.com/shakacode/react_on_rails" %> 4 | and the <%= link_to "Shakapacker gem", "https://github.com/shakacode/shakapacker" %> 5 |

6 |

Using <%= link_to "Ruby on Rails", "http://rubyonrails.org/" %> with 7 | <%= link_to "Action Cable", "http://guides.rubyonrails.org/action_cable_overview.html" %> + 8 | <%= link_to "React", "http://facebook.github.io/react/" %> (Server rendering) + 9 | <%= link_to "Redux", "https://github.com/reactjs/redux" %> + 10 | <%= link_to "React Router", "https://github.com/reactjs/react-router" %> 11 |

12 | <%= render "header" %> 13 | 14 | 15 | <%= react_component('RouterApp', id: "RouterApp-react-component-0") %> 16 | -------------------------------------------------------------------------------- /app/views/pages/no_router.html.erb: -------------------------------------------------------------------------------- 1 |

Using React (Server rendering) + Redux + Rails Backend (using the 2 | react_on_rails gem)

3 | <%= render "header" %> 4 | 5 | 6 | <%= react_component('App') %> 7 | -------------------------------------------------------------------------------- /app/views/pages/rescript.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component "RescriptShow", prerender: true %> 2 | -------------------------------------------------------------------------------- /app/views/pages/simple.html.erb: -------------------------------------------------------------------------------- 1 |

Using React (no Flux framework) + Rails Backend (with 2 | react_on_rails gem)

3 | 4 |

This example is much simpler than the one using React + Redux and is appropriate when:

5 | 6 | 12 | 13 |
14 | 15 | 16 | <%= react_component('SimpleCommentScreen', props: {}, prerender: false) %> 17 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | const defaultConfigFunc = require('shakapacker/package/babel/preset.js'); 3 | const resultConfig = defaultConfigFunc(api); 4 | const isProductionEnv = api.env('production'); 5 | 6 | const changesOnDefault = { 7 | presets: [ 8 | [ 9 | '@babel/preset-react', 10 | { 11 | runtime: 'automatic', 12 | development: !isProductionEnv, 13 | useBuiltIns: true, 14 | }, 15 | ], 16 | ].filter(Boolean), 17 | plugins: [ 18 | process.env.WEBPACK_SERVE && 'react-refresh/babel', 19 | isProductionEnv && [ 20 | 'babel-plugin-transform-react-remove-prop-types', 21 | { 22 | removeImport: true, 23 | }, 24 | ], 25 | ].filter(Boolean), 26 | }; 27 | 28 | resultConfig.presets = [...resultConfig.presets, ...changesOnDefault.presets]; 29 | resultConfig.plugins = [...resultConfig.plugins, ...changesOnDefault.plugins]; 30 | 31 | return resultConfig; 32 | }; 33 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | def installed?(process) 5 | IO.popen "#{process} -v" 6 | rescue Errno::ENOENT 7 | false 8 | end 9 | 10 | def run(process) 11 | system "#{process} start -f Procfile.dev" 12 | rescue Errno::ENOENT 13 | warn <<~MSG 14 | ERROR: 15 | Please ensure `Procfile.dev` exists in your project! 16 | MSG 17 | exit! 18 | end 19 | 20 | if installed? "overmind" 21 | run "overmind" 22 | elsif installed? "foreman" 23 | run "foreman" 24 | else 25 | warn <<~MSG 26 | NOTICE: 27 | For this script to run, you need either 'overmind' or 'foreman' installed on your machine. Please try this script after installing one of them. 28 | MSG 29 | exit! 30 | end 31 | -------------------------------------------------------------------------------- /bin/dev-static: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | def installed?(process) 5 | IO.popen "#{process} -v" 6 | rescue Errno::ENOENT 7 | false 8 | end 9 | 10 | def run(process) 11 | system "#{process} start -f Procfile.dev-static" 12 | rescue Errno::ENOENT 13 | warn <<~MSG 14 | ERROR: 15 | Please ensure `Procfile.dev-static` exists in your project! 16 | MSG 17 | exit! 18 | end 19 | 20 | if installed? "overmind" 21 | run "overmind" 22 | elsif installed? "foreman" 23 | run "foreman" 24 | else 25 | warn <<~MSG 26 | NOTICE: 27 | For this script to run, you need either 'overmind' or 'foreman' installed on your machine. Please try this script after installing one of them. 28 | MSG 29 | exit! 30 | end 31 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | # 8 | # This file was generated by Bundler. 9 | # 10 | # The application 'rspec' is installed as part of a gem, and 11 | # this file is here to facilitate running it. 12 | # 13 | 14 | require "pathname" 15 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 16 | Pathname.new(__FILE__).realpath) 17 | 18 | require "rubygems" 19 | require "bundler/setup" 20 | 21 | load Gem.bin_path("rspec-core", "rspec") 22 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | APP_ROOT = File.expand_path("..", __dir__) 5 | 6 | def system!(*args) 7 | system(*args, exception: true) 8 | end 9 | 10 | FileUtils.chdir APP_ROOT do 11 | # This script is a way to set up or update your development environment automatically. 12 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 13 | # Add necessary setup steps to this file. 14 | 15 | puts "== Installing dependencies ==" 16 | system! "gem install bundler --conservative" 17 | system("bundle check") || system!("bundle install") 18 | 19 | # Install JavaScript dependencies 20 | system! "bin/yarn" 21 | 22 | # puts "\n== Copying sample files ==" 23 | # unless File.exist?("config/database.yml") 24 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 25 | # end 26 | 27 | puts "\n== Preparing database ==" 28 | system! "bin/rails db:prepare" 29 | 30 | puts "\n== Removing old logs and tempfiles ==" 31 | system! "bin/rails log:clear tmp:clear" 32 | 33 | unless ARGV.include?("--skip-server") 34 | puts "\n== Starting development server ==" 35 | STDOUT.flush # flush the output before exec(2) so that it displays 36 | exec "bin/dev" 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /bin/shakapacker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= "development" 4 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__) 5 | 6 | require "bundler/setup" 7 | require "shakapacker" 8 | require "shakapacker/webpack_runner" 9 | 10 | APP_ROOT = File.expand_path("..", __dir__) 11 | Dir.chdir(APP_ROOT) do 12 | Shakapacker::WebpackRunner.run(ARGV) 13 | end 14 | -------------------------------------------------------------------------------- /bin/shakapacker-dev-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ENV["RAILS_ENV"] ||= "development" 4 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__) 5 | 6 | require "bundler/setup" 7 | require "shakapacker" 8 | require "shakapacker/dev_server_runner" 9 | 10 | APP_ROOT = File.expand_path("..", __dir__) 11 | Dir.chdir(APP_ROOT) do 12 | Shakapacker::DevServerRunner.run(ARGV) 13 | end 14 | -------------------------------------------------------------------------------- /bin/thrust: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | load Gem.bin_path("thruster", "thrust") 6 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system 'bundle check' or system! 'bundle install' 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | APP_ROOT = File.expand_path("..", __dir__) 4 | Dir.chdir(APP_ROOT) do 5 | yarn = ENV["PATH"].split(File::PATH_SEPARATOR). 6 | select { |dir| File.expand_path(dir) != __dir__ }. 7 | product(["yarn", "yarnpkg", "yarn.cmd", "yarn.ps1"]). 8 | map { |dir, file| File.expand_path(file, dir) }. 9 | find { |file| File.executable?(file) } 10 | 11 | if yarn 12 | exec yarn, *ARGV 13 | else 14 | $stderr.puts "Yarn executable was not detected in the system." 15 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 16 | exit 1 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-webpack-rails-tutorial", 3 | "sources": [ 4 | { 5 | "dir": "client/app/bundles/comments/rescript", 6 | "subdirs": true 7 | } 8 | ], 9 | "package-specs": [ 10 | { 11 | "module": "es6", 12 | "in-source": true 13 | } 14 | ], 15 | "bsc-flags": ["-open JsonCombinators", "-open Belt"], 16 | "suffix": ".bs.js", 17 | "bs-dependencies": [ 18 | "@rescript/react", 19 | "@rescript/core", 20 | "@glennsl/rescript-fetch", 21 | "@glennsl/rescript-json-combinators", 22 | "rescript-react-on-rails" 23 | ], 24 | "jsx": { 25 | "version": 4, 26 | "mode": "automatic" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | Please see parent directory README.md. 2 | 3 | Classes and React 4 | ========================= 5 | We switched to ES6 Classes from React.createClass(). Thus your React components extend `React.Component`. 6 | 7 | * [React.createClass Api](https://facebook.github.io/react/docs/top-level-api.html#react.createclass) 8 | * [React ES6 Classes](https://facebook.github.io/react/docs/reusable-components.html#es6-classes) 9 | * [How to Use Classes and Sleep at Night, Dan Abramov](https://medium.com/@dan_abramov/how-to-use-classes-and-sleep-at-night-9af8de78ccb4) 10 | 11 | 12 | ESLint 13 | ========================== 14 | The `.eslintrc` file is based on the AirBnb [eslintrc](https://github.com/airbnb/javascript/blob/master/linters/.eslintrc). 15 | 16 | It also includes many eslint defaults that the AirBnb eslint does not include. 17 | 18 | Running linter: 19 | =========================== 20 | 21 | Soon to be in gulpfile....but gulp-eslint depends on eslint depends on 22 | 23 | ``` 24 | "eslint-plugin-react": "^2.0.2", 25 | ``` 26 | 27 | So don't use `yarn run gulp lint` yet. 28 | 29 | From either top level or within `client` directory 30 | 31 | yarn run lint 32 | 33 | 34 | Updating Node Dependencies 35 | =========================== 36 | 37 | ``` 38 | yarn global add npm-check-updates 39 | ``` 40 | 41 | Then run this to update the dependencies (starting at the top level). 42 | 43 | ``` 44 | # Make sure you are in the top directory, then run: 45 | cd client 46 | ncu -u 47 | yarn upgrade 48 | ``` 49 | 50 | Adding Node Modules 51 | ===================================== 52 | Suppose you want to add a dependency to "module_name".... 53 | 54 | Before you do so, consider: 55 | 56 | 1. Do we really need the module and the extra JS code? 57 | 2. Is the module well maintained? 58 | 59 | ```bash 60 | cd client 61 | yarn add module_name@version 62 | ``` 63 | -------------------------------------------------------------------------------- /client/app/assets/fonts/OpenSans-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/client/app/assets/fonts/OpenSans-Bold.eot -------------------------------------------------------------------------------- /client/app/assets/fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/client/app/assets/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /client/app/assets/fonts/OpenSans-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/client/app/assets/fonts/OpenSans-Bold.woff -------------------------------------------------------------------------------- /client/app/assets/fonts/OpenSans-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/client/app/assets/fonts/OpenSans-Light.eot -------------------------------------------------------------------------------- /client/app/assets/fonts/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/client/app/assets/fonts/OpenSans-Light.ttf -------------------------------------------------------------------------------- /client/app/assets/fonts/OpenSans-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/client/app/assets/fonts/OpenSans-Light.woff -------------------------------------------------------------------------------- /client/app/assets/images/railsonmaui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/client/app/assets/images/railsonmaui.png -------------------------------------------------------------------------------- /client/app/assets/images/twitter_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/client/app/assets/images/twitter_64.png -------------------------------------------------------------------------------- /client/app/assets/styles/app-variables.scss: -------------------------------------------------------------------------------- 1 | // $animation-duration must correspond to CSSTransition timeout value in: 2 | // client/app/bundles/comments/components/CommentBox/CommentList.jsx 3 | 4 | $animation-duration: 0.5s; 5 | -------------------------------------------------------------------------------- /client/app/assets/styles/application.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'OpenSans-Light'; 3 | src: url('../../assets/fonts/OpenSans-Light.ttf') format('truetype'); 4 | } 5 | 6 | @import 'tailwindcss/base'; 7 | @import 'tailwindcss/components'; 8 | @import 'tailwindcss/utilities'; 9 | 10 | h2 { 11 | @apply text-3xl font-medium mt-5 mb-2.5; 12 | } 13 | 14 | h3 { 15 | @apply text-xl font-medium mt-5 mb-2.5 16 | } 17 | 18 | a { 19 | @apply text-sky-700 20 | } 21 | -------------------------------------------------------------------------------- /client/app/assets/styles/stimulus.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/client/app/assets/styles/stimulus.scss -------------------------------------------------------------------------------- /client/app/bundles/comments/actions/commentsActionCreators.js: -------------------------------------------------------------------------------- 1 | import requestsManager from '../../../libs/requestsManager'; 2 | import * as actionTypes from '../constants/commentsConstants'; 3 | 4 | export function setIsFetching() { 5 | return { 6 | type: actionTypes.SET_IS_FETCHING, 7 | }; 8 | } 9 | 10 | export function setIsSaving() { 11 | return { 12 | type: actionTypes.SET_IS_SAVING, 13 | }; 14 | } 15 | 16 | export function fetchCommentsSuccess(data) { 17 | return { 18 | type: actionTypes.FETCH_COMMENTS_SUCCESS, 19 | comments: data.comments, 20 | }; 21 | } 22 | 23 | export function fetchCommentsFailure(error) { 24 | return { 25 | type: actionTypes.FETCH_COMMENTS_FAILURE, 26 | error, 27 | }; 28 | } 29 | 30 | export function messageReceived(comment) { 31 | return { 32 | type: actionTypes.MESSAGE_RECEIVED, 33 | comment, 34 | }; 35 | } 36 | 37 | export function submitCommentSuccess(comment) { 38 | return { 39 | type: actionTypes.SUBMIT_COMMENT_SUCCESS, 40 | comment, 41 | }; 42 | } 43 | 44 | export function submitCommentFailure(error) { 45 | return { 46 | type: actionTypes.SUBMIT_COMMENT_FAILURE, 47 | error, 48 | }; 49 | } 50 | 51 | export function fetchComments() { 52 | return (dispatch) => { 53 | dispatch(setIsFetching()); 54 | return requestsManager 55 | .fetchEntities() 56 | .then((res) => dispatch(fetchCommentsSuccess(res.data))) 57 | .catch((error) => dispatch(fetchCommentsFailure(error))); 58 | }; 59 | } 60 | 61 | export function submitComment(comment) { 62 | return (dispatch) => { 63 | dispatch(setIsSaving()); 64 | return requestsManager 65 | .submitEntity({ comment }) 66 | .then((res) => dispatch(submitCommentSuccess(res.data))) 67 | .catch((error) => dispatch(submitCommentFailure(error))); 68 | }; 69 | } 70 | 71 | export function setLocale(locale) { 72 | return { 73 | type: actionTypes.SET_LOCALE, 74 | locale, 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /client/app/bundles/comments/components/CommentBox/CommentBox.module.scss: -------------------------------------------------------------------------------- 1 | // $animation-duration is set in client/app/assets/styles/app-variables.scss 2 | 3 | .elementEnter { 4 | opacity: 0.01; 5 | 6 | &.elementEnterActive { 7 | opacity: 1; 8 | transition: opacity $animation-duration ease-in; 9 | } 10 | } 11 | 12 | .elementLeave { 13 | opacity: 1; 14 | 15 | &.elementLeaveActive { 16 | opacity: 0.01; 17 | transition: opacity $animation-duration ease-in; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { marked } from 'marked'; 5 | import { gfmHeadingId } from 'marked-gfm-heading-id'; 6 | import { mangle } from 'marked-mangle'; 7 | import sanitizeHtml from 'sanitize-html'; 8 | 9 | marked.use(gfmHeadingId()); 10 | marked.use(mangle()); 11 | 12 | const Comment = React.forwardRef((props, ref) => { 13 | const { author, text } = props; 14 | const rawMarkup = marked(text, { gfm: true }); 15 | const sanitizedRawMarkup = sanitizeHtml(rawMarkup); 16 | 17 | /* eslint-disable react/no-danger */ 18 | return ( 19 |
20 |

{author}

21 | 22 |
23 |
24 | ); 25 | }); 26 | 27 | Comment.displayName = 'Comment'; 28 | 29 | Comment.propTypes = { 30 | author: PropTypes.string.isRequired, 31 | text: PropTypes.string.isRequired, 32 | }; 33 | 34 | export default React.memo(Comment); 35 | -------------------------------------------------------------------------------- /client/app/bundles/comments/components/CommentBox/CommentList/Comment/Comment.spec.jsx: -------------------------------------------------------------------------------- 1 | import { React, TestUtils } from '../../../../../../libs/testHelper'; 2 | 3 | import Comment from './Comment.jsx'; 4 | 5 | const { renderIntoDocument, findRenderedDOMComponentWithClass, findRenderedDOMComponentWithTag } = TestUtils; 6 | 7 | describe('Comment', () => { 8 | it('renders an author and comment with proper css classes', () => { 9 | const component = renderIntoDocument(); 10 | 11 | const comment = findRenderedDOMComponentWithTag(component, 'div'); 12 | expect(comment.className).toEqual('comment'); 13 | const author = findRenderedDOMComponentWithTag(component, 'h2'); 14 | expect(author.className).toEqual('commentAuthor js-comment-author'); 15 | const text = findRenderedDOMComponentWithTag(component, 'span'); 16 | expect(text.className).toEqual('js-comment-text'); 17 | }); 18 | 19 | it('shows the author', () => { 20 | const component = renderIntoDocument(); 21 | 22 | const author = findRenderedDOMComponentWithClass(component, 'js-comment-author'); 23 | expect(author.textContent).toEqual('Frank'); 24 | }); 25 | 26 | it('shows the comment text in markdown', () => { 27 | const component = renderIntoDocument(); 28 | 29 | const comment = findRenderedDOMComponentWithClass(component, 'js-comment-text'); 30 | expect(comment.textContent).toEqual('Hi!\n'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /client/app/bundles/comments/components/CommentBox/CommentList/CommentList.spec.jsx: -------------------------------------------------------------------------------- 1 | import { List, Map } from 'immutable'; 2 | import { React, TestUtils } from '../../../../../libs/testHelper'; 3 | 4 | import CommentList from './CommentList.jsx'; 5 | import Comment from './Comment/Comment.jsx'; 6 | 7 | const { renderIntoDocument, findRenderedDOMComponentWithTag, scryRenderedComponentsWithType } = TestUtils; 8 | 9 | const cssTransitionGroupClassNames = { 10 | enter: 'elementEnter', 11 | enterActive: 'elementEnterActive', 12 | exit: 'elementLeave', 13 | exitActive: 'elementLeaveActive', 14 | }; 15 | 16 | describe('CommentList', () => { 17 | const comments = List.of( 18 | new Map({ 19 | id: 1, 20 | author: 'Frank', 21 | text: 'hi', 22 | nodeRef: React.createRef(null), 23 | }), 24 | new Map({ 25 | id: 2, 26 | author: 'Furter', 27 | text: 'ho', 28 | nodeRef: React.createRef(null), 29 | }), 30 | ); 31 | 32 | it('renders a list of Comments in normal order', () => { 33 | const component = renderIntoDocument( 34 | , 35 | ); 36 | const list = scryRenderedComponentsWithType(component, Comment); 37 | expect(list.length).toEqual(2); 38 | expect(list[0].props.author).toEqual('Frank'); 39 | expect(list[1].props.author).toEqual('Furter'); 40 | }); 41 | 42 | it('renders an alert if errors', () => { 43 | const component = renderIntoDocument( 44 | , 49 | ); 50 | 51 | const alert = findRenderedDOMComponentWithTag(component, 'strong'); 52 | expect(alert.textContent).toEqual('Comments could not be retrieved. '); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /client/app/bundles/comments/components/CommentScreen/CommentScreen.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import BaseComponent from 'libs/components/BaseComponent'; 5 | 6 | import CommentBox from '../CommentBox/CommentBox'; 7 | 8 | export default class CommentScreen extends BaseComponent { 9 | static propTypes = { 10 | actions: PropTypes.oneOfType([PropTypes.object]).isRequired, 11 | data: PropTypes.oneOfType([PropTypes.object]).isRequired, 12 | locationState: PropTypes.oneOfType([PropTypes.object]), 13 | }; 14 | 15 | renderNotification() { 16 | const { locationState } = this.props; 17 | 18 | if (!locationState || !locationState.redirectFrom) return null; 19 | 20 | window.history.replaceState({}, document.title); 21 | 22 | return ( 23 |
24 | You have been redirected from 25 | {locationState.redirectFrom} 26 |
27 | ); 28 | } 29 | 30 | render() { 31 | const { data, actions } = this.props; 32 | 33 | return ( 34 |
35 | {this.renderNotification()} 36 |
37 | 43 |
44 |
45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/app/bundles/comments/components/Footer/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseComponent from 'libs/components/BaseComponent'; 3 | 4 | export default class Footer extends BaseComponent { 5 | render() { 6 | return ( 7 | 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/app/bundles/comments/components/NavigationBar/CommentsCount.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const href = 5 | 'https://github.com/shakacode/react_on_rails/blob/master/README.md#multiple-react-' + 6 | 'components-on-a-page-with-one-store'; 7 | function CommentsCount(props) { 8 | const { commentsCount } = props; 9 | return ( 10 |
  • 11 | 12 | Comments: {commentsCount} 13 | 14 |
  • 15 | ); 16 | } 17 | 18 | CommentsCount.propTypes = { 19 | commentsCount: PropTypes.number.isRequired, 20 | }; 21 | 22 | export default CommentsCount; 23 | -------------------------------------------------------------------------------- /client/app/bundles/comments/components/SimpleCommentScreen/SimpleCommentScreen.module.scss: -------------------------------------------------------------------------------- 1 | .elementEnter { 2 | opacity: 0.01; 3 | 4 | &.elementEnterActive { 5 | opacity: 1; 6 | transition: opacity $animation-duration ease-in; 7 | } 8 | } 9 | 10 | .elementLeave { 11 | opacity: 1; 12 | 13 | &.elementLeaveActive { 14 | opacity: 0.01; 15 | transition: opacity $animation-duration ease-in; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/app/bundles/comments/components/TestReactRouter/TestReactRouter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import BaseComponent from '../../../../libs/components/BaseComponent.jsx'; 4 | 5 | export default class TestReactRouter extends BaseComponent { 6 | render() { 7 | return ( 8 |
    9 |

    React Router is working!

    10 |

    11 | Woohoo, we can use react-router here! 12 |

    13 |
    14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/app/bundles/comments/components/TestReactRouterRedirect/TestReactRouterRedirect.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navigate, useLocation } from 'react-router-dom'; 3 | 4 | function TestReactRouterRedirect() { 5 | const location = useLocation(); 6 | 7 | function checkAuth() { 8 | // Hard code this to demonstrate the effect 9 | const notAuthorized = true; 10 | return notAuthorized; 11 | } 12 | 13 | if (checkAuth()) { 14 | return ; 15 | } 16 | 17 | return
    Nope.
    ; 18 | } 19 | 20 | export default React.memo(TestReactRouterRedirect); 21 | -------------------------------------------------------------------------------- /client/app/bundles/comments/constants/commentsConstants.js: -------------------------------------------------------------------------------- 1 | export const FETCH_COMMENTS_SUCCESS = 'FETCH_COMMENTS_SUCCESS'; 2 | export const FETCH_COMMENTS_FAILURE = 'FETCH_COMMENTS_FAILURE'; 3 | 4 | export const SUBMIT_COMMENT_SUCCESS = 'SUBMIT_COMMENT_SUCCESS'; 5 | export const SUBMIT_COMMENT_FAILURE = 'SUBMIT_COMMENT_FAILURE'; 6 | export const MESSAGE_RECEIVED = 'MESSAGE_RECEIVED'; 7 | 8 | export const SET_IS_FETCHING = 'SET_IS_FETCHING'; 9 | export const SET_IS_SAVING = 'SET_IS_SAVING'; 10 | 11 | export const SET_LOCALE = 'SET_LOCALE'; 12 | -------------------------------------------------------------------------------- /client/app/bundles/comments/constants/paths.js: -------------------------------------------------------------------------------- 1 | export const ROUTER_PATH = '/'; 2 | export const REACT_ROUTER_PATH = '/react-router'; 3 | export const NO_ROUTER_PATH = '/no-router'; 4 | export const RESCRIPT_PATH = '/rescript'; 5 | export const SIMPLE_REACT_PATH = '/simple'; 6 | export const STIMULUS_PATH = '/stimulus'; 7 | export const RAILS_PATH = '/comments'; 8 | -------------------------------------------------------------------------------- /client/app/bundles/comments/containers/NavigationBarContainer.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import BaseComponent from '../../../libs/components/BaseComponent.jsx'; 6 | 7 | import NavigationBar from '../components/NavigationBar/NavigationBar.jsx'; 8 | 9 | function stateToProps(state) { 10 | // Which part of the Redux global state does our component want to receive as props? 11 | if (state.$$commentsStore) { 12 | return { 13 | commentsCount: state.$$commentsStore.get('$$comments').size, 14 | pathname: state.railsContext.pathname, 15 | }; 16 | } 17 | return {}; 18 | } 19 | 20 | class NavigationBarContainer extends BaseComponent { 21 | static propTypes = { 22 | commentsCount: PropTypes.number.isRequired, 23 | pathname: PropTypes.string.isRequired, 24 | }; 25 | 26 | render() { 27 | const { commentsCount, pathname } = this.props; 28 | 29 | return ; 30 | } 31 | } 32 | 33 | // Don't forget to actually use connect! 34 | export default connect(stateToProps)(NavigationBarContainer); 35 | -------------------------------------------------------------------------------- /client/app/bundles/comments/containers/NonRouterCommentsContainer.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { bindActionCreators } from 'redux'; 5 | // polyfill for server-side rendering, required by react-intl 6 | import Intl from 'intl'; 7 | import { IntlProvider } from 'react-intl'; 8 | import BaseComponent from '../../../libs/components/BaseComponent.jsx'; 9 | import { translations } from '../../../libs/i18n/translations'; 10 | import { defaultLocale } from '../../../libs/i18n/default'; 11 | 12 | import CommentScreen from '../components/CommentScreen/CommentScreen.jsx'; 13 | import * as commentsActionCreators from '../actions/commentsActionCreators'; 14 | 15 | global.Intl = Intl; 16 | 17 | function select(state) { 18 | // Which part of the Redux global state does our component want to receive as props? 19 | return { data: state.$$commentsStore }; 20 | } 21 | 22 | class NonRouterCommentsContainer extends BaseComponent { 23 | static propTypes = { 24 | dispatch: PropTypes.func.isRequired, 25 | data: PropTypes.oneOfType([PropTypes.object]).isRequired, 26 | }; 27 | 28 | render() { 29 | const { dispatch, data } = this.props; 30 | const actions = bindActionCreators(commentsActionCreators, dispatch); 31 | const locale = data.get('locale') || defaultLocale; 32 | const messages = translations[locale]; 33 | 34 | return ( 35 | 36 | 37 | 38 | ); 39 | } 40 | } 41 | 42 | // Don't forget to actually use connect! 43 | export default connect(select)(NonRouterCommentsContainer); 44 | -------------------------------------------------------------------------------- /client/app/bundles/comments/containers/RouterCommentsContainer.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { useLocation } from 'react-router-dom'; 5 | import { bindActionCreators } from 'redux'; 6 | import { IntlProvider } from 'react-intl'; 7 | import Intl from 'intl'; 8 | import { defaultLocale } from '../../../libs/i18n/default'; 9 | import { translations } from '../../../libs/i18n/translations'; 10 | 11 | import CommentScreen from '../components/CommentScreen/CommentScreen.jsx'; 12 | import * as commentsActionCreators from '../actions/commentsActionCreators'; 13 | 14 | global.Intl = Intl; 15 | 16 | function select(state) { 17 | // Which part of the Redux global state does our component want to receive as props? 18 | return { data: state.$$commentsStore }; 19 | } 20 | 21 | function RouterCommentsContainer(props) { 22 | const { dispatch, data } = props; 23 | const actions = bindActionCreators(commentsActionCreators, dispatch); 24 | const location = useLocation(); 25 | const locationState = location.state; 26 | const locale = data.get('locale') || defaultLocale; 27 | const messages = translations[locale]; 28 | 29 | return ( 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | RouterCommentsContainer.propTypes = { 37 | dispatch: PropTypes.func.isRequired, 38 | data: PropTypes.oneOfType([PropTypes.object]).isRequired, 39 | }; 40 | 41 | // Don't forget to actually use connect! 42 | export default React.memo(connect(select)(RouterCommentsContainer)); 43 | -------------------------------------------------------------------------------- /client/app/bundles/comments/layout/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { NavLink as Link } from 'react-router-dom'; 4 | 5 | /* eslint-disable react/prefer-stateless-function */ 6 | export default class Layout extends Component { 7 | /* eslint-disable react/no-unescaped-entities */ 8 | render() { 9 | const { children } = this.props; 10 | return ( 11 |
    12 |
    13 |
      14 |
    • 15 | (isActive ? ' activated' : '')}> 16 | Comments (Root URL) 17 | 18 |
    • 19 |
    • 20 | (isActive ? ' activated' : '')}> 21 | Test React Router ('/react-router') 22 | 23 |
    • 24 |
    • 25 | (isActive ? ' activated' : '')}> 26 | Test Redirect (url to '/react-router/redirect' which goes to root '/') 27 | 28 |
    • 29 |
    30 |
    31 | {children} 32 |
    33 | ); 34 | } 35 | } 36 | 37 | Layout.propTypes = { 38 | children: PropTypes.oneOfType([PropTypes.object]).isRequired, 39 | }; 40 | -------------------------------------------------------------------------------- /client/app/bundles/comments/reducers/commentsReducer.js: -------------------------------------------------------------------------------- 1 | /* eslint new-cap: 0 */ 2 | 3 | import React from 'react'; 4 | import Immutable from 'immutable'; 5 | 6 | import * as actionTypes from '../constants/commentsConstants'; 7 | 8 | export const $$initialState = Immutable.fromJS({ 9 | $$comments: [], 10 | fetchCommentError: null, 11 | submitCommentError: null, 12 | isFetching: false, 13 | isSaving: false, 14 | locale: null, 15 | }); 16 | 17 | export default function commentsReducer($$state = $$initialState, action = null) { 18 | const { type, comment, comments, error, locale } = action; 19 | 20 | switch (type) { 21 | case actionTypes.FETCH_COMMENTS_SUCCESS: { 22 | return $$state.merge({ 23 | $$comments: Immutable.fromJS(comments), 24 | fetchCommentError: null, 25 | isFetching: false, 26 | }); 27 | } 28 | 29 | case actionTypes.FETCH_COMMENTS_FAILURE: { 30 | return $$state.merge({ 31 | fetchCommentError: error, 32 | isFetching: false, 33 | }); 34 | } 35 | 36 | case actionTypes.MESSAGE_RECEIVED: { 37 | comment.nodeRef = React.createRef(null); 38 | return $$state.withMutations((state) => 39 | state.updateIn(['$$comments'], ($$comments) => 40 | $$comments.findIndex((com) => com.get('id') === comment.get('id')) === -1 41 | ? $$comments.unshift(Immutable.fromJS(comment)) 42 | : $$comments, 43 | ), 44 | ); 45 | } 46 | 47 | case actionTypes.SUBMIT_COMMENT_SUCCESS: { 48 | comment.nodeRef = React.createRef(null); 49 | return $$state.withMutations((state) => 50 | state 51 | .updateIn(['$$comments'], ($$comments) => { 52 | const index = $$comments.findIndex((com) => com.get('id') === comment.id); 53 | return index === -1 54 | ? $$comments.unshift(Immutable.fromJS(comment)) 55 | : $$comments.set({ index, value: Immutable.fromJS(comment) }); 56 | }) 57 | .merge({ 58 | submitCommentError: null, 59 | isSaving: false, 60 | }), 61 | ); 62 | } 63 | 64 | case actionTypes.SUBMIT_COMMENT_FAILURE: { 65 | return $$state.merge({ 66 | submitCommentError: error, 67 | isSaving: false, 68 | }); 69 | } 70 | 71 | case actionTypes.SET_IS_FETCHING: { 72 | return $$state.merge({ 73 | isFetching: true, 74 | }); 75 | } 76 | 77 | case actionTypes.SET_IS_SAVING: { 78 | return $$state.merge({ 79 | isSaving: true, 80 | }); 81 | } 82 | 83 | case actionTypes.SET_LOCALE: { 84 | return $$state.merge({ 85 | locale, 86 | }); 87 | } 88 | 89 | default: { 90 | return $$state; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /client/app/bundles/comments/reducers/index.js: -------------------------------------------------------------------------------- 1 | import commentsReducer, { $$initialState as $$commentsState } from './commentsReducer'; 2 | import railsContextReducer, { initialState as railsContextState } from './railsContextReducer'; 3 | 4 | export default { 5 | $$commentsStore: commentsReducer, 6 | railsContext: railsContextReducer, 7 | }; 8 | 9 | export const initialStates = { 10 | $$commentsState, 11 | railsContextState, 12 | }; 13 | -------------------------------------------------------------------------------- /client/app/bundles/comments/reducers/railsContextReducer.js: -------------------------------------------------------------------------------- 1 | export const initialState = {}; 2 | 3 | export default function railsContextReducer(state = initialState) { 4 | return state; 5 | } 6 | -------------------------------------------------------------------------------- /client/app/bundles/comments/rescript/Actions/Actions.res: -------------------------------------------------------------------------------- 1 | // TODO : use only one way to make http requests either Axios or Fetch 2 | module Create = { 3 | type t = { 4 | author: string, 5 | text: string, 6 | } 7 | 8 | let storeComment = async (comment: t) => { 9 | let _ = await Axios.post( 10 | "comments.json", 11 | { 12 | "author": comment.author, 13 | "text": comment.text, 14 | }, 15 | { 16 | "responseType": "json", 17 | "headers": { 18 | // see https://github.com/shakacode/react_on_rails/blob/249c69812474e0f532df432581bf5e618df0e1ec/node_package/src/Authenticity.ts#L13C1-L18C1 19 | "X-CSRF-Token": ReactOnRails.authenticityToken(), 20 | "X-Requested-With": "XMLHttpRequest", 21 | }, 22 | }, 23 | ) 24 | } 25 | } 26 | 27 | module Fetch = { 28 | type t = { 29 | author: string, 30 | text: string, 31 | id: int, 32 | } 33 | 34 | type comments = array 35 | 36 | type commentsRes = {comments: comments} 37 | 38 | let fetchComments = async (): result => { 39 | open Json.Decode 40 | 41 | let response = await Fetch.get("comments.json") 42 | let jsonRes = await response->Fetch.Response.json 43 | 44 | let jsonComment = Json.Decode.object(field => { 45 | author: field.required(. "author", string), 46 | text: field.required(. "text", string), 47 | id: field.required(. "id", int), 48 | }) 49 | 50 | let jsonComments = Json.Decode.object(field => { 51 | comments: field.required(. "comments", array(jsonComment)), 52 | }) 53 | 54 | switch jsonRes->Json.decode(jsonComments) { 55 | | Ok(decodedRes) => Ok(decodedRes.comments) 56 | | Error(e) => Error(e) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /client/app/bundles/comments/rescript/CommentForm/forms/HorizontalForm.res: -------------------------------------------------------------------------------- 1 | @react.component 2 | let make = (~author, ~handleAuthorChange, ~text, ~handleTextChange, ~handleSubmit, ~disabled) => { 3 |
    8 |
    9 | 12 | 23 |
    24 |
    25 | 28 | 39 |
    40 |
    41 |
    42 | 48 |
    49 | 50 | } 51 | -------------------------------------------------------------------------------- /client/app/bundles/comments/rescript/CommentForm/forms/InlineForm.res: -------------------------------------------------------------------------------- 1 | @react.component 2 | let make = (~author, ~handleAuthorChange, ~text, ~handleTextChange, ~handleSubmit, ~disabled) => { 3 |
    8 |
    9 | 10 | 21 |
    22 |
    23 | 24 | 35 |
    36 |
    37 | 43 |
    44 |
    45 | } 46 | -------------------------------------------------------------------------------- /client/app/bundles/comments/rescript/CommentForm/forms/StackedForm.res: -------------------------------------------------------------------------------- 1 | @react.component 2 | let make = (~author, ~handleAuthorChange, ~text, ~handleTextChange, ~handleSubmit, ~disabled) => { 3 |
    8 |
    9 | 10 | 21 |
    22 |
    23 | 24 | 35 |
    36 |
    37 | 43 |
    44 |
    45 | } 46 | -------------------------------------------------------------------------------- /client/app/bundles/comments/rescript/CommentList/AlertError/AlertError.res: -------------------------------------------------------------------------------- 1 | @module("../../ReScriptShow.module.scss") external css: {..} = "default" 2 | 3 | @react.component 4 | let make = (~errorMsg: string) => { 5 | let nodeRef = React.useRef(Js.Nullable.null) 6 | 7 | let cssTransitionGroupClassNames: ReactTransitionGroup.CSSTransition.t = { 8 | enter: css["elementEnter"], 9 | enterActive: css["elementEnterActive"], 10 | exit: css["elementLeave"], 11 | exitActive: css["elementLeaveActive"], 12 | } 13 | 14 | // The 500 must correspond to the 0.5s in: 15 | // ../../RescriptShow.module.scss:9 16 | 21 |
    23 | {errorMsg->React.string} 24 |
    25 |
    26 | } 27 | -------------------------------------------------------------------------------- /client/app/bundles/comments/rescript/CommentList/Comment/Comment.res: -------------------------------------------------------------------------------- 1 | @react.component 2 | let make = (~comment: Actions.Fetch.t, ~cssTransitionGroupClassNames) => { 3 | let rawMarkup = Marked.marked(comment.text, {gfm: true}) 4 | let innerHTML = {"__html": rawMarkup} 5 | let nodeRef = React.useRef(Js.Nullable.null) 6 | 7 | // The 500 must correspond to the 0.5s in: 8 | // ../../RescriptShow.module.scss:9 9 | 14 |
    15 |

    {comment.author->React.string}

    16 | 17 |
    18 |
    19 | } 20 | -------------------------------------------------------------------------------- /client/app/bundles/comments/rescript/CommentList/CommentList.res: -------------------------------------------------------------------------------- 1 | @module("../ReScriptShow.module.scss") external css: {..} = "default" 2 | 3 | @react.component 4 | let make = (~comments: Actions.Fetch.comments) => { 5 | let cssTransitionGroupClassNames: ReactTransitionGroup.CSSTransition.t = { 6 | enter: css["elementEnter"], 7 | enterActive: css["elementEnterActive"], 8 | exit: css["elementLeave"], 9 | exitActive: css["elementLeaveActive"], 10 | } 11 | 12 |
    13 | 14 | {comments 15 | ->Array.map(comment => 16 | Int.toString} 18 | /> 19 | ) 20 | ->React.array} 21 | 22 |
    23 | } 24 | -------------------------------------------------------------------------------- /client/app/bundles/comments/rescript/Header/Header.res: -------------------------------------------------------------------------------- 1 | @react.component 2 | let make = () => { 3 |
    5 | 53 |
    54 |
    55 | } 56 | -------------------------------------------------------------------------------- /client/app/bundles/comments/rescript/ReScriptShow.module.scss: -------------------------------------------------------------------------------- 1 | // The 0.5s must correspond to the 500s in: 2 | // ./CommentList/AlertError/AlertError.res:10 3 | // ./CommentList/Comment/Comment.res:18 4 | .elementEnter { 5 | opacity: 0.01; 6 | 7 | &.elementEnterActive { 8 | opacity: 1; 9 | transition: opacity 0.5s ease-in; 10 | } 11 | } 12 | 13 | .elementLeave { 14 | opacity: 1; 15 | 16 | &.elementLeaveActive { 17 | opacity: 0.01; 18 | transition: opacity 0.5s ease-in; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/app/bundles/comments/rescript/bindings/ActionCable.res: -------------------------------------------------------------------------------- 1 | type webSocket 2 | type optionalWebSocket = option 3 | type subscriptions 4 | type consumer = { 5 | disconnect: unit => unit, 6 | subscriptions: subscriptions, 7 | } 8 | type actionCable = {createConsumer: unit => consumer} 9 | type createConsumer 10 | type subscription<'sendData> = { 11 | consumer: consumer, 12 | send: 'sendData => unit, 13 | } 14 | type subscriprtionCallbacks<'updateData> = { 15 | connected: unit => unit, 16 | received: 'updateData => unit, 17 | disconnected: unit => unit, 18 | } 19 | 20 | @val @scope("window") @return(nullable) 21 | external webSocket: option = "WebSocket" 22 | @val external actionCable: actionCable = "ActionCable" 23 | @send 24 | external createSubscription: ( 25 | subscriptions, 26 | string, 27 | subscriprtionCallbacks<'updateData>, 28 | ) => subscription<'sendData'> = "create" 29 | @send external sendData: (subscription<'sendData>, 'sendData) => unit = "send" 30 | @send external unsubscribeSubscription: subscription<'sendData'> => unit = "unsubscribe" 31 | @send external diconnectConsumer: consumer => unit = "disconnect" 32 | 33 | let subscribeConsumer = ( 34 | channnelName: string, 35 | subscriprtionCallbacks: subscriprtionCallbacks<'updateData>, 36 | ) => { 37 | let consumer = actionCable.createConsumer() 38 | createSubscription(consumer.subscriptions, channnelName, subscriprtionCallbacks) 39 | } 40 | -------------------------------------------------------------------------------- /client/app/bundles/comments/rescript/bindings/Axios.res: -------------------------------------------------------------------------------- 1 | @module("axios") external post: (string, {..}, {..}) => promise = "post" 2 | -------------------------------------------------------------------------------- /client/app/bundles/comments/rescript/bindings/Marked.res: -------------------------------------------------------------------------------- 1 | type markedOptions = {gfm: bool} 2 | @module("marked") external marked: (string, markedOptions) => string = "marked" 3 | -------------------------------------------------------------------------------- /client/app/bundles/comments/rescript/bindings/ReactTransitionGroup.res: -------------------------------------------------------------------------------- 1 | module TransitionGroup = { 2 | @react.component @module("react-transition-group") 3 | external make: ( 4 | ~children: React.element, 5 | ~className: string, 6 | ~component: string, 7 | ) => React.element = "TransitionGroup" 8 | } 9 | 10 | module CSSTransition = { 11 | type t = { 12 | enter: string, 13 | enterActive: string, 14 | exit: string, 15 | exitActive: string, 16 | } 17 | 18 | @react.component @module("react-transition-group") 19 | external make: ( 20 | ~children: React.element, 21 | ~key: string, 22 | ~timeout: int, 23 | ~nodeRef: React.ref>, 24 | ~classNames: t, 25 | ) => React.element = "CSSTransition" 26 | } 27 | -------------------------------------------------------------------------------- /client/app/bundles/comments/routes/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | import Layout from '../layout/Layout.jsx'; 4 | import TestReactRouter from '../components/TestReactRouter/TestReactRouter.jsx'; 5 | import TestReactRouterRedirect from '../components/TestReactRouterRedirect/TestReactRouterRedirect.jsx'; 6 | import RouterCommentsContainer from '../containers/RouterCommentsContainer.jsx'; 7 | 8 | export default ( 9 | 10 | 11 | } /> 12 | } /> 13 | } /> 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /client/app/bundles/comments/startup/App.jsx: -------------------------------------------------------------------------------- 1 | import { Provider } from 'react-redux'; 2 | import React from 'react'; 3 | import ReactOnRails from 'react-on-rails'; 4 | 5 | import NonRouterCommentsContainer from '../containers/NonRouterCommentsContainer.jsx'; 6 | import 'intl/locale-data/jsonp/en'; 7 | import 'intl/locale-data/jsonp/de'; 8 | import 'intl/locale-data/jsonp/ja'; 9 | import 'intl/locale-data/jsonp/zh'; 10 | 11 | function App(_props, _railsContext) { 12 | const store = ReactOnRails.getStore('commentsStore'); 13 | 14 | return function AppComponent() { 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /client/app/bundles/comments/startup/ClientRouterApp.jsx: -------------------------------------------------------------------------------- 1 | // Compare to ../ServerRouterApp.jsx 2 | import { Provider } from 'react-redux'; 3 | import React from 'react'; 4 | import ReactOnRails from 'react-on-rails'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | import routes from '../routes/routes.jsx'; 7 | 8 | function ClientRouterApp(_props) { 9 | const store = ReactOnRails.getStore('routerCommentsStore'); 10 | 11 | // eslint-disable-next-line react/display-name 12 | return ( 13 | 14 | {routes} 15 | 16 | ); 17 | } 18 | 19 | export default ClientRouterApp; 20 | -------------------------------------------------------------------------------- /client/app/bundles/comments/startup/ClientRouterAppExpress.jsx: -------------------------------------------------------------------------------- 1 | // Compare to ../ServerRouterApp.jsx 2 | import { Provider } from 'react-redux'; 3 | import React from 'react'; 4 | import ReactOnRails from 'react-on-rails'; 5 | import { Router, browserHistory } from 'react-router'; 6 | import { syncHistoryWithStore } from 'react-router-redux'; 7 | 8 | import routes from '../routes/routes.jsx'; 9 | 10 | // Because of https://github.com/shakacode/react_on_rails/issues/504 11 | // we need to skip using a shared store for the express server startup. 12 | import createStore from '../store/routerCommentsStore'; 13 | 14 | function RouterAppExpress(_props, _railsContext) { 15 | // See comment above 16 | const store = createStore(_props); 17 | 18 | // Create an enhanced history that syncs navigation events with the store 19 | const history = syncHistoryWithStore(browserHistory, store); 20 | 21 | return ( 22 | 23 | {routes} 24 | 25 | ); 26 | } 27 | 28 | ReactOnRails.register({ 29 | RouterAppExpress, 30 | }); 31 | -------------------------------------------------------------------------------- /client/app/bundles/comments/startup/NavigationBarApp.jsx: -------------------------------------------------------------------------------- 1 | // Top level component for client side. 2 | // Compare this to the ./ServerApp.jsx file which is used for server side rendering. 3 | 4 | import { Provider } from 'react-redux'; 5 | import React from 'react'; 6 | import ReactOnRails from 'react-on-rails'; 7 | 8 | import NavigationBar from '../components/NavigationBar/NavigationBar.jsx'; 9 | import NavigationBarContainer from '../containers/NavigationBarContainer.jsx'; 10 | import * as paths from '../constants/paths'; 11 | 12 | /* 13 | * Export a function that returns a ReactComponent, depending on a store named SharedReduxStore. 14 | * This is used for the client rendering hook after the page html is rendered. 15 | * React will see that the state is the same and not do anything. 16 | */ 17 | function NavigationBarAppFactory(_props, railsContext) { 18 | // This is where we get the existing store. 19 | const { pathname } = railsContext; 20 | let store; 21 | if (pathname === paths.ROUTER_PATH) { 22 | store = ReactOnRails.getStore('routerCommentsStore', false); 23 | } else if (pathname === paths.NO_ROUTER_PATH) { 24 | store = ReactOnRails.getStore('commentsStore', false); 25 | } else { 26 | return function NavigationBarApp() { 27 | return ; 28 | }; 29 | } 30 | 31 | // eslint interprets the return as a new component definition, which is not the case 32 | // eslint-disable-next-line react/display-name, react/no-unstable-nested-components 33 | return function NavigationBarApp() { 34 | return ( 35 | 36 | 37 | 38 | ); 39 | }; 40 | } 41 | 42 | export default NavigationBarAppFactory; 43 | -------------------------------------------------------------------------------- /client/app/bundles/comments/startup/ServerRouterApp.jsx: -------------------------------------------------------------------------------- 1 | // Compare to ../ClientRouterApp.jsx 2 | import { Provider } from 'react-redux'; 3 | import React from 'react'; 4 | import { StaticRouter } from 'react-router-dom/server'; 5 | import ReactOnRails from 'react-on-rails'; 6 | import routes from '../routes/routes.jsx'; 7 | 8 | function ServerRouterApp(_props, railsContext) { 9 | const store = ReactOnRails.getStore('routerCommentsStore'); 10 | 11 | let error; 12 | let redirectLocation; 13 | const { location } = railsContext; 14 | 15 | // This tell react_on_rails to skip server rendering any HTML. Note, client rendering 16 | // will handle the redirect. What's key is that we don't try to render. 17 | // Critical to return the Object properties to match this { error, redirectLocation } 18 | if (error || redirectLocation) { 19 | return { error, redirectLocation }; 20 | } 21 | 22 | // Allows components to add properties to the object to store 23 | // information about the render. 24 | const context = {}; 25 | 26 | // Important that you don't do this if you are redirecting or have an error. 27 | // eslint-disable-next-line react/display-name 28 | return function ServerRouter() { 29 | return ( 30 | 31 | 32 | {routes} 33 | 34 | 35 | ); 36 | }; 37 | } 38 | 39 | export default ServerRouterApp; 40 | -------------------------------------------------------------------------------- /client/app/bundles/comments/startup/serverRegistration.jsx: -------------------------------------------------------------------------------- 1 | // Example of React + Redux 2 | import ReactOnRails from 'react-on-rails'; 3 | 4 | import App from './App'; 5 | import RouterApp from './ServerRouterApp'; 6 | import SimpleCommentScreen from '../components/SimpleCommentScreen/SimpleCommentScreen'; 7 | import NavigationBarApp from './NavigationBarApp'; 8 | import routerCommentsStore from '../store/routerCommentsStore'; 9 | import commentsStore from '../store/commentsStore'; 10 | import Footer from '../components/Footer/Footer'; 11 | 12 | ReactOnRails.register({ 13 | App, 14 | RouterApp, 15 | NavigationBarApp, 16 | SimpleCommentScreen, 17 | Footer, 18 | }); 19 | 20 | ReactOnRails.registerStore({ 21 | routerCommentsStore, 22 | commentsStore, 23 | }); 24 | -------------------------------------------------------------------------------- /client/app/bundles/comments/store/commentsStore.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | import Immutable from 'immutable'; 3 | import { compose, createStore, applyMiddleware, combineReducers } from 'redux'; 4 | import thunkMiddleware from 'redux-thunk'; 5 | import loggerMiddleware from '../../../libs/middlewares/loggerMiddleware'; 6 | import reducers, { initialStates } from '../reducers'; 7 | 8 | export default (props, railsContext) => { 9 | const initialComments = props.comments; 10 | const { $$commentsState } = initialStates; 11 | 12 | const initialState = { 13 | $$commentsStore: $$commentsState.merge({ 14 | $$comments: Immutable.fromJS(initialComments), 15 | }), 16 | railsContext, 17 | }; 18 | 19 | const reducer = combineReducers(reducers); 20 | const composedStore = compose(applyMiddleware(thunkMiddleware, loggerMiddleware)); 21 | 22 | return composedStore(createStore)(reducer, initialState); 23 | }; 24 | -------------------------------------------------------------------------------- /client/app/bundles/comments/store/routerCommentsStore.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | import Immutable from 'immutable'; 3 | import { applyMiddleware, compose, createStore, combineReducers } from 'redux'; 4 | import { routerReducer } from 'react-router-redux'; 5 | import thunkMiddleware from 'redux-thunk'; 6 | 7 | import loggerMiddleware from '../../../libs/middlewares/loggerMiddleware'; 8 | 9 | import reducers, { initialStates } from '../reducers'; 10 | 11 | export default (props, railsContext) => { 12 | const initialComments = props.comments; 13 | const { $$commentsState } = initialStates; 14 | 15 | const initialState = { 16 | $$commentsStore: $$commentsState.merge({ 17 | $$comments: Immutable.fromJS(initialComments), 18 | }), 19 | railsContext, 20 | }; 21 | 22 | // https://github.com/reactjs/react-router-redux 23 | const reducer = combineReducers({ 24 | ...reducers, 25 | routing: routerReducer, 26 | }); 27 | 28 | // Sync dispatched route actions to the history 29 | const finalCreateStore = compose(applyMiddleware(thunkMiddleware, loggerMiddleware))(createStore); 30 | 31 | return finalCreateStore(reducer, initialState); 32 | }; 33 | -------------------------------------------------------------------------------- /client/app/controllers/comments_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from '@hotwired/stimulus'; 2 | import * as ActionCable from '@rails/actioncable'; 3 | import * as marked from 'marked'; 4 | import { gfmHeadingId } from 'marked-gfm-heading-id'; 5 | import { mangle } from 'marked-mangle'; 6 | 7 | marked.use(gfmHeadingId()); 8 | marked.use(mangle()); 9 | 10 | export default class extends Controller { 11 | static targets = ['commentList', 'commentAuthor', 'commentText', 'commentRefresh', 'alertDiv', 'errorList']; 12 | 13 | resetText() { 14 | const inputAuthor = this.commentAuthorTarget; 15 | const inputText = this.commentTextTarget; 16 | const alertDiv = this.alertDivTarget; 17 | const errorList = this.errorListTarget; 18 | 19 | const errors = []; 20 | 21 | if (!inputAuthor.value || !inputText.value) { 22 | errorList.innerHTML = ''; 23 | if (!inputAuthor.value) { 24 | errors.push('Author'); 25 | } else if (!inputText.value) { 26 | errors.push('Text'); 27 | } else { 28 | errors.push('Author'); 29 | errors.push('Text'); 30 | } 31 | errors.forEach((error) => { 32 | const errorString = `
  • ${error}: can't be blank
  • `; 33 | errorList.insertAdjacentHTML('afterbegin', errorString); 34 | }); 35 | alertDiv.classList.remove('hidden'); 36 | } else { 37 | alertDiv.classList.add('hidden'); 38 | errorList.innerHTML = ''; 39 | inputText.value = ''; 40 | this.refreshCommentList(); 41 | } 42 | } 43 | 44 | refreshCommentList() { 45 | const refreshBtn = this.commentRefreshTarget; 46 | refreshBtn.click(); 47 | } 48 | 49 | connect() { 50 | console.log('connected to Stimulus comments_controller'); 51 | 52 | const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; 53 | const cableUrl = `${protocol}${window.location.hostname}:${window.location.port}/cable`; 54 | 55 | this.cable = ActionCable.createConsumer(cableUrl); 56 | 57 | this.cable.subscriptions.create('CommentsChannel', { 58 | connected: () => { 59 | console.log('connected to comments channel using Stimulus controller'); 60 | }, 61 | disconnected: () => { 62 | console.log('disconnected from comments channel via Stimulus'); 63 | }, 64 | received: (comment) => { 65 | const htmlText = marked.parse(comment.text); 66 | const htmlComment = `

    ${comment.author}

    ${htmlText}
    `; 67 | this.commentListTarget.insertAdjacentHTML('afterbegin', htmlComment); 68 | }, 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /client/app/controllers/index.js: -------------------------------------------------------------------------------- 1 | import { Application } from '@hotwired/stimulus'; 2 | import { definitionsFromContext } from '@hotwired/stimulus-webpack-helpers'; 3 | 4 | const application = Application.start(); 5 | const context = require.context('.', true, /\.js$/); 6 | application.load(definitionsFromContext(context)); 7 | -------------------------------------------------------------------------------- /client/app/libs/components/BaseComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* eslint-disable react/prefer-stateless-function */ 4 | export default class BaseComponent extends React.PureComponent {} 5 | -------------------------------------------------------------------------------- /client/app/libs/i18n/selectLanguage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { defaultLocale } from './default'; 4 | 5 | function SelectLanguage(onChange, locale = defaultLocale) { 6 | return ( 7 | 18 | ); 19 | } 20 | 21 | export default SelectLanguage; 22 | -------------------------------------------------------------------------------- /client/app/libs/middlewares/loggerMiddleware.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | export default function logger({ getState }) { 4 | return (next) => (action) => { 5 | // TODO: Replace this file with redux-logger and move this conditional to helper 6 | // TODO: where we're setting up the included middleware. 7 | if (process.env.NODE_ENV !== 'development') { 8 | return next(action); 9 | } 10 | 11 | console.log('will dispatch', action); 12 | 13 | // Call the next dispatch method in the middleware chain. 14 | const result = next(action); 15 | 16 | // We can't _read_ immutable objects in console out-of-the-box. 17 | const state = getState(); 18 | const readableState = {}; 19 | 20 | Object.keys(state).forEach((storeItem) => { 21 | readableState[storeItem] = state[storeItem].toJS ? state[storeItem].toJS() : state[storeItem]; 22 | }); 23 | 24 | console.log('state after dispatch', readableState); 25 | 26 | // This will likely be the action itself, unless 27 | // a middleware further in chain changed it. 28 | return result; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /client/app/libs/requestsManager.js: -------------------------------------------------------------------------------- 1 | import request from 'axios'; 2 | import ReactOnRails from 'react-on-rails'; 3 | 4 | const API_URL = 'comments.json'; 5 | 6 | export default { 7 | /** 8 | * Retrieve list of entities from server using AJAX call. 9 | * 10 | * @returns {Promise} - Result of ajax call. 11 | */ 12 | fetchEntities() { 13 | return request({ 14 | method: 'GET', 15 | url: API_URL, 16 | responseType: 'json', 17 | }); 18 | }, 19 | 20 | /** 21 | * Submit new entity to server using AJAX call. 22 | * 23 | * @param {Object} entity - Request body to post. 24 | * @returns {Promise} - Result of ajax call. 25 | */ 26 | submitEntity(entity) { 27 | return request({ 28 | method: 'POST', 29 | url: API_URL, 30 | responseType: 'json', 31 | headers: ReactOnRails.authenticityHeaders(), 32 | data: entity, 33 | }); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /client/app/libs/testHelper.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import React from 'react'; 3 | import TestUtils from 'react-dom/test-utils'; 4 | 5 | export { React, TestUtils }; 6 | -------------------------------------------------------------------------------- /client/app/packs/client-bundle.js: -------------------------------------------------------------------------------- 1 | import ReactOnRails from 'react-on-rails'; 2 | // eslint-disable-next-line import/no-webpack-loader-syntax 3 | import 'expose-loader?exposes=$,jQuery!jquery'; 4 | import 'jquery-ujs'; 5 | 6 | import App from '../bundles/comments/startup/App'; 7 | import RouterApp from '../bundles/comments/startup/ClientRouterApp'; 8 | import SimpleCommentScreen from '../bundles/comments/components/SimpleCommentScreen/SimpleCommentScreen'; 9 | import routerCommentsStore from '../bundles/comments/store/routerCommentsStore'; 10 | import commentsStore from '../bundles/comments/store/commentsStore'; 11 | import NavigationBarApp from '../bundles/comments/startup/NavigationBarApp'; 12 | import Footer from '../bundles/comments/components/Footer/Footer'; 13 | import RescriptShow from '../bundles/comments/rescript/ReScriptShow.bs.js'; 14 | 15 | import '../assets/styles/application'; 16 | 17 | ReactOnRails.setOptions({ 18 | // traceTurbolinks: process.env.TRACE_TURBOLINKS, // eslint-disable-line no-undef 19 | // process.env.TRACE_TURBOLINKS -> error: development is not defined 20 | traceTurbolinks: true, 21 | }); 22 | 23 | ReactOnRails.register({ 24 | App, 25 | RouterApp, 26 | NavigationBarApp, 27 | SimpleCommentScreen, 28 | Footer, 29 | RescriptShow, 30 | }); 31 | 32 | ReactOnRails.registerStore({ 33 | routerCommentsStore, 34 | commentsStore, 35 | }); 36 | -------------------------------------------------------------------------------- /client/app/packs/server-bundle.js: -------------------------------------------------------------------------------- 1 | // Example of React + Redux 2 | import ReactOnRails from 'react-on-rails'; 3 | 4 | import App from '../bundles/comments/startup/App'; 5 | import RouterApp from '../bundles/comments/startup/ServerRouterApp'; 6 | import SimpleCommentScreen from '../bundles/comments/components/SimpleCommentScreen/SimpleCommentScreen'; 7 | import NavigationBarApp from '../bundles/comments/startup/NavigationBarApp'; 8 | import routerCommentsStore from '../bundles/comments/store/routerCommentsStore'; 9 | import commentsStore from '../bundles/comments/store/commentsStore'; 10 | import Footer from '../bundles/comments/components/Footer/Footer'; 11 | import RescriptShow from '../bundles/comments/rescript/ReScriptShow.bs.js'; 12 | 13 | ReactOnRails.register({ 14 | App, 15 | RouterApp, 16 | NavigationBarApp, 17 | SimpleCommentScreen, 18 | Footer, 19 | RescriptShow, 20 | }); 21 | 22 | ReactOnRails.registerStore({ 23 | routerCommentsStore, 24 | commentsStore, 25 | }); 26 | -------------------------------------------------------------------------------- /client/app/packs/stimulus-bundle.js: -------------------------------------------------------------------------------- 1 | import ReactOnRails from 'react-on-rails'; 2 | import 'jquery-ujs'; 3 | import { Turbo } from '@hotwired/turbo-rails'; 4 | 5 | // eslint-disable-next-line no-unused-vars 6 | import controllers from '../controllers'; 7 | 8 | import NavigationBarApp from '../bundles/comments/startup/NavigationBarApp'; 9 | import Footer from '../bundles/comments/components/Footer/Footer'; 10 | 11 | import '../assets/styles/application'; 12 | 13 | Turbo.session.drive = false; 14 | 15 | ReactOnRails.setOptions({ 16 | // traceTurbolinks: process.env.TRACE_TURBOLINKS, // eslint-disable-line no-undef 17 | // process.env.TRACE_TURBOLINKS -> error: development is not defined 18 | traceTurbolinks: true, 19 | }); 20 | 21 | ReactOnRails.register({ 22 | NavigationBarApp, 23 | Footer, 24 | }); 25 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative "config/environment" 6 | 7 | run Rails.application 8 | Rails.application.load_server 9 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "boot" 4 | 5 | require "rails/all" 6 | 7 | # Require the gems listed in Gemfile, including any gems 8 | # you've limited to :test, :development, or :production. 9 | Bundler.require(*Rails.groups) 10 | 11 | module RailsReactTutorial 12 | class Application < Rails::Application 13 | # Initialize configuration defaults for originally generated Rails version. 14 | config.load_defaults 8.0 15 | 16 | # Please, add to the `ignore` list any other `lib` subdirectories that do 17 | # not contain `.rb` files, or that should not be reloaded or eager loaded. 18 | # Common ones are `templates`, `generators`, or `middleware`, for example. 19 | config.autoload_lib(ignore: %w[assets tasks]) 20 | 21 | # Configuration for the application, engines, and railties goes here. 22 | # 23 | # These settings can be overridden in specific environments using the files 24 | # in config/environments, which are processed later. 25 | 26 | config.active_support.to_time_preserves_timezone = false 27 | # config.time_zone = "Central Time (US & Canada)" 28 | # config.eager_load_paths << Rails.root.join("extras") 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 4 | 5 | require "bundler/setup" # Set up gems listed in the Gemfile. 6 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | production: 2 | adapter: redis 3 | url: <%#= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 4 | channel_prefix: rails_react_tutorial_production 5 | 6 | development: 7 | adapter: redis 8 | url: redis://localhost:6379/1 9 | 10 | test: 11 | adapter: async 12 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | # NOTE: Real world apps would not have a different DB for development 8 | #default: &default 9 | # adapter: sqlite3 10 | # pool: 5 11 | # timeout: 5000 12 | # 13 | #development: 14 | # <<: *default 15 | # database: db/development.sqlite3 16 | # 17 | ## Warning: The database defined as "test" will be erased and 18 | ## re-generated from your development database when you run "rake". 19 | ## Do not set this db to the same as development or production. 20 | #test: 21 | # <<: *default 22 | # database: db/test.sqlite3 23 | # 24 | #production: 25 | # <<: *default 26 | # database: db/production.sqlite3 27 | 28 | # Uncomment below for a setup with just postgres and change your Gemfile to reflect this 29 | default: &default 30 | adapter: postgresql 31 | 32 | development: 33 | <<: *default 34 | database: react-webpack-rails-tutoria-developmentl 35 | 36 | # Warning: The database defined as "test" will be erased and 37 | # re-generated from your development database when you run "rake". 38 | # Do not set this db to the same as development or production. 39 | test: 40 | <<: *default 41 | database: react-webpack-rails-tutorial-test 42 | 43 | production: 44 | <<: *default 45 | database: react-webpack-rails-tutorial 46 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require_relative "application" 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | # While tests run files are not watched, reloading is not necessary. 12 | config.enable_reloading = false 13 | 14 | # Eager loading loads your entire application. When running a single test locally, 15 | # this is usually not necessary, and can slow down your test suite. However, it's 16 | # recommended that you enable it in continuous integration systems to ensure eager 17 | # loading is working properly before deploying your code. 18 | config.eager_load = ENV["CI"].present? 19 | 20 | # Configure public file server for tests with cache-control for performance. 21 | config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } 22 | 23 | # Show full error reports. 24 | config.consider_all_requests_local = true 25 | config.cache_store = :null_store 26 | 27 | # Render exception templates for rescuable exceptions and raise for other exceptions. 28 | config.action_dispatch.show_exceptions = :rescuable 29 | 30 | # Disable request forgery protection in test environment. 31 | config.action_controller.allow_forgery_protection = false 32 | 33 | # Store uploaded files on the local file system in a temporary directory. 34 | config.active_storage.service = :test 35 | 36 | # Tell Action Mailer not to deliver emails to the real world. 37 | # The :test delivery method accumulates sent emails in the 38 | # ActionMailer::Base.deliveries array. 39 | config.action_mailer.delivery_method = :test 40 | 41 | # Set host to be used by links generated in mailer templates. 42 | # config.action_mailer.default_url_options = { host: "example.com" } 43 | 44 | # Print deprecation notices to the stderr. 45 | config.active_support.deprecation = :stderr 46 | 47 | # Raises error for missing translations. 48 | # config.i18n.raise_on_missing_translations = true 49 | 50 | # Annotate rendered view with file names. 51 | # config.action_view.annotate_rendered_view_with_filenames = true 52 | 53 | # Raise error when a before_action's only/except options reference missing actions. 54 | config.action_controller.raise_on_missing_callback_actions = true 55 | end 56 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # ActiveSupport::Reloader.to_prepare do 6 | # ApplicationController.renderer.defaults.merge!( 7 | # http_host: 'example.org', 8 | # https: false 9 | # ) 10 | # end 11 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Version of your assets, change this if you want to expire all your assets. 6 | Rails.application.config.assets.version = "1.0" 7 | 8 | # Add additional assets to the asset load path. 9 | # Rails.application.config.assets.paths << Emoji.images_path 10 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 6 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 7 | 8 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 9 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 10 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 11 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Define an application-wide content security policy. 6 | # See the Securing Rails Applications Guide for more information: 7 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 8 | 9 | # Rails.application.configure do 10 | # config.content_security_policy do |policy| 11 | # policy.default_src :self, :https 12 | # policy.font_src :self, :https, :data 13 | # policy.img_src :self, :https, :data 14 | # policy.object_src :none 15 | # policy.script_src :self, :https 16 | # policy.style_src :self, :https 17 | # # Specify URI for violation reports 18 | # # policy.report_uri "/csp-violation-report-endpoint" 19 | # end 20 | # 21 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 22 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 23 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 24 | # 25 | # # Report violations without enforcing the policy. 26 | # # config.content_security_policy_report_only = true 27 | # end 28 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Specify a serializer for the signed and encrypted cookie jars. 6 | # Valid options are :json, :marshal, and :hybrid. 7 | Rails.application.config.action_dispatch.cookies_serializer = :json 8 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Avoid CORS issues when API is called from the frontend app. 6 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin Ajax requests. 7 | 8 | # Read more: https://github.com/cyu/rack-cors 9 | 10 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do 11 | # allow do 12 | # origins "example.com" 13 | # 14 | # resource "*", 15 | # headers: :any, 16 | # methods: [:get, :post, :put, :patch, :delete, :options, :head] 17 | # end 18 | # end 19 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 6 | # Use this to limit dissemination of sensitive information. 7 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 8 | Rails.application.config.filter_parameters += %i[ 9 | passw email secret token _key crypt salt certificate otp ssn cvv cvc 10 | ] 11 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Add new inflection rules using the following format. Inflections 6 | # are locale specific, and you may define rules for as many different 7 | # locales as you wish. All of these examples are active by default: 8 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 9 | # inflect.plural /^(ox)$/i, "\\1en" 10 | # inflect.singular /^(ox)en/i, "\\1" 11 | # inflect.irregular "person", "people" 12 | # inflect.uncountable %w( fish sheep ) 13 | # end 14 | 15 | # These inflection rules are supported but not enabled by default: 16 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 17 | # inflect.acronym "RESTful" 18 | # end 19 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Add new mime types for use in respond_to blocks: 6 | # Mime::Type.register "text/richtext", :rtf 7 | -------------------------------------------------------------------------------- /config/initializers/per_form_csrf_tokens.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Enable per-form CSRF tokens. 6 | Rails.application.config.action_controller.per_form_csrf_tokens = true 7 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Define an application-wide HTTP permissions policy. For further 6 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy 7 | 8 | # Rails.application.config.permissions_policy do |policy| 9 | # policy.camera :none 10 | # policy.gyroscope :none 11 | # policy.microphone :none 12 | # policy.usb :none 13 | # policy.fullscreen :self 14 | # policy.payment :self, "https://secure.example.com" 15 | # end 16 | -------------------------------------------------------------------------------- /config/initializers/request_forgery_protection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Enable origin-checking CSRF mitigation. 6 | Rails.application.config.action_controller.forgery_protection_origin_check = true 7 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | Rails.application.config.session_store :cookie_store, key: "_rails-react-tutorial_session" 6 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # This file contains settings for ActionController::ParamsWrapper which 6 | # is enabled by default. 7 | 8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 9 | ActiveSupport.on_load(:action_controller) do 10 | wrap_parameters format: [:json] 11 | end 12 | 13 | # To enable root element in JSON for ActiveRecord objects. 14 | # ActiveSupport.on_load(:active_record) do 15 | # self.include_root_in_json = true 16 | # end 17 | -------------------------------------------------------------------------------- /config/locales/de.yml: -------------------------------------------------------------------------------- 1 | de: 2 | type: "Deutsch" 3 | comments: "Kommentare " 4 | loading: "Laden..." 5 | description: 6 | force_refrech: "Erzwingt die Aktualisierung aller Kommentare." 7 | support_markdown: "Der Text unterstützt Github Flavored Markdown." 8 | delete_rule: "Kommentare, die aelter als 24 Stunden sind, werden gelöscht." 9 | submit_rule: "Der Name wird beibehalten. Der Text wird während des Postens zurücksetzt." 10 | see_action_cable: "So sehen Action Cable sofort aktualisieren zwei Browser, öffnen Sie zwei Browser und senden Sie einen Kommentar!" 11 | form: 12 | horizontal: "Horizontale Form" 13 | stacked: "Gestapelte Form" 14 | inline: "Inline Form" 15 | input: 16 | name: 17 | label: "Name" 18 | placeholder: "Dein Name" 19 | text: 20 | label: "Text" 21 | placeholder: "Sagen Sie etwas mit markdown..." 22 | saving: "Speichern" 23 | post: "Schicken" 24 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | type: "English" 3 | comments: "Comments " 4 | loading: "Loading..." 5 | description: 6 | force_refrech: "Force Refresh of All Comments." 7 | support_markdown: "Text supports Github Flavored Markdown." 8 | delete_rule: "Comments older than 24 hours are deleted." 9 | submit_rule: "Name is preserved. Text is reset, between submits." 10 | see_action_cable: "To see Action Cable instantly update two browsers, open two browsers and submit a comment!" 11 | form: 12 | horizontal: "Horizontal Form" 13 | stacked: "Stacked Form" 14 | inline: "Inline Form" 15 | input: 16 | name: 17 | label: "Name" 18 | placeholder: "Your Name" 19 | text: 20 | label: "Text" 21 | placeholder: "Say something using markdown..." 22 | saving: "Saving" 23 | post: "Post" 24 | comment_was_successfully_created: "Comment was successfully created." 25 | comment_was_successfully_updated: "Comment was successfully updated." 26 | comment_was_successfully_destroyed: "Comment was successfully destroyed." 27 | -------------------------------------------------------------------------------- /config/locales/ja.yml: -------------------------------------------------------------------------------- 1 | ja: 2 | type: "日本語" 3 | comments: "コメント " 4 | loading: "読み込んでいます..." 5 | description: 6 | force_refrech: "すべてのコメントを強制的にリフレッシュします。" 7 | support_markdown: "テキストは Github Flavored Markdown をサポートしています。" 8 | delete_rule: "コメントは24時間後削除されます。" 9 | submit_rule: "送信の間に名前は保存され、テキストがリセットされます。" 10 | see_action_cable: "Action Cableが即座に2つのブラウザを更新することを確認するには、2つのブラウザを開いてコメントを送信してください!" 11 | form: 12 | horizontal: "水平フォーム" 13 | stacked: "積み上げフォーム" 14 | inline: "インラインフォーム" 15 | input: 16 | name: 17 | label: "名前" 18 | placeholder: "あなたの名前" 19 | text: 20 | label: "本文" 21 | placeholder: "何かが Markdown を使用して言います..." 22 | saving: "保存" 23 | post: "サブミット" 24 | -------------------------------------------------------------------------------- /config/locales/zh-CN.yml: -------------------------------------------------------------------------------- 1 | zh-CN: 2 | type: "简体中文" 3 | comments: "评论 " 4 | loading: "载入中..." 5 | description: 6 | force_refrech: "强制更新所有评论。" 7 | support_markdown: "支援 Github Flavored Markdown。" 8 | delete_rule: "评论于24小时后移除。" 9 | submit_rule: "送出评论后, 名字保留并清除内容。" 10 | see_action_cable: "要查看 Action Cable 立即更新两个浏览器,打开两个浏览器并提交评论!" 11 | form: 12 | horizontal: "水平格式" 13 | stacked: "堆叠格式" 14 | inline: "内嵌格式" 15 | input: 16 | name: 17 | label: "名字" 18 | placeholder: "你的名字" 19 | text: 20 | label: "内容" 21 | placeholder: "使用markdown说些什么..." 22 | saving: "储存" 23 | post: "发表" 24 | -------------------------------------------------------------------------------- /config/locales/zh-TW.yml: -------------------------------------------------------------------------------- 1 | zh-TW: 2 | type: "正體中文" 3 | comments: "評論 " 4 | loading: "載入中..." 5 | description: 6 | force_refrech: "強制更新所有評論。" 7 | support_markdown: "支援 Github Flavored Markdown。" 8 | delete_rule: "評論於24小時後移除。" 9 | submit_rule: "送出評論後, 名字保留並清除內容。" 10 | see_action_cable: "要查看 Action Cable 立即更新兩個瀏覽器,打開兩個瀏覽器並提交評論!" 11 | form: 12 | horizontal: "水平格式" 13 | stacked: "堆疊格式" 14 | inline: "內嵌格式" 15 | input: 16 | name: 17 | label: "名字" 18 | placeholder: "你的名字" 19 | text: 20 | label: "內容" 21 | placeholder: "使用markdown說些什麼..." 22 | saving: "儲存" 23 | post: "發表" 24 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This configuration file will be evaluated by Puma. The top-level methods that 4 | # are invoked here are part of Puma's configuration DSL. For more information 5 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. 6 | # 7 | # Puma starts a configurable number of processes (workers) and each process 8 | # serves each request in a thread from an internal thread pool. 9 | # 10 | # You can control the number of workers using ENV["WEB_CONCURRENCY"]. You 11 | # should only set this value when you want to run 2 or more workers. The 12 | # default is already 1. 13 | # 14 | # The ideal number of threads per worker depends both on how much time the 15 | # application spends waiting for IO operations and on how much you wish to 16 | # prioritize throughput over latency. 17 | # 18 | # As a rule of thumb, increasing the number of threads will increase how much 19 | # traffic a given process can handle (throughput), but due to CRuby's 20 | # Global VM Lock (GVL) it has diminishing returns and will degrade the 21 | # response time (latency) of the application. 22 | # 23 | # The default is set to 3 threads as it's deemed a decent compromise between 24 | # throughput and latency for the average Rails application. 25 | # 26 | # Any libraries that use a connection pool or another resource pool should 27 | # be configured to provide at least as many connections as the number of 28 | # threads. This includes Active Record's `pool` parameter in `database.yml`. 29 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) 30 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 31 | threads min_threads_count, max_threads_count 32 | 33 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 34 | # terminating a worker in development environments. 35 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 36 | 37 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 38 | port ENV.fetch("PORT", 3000) 39 | 40 | # Allow puma to be restarted by `bin/rails restart` command. 41 | plugin :tmp_restart 42 | 43 | # Run the Solid Queue supervisor inside of Puma for single-server deployments 44 | # plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] 45 | 46 | # Specifies the `environment` that Puma will run in. 47 | environment ENV.fetch("RAILS_ENV", "development") 48 | 49 | # Specify the PID file. Defaults to tmp/pids/server.pid in development. 50 | # In other environments, only set the PID file if requested. 51 | pidfile ENV["PIDFILE"] if ENV["PIDFILE"] 52 | -------------------------------------------------------------------------------- /config/rails_best_practices.yml: -------------------------------------------------------------------------------- 1 | AddModelVirtualAttributeCheck: { } 2 | AlwaysAddDbIndexCheck: { } 3 | CheckSaveReturnValueCheck: { } 4 | CheckDestroyReturnValueCheck: { } 5 | DefaultScopeIsEvilCheck: { } 6 | DryBundlerInCapistranoCheck: { } 7 | HashSyntaxCheck: { } 8 | IsolateSeedDataCheck: { } 9 | KeepFindersOnTheirOwnModelCheck: { } 10 | LawOfDemeterCheck: { } 11 | #LongLineCheck: { max_line_length: 80 } 12 | MoveCodeIntoControllerCheck: { } 13 | MoveCodeIntoHelperCheck: { array_count: 3 } 14 | MoveCodeIntoModelCheck: { use_count: 2 } 15 | MoveFinderToNamedScopeCheck: { } 16 | MoveModelLogicIntoModelCheck: { use_count: 4 } 17 | NeedlessDeepNestingCheck: { nested_count: 2 } 18 | NotRescueExceptionCheck: { } 19 | NotUseDefaultRouteCheck: { } 20 | NotUseTimeAgoInWordsCheck: { } 21 | OveruseRouteCustomizationsCheck: { customize_count: 3 } 22 | ProtectMassAssignmentCheck: { } 23 | RemoveEmptyHelpersCheck: { } 24 | RemoveTabCheck: { } 25 | RemoveTrailingWhitespaceCheck: { } 26 | RemoveUnusedMethodsInControllersCheck: { except_methods: [] } 27 | RemoveUnusedMethodsInHelpersCheck: { except_methods: [] } 28 | RemoveUnusedMethodsInModelsCheck: { except_methods: [ 29 | GitCommitSha#reset_current_sha 30 | ]} 31 | ReplaceComplexCreationWithFactoryMethodCheck: { attribute_assignment_count: 2 } 32 | ReplaceInstanceVariableWithLocalVariableCheck: { } 33 | RestrictAutoGeneratedRoutesCheck: { } 34 | SimplifyRenderInControllersCheck: { } 35 | SimplifyRenderInViewsCheck: { } 36 | UseBeforeFilterCheck: { customize_count: 2 } 37 | UseModelAssociationCheck: { } 38 | UseMultipartAlternativeAsContentTypeOfEmailCheck: { } 39 | UseParenthesesInMethodDefCheck: { } 40 | UseObserverCheck: { } 41 | UseQueryAttributeCheck: { } 42 | UseSayWithTimeInMigrationsCheck: { } 43 | UseScopeAccessCheck: { } 44 | UseTurboSprocketsRails3Check: { } 45 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 5 | 6 | # Serve websocket cable requests in-process 7 | # mount ActionCable.server => '/cable' 8 | 9 | root "pages#index" 10 | 11 | get "simple", to: "pages#simple" 12 | get "rescript", to: "pages#rescript" 13 | get "no-router", to: "pages#no_router" 14 | 15 | # React Router needs a wildcard 16 | get "react-router(/*all)", to: "pages#index" 17 | 18 | get "stimulus", to: "comments#stimulus" 19 | get "horizontal-form", to: "comments#horizontal_form" 20 | get "stacked-form", to: "comments#stacked_form" 21 | get "inline-form", to: "comments#inline_form" 22 | get "comment-list", to: "comments#comment_list" 23 | resources :comments 24 | mount ActionCable.server => "/cable" 25 | end 26 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 231bf79489c63f8c8facd7bf27db1c2582a42a7f4302fccdb74ef35bc5dc91fb4e19dbf167f3003bdb4073818dfab4a9916890d193d535a7be458dbef1609800 15 | 16 | test: 17 | secret_key_base: 1ab8adbcf8410aebbce9b6dd6db7b5d090297bd22cf789b91ff44ae02711e8c128453d3e5c97eadf9066efe1a1e0dc1921faf7314d566c114d3ed60ae7ea614c 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /config/shakapacker.yml: -------------------------------------------------------------------------------- 1 | # Note: You must restart bin/shakapacker-dev-server for changes to take effect 2 | 3 | default: &default 4 | source_path: client/app 5 | source_entry_path: packs 6 | public_root_path: public 7 | public_output_path: packs 8 | cache_path: tmp/shakapacker 9 | webpack_compile_output: true 10 | 11 | # Additional paths webpack should lookup modules 12 | # ['app/assets', 'engine/foo/app/assets'] 13 | additional_paths: [] 14 | 15 | # Reload manifest.json on all requests so we reload latest compiled packs 16 | cache_manifest: false 17 | 18 | # Use the config.build_production_command in config/initializers/react_on_rails.rb 19 | shakapacker_precompile: false 20 | 21 | development: 22 | <<: *default 23 | # This is false since we're running `bin/shakapacker -w` in Procfile.dev-setic 24 | compile: false 25 | 26 | # Reference: https://webpack.js.org/configuration/dev-server/ 27 | dev_server: 28 | https: false 29 | host: localhost 30 | port: 3035 31 | # Hot Module Replacement updates modules while the application is running without a full reload 32 | hmr: true 33 | client: 34 | # Should we show a full-screen overlay in the browser when there are compiler errors or warnings? 35 | overlay: true 36 | # May also be a string 37 | # webSocketURL: 38 | # hostname: "0.0.0.0" 39 | # pathname: "/ws" 40 | # port: 8080 41 | compress: true 42 | # Note that apps that do not check the host are vulnerable to DNS rebinding attacks 43 | allowed_hosts: [ 'localhost' ] 44 | pretty: true 45 | headers: 46 | 'Access-Control-Allow-Origin': '*' 47 | static: 48 | watch: 49 | ignored: '**/node_modules/**' 50 | 51 | test: 52 | <<: *default 53 | compile: true 54 | 55 | production: 56 | <<: *default 57 | 58 | # Production depends on precompilation of packs prior to booting for performance. 59 | compile: false 60 | 61 | # Cache manifest.json for performance 62 | cache_manifest: true 63 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/webpack/DEBUGGING.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | # https://github.com/rails/webpacker/pull/2018 4 | 5 | To debug: 6 | ```sh 7 | bin/shakapacker --debug-shakapacker 8 | ``` 9 | 10 | And put a debugger statement in the file you're editing. 11 | 12 | Here's how to configure the customizations: 13 | 14 | [Webpack Configuration](https://github.com/shakacode/shakapacker#webpack-configuration) 15 | -------------------------------------------------------------------------------- /config/webpack/alias.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | 3 | module.exports = { 4 | resolve: { 5 | alias: { 6 | Assets: resolve(__dirname, '..', '..', 'client', 'app', 'assets'), 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /config/webpack/client.js: -------------------------------------------------------------------------------- 1 | const devBuild = process.env.NODE_ENV === 'development'; 2 | const isHMR = process.env.WEBPACK_DEV_SERVER === 'TRUE'; 3 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 4 | const webpack = require('webpack'); 5 | const environment = require('./environment'); 6 | 7 | if (devBuild && !isHMR) { 8 | environment.loaders.get('sass').use.find((item) => item.loader === 'sass-loader').options.sourceMap = false; 9 | } 10 | 11 | environment.plugins.append( 12 | 'Provide', 13 | new webpack.ProvidePlugin({ 14 | $: 'jquery', 15 | jQuery: 'jquery', 16 | jquery: 'jquery', 17 | 'window.jQuery': 'jquery', 18 | Popper: ['popper.js', 'default'], 19 | }), 20 | ); 21 | 22 | if (devBuild && isHMR) { 23 | environment.plugins.insert('ReactRefreshWebpackPlugin', new ReactRefreshWebpackPlugin()); 24 | } 25 | 26 | module.exports = environment; 27 | -------------------------------------------------------------------------------- /config/webpack/clientWebpackConfig.js: -------------------------------------------------------------------------------- 1 | // The source code including full typescript support is available at: 2 | // https://github.com/shakacode/react_on_rails_tutorial_with_ssr_and_hmr_fast_refresh/blob/master/config/webpack/clientWebpackConfig.js 3 | 4 | const webpack = require('webpack'); 5 | const commonWebpackConfig = require('./commonWebpackConfig'); 6 | 7 | const configureClient = () => { 8 | const clientConfig = commonWebpackConfig(); 9 | 10 | clientConfig.plugins.push( 11 | new webpack.ProvidePlugin({ 12 | $: 'jquery', 13 | jQuery: 'jquery', 14 | ActionCable: '@rails/actioncable', 15 | }), 16 | ); 17 | 18 | // server-bundle is special and should ONLY be built by the serverConfig 19 | // In case this entry is not deleted, a very strange "window" not found 20 | // error shows referring to window["webpackJsonp"]. That is because the 21 | // client config is going to try to load chunks. 22 | delete clientConfig.entry['server-bundle']; 23 | 24 | return clientConfig; 25 | }; 26 | 27 | module.exports = configureClient; 28 | -------------------------------------------------------------------------------- /config/webpack/commonWebpackConfig.js: -------------------------------------------------------------------------------- 1 | // The source code including full typescript support is available at: 2 | // https://github.com/shakacode/react_on_rails_tutorial_with_ssr_and_hmr_fast_refresh/blob/master/config/webpack/commonWebpackConfig.js 3 | 4 | // Common configuration applying to client and server configuration 5 | const { generateWebpackConfig, merge } = require('shakapacker'); 6 | 7 | const baseClientWebpackConfig = generateWebpackConfig(); 8 | const commonOptions = { 9 | resolve: { 10 | extensions: ['.css', '.ts', '.tsx'], 11 | }, 12 | }; 13 | 14 | // add sass resource loader 15 | const sassLoaderConfig = { 16 | loader: 'sass-resources-loader', 17 | options: { 18 | resources: './client/app/assets/styles/app-variables.scss', 19 | }, 20 | }; 21 | 22 | const ignoreWarningsConfig = { 23 | ignoreWarnings: [/Module not found: Error: Can't resolve 'react-dom\/client'/], 24 | }; 25 | 26 | const scssConfigIndex = baseClientWebpackConfig.module.rules.findIndex((config) => 27 | '.scss'.match(config.test), 28 | ); 29 | 30 | baseClientWebpackConfig.module.rules[scssConfigIndex].use.push(sassLoaderConfig); 31 | 32 | // Copy the object using merge b/c the baseClientWebpackConfig and commonOptions are mutable globals 33 | const commonWebpackConfig = () => merge({}, baseClientWebpackConfig, commonOptions, ignoreWarningsConfig); 34 | 35 | module.exports = commonWebpackConfig; 36 | -------------------------------------------------------------------------------- /config/webpack/development.js: -------------------------------------------------------------------------------- 1 | // The source code including full typescript support is available at: 2 | // https://github.com/shakacode/react_on_rails_tutorial_with_ssr_and_hmr_fast_refresh/blob/master/config/webpack/development.js 3 | 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 5 | 6 | const { devServer, inliningCss } = require('shakapacker'); 7 | 8 | const webpackConfig = require('./webpackConfig'); 9 | 10 | const developmentEnvOnly = (clientWebpackConfig, _serverWebpackConfig) => { 11 | // plugins 12 | if (inliningCss) { 13 | // Note, when this is run, we're building the server and client bundles in separate processes. 14 | // Thus, this plugin is not applied to the server bundle. 15 | 16 | // eslint-disable-next-line global-require 17 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 18 | clientWebpackConfig.plugins.push( 19 | new ReactRefreshWebpackPlugin({ 20 | overlay: { 21 | sockPort: devServer.port, 22 | }, 23 | }), 24 | ); 25 | } 26 | }; 27 | 28 | module.exports = webpackConfig(developmentEnvOnly); 29 | -------------------------------------------------------------------------------- /config/webpack/production.js: -------------------------------------------------------------------------------- 1 | // The source code including full typescript support is available at: 2 | // https://github.com/shakacode/react_on_rails_tutorial_with_ssr_and_hmr_fast_refresh/blob/master/config/webpack/production.js 3 | 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'production'; 5 | 6 | const webpackConfig = require('./webpackConfig'); 7 | 8 | const productionEnvOnly = (_clientWebpackConfig, _serverWebpackConfig) => { 9 | // place any code here that is for production only 10 | }; 11 | 12 | module.exports = webpackConfig(productionEnvOnly); 13 | -------------------------------------------------------------------------------- /config/webpack/server.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | 3 | const devBuild = process.env.NODE_ENV === 'production' ? 'production' : 'development'; 4 | const webpack = require('webpack'); 5 | 6 | const environment = require('./environment'); 7 | 8 | // React Server Side Rendering shakapacker config 9 | // Builds a Node compatible file that React on Rails can load, never served to the client. 10 | 11 | environment.plugins.insert( 12 | 'DefinePlugin', 13 | new webpack.DefinePlugin({ 14 | TRACE_TURBOLINKS: true, 15 | 'process.env': { 16 | NODE_ENV: devBuild, 17 | }, 18 | }), 19 | { after: 'Environment' }, 20 | ); 21 | const serverConfig = merge(environment.toWebpackConfig(), { 22 | target: 'web', 23 | entry: './client/app/bundles/comments/startup/serverRegistration.jsx', 24 | output: { 25 | filename: 'server-bundle.js', 26 | 27 | // https://webpack.js.org/configuration/output/#outputglobalobject 28 | globalObject: 'this', 29 | }, 30 | optimization: { 31 | minimize: false, 32 | }, 33 | }); 34 | 35 | // Don't hash the server bundle. No need. 36 | serverConfig.plugins = serverConfig.plugins.filter( 37 | (plugin) => plugin.constructor.name !== 'WebpackAssetsManifest', 38 | ); 39 | 40 | module.exports = serverConfig; 41 | -------------------------------------------------------------------------------- /config/webpack/test.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('./webpackConfig'); 2 | 3 | const testOnly = (_clientWebpackConfig, _serverWebpackConfig) => { 4 | // place any code here that is for test only 5 | }; 6 | 7 | module.exports = webpackConfig(testOnly); 8 | -------------------------------------------------------------------------------- /config/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { env, generateWebpackConfig } = require('shakapacker'); 2 | const { existsSync } = require('fs'); 3 | const { resolve } = require('path'); 4 | 5 | const envSpecificConfig = () => { 6 | const path = resolve(__dirname, `${env.nodeEnv}.js`); 7 | if (existsSync(path)) { 8 | console.log(`Loading ENV specific webpack configuration file ${path}`); 9 | return require(path); 10 | } 11 | 12 | return generateWebpackConfig(); 13 | }; 14 | 15 | module.exports = envSpecificConfig(); 16 | -------------------------------------------------------------------------------- /config/webpack/webpackConfig.js: -------------------------------------------------------------------------------- 1 | // The source code including full typescript support is available at: 2 | // https://github.com/shakacode/react_on_rails_tutorial_with_ssr_and_hmr_fast_refresh/blob/master/config/webpack/webpackConfig.js 3 | 4 | const clientWebpackConfig = require('./clientWebpackConfig'); 5 | const serverWebpackConfig = require('./serverWebpackConfig'); 6 | 7 | const webpackConfig = (envSpecific) => { 8 | const clientConfig = clientWebpackConfig(); 9 | const serverConfig = serverWebpackConfig(); 10 | 11 | if (envSpecific) { 12 | envSpecific(clientConfig, serverConfig); 13 | } 14 | 15 | let result; 16 | // For HMR, need to separate the the client and server webpack configurations 17 | if (process.env.WEBPACK_SERVE || process.env.CLIENT_BUNDLE_ONLY) { 18 | // eslint-disable-next-line no-console 19 | console.log('[React on Rails] Creating only the client bundles.'); 20 | result = clientConfig; 21 | } else if (process.env.SERVER_BUNDLE_ONLY) { 22 | // eslint-disable-next-line no-console 23 | console.log('[React on Rails] Creating only the server bundle.'); 24 | result = serverConfig; 25 | } else { 26 | // default is the standard client and server build 27 | // eslint-disable-next-line no-console 28 | console.log('[React on Rails] Creating both client and server bundles.'); 29 | result = [clientConfig, serverConfig]; 30 | } 31 | 32 | // To debug, uncomment next line and inspect "result" 33 | // debugger 34 | return result; 35 | }; 36 | 37 | module.exports = webpackConfig; 38 | -------------------------------------------------------------------------------- /db/.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - 'schema.rb' 4 | - 'seeds.rb' 5 | 6 | Style/Documentation: 7 | Enabled: false 8 | -------------------------------------------------------------------------------- /db/migrate/20140823052830_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :comments do |t| 4 | t.string :author 5 | t.text :text 6 | 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20151015160035_set_default_for_author_and_text.rb: -------------------------------------------------------------------------------- 1 | class SetDefaultForAuthorAndText < ActiveRecord::Migration[5.1] 2 | def up 3 | change_column_default(:comments, :author, "") 4 | change_column_default(:comments, :text, "") 5 | end 6 | 7 | def down 8 | change_column_default(:comments, :author, nil) 9 | change_column_default(:comments, :text, nil) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20151015160334_change_author_and_text_to_not_null.rb: -------------------------------------------------------------------------------- 1 | class ChangeAuthorAndTextToNotNull < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column_null(:comments, :author, false, "") 4 | change_column_null(:comments, :text, false, "") 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20220103221124_create_active_storage_tables.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20170806125915) 2 | class CreateActiveStorageTables < ActiveRecord::Migration[5.2] 3 | def change 4 | create_table :active_storage_blobs do |t| 5 | t.string :key, null: false 6 | t.string :filename, null: false 7 | t.string :content_type 8 | t.text :metadata 9 | t.string :service_name, null: false 10 | t.bigint :byte_size, null: false 11 | t.string :checksum, null: false 12 | t.datetime :created_at, null: false 13 | 14 | t.index [ :key ], unique: true 15 | end 16 | 17 | create_table :active_storage_attachments do |t| 18 | t.string :name, null: false 19 | t.references :record, null: false, polymorphic: true, index: false 20 | t.references :blob, null: false 21 | 22 | t.datetime :created_at, null: false 23 | 24 | t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true 25 | t.foreign_key :active_storage_blobs, column: :blob_id 26 | end 27 | 28 | create_table :active_storage_variant_records do |t| 29 | t.belongs_to :blob, null: false, index: false 30 | t.string :variation_digest, null: false 31 | 32 | t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true 33 | t.foreign_key :active_storage_blobs, column: :blob_id 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /db/migrate/20230119143747_add_service_name_to_active_storage_blobs.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20190112182829) 2 | class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] 3 | def up 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | unless column_exists?(:active_storage_blobs, :service_name) 7 | add_column :active_storage_blobs, :service_name, :string 8 | 9 | if configured_service = ActiveStorage::Blob.service.name 10 | ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) 11 | end 12 | 13 | change_column :active_storage_blobs, :service_name, :string, null: false 14 | end 15 | end 16 | 17 | def down 18 | return unless table_exists?(:active_storage_blobs) 19 | 20 | remove_column :active_storage_blobs, :service_name 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20230119143748_create_active_storage_variant_records.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20191206030411) 2 | class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] 3 | def change 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | # Use Active Record's configured type for primary key 7 | create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t| 8 | t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type 9 | t.string :variation_digest, null: false 10 | 11 | t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true 12 | t.foreign_key :active_storage_blobs, column: :blob_id 13 | end 14 | end 15 | 16 | private 17 | def primary_key_type 18 | config = Rails.configuration.generators 19 | config.options[config.orm][:primary_key_type] || :primary_key 20 | end 21 | 22 | def blobs_primary_key_type 23 | pkey_name = connection.primary_key(:active_storage_blobs) 24 | pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } 25 | pkey_column.bigint? ? :bigint : pkey_column.type 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /db/migrate/20230119162216_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20211119233751) 2 | class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] 3 | def change 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | change_column_null(:active_storage_blobs, :checksum, true) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[8.0].define(version: 2023_01_19_162216) do 14 | # These are extensions that must be enabled in order to support this database 15 | enable_extension "pg_catalog.plpgsql" 16 | 17 | create_table "active_storage_attachments", force: :cascade do |t| 18 | t.string "name", null: false 19 | t.string "record_type", null: false 20 | t.bigint "record_id", null: false 21 | t.bigint "blob_id", null: false 22 | t.datetime "created_at", precision: nil, null: false 23 | t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" 24 | t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true 25 | end 26 | 27 | create_table "active_storage_blobs", force: :cascade do |t| 28 | t.string "key", null: false 29 | t.string "filename", null: false 30 | t.string "content_type" 31 | t.text "metadata" 32 | t.string "service_name", null: false 33 | t.bigint "byte_size", null: false 34 | t.string "checksum" 35 | t.datetime "created_at", precision: nil, null: false 36 | t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true 37 | end 38 | 39 | create_table "active_storage_variant_records", force: :cascade do |t| 40 | t.bigint "blob_id", null: false 41 | t.string "variation_digest", null: false 42 | t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true 43 | end 44 | 45 | create_table "comments", force: :cascade do |t| 46 | t.string "author", default: "", null: false 47 | t.text "text", default: "", null: false 48 | t.datetime "created_at", precision: nil, null: false 49 | t.datetime "updated_at", precision: nil, null: false 50 | end 51 | 52 | add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" 53 | add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" 54 | end 55 | -------------------------------------------------------------------------------- /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 rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /docs/bourbon-integration.md: -------------------------------------------------------------------------------- 1 | ## Bourbon integration 2 | 3 | To use [bourbon](https://github.com/thoughtbot/bourbon) take the following steps: 4 | 5 | - Install node-bourbon `cd client && yarn install --save node-bourbon` 6 | - Update [bootstrap-sass.js](https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/client/bootstrap-sass.config.js) to use the right paths: 7 | ``` 8 | // Add this 9 | var bourbonPaths = require('node-bourbon').includePaths; 10 | module.exports = { 11 | // ... 12 | // And update this 13 | styleLoader: 'style-loader!css-loader!sass-loader?imagePath=/assets/images&includePaths[]=' + bourbonPaths, 14 | ``` 15 | - `@import 'bourbon';` Import bourbon from your [scss file](https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/client/assets/stylesheets/_app-styling-post-bootstrap-loading.scss) 16 | 17 | -------------------------------------------------------------------------------- /docs/codeship-ci.md: -------------------------------------------------------------------------------- 1 | Codeship CI Configuration 2 | 3 | ## Overview 4 | Pick "I want to create my own custom commands" on the Project Settings, Test tab. 5 | 6 | ## Setup Commands 7 | ```bash 8 | #!/bin/bash 9 | rvm use 2.5.3 10 | gem update --system 11 | gem install bundler 12 | bundle config build.nokogiri --use-system-libraries 13 | bundle config build.json --use-system-libraries 14 | bundle install 15 | chromedriver-update 16 | nvm install stable && nvm alias default stable 17 | npm install npm@latest -g 18 | npm install --global yarn 19 | yarn install 20 | export RAILS_ENV=test 21 | bundle exec rake db:schema:load 22 | ``` 23 | 24 | ## Test Pipeline 25 | 26 | ```bash 27 | COVERALLS_REPO_TOKEN=YynC bundle exec rake 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/contributors.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | First, we love community contributions, as small as a one character doc fix! Please fork and submit a PR if you see something to improve of fix. 4 | 5 | ## Notable contributions include (please submit PR if I miss any!): 6 | 7 | See the full list of [Contributors](https://github.com/shakacode/react-webpack-rails-tutorial/graphs/contributors) 8 | 9 | * [Justin Gordon](https://github.com/justin808/): Started this, leads this 10 | * [Alex Fedoseev](https://github.com/alexfedoseev): Added integration of Rails hot loading and CSS modules, plus much more 11 | * [Martin Breining](https://github.com/mbreining): For adding flux at first 12 | * [Dylan Grafmyre](https://github.com/Dgrafmyre): For ci setup 13 | * [Rob Wise](https://github.com/robwise): Many doc improvements 14 | * [Josias Schneider](https://github.com/josiasd): Conversion to the new react_on_rails format 15 | -------------------------------------------------------------------------------- /docs/heroku.md: -------------------------------------------------------------------------------- 1 | # Deploying to Heroku 2 | 3 | First consult this article: [Using Multiple Buildpacks for an App](https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app). 4 | 5 | In order to deploy to heroku, you'll need to run this command once to set a custom 6 | buildpack: 7 | 8 | ``` 9 | heroku config:add BUILDPACK_URL=https://github.com/ddollar/heroku-buildpack-multi.git 10 | ``` 11 | 12 | This runs the two buildpacks in the `.buildpacks` directory. 13 | 14 | Also make sure you are running the latest heroku stack, cedar-14, to avoid running 15 | into the [following issue](https://github.com/sass/node-sass/issues/467#issuecomment-61729195). 16 | 17 | This is now the current default. 18 | 19 | ``` 20 | heroku stack:set cedar-14 -a react-webpack-rails-tutorial 21 | ``` 22 | 23 | To deploy the app on Heroku: 24 | ``` 25 | git push heroku master 26 | ``` 27 | 28 | 29 | ## GOTCHAS 30 | 31 | 1. Use "dependencies" and not "devDependencies" for anything deployment related. Be sure to put all normall "dev only" tools in "dependencies" and not "devDependencies" in your `client/package.json`. This is because the Heroku default buildpack will only install what's in "dependencies". So only use `npm --save-dev` for things like linters and anything **only** related to the Webpack Dev Server. 32 | 2. The file system is case sensitive on Heroku, so don't mix the case of your import with the real file name. For example, if you name a file `BaseApi` and you do this: `import BaseAPI from 'lib/utils/BaseAPI'`, that will work locally, but when you deploy, you'll get an error like: `Uncaught Error: Cannot find module "lib/utils/baseAPI"` 33 | -------------------------------------------------------------------------------- /docs/integration-test-notes.md: -------------------------------------------------------------------------------- 1 | # Integration Test Notes 2 | 3 | See [Yak Shaving Failing Integration Tests with React and Rails](https://blog.shakacode.com/yak-shaving-failing-integration-tests-with-react-a93444886c8c#.io9464uvz) 4 | 5 | ## CI 6 | See the .travis.yml file, at the bottom, to see what driver is used by Travis. 7 | 8 | `DRIVER=selenium bundle exec rake` 9 | 10 | Codeship is set to use the default driver. 11 | 12 | ## Driver Options 13 | 14 | Support is included for the following drivers: 15 | 16 | 1. Selenium Chrome (`DRIVER=selenium_chrome rspec`) 17 | 1. Selenium Firefox (`DRIVER=selenium_firefox rspec`) 18 | 19 | You may want to experiment with using the different drivers. 20 | 21 | ## Rspec-retry 22 | * The current retry count is 4, configured in spec_helper.rb. 23 | * When developing new tests, you do not want retry a failure multiple times. 24 | * Set this env value: `export RSPEC_RETRY_RETRY_COUNT=1` 25 | 26 | ## Selenium Chrome 27 | 28 | You may want to see the [chromedriver-helper docs](https://github.com/flavorjones/chromedriver-helper) and run chromedriver-update to get the latest version of the Chrome driver. 29 | 30 | -------------------------------------------------------------------------------- /docs/jbuilder.md: -------------------------------------------------------------------------------- 1 | # JBuilder Notes 2 | There's a bunch of gotchas with using [Jbuilder](https://github.com/rails/jbuilder) to create the string version of the props to be sent to the react_on_rails_gem. The main thing is that if you follow the example and call Jbuilder like this, you don't run into a number of issues. 3 | 4 | ```erb 5 | <%= react_component('App', props: render(template: "/comments/index.json.jbuilder"), 6 | prerender: true) %> 7 | ``` 8 | 9 | However, if you try to set the value of the JSON string inside of the controller, then you will run into several issues with rendering the Jbuilder template from the controller. See the notes in this the example code for app/controllers/pages_controller.rb. 10 | 11 | Here's the samples of Jbuilder that we use: 12 | 13 | ### comments/_comment.json.jbuilder: 14 | 15 | ```ruby 16 | json.extract! comment, :id, :author, :text, :created_at, :updated_at 17 | ``` 18 | 19 | ### comments/index.json.jbuilder: 20 | 21 | ```ruby 22 | # Specify the partial, as well as the name of the variable used in the partial 23 | json.array! @comments, { partial: "comments/comment", as: :comment } 24 | ``` 25 | 26 | ### comments/show.json.jbuilder: 27 | 28 | ```ruby 29 | json.partial! 'comment', comment: @comment 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /docs/jquery-with-react-on-rails.md: -------------------------------------------------------------------------------- 1 | # jQuery with Rails and Webpack 2 | 3 | jQuery and jQuery-ujs are not required within `app/assets/javascript/application.js` and have been moved under`/client` and are managed by npm. The modules are exposed via entry point by [client/webpack.client.base.config.js](client/webpack.client.base.config.js) and, for `jquery-ujs`, in the [client/webpack.client.rails.build.config.js](client/webpack.client.rails.build.config.js) and the [client/webpack.client.rails.hot.config.js](client/webpack.client.rails.hot.config.js). 4 | 5 | In `application_non_webpack.js` and `application_static.js.erb`, it's critical that any libraries that depend on jQuery come after the inclusion 6 | of the Webpack bundle. 7 | 8 | Please refer to [Considerations for jQuery with Rails and Webpack](http://forum.shakacode.com/t/considerations-for-jquery-with-rails-and-webpack/344) for further info. 9 | -------------------------------------------------------------------------------- /docs/linters.md: -------------------------------------------------------------------------------- 1 | # Linting and Code Inspection 2 | ## Running Lint and CI tasks 3 | * Default rake task runs tests and linting (yes, repeating this!) (see `ci.rake`) 4 | * See file [README.md](../client/README.md) for how to run ESLint 5 | * See script `scripts/lint`. 6 | * We're using the [AirBnb JavaScript style guidelines](https://github.com/airbnb/javascript). 7 | 8 | ### RubyMine/Webstorm Linting Configuration 9 | * I started out trying to make RubyMine and WebStorm catch and fix linting errors. However, I find it faster to just do this with the command line. Your mileage may vary. 10 | * Create a custom scope like this for RubyMine, named "Inspection Scope" 11 | 12 | file[react-rails-tutorial]:*/&&!file[react-rails-tutorial]:tmp//*&&!file[react-rails-tutorial]:log//*&&!file[react-rails-tutorial]:client/node_modules//*&&!file[react-rails-tutorial]:client/assets/fonts//*&&!file[react-rails-tutorial]:app/assets/fonts//*&&!file[react-rails-tutorial]:bin//*&&!file[react-rails-tutorial]:app/assets/javascripts//* 13 | 14 | * Install the code style and inspection files in [client/jetbrains](client/jetbrains) 15 | * Use the installed inspection settings and new Inspection Scope for code inspection. 16 | * RubyMine configuration is optional. All linters run from the command line. 17 | 18 | ## Linters 19 | 1. [Rubocop](https://github.com/bbatsov/rubocop) 20 | 2. [Ruby-Lint](https://github.com/YorickPeterse/ruby-lint) 21 | 3. [Eslint](http://eslint.org/) 22 | 5. [scss-lint](https://github.com/brigade/scss-lint) 23 | 6. [brakeman](http://brakemanscanner.org/) 24 | 7. [bundle-audit](https://github.com/rubysec/bundler-audit) 25 | -------------------------------------------------------------------------------- /docs/testing-deployment.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | 1. `rake` runs the test suite 4 | 2. To test production with precompiled assets: 5 | 6 | ```sh 7 | export SECRET_KEY_BASE=`rake secret` 8 | alias test-prod='rake assets:clobber && RAILS_ENV=production bin/rake assets:precompile \ 9 | && rails s -e production' 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/tests-and-ci.md: -------------------------------------------------------------------------------- 1 | # Running Tests and CI 2 | 3 | *Default rake task runs feature specs, jest tests and linting* 4 | 5 | We have: 6 | 7 | * feature tests in /spec/features 8 | * component unit tests in /client/test/ 9 | * javascript linting 10 | 11 | From the root of the project, you can run all specs+tests+linter with 12 | 13 | yarn run test 14 | 15 | Run the feature specs individually with `rspec`. 16 | 17 | Run the React unit tests (all .js and .jsx files) from the `client` dir with; 18 | 19 | cd client 20 | yarn run test --silent 21 | 22 | Run the js tests continually with; 23 | 24 | cd client 25 | yarn run test --watch 26 | 27 | ## CI configuration 28 | Add those lines to your CI scripts after `bundle install` 29 | 30 | ```sh 31 | yarn install 32 | ``` 33 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/ci.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if Rails.env.development? || Rails.env.test? 4 | # See tasks/linters.rake 5 | 6 | task js_tests: :environment do 7 | puts Rainbow("Running JavaScript tests").green 8 | sh "yarn run test:client" 9 | end 10 | 11 | task rspec_tests: :environment do 12 | puts Rainbow("Running RSpec tests").green 13 | sh "rspec" 14 | end 15 | 16 | task build_rescript: :environment do 17 | puts Rainbow("Building ReScript files").green 18 | sh "yarn res:build" 19 | end 20 | 21 | namespace :ci do 22 | desc "Run all audits and tests" 23 | # rspec_tests must be before lint and js_tests to build the locale files 24 | task all: %i[environment build_rescript rspec_tests lint js_tests] do 25 | puts "All CI tasks" 26 | puts Rainbow("PASSED").green 27 | puts "" 28 | rescue StandardError => e 29 | puts e 30 | puts Rainbow("FAILED").red 31 | puts "" 32 | raise(e) 33 | end 34 | 35 | desc "Run CI rspec tests" 36 | task rspec: %i[environment build_rescript rspec_tests] do 37 | puts "CI rspec tests" 38 | puts Rainbow("PASSED").green 39 | puts "" 40 | rescue StandardError => e 41 | puts e 42 | puts Rainbow("FAILED").red 43 | puts "" 44 | raise(e) 45 | end 46 | 47 | desc "Run CI js_tests" 48 | task js: %i[environment build_rescript js_tests] do 49 | puts "CI js_tests" 50 | puts Rainbow("PASSED").green 51 | puts "" 52 | rescue StandardError => e 53 | puts e 54 | puts Rainbow("FAILED").red 55 | puts "" 56 | raise(e) 57 | end 58 | end 59 | 60 | task ci: "ci:all" 61 | 62 | task(:default).clear.enhance([:ci]) 63 | end 64 | -------------------------------------------------------------------------------- /lib/tasks/daily.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | task daily: :environment do 4 | t = 1.day.ago 5 | older_comments = Comment.where("created_at < ?", t) 6 | newer_comments = Comment.where("created_at >= ?", t) 7 | puts "Deleting #{older_comments.count} comments older than #{t}" 8 | puts "Keeping #{newer_comments.count} comments newer than #{t}" 9 | older_comments.delete_all 10 | end 11 | -------------------------------------------------------------------------------- /lib/tasks/linters.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if %w[development test].include? Rails.env 4 | namespace :lint do 5 | # require "rubocop/rake_task" 6 | # require "slim_lint/rake_task" 7 | require "scss_lint/rake_task" 8 | 9 | # This fails: https://github.com/bbatsov/rubocop/issues/1840 10 | # RuboCop::RakeTask.new 11 | 12 | desc "Run Rubocop lint as shell. Specify option fix to auto-correct (and don't have uncommitted files!)." 13 | task :rubocop, [:fix] => [:environment] do |_t, args| 14 | def to_bool(str) 15 | return true if /^(true|t|yes|y|1)$/i.match?(str) 16 | return false if str.blank? || str =~ /^(false|f|no|n|0)$/i 17 | 18 | raise ArgumentError, "invalid value for Boolean: \"#{str}\"" 19 | end 20 | 21 | fix = (args.fix == "fix") || to_bool(args.fix) 22 | cmd = "rubocop -S -D#{fix ? ' -a' : ''} ." 23 | puts "Running Rubocop Linters via `#{cmd}`#{fix ? ' auto-correct is turned on!' : ''}" 24 | sh cmd 25 | end 26 | 27 | # SlimLint::RakeTask.new do |t| 28 | # t.files = ["app/views"] 29 | # end 30 | 31 | SCSSLint::RakeTask.new do |t| 32 | t.files = ["client/"] 33 | end 34 | 35 | desc "eslint" 36 | task eslint: :environment do 37 | cmd = "yarn run lint" 38 | puts "Running eslint via `#{cmd}`" 39 | sh cmd 40 | end 41 | 42 | desc "JS Linting" 43 | task js: [:eslint] do 44 | puts "Completed running all JavaScript Linters" 45 | end 46 | 47 | # desc "See docs for task 'slim_lint'" 48 | # task slim: :slim_lint 49 | 50 | desc "See docs for task 'scss_lint'" 51 | task scss: :scss_lint 52 | 53 | task lint: %i[build_rescript rubocop js scss] do 54 | puts "Completed all linting" 55 | end 56 | end 57 | 58 | desc "Runs all linters. Run `rake -D lint` to see all available lint options" 59 | task lint: ["lint:lint"] 60 | end 61 | -------------------------------------------------------------------------------- /lib/tasks/rails_best_practices.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :rails_best_practices do 4 | desc "Run Rails Best Practices" 5 | task run: :environment do 6 | sh "rails_best_practices" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/log/.keep -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/public/favicon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/public/icon.png -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo Linting with Rubocop, ruby-lint, eslint, jsrc, and slim-lint 3 | rubocop . 4 | 5 | # NOTE: Ruby-lint is experimental. Did not add value to rubocop 6 | echo Linting with ruby-lint 7 | ruby-lint app config spec 8 | 9 | echo Linting with eslint 10 | (cd client && yarn run lint) 11 | 12 | scss-lint client/**/*.scss 13 | 14 | echo Done linting with Rubocop, ruby-lint, eslint, jsrc, and slim-lint 15 | -------------------------------------------------------------------------------- /scripts/start-production-server: -------------------------------------------------------------------------------- 1 | # Script to test out production mode 2 | 3 | echo Be SURE to run rake assets:clobber after you quit with ctrl-c! 4 | echo Or else you will not see changes made during development possibly! 5 | 6 | rake assets:clobber && \ 7 | RAILS_ENV=production bin/rake assets:precompile && \ 8 | SECRET_KEY_BASE=227a31d0cf655c89fa860d111c9baccb3bf9ceeccd364fd5f5d49cd17e025c4b22d200ce3eada35cdeb1045d2ce9581ef9516ccc717851066d80fa71fecf2f2e \ 9 | rails s -e production 10 | -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This will guess the User class 4 | FactoryBot.define do 5 | factory :comment do 6 | author { "John" } 7 | text { "This is a comment text." } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/requests/server_render_check_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe "Server Rendering" do 6 | it "generates server rendered HTML if server rendering enabled" do 7 | get root_path 8 | html_nodes = Nokogiri::HTML(response.body) 9 | expect(html_nodes.css("div#RouterApp-react-component-0").children.size).to eq(1) 10 | expect(html_nodes.css("div#RouterApp-react-component-0 section h2").text) 11 | .to include("Comments") 12 | end 13 | 14 | it "generates no server rendered HTML if server rendering not enabled" do 15 | get simple_path 16 | html_nodes = Nokogiri::HTML(response.body) 17 | expect(html_nodes.css("div#SimpleCommentScreen-react-component-0").children.size).to eq(0) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/driver_registration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DriverRegistration 4 | def self.register_selenium_chrome 5 | return if @selenium_chrome_registered 6 | 7 | Capybara.register_driver :selenium_chrome do |app| 8 | Capybara::Selenium::Driver.new(app, browser: :chrome) 9 | end 10 | Capybara::Screenshot.register_driver(:selenium_chrome) do |js_driver, path| 11 | js_driver.browser.save_screenshot(path) 12 | end 13 | @selenium_chrome_registered = true 14 | end 15 | 16 | def self.register_selenium_firefox 17 | return if @selenium_firefox_registered 18 | 19 | Capybara.register_driver :selenium_firefox do |app| 20 | Capybara::Selenium::Driver.new(app, browser: :firefox) 21 | end 22 | Capybara::Screenshot.register_driver(:selenium_firefox) do |js_driver, path| 23 | js_driver.browser.save_screenshot(path) 24 | end 25 | @selenium_firefox_registered = true 26 | end 27 | 28 | def self.register_selenium_chrome_headless 29 | return if @selenium_headless_registered 30 | 31 | Capybara.register_driver :selenium_chrome_headless do |app| 32 | capabilities = Selenium::WebDriver::Chrome::Options.new( 33 | "goog:chromeOptions" => { args: %w[headless disable-gpu no-sandbox disable-dev-shm-usage] } 34 | ) 35 | Capybara::Selenium::Driver.new app, browser: :chrome, options: capabilities 36 | end 37 | Capybara::Screenshot.register_driver(:selenium_chrome_headless) do |js_driver, path| 38 | js_driver.browser.save_screenshot(path) 39 | end 40 | @selenium_headless_registered = true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/support/phantomjs-disable-animations.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/andyjbas/9962218 2 | // disable_animations.js 3 | 4 | // No idea which of these is more helpful. 5 | // They both resulted in crashes: 6 | // https://travis-ci.org/shakacode/react-webpack-rails-tutorial/builds/178794772 7 | // http://marcgg.com/blog/2015/01/05/css-animations-failing-capybara-specs/ 8 | const disableAnimationStyles = 9 | '-webkit-transition: none !important;' + 10 | '-moz-transition: none !important;' + 11 | '-ms-transition: none !important;' + 12 | '-o-transition: none !important;' + 13 | 'transition: none !important;' + 14 | '-webkit-transition-duration: 0.0s !important;' + 15 | '-moz-transition-duration: 0.0s !important;' + 16 | '-ms-transition-duration: 0.0s !important;' + 17 | '-o-transition-duration: 0.0s !important;' + 18 | 'transition-duration: 0.0s !important;' + 19 | 'transition-property: none !important;' + 20 | '-o-transition-property: none !important;' + 21 | '-moz-transition-property: none !important;' + 22 | '-ms-transition-property: none !important;' + 23 | '-webkit-transition-property: none !important;' + 24 | 'transform: none !important;' + 25 | '-o-transform: none !important;' + 26 | '-moz-transform: none !important;' + 27 | '-ms-transform: none !important;' + 28 | '-webkit-transform: none !important;' + 29 | 'animation: none !important;' + 30 | '-o-animation: none !important;' + 31 | '-moz-animation: none !important;' + 32 | '-ms-animation: none !important;' + 33 | '-webkit-animation: none !important;'; 34 | 35 | window.onload = () => { 36 | const animationStyles = document.createElement('style'); 37 | animationStyles.type = 'text/css'; 38 | animationStyles.innerHTML = `* {${ disableAnimationStyles }}`; 39 | document.head.appendChild(animationStyles); 40 | }; 41 | -------------------------------------------------------------------------------- /spec/system/destroy_comment_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | require "system/shared/contexts" 5 | 6 | describe "Destroy a comment", :existing_comment do 7 | context "when from classic page" do 8 | let(:comment) { build(:comment) } 9 | 10 | it "clicking destroy link destroys comment" do 11 | visit comments_path 12 | 13 | click_link "New Comment" 14 | submit_form(name: comment.author, text: comment.text) 15 | click_link "Back" 16 | 17 | accept_confirm do 18 | click_button "Destroy" 19 | end 20 | 21 | expect(page).not_to have_css(".comment", text: comment.author) 22 | expect(page).not_to have_css(".comment", text: comment.text) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/system/edit_comment_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | require "system/shared/contexts" 5 | 6 | describe "Edit a comment", :existing_comment do 7 | let(:comment) { build(:comment) } 8 | let(:edited_name) { "Abraham Lincoln" } 9 | 10 | context "when from classic page" do 11 | it "comment is updated when edit is submitted" do 12 | visit comments_path 13 | click_link "New Comment" 14 | submit_form 15 | 16 | click_link "Edit", match: :first 17 | 18 | submit_form(name: :edited_name) 19 | expect(page).to have_css(".comment", text: :edited_name) 20 | expect(page).to have_css("#notice", text: "Comment was successfully updated.") 21 | end 22 | 23 | it "comment is not updated when edit is submitted with blank fields", :blank_form_submitted do 24 | visit comments_path 25 | click_link "New Comment" 26 | submit_form 27 | 28 | click_link "Edit", match: :first 29 | submit_form(name: "", text: "") 30 | 31 | expect(page).not_to have_success_message 32 | expect(page).to have_failure_message 33 | expect(page).not_to have_css(".comment", text: "") 34 | end 35 | end 36 | end 37 | 38 | private 39 | 40 | def have_success_message 41 | have_css("#notice", text: "Comment was successfully created.") 42 | end 43 | 44 | def have_failure_message 45 | have_css("#error_explanation") 46 | end 47 | -------------------------------------------------------------------------------- /spec/system/pages_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | shared_examples "Git Commit SHA" do 6 | before { visit root_path } 7 | 8 | it "displays the current git commit" do 9 | el = find("#git-commit-sha") 10 | expect(el.text).to eq expected_text 11 | end 12 | end 13 | 14 | describe "Git Commit SHA" do 15 | before do 16 | # sha gets cached as an instance variable, so need to start fresh 17 | GitCommitSha.reset_current_sha 18 | end 19 | 20 | context "when GIT_COMMIT_SHA env var exists" do 21 | let(:sha) { "94d92356828a56db25fccff9d50f41c525eead5z" } 22 | let(:expected_text) { "94d9235" } 23 | 24 | before do 25 | ENV["GIT_COMMIT_SHA"] = sha 26 | end 27 | 28 | after do 29 | ENV.delete("GIT_COMMIT_SHA") 30 | end 31 | 32 | it_behaves_like "Git Commit SHA" 33 | end 34 | 35 | context "when .source_version file exists" do 36 | let(:sha) { "94d92356828a56db25fccff9d50f41c525eead5y" } 37 | let(:expected_text) { "94d9235" } 38 | 39 | before { `cd #{Rails.root} && echo #{sha} > .source_version` } 40 | 41 | after { `cd #{Rails.root} && rm .source_version` } 42 | 43 | it_behaves_like "Git Commit SHA" 44 | end 45 | 46 | context "when falling back to git command" do 47 | let(:sha) { "94d92356828a56db25fccff9d50f41c525eead5x" } 48 | let(:expected_text) { "94d9235" } 49 | 50 | before do 51 | # stub this method since we need to control what the sha actually is 52 | allow(GitCommitSha).to receive(:retrieve_sha_from_git) { sha } 53 | end 54 | 55 | it_behaves_like "Git Commit SHA" 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/system/react_router_demo_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | require "system/shared/contexts" 5 | 6 | describe "React Router Routes" do 7 | context "when Root URL", page: :main do 8 | it "shows comments section" do 9 | visit root_path 10 | click_link "Comments (Root URL)" 11 | expect(page).to have_selector("h2", text: "Comments") 12 | end 13 | end 14 | 15 | context "when /react-router URL", page: :main do 16 | it "shows 'React Router is working!' message" do 17 | visit root_path 18 | click_link "Test React Router ('/react-router')" 19 | expect(page).to have_selector("h1", text: "React Router is working!") 20 | end 21 | end 22 | 23 | context "when /react-router/redirect URL", page: :main do 24 | before do 25 | visit root_path 26 | click_link "Test Redirect (url to '/react-router/redirect' which goes to root '/')" 27 | end 28 | 29 | it "shows comments section" do 30 | expect(page).to have_selector("h2", text: "Comments") 31 | end 32 | 33 | it "shows redirect message" do 34 | expect(page).to have_selector(".bg-success", text: "You have been redirected from/react-router/redirect") 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/system/shared/contexts.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | # Form Submission 6 | def submit_form(name: "Spicoli", text: "dude!") 7 | fill_in "Your Name", with: name 8 | fill_in "Say something using markdown...", with: text 9 | click_button "Post" 10 | end 11 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | const defaultTheme = require('tailwindcss/defaultTheme'); 4 | 5 | module.exports = { 6 | content: ['./app/**/*.{html,html.erb}', './client/**/*.{html,js,jsx}'], 7 | theme: { 8 | extend: { 9 | fontFamily: { 10 | sans: ['OpenSans-Light', ...defaultTheme.fontFamily.sans], 11 | }, 12 | }, 13 | }, 14 | plugins: [ 15 | // eslint-disable-next-line global-require 16 | require('@tailwindcss/typography'), 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakacode/react-webpack-rails-tutorial/4be1e1b3f56985a078b906b1e440c21ce4791f96/vendor/assets/stylesheets/.keep --------------------------------------------------------------------------------