├── .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 |
7 | <% comment.errors.full_messages.each do |message| %>
8 | <%= message %>
9 | <% end %>
10 |
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 | Author
16 | Text
17 |
18 |
19 |
20 |
21 |
22 | <% @comments.each do |comment| %>
23 |
24 | <%= comment.author %>
25 | <%= markdown_to_html(comment.text) %>
26 |
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 |
31 |
32 | <% end %>
33 |
34 |
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 |
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 |
2 |
3 | <%= render "pages/header" %>
4 |
5 |
6 |
Comments
7 |
8 |
9 | Force Refresh of All Comments.
10 | Text supports Github Flavored Markdown.
11 | Comments older than 24 hours are deleted.
12 | Name is preserved. Text is reset, between submits
13 | To see Action Cable instantly update two browsers, open two browsers and submit a comment!
14 |
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 |
2 |
Your comment was not saved!
3 |
5 |
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 |
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 |
9 |
10 | <%= link_to "Can ShakaCode Help You?",
11 | "https://blog.shakacode.com/can-shakacode-help-you-4a5b1e5a8a63#.jex6tg9w9" %>
12 | We're actively seeking new projects with React, React-Native, or Ruby on Rails
13 |
14 |
15 | This project is deployed on
16 | <%= link_to "Control Plane",
17 | "https://shakacode.controlplane.com" %>
18 | using
19 | <%= link_to "Heroku to Control Plane",
20 | "https://github.com/shakacode/heroku-to-control-plane" %>
21 | Ruby gem.
22 |
23 |
24 | See the
25 | <%= link_to "github.com/shakacode/react-webpack-rails-tutorial/README.md",
26 | "https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/README.md" %>
27 | for details of how this example site was built.
28 |
29 |
30 | Read <%= link_to "Documentation for React on Rails",
31 | "https://shakacode.gitbooks.io/react-on-rails/content/" %> and
32 | <%= link_to "The React on Rails Doctrine",
33 | "https://www.shakacode.com/blog/the-react-on-rails-doctrine" %>.
34 |
35 |
36 | Watch the <%= link_to "React On Rails Tutorial Series",
37 | "https://www.youtube.com/playlist?list=PL5VAKH-U1M6dj84BApfUtvBjvF-0-JfEU" %>.
38 |
39 |
40 | <%= link_to "ShakaCode", "http://www.shakacode.com"%>
41 | is doing support for React on Rails, including a private Slack channel, source code reviews, and pair programming sessions.
42 | <%= link_to "Click here", "http://www.shakacode.com/work/index.html" %> for more information.
43 |
44 |
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 |
7 | No or minimal MVC
8 | No async necessary
9 | No server rendering
10 | No pre-population of props
11 |
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 |
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 |
50 | }
51 |
--------------------------------------------------------------------------------
/client/app/bundles/comments/rescript/CommentForm/forms/InlineForm.res:
--------------------------------------------------------------------------------
1 | @react.component
2 | let make = (~author, ~handleAuthorChange, ~text, ~handleTextChange, ~handleSubmit, ~disabled) => {
3 |
45 | }
46 |
--------------------------------------------------------------------------------
/client/app/bundles/comments/rescript/CommentForm/forms/StackedForm.res:
--------------------------------------------------------------------------------
1 | @react.component
2 | let make = (~author, ~handleAuthorChange, ~text, ~handleTextChange, ~handleSubmit, ~disabled) => {
3 |
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 |
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 | onChange(e.target.value)}
9 | value={locale}
10 | className="px-3 py-1 rounded cursor-pointer border border-gray-400 hover:bg-gray-300"
11 | >
12 | English
13 | Deutsch
14 | 日本語
15 | 简体中文
16 | 正體中文
17 |
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
--------------------------------------------------------------------------------
<%= notice %>
4 | <% end %> 5 | 6 |7 | Author: 8 | <%= @comment.author %> 9 |
10 | 11 |12 | Text: 13 | <%= @comment.text %> 14 |
15 |