├── log
└── .keep
├── storage
└── .keep
├── tmp
├── .keep
├── pids
│ └── .keep
└── storage
│ └── .keep
├── vendor
├── .keep
└── javascript
│ └── .keep
├── lib
├── assets
│ └── .keep
├── tasks
│ └── .keep
├── default_renderer.rb
├── preview_renderer.rb
├── svg_inline.rb
└── one_dark.rb
├── test
├── helpers
│ └── .keep
├── mailers
│ └── .keep
├── controllers
│ └── .keep
├── integration
│ └── .keep
├── fixtures
│ └── files
│ │ └── .keep
├── models
│ ├── user_test.rb
│ ├── category_test.rb
│ ├── sample_test.rb
│ └── sample_file_test.rb
├── factories
│ ├── category_factory.rb
│ └── user_factory.rb
├── application_system_test_case.rb
├── test_helper.rb
└── system
│ └── samples_test.rb
├── app
├── assets
│ ├── builds
│ │ └── .keep
│ ├── images
│ │ ├── .keep
│ │ ├── opengraph.png
│ │ └── avatar.svg
│ ├── config
│ │ └── manifest.js
│ └── stylesheets
│ │ ├── rails.scss
│ │ └── application.css
├── models
│ ├── concerns
│ │ └── .keep
│ ├── application_record.rb
│ ├── guest_user.rb
│ ├── category.rb
│ ├── sample.rb
│ ├── file_tree.rb
│ ├── user.rb
│ └── sample_file.rb
├── controllers
│ ├── concerns
│ │ └── .keep
│ ├── account
│ │ ├── samples_controller.rb
│ │ ├── accounts_controller.rb
│ │ ├── passwords_controller.rb
│ │ └── avatars_controller.rb
│ ├── home_controller.rb
│ ├── sample_files_controller.rb
│ ├── users_controller.rb
│ ├── resizes_controller.rb
│ ├── errors_controller.rb
│ ├── turbo_controller.rb
│ ├── application_controller.rb
│ ├── categories_controller.rb
│ └── samples_controller.rb
├── views
│ ├── layouts
│ │ ├── mailer.text.erb
│ │ ├── _header.html.erb
│ │ ├── _flash.html.erb
│ │ ├── mailer.html.erb
│ │ ├── application.html.erb
│ │ └── _navigation.html.erb
│ ├── sample_files
│ │ ├── _description.html.erb
│ │ ├── _left_menu.html.erb
│ │ ├── _contents.html.erb
│ │ ├── _top_menu.html.erb
│ │ ├── show.html.erb
│ │ ├── _editor.html.erb
│ │ └── _sample_file.svg.erb
│ ├── devise
│ │ ├── mailer
│ │ │ ├── password_change.html.erb
│ │ │ ├── confirmation_instructions.html.erb
│ │ │ ├── unlock_instructions.html.erb
│ │ │ ├── email_changed.html.erb
│ │ │ └── reset_password_instructions.html.erb
│ │ ├── shared
│ │ │ └── _error_messages.html.erb
│ │ ├── passwords
│ │ │ ├── new.html.erb
│ │ │ └── edit.html.erb
│ │ ├── confirmations
│ │ │ └── new.html.erb
│ │ ├── sessions
│ │ │ └── new.html.erb
│ │ └── registrations
│ │ │ └── new.html.erb
│ ├── icons
│ │ └── heroicons
│ │ │ ├── solid
│ │ │ ├── _folder.html.erb
│ │ │ ├── _plus-sm.html.erb
│ │ │ ├── _lock-closed.html.erb
│ │ │ └── _chevron-down.html.erb
│ │ │ └── outline
│ │ │ └── _document-text.html.erb
│ ├── samples
│ │ ├── edit.html.erb
│ │ ├── new.html.erb
│ │ ├── _sample_file_fields.html.erb
│ │ ├── _sample.html.erb
│ │ └── _form.html.erb
│ ├── categories
│ │ ├── edit.html.erb
│ │ ├── new.html.erb
│ │ ├── _category_mini.html.erb
│ │ ├── index.html.erb
│ │ ├── _category.html.erb
│ │ ├── show.html.erb
│ │ └── _form.html.erb
│ ├── users
│ │ ├── index.html.erb
│ │ ├── show.html.erb
│ │ └── _user.html.erb
│ ├── errors
│ │ ├── not_found.html.erb
│ │ ├── internal_error.html.erb
│ │ └── unprocessable.html.erb
│ ├── account
│ │ ├── samples
│ │ │ └── show.html.erb
│ │ └── accounts
│ │ │ └── show.html.erb
│ └── home
│ │ └── index.html.erb
├── javascript
│ ├── application.js
│ └── controllers
│ │ ├── application.js
│ │ ├── index.js
│ │ ├── toggle_controller.js
│ │ ├── resize_width_controller.js
│ │ └── resize_height_controller.js
├── components
│ ├── file_tree_component.rb
│ ├── header_component.rb
│ ├── header_component.html.erb
│ └── file_tree_component.html.erb
├── mailers
│ └── application_mailer.rb
├── jobs
│ ├── process_sample_job.rb
│ └── application_job.rb
├── policies
│ ├── user_profile_policy.rb
│ ├── category_policy.rb
│ ├── application_policy.rb
│ └── sample_policy.rb
├── validators
│ ├── slug_validator.rb
│ ├── file_path_validator.rb
│ └── username_validator.rb
├── services
│ └── create_sample_file_image.rb
└── helpers
│ ├── tailwind_builder.rb
│ └── application_helper.rb
├── .ruby-version
├── Aptfile
├── Procfile
├── public
├── favicon.ico
├── icon-192.png
├── icon-512.png
├── apple-touch-icon.png
├── robots.txt
├── apple-touch-icon-precomposed.png
├── manifest.webmanifest
└── icon.svg
├── config
├── initializers
│ ├── sentry.rb
│ ├── filter_parameter_logging.rb
│ ├── permissions_policy.rb
│ ├── assets.rb
│ ├── inflections.rb
│ ├── content_security_policy.rb
│ └── devise.rb
├── environment.rb
├── boot.rb
├── database.yml
├── storage.yml
├── credentials.yml.enc
├── locales
│ ├── en.yml
│ └── devise.en.yml
├── routes.rb
├── application.rb
├── puma.rb
└── environments
│ ├── test.rb
│ ├── development.rb
│ └── production.rb
├── .fonts
├── Recursive-Regular.ttf
└── RecMonoLinear-Regular.ttf
├── Procfile.dev
├── bin
├── rake
├── rails
├── dev
└── setup
├── README.md
├── config.ru
├── db
├── migrate
│ ├── 20220904034333_add_admin_to_users.rb
│ ├── 20220907213615_add_deactivated_at_to_users.rb
│ ├── 20220904170850_add_unconfirmed_email_to_users.rb
│ ├── 20241022110917_add_discarded_at_to_samples.rb
│ ├── 20220904043114_add_username_to_users.rb
│ ├── 20220904195045_add_status_to_samples.rb
│ ├── 20220821010120_create_categories.rb
│ ├── 20220821012828_create_sample_files.rb
│ ├── 20220821010831_create_samples.rb
│ ├── 20220819023829_devise_create_users.rb
│ └── 20220904183125_create_active_storage_tables.active_storage.rb
├── seeds.rb
└── schema.rb
├── .env.example
├── Rakefile
├── .github
├── dependabot.yml
└── workflows
│ └── main.yml
├── .gitattributes
├── esbuild.config.js
├── postcss.config.js
├── tailwind.config.js
├── .gitignore
├── Gemfile
├── package.json
└── Gemfile.lock
/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/pids/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/builds/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/javascript/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-3.1.2
2 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/Aptfile:
--------------------------------------------------------------------------------
1 | libglib2.0-0
2 | libglib2.0-dev
3 | libpoppler-glib8
4 |
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_tree ../builds
3 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | release: bundle exec rake db:migrate
2 | web: bundle exec puma -C config/puma.rb
3 |
--------------------------------------------------------------------------------
/app/javascript/application.js:
--------------------------------------------------------------------------------
1 | import "@hotwired/turbo-rails"
2 | import "./controllers"
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremysmithco/railsinspire/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/config/initializers/sentry.rb:
--------------------------------------------------------------------------------
1 | Sentry.init do |config|
2 | config.dsn = ENV["SENTRY_DSN"]
3 | end
4 |
--------------------------------------------------------------------------------
/public/icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremysmithco/railsinspire/HEAD/public/icon-192.png
--------------------------------------------------------------------------------
/public/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremysmithco/railsinspire/HEAD/public/icon-512.png
--------------------------------------------------------------------------------
/app/views/layouts/_header.html.erb:
--------------------------------------------------------------------------------
1 | <% if content_for?(:header) %>
2 | <%= yield :header %>
3 | <% end %>
4 |
--------------------------------------------------------------------------------
/.fonts/Recursive-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremysmithco/railsinspire/HEAD/.fonts/Recursive-Regular.ttf
--------------------------------------------------------------------------------
/Procfile.dev:
--------------------------------------------------------------------------------
1 | web: bin/rails server -p 3000 -b 0.0.0.0
2 | js: yarn build --watch
3 | css: yarn build:css --watch
4 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | primary_abstract_class
3 | end
4 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative "../config/boot"
3 | require "rake"
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremysmithco/railsinspire/HEAD/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/.fonts/RecMonoLinear-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremysmithco/railsinspire/HEAD/.fonts/RecMonoLinear-Regular.ttf
--------------------------------------------------------------------------------
/app/assets/images/opengraph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremysmithco/railsinspire/HEAD/app/assets/images/opengraph.png
--------------------------------------------------------------------------------
/lib/default_renderer.rb:
--------------------------------------------------------------------------------
1 | class DefaultRenderer < Redcarpet::Render::HTML
2 | include Redcarpet::Render::SmartyPants
3 | end
4 |
--------------------------------------------------------------------------------
/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeremysmithco/railsinspire/HEAD/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RailsInspire
2 |
3 | This is the Rails app powering railsinspire.com, a curated collection of code samples from Ruby on Rails projects.
4 |
--------------------------------------------------------------------------------
/app/components/file_tree_component.rb:
--------------------------------------------------------------------------------
1 | class FileTreeComponent < ViewComponent::Base
2 | def initialize(tree)
3 | @tree = tree
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: "from@example.com"
3 | layout "mailer"
4 | end
5 |
--------------------------------------------------------------------------------
/app/views/sample_files/_description.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= markdown sample_file.description %>
3 |
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/test/models/user_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class UserTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/password_change.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.email %>!
2 |
3 | We're contacting you to notify you that your password has been changed.
4 |
--------------------------------------------------------------------------------
/test/models/category_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class CategoryTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/models/sample_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class SampleTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/models/sample_file_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class SampleFileTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/app/controllers/account/samples_controller.rb:
--------------------------------------------------------------------------------
1 | class Account::SamplesController < ApplicationController
2 | def show
3 | @samples = current_user.samples.kept
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/app/components/header_component.rb:
--------------------------------------------------------------------------------
1 | class HeaderComponent < ViewComponent::Base
2 | renders_one :parent
3 | renders_one :actions
4 | renders_one :title
5 | renders_one :details
6 | end
7 |
--------------------------------------------------------------------------------
/app/models/guest_user.rb:
--------------------------------------------------------------------------------
1 | class GuestUser
2 | def id
3 | nil
4 | end
5 |
6 | def active?
7 | false
8 | end
9 |
10 | def admin?
11 | false
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/views/layouts/_flash.html.erb:
--------------------------------------------------------------------------------
1 | <% flash.each do |key, value| %>
2 | <%= content_tag :div, value, class: "text-white px-2 py-1 mb-4 #{flash_class(key)}" if value.present? %>
3 | <% end %>
4 |
--------------------------------------------------------------------------------
/test/factories/category_factory.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :category do
3 | sequence(:name) { |n| "Category #{n}" }
4 | sequence(:slug) { |n| "category-#{n}" }
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/bin/dev:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if ! command -v foreman &> /dev/null
4 | then
5 | echo "Installing foreman..."
6 | gem install foreman
7 | fi
8 |
9 | foreman start -f Procfile.dev "$@"
10 |
--------------------------------------------------------------------------------
/db/migrate/20220904034333_add_admin_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddAdminToUsers < ActiveRecord::Migration[7.0]
2 | def change
3 | add_column :users, :admin, :boolean, null: false, default: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20220907213615_add_deactivated_at_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddDeactivatedAtToUsers < ActiveRecord::Migration[7.0]
2 | def change
3 | add_column :users, :deactivated_at, :datetime
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20220904170850_add_unconfirmed_email_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddUnconfirmedEmailToUsers < ActiveRecord::Migration[7.0]
2 | def change
3 | add_column :users, :unconfirmed_email, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/public/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "icons": [
3 | { "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
4 | { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/app/models/category.rb:
--------------------------------------------------------------------------------
1 | class Category < ApplicationRecord
2 | has_many :samples
3 |
4 | validates :name, presence: true, uniqueness: true
5 | validates :slug, presence: true, uniqueness: true, slug: true
6 | end
7 |
--------------------------------------------------------------------------------
/test/application_system_test_case.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
4 | driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
5 | end
6 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=postgres://postgres:password@postgres/railsinspire_example
2 | POSTMARK_API_TOKEN=
3 | REDIS_URL=redis://redis/0
4 | S3_ACCESS_KEY_ID=
5 | S3_BUCKET=
6 | S3_REGION=
7 | S3_SECRET_ACCESS_KEY=
8 | SENTRY_DSN=
9 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/rails.scss:
--------------------------------------------------------------------------------
1 | .field_with_errors {
2 | @apply inline w-full;
3 |
4 | label {
5 | @apply text-red-500;
6 | }
7 |
8 | input, select, textarea {
9 | @apply border-red-500;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/views/sample_files/_left_menu.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= tag.div(class: "-ml-4") do %>
3 | <%= render FileTreeComponent.new(
4 | FileTree.new(sample_files).tree
5 | ) %>
6 | <% end %>
7 |
8 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
2 |
3 | require "bundler/setup" # Set up gems listed in the Gemfile.
4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations.
5 |
--------------------------------------------------------------------------------
/app/views/icons/heroicons/solid/_folder.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/db/migrate/20241022110917_add_discarded_at_to_samples.rb:
--------------------------------------------------------------------------------
1 | class AddDiscardedAtToSamples < ActiveRecord::Migration[7.0]
2 | def change
3 | add_column :samples, :discarded_at, :datetime
4 | add_index :samples, :discarded_at
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative "config/application"
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss/base";
2 | @import "tailwindcss/components";
3 | @import "tailwindcss/utilities";
4 |
5 | @import "rails.scss";
6 |
7 | .font-recursive-mono {
8 | font-variation-settings: "MONO" 1;
9 | }
10 |
--------------------------------------------------------------------------------
/db/migrate/20220904043114_add_username_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddUsernameToUsers < ActiveRecord::Migration[7.0]
2 | def change
3 | add_column :users, :username, :string, null: false, default: ""
4 | add_index :users, :username, unique: true
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "bundler"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | - package-ecosystem: "npm"
8 | directory: "/"
9 | schedule:
10 | interval: "daily"
11 |
--------------------------------------------------------------------------------
/lib/preview_renderer.rb:
--------------------------------------------------------------------------------
1 | require 'redcarpet/render_strip'
2 |
3 | class PreviewRenderer < Redcarpet::Render::StripDown
4 | def link(link, title, content)
5 | content
6 | end
7 |
8 | def image(link, title, content)
9 | ""
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/factories/user_factory.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :user do
3 | sequence(:username) { |n| "user#{n}" }
4 | sequence(:email) { |n| "user#{n}@example.com" }
5 | password { "password" }
6 | confirmed_at { 1.minute.ago }
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/confirmation_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Welcome <%= @email %>!
2 |
3 | You can confirm your account email through the link below:
4 |
5 | <%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>
6 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | default: &default
2 | adapter: postgresql
3 | encoding: unicode
4 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
5 |
6 | development:
7 | <<: *default
8 |
9 | test:
10 | <<: *default
11 |
12 | production:
13 | <<: *default
14 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files.
2 |
3 | # Mark the database schema as having been generated.
4 | db/schema.rb linguist-generated
5 |
6 | # Mark any vendored files as having been vendored.
7 | vendor/* linguist-vendored
8 |
--------------------------------------------------------------------------------
/app/controllers/home_controller.rb:
--------------------------------------------------------------------------------
1 | class HomeController < ApplicationController
2 | skip_before_action :authenticate_user!
3 |
4 | def index
5 | @samples = Sample.kept.status_public.order(created_at: :desc).limit(10)
6 | @categories = Category.all.order(:name)
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/20220904195045_add_status_to_samples.rb:
--------------------------------------------------------------------------------
1 | class AddStatusToSamples < ActiveRecord::Migration[7.0]
2 | def change
3 | create_enum :sample_status, ["private", "public"]
4 |
5 | add_column :samples, :status, :sample_status, default: "private", null: false
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/jobs/process_sample_job.rb:
--------------------------------------------------------------------------------
1 | class ProcessSampleJob
2 | include Sidekiq::Job
3 |
4 | def perform(sample_id)
5 | sample = Sample.find(sample_id)
6 |
7 | sample.sample_files.each do |sample_file|
8 | sample_file.update_open_graph_image
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/policies/user_profile_policy.rb:
--------------------------------------------------------------------------------
1 | class UserProfilePolicy < ApplicationPolicy
2 | attr_reader :user, :user_profile
3 |
4 | def initialize(user, user_profile)
5 | @user = user
6 | @user_profile = user_profile
7 | end
8 |
9 | def show?
10 | true
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/javascript/controllers/application.js:
--------------------------------------------------------------------------------
1 | import { Application } from "@hotwired/stimulus"
2 |
3 | const application = Application.start()
4 |
5 | // Configure Stimulus development experience
6 | application.debug = false
7 | window.Stimulus = application
8 |
9 | export { application }
10 |
--------------------------------------------------------------------------------
/app/views/icons/heroicons/solid/_plus-sm.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/controllers/sample_files_controller.rb:
--------------------------------------------------------------------------------
1 | class SampleFilesController < ApplicationController
2 | skip_before_action :authenticate_user!
3 |
4 | def show
5 | @sample = Sample.find(params[:sample_id])
6 | @sample_file = SampleFile.find(params[:id])
7 | authorize @sample
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | # Automatically retry jobs that encountered a deadlock
3 | # retry_on ActiveRecord::Deadlocked
4 |
5 | # Most jobs are safe to ignore if the underlying records are no longer available
6 | # discard_on ActiveJob::DeserializationError
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/icons/heroicons/solid/_lock-closed.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/views/icons/heroicons/solid/_chevron-down.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/db/migrate/20220821010120_create_categories.rb:
--------------------------------------------------------------------------------
1 | class CreateCategories < ActiveRecord::Migration[7.0]
2 | def change
3 | create_table :categories do |t|
4 | t.string :name, null: false
5 | t.string :description
6 | t.string :slug, null: false
7 |
8 | t.timestamps
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/views/samples/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Edit Sample" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Edit Sample") %>
6 | <% end %>
7 | <% end %>
8 |
9 |
10 | <%= render "form", sample: @sample %>
11 |
12 |
--------------------------------------------------------------------------------
/app/views/samples/new.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "New Sample" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("New Sample") %>
6 | <% end %>
7 | <% end %>
8 |
9 |
10 | <%= render "form", sample: @sample %>
11 |
12 |
--------------------------------------------------------------------------------
/app/views/categories/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Edit Category" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Edit Category") %>
6 | <% end %>
7 | <% end %>
8 |
9 |
10 | <%= render "form", category: @category %>
11 |
12 |
--------------------------------------------------------------------------------
/app/views/categories/new.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "New Category" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("New Category") %>
6 | <% end %>
7 | <% end %>
8 |
9 |
10 | <%= render "form", category: @category %>
11 |
12 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/unlock_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.email %>!
2 |
3 | Your account has been locked due to an excessive number of unsuccessful sign in attempts.
4 |
5 | Click the link below to unlock your account:
6 |
7 | <%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>
8 |
--------------------------------------------------------------------------------
/app/views/users/index.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Users" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Users") %>
6 | <% end %>
7 | <% end %>
8 |
9 |
10 | <%= render @users %>
11 |
12 |
--------------------------------------------------------------------------------
/app/views/icons/heroicons/outline/_document-text.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/db/migrate/20220821012828_create_sample_files.rb:
--------------------------------------------------------------------------------
1 | class CreateSampleFiles < ActiveRecord::Migration[7.0]
2 | def change
3 | create_table :sample_files do |t|
4 | t.references :sample, null: false, foreign_key: true
5 | t.string :path
6 | t.text :contents
7 | t.text :description
8 |
9 | t.timestamps
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/javascript/controllers/index.js:
--------------------------------------------------------------------------------
1 | import { application } from "./application"
2 | import controllers from "./**/*_controller.js"
3 |
4 | import NestedForm from "stimulus-rails-nested-form";
5 | application.register("nested-form", NestedForm);
6 |
7 | controllers.forEach((controller) => {
8 | application.register(controller.name, controller.module.default)
9 | })
10 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/email_changed.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @email %>!
2 |
3 | <% if @resource.try(:unconfirmed_email?) %>
4 | We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.
5 | <% else %>
6 | We're contacting you to notify you that your email has been changed to <%= @resource.email %>.
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/app/views/sample_files/_contents.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% (1..sample_file.line_count_or_default).each do |l| %>
4 | <%= l %>
5 | <% end %>
6 |
7 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/db/migrate/20220821010831_create_samples.rb:
--------------------------------------------------------------------------------
1 | class CreateSamples < ActiveRecord::Migration[7.0]
2 | def change
3 | create_table :samples do |t|
4 | t.references :category, null: false, foreign_key: true
5 | t.references :user, null: false, foreign_key: true
6 | t.string :title
7 | t.text :description
8 |
9 | t.timestamps
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/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 bin/rails db:seed command (or created alongside the database with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }])
7 | # Character.create(name: "Luke", movie: movies.first)
8 |
--------------------------------------------------------------------------------
/app/views/errors/not_found.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Error" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Error") %>
6 | <% end %>
7 | <% end %>
8 |
9 |
10 | The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved.
11 |
12 |
--------------------------------------------------------------------------------
/app/models/sample.rb:
--------------------------------------------------------------------------------
1 | class Sample < ApplicationRecord
2 | include Discard::Model
3 |
4 | enum :status, { private: "private", public: "public" }, prefix: :status
5 |
6 | belongs_to :category
7 | belongs_to :user
8 | has_many :sample_files, -> { order(:path) }
9 | accepts_nested_attributes_for :sample_files, reject_if: :all_blank, allow_destroy: true
10 |
11 | validates :title, presence: true
12 | end
13 |
--------------------------------------------------------------------------------
/app/views/errors/internal_error.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Error" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Error") %>
6 | <% end %>
7 | <% end %>
8 |
9 |
10 | We’re sorry, but something went wrong. We’ve been notified about this issue and we’ll take a look at it shortly.
11 |
12 |
--------------------------------------------------------------------------------
/app/validators/slug_validator.rb:
--------------------------------------------------------------------------------
1 | class SlugValidator < ActiveModel::EachValidator
2 | def validate_each(record, attribute, value)
3 | return if value.blank?
4 | return if value.to_s.match?(/\A([a-z0-9_-]+)\Z/)
5 |
6 | record.errors.add(attribute, (options[:message] || default_message))
7 | end
8 |
9 | private
10 |
11 | def default_message
12 | "is not a valid slug format"
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/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 | amazon:
10 | public: true
11 | service: S3
12 | access_key_id: <%= ENV["S3_ACCESS_KEY_ID"] %>
13 | secret_access_key: <%= ENV["S3_SECRET_ACCESS_KEY"] %>
14 | region: <%= ENV["S3_REGION"] %>
15 | bucket: <%= ENV["S3_BUCKET"] %>
16 |
--------------------------------------------------------------------------------
/esbuild.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path")
2 | const rails = require("esbuild-rails")
3 |
4 | require("esbuild").build({
5 | entryPoints: ["application.js"],
6 | bundle: true,
7 | outdir: path.join(process.cwd(), "app/assets/builds"),
8 | absWorkingDir: path.join(process.cwd(), "app/javascript"),
9 | watch: process.argv.includes("--watch"),
10 | plugins: [rails()]
11 | }).catch(() => process.exit(1))
12 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('postcss-import'),
4 | require('tailwindcss/nesting'),
5 | require('tailwindcss')('./tailwind.config.js'),
6 | require('autoprefixer'),
7 | require('postcss-flexbugs-fixes'),
8 | require('postcss-preset-env')({
9 | autoprefixer: {
10 | flexbox: 'no-2009'
11 | },
12 | stage: 3
13 | })
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/app/assets/images/avatar.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/views/categories/_category_mini.html.erb:
--------------------------------------------------------------------------------
1 | <%= link_to category_path(category.slug), class: "bg-white rounded shadow-sm px-4 py-3" do %>
2 |
3 |
4 | <%= category.name %>
5 |
6 |
7 |
8 | <%= pluralize(category.samples.status_public.size, "sample") %>
9 |
10 |
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 | skip_before_action :authenticate_user!
3 |
4 | def index
5 | @users = User.all.includes(:samples).references(:samples).merge(Sample.status_public)
6 | end
7 |
8 | def show
9 | @user = User.find_by(username: params[:username])
10 | authorize @user, policy_class: UserProfilePolicy
11 | @samples = @user.samples.status_public
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of
4 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
5 | # notations and behaviors.
6 | Rails.application.config.filter_parameters += [
7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
8 | ]
9 |
--------------------------------------------------------------------------------
/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Define an application-wide HTTP permissions policy. For further
2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy
3 | #
4 | # Rails.application.config.permissions_policy do |f|
5 | # f.camera :none
6 | # f.gyroscope :none
7 | # f.microphone :none
8 | # f.usb :none
9 | # f.fullscreen :self
10 | # f.payment :self, "https://secure.example.com"
11 | # end
12 |
--------------------------------------------------------------------------------
/app/validators/file_path_validator.rb:
--------------------------------------------------------------------------------
1 | class FilePathValidator < ActiveModel::EachValidator
2 | FILE_PATH_REGEX = /\A[\w.-][\w.-\/]+[\w.-]\Z/i
3 |
4 | def validate_each(record, attribute, value)
5 | return if value.blank?
6 | return if value =~ FILE_PATH_REGEX
7 |
8 | record.errors.add(attribute, (options[:message] || default_message))
9 | end
10 |
11 | private
12 |
13 | def default_message
14 | "is not a valid file path"
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/reset_password_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.email %>!
2 |
3 | Someone has requested a link to change your password. You can do this through the link below.
4 |
5 | <%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>
6 |
7 | If you didn't request this, please ignore this email.
8 | Your password won't change until you access the link above and create a new one.
9 |
--------------------------------------------------------------------------------
/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | UmJ2QpxSU2N7PQwMKqq+iorNTXut119tQ0U7LN/n2X+xyoa2fqWoDiVMs4lFCf2f5Az6hdjy+0gZccHpdCa7xXnzIPDTHtop5M9dUg5xdUhqInzHVg9kBxAhWMjvj7HCU+oIE2wY1knkNKDMan5zDVhCO6qx8FemK9C/bYwW0Pp/UgFMi9DPhN/FgYbzO/NXGgsyb+gucKVOrO9ZQSf/jr4tmZEQYMmE6K0LoYhSbQ5OoL4l0bYJGzmlUHaxBZeFgAvSLpdWn3cUdF+g9aQUaO/973i8kMRDsiSUbzqYnwdZi75ow0qnziT0yrt9DL4fAAnviwOR1/5TeWNh2BCYnHaqJXRJ8mTceTo03xWbo/iNsAKyck5UkT17Fc8+bWGC6GJEtXWTnmkaQrWF4iOTxXWlgROX5112fF/p--qDatbwVjPmKndOup--cY/0WOiV5yK0RH3kxKdPQg==
--------------------------------------------------------------------------------
/app/policies/category_policy.rb:
--------------------------------------------------------------------------------
1 | class CategoryPolicy < ApplicationPolicy
2 | attr_reader :user, :category
3 |
4 | def initialize(user, category)
5 | @user = user
6 | @category = category
7 | end
8 |
9 | def show?
10 | true
11 | end
12 |
13 | def create?
14 | admin?
15 | end
16 |
17 | def update?
18 | admin?
19 | end
20 |
21 | def destroy?
22 | admin?
23 | end
24 |
25 | private
26 |
27 | def admin?
28 | user.admin?
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/views/errors/unprocessable.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Error" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Error") %>
6 | <% end %>
7 | <% end %>
8 |
9 |
10 | This site requires cookies. Please make sure you have cookies enabled for this site. If you already have cookies enabled, it may be that you tried to change something you didn’t have access to.
11 |
12 |
--------------------------------------------------------------------------------
/app/controllers/resizes_controller.rb:
--------------------------------------------------------------------------------
1 | class ResizesController < ApplicationController
2 | skip_before_action :authenticate_user!
3 |
4 | def update
5 | case params[:setting]
6 | when "menu_width"
7 | cookies[:menu_width] = params[:width].to_i
8 | when "contents_width"
9 | cookies[:contents_width] = params[:width].to_i
10 | when "editor_height"
11 | cookies[:editor_height] = params[:height].to_i
12 | end
13 |
14 | head :no_content
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/views/devise/shared/_error_messages.html.erb:
--------------------------------------------------------------------------------
1 | <% if resource.errors.any? %>
2 |
3 |
4 | <%= I18n.t("errors.messages.not_saved",
5 | count: resource.errors.count,
6 | resource: resource.class.model_name.human.downcase)
7 | %>
8 |
9 |
10 | <% resource.errors.full_messages.each do |message| %>
11 | <%= message %>
12 | <% end %>
13 |
14 |
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/app/controllers/account/accounts_controller.rb:
--------------------------------------------------------------------------------
1 | class Account::AccountsController < ApplicationController
2 | def show
3 | end
4 |
5 | def update
6 | if current_user.update(account_params)
7 | redirect_to account_account_path, notice: "Account updated"
8 | else
9 | render :show, status: :unprocessable_entity
10 | end
11 | end
12 |
13 | private
14 |
15 | def account_params
16 | params.require(:user).permit(
17 | :avatar, :email, :username
18 | )
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/javascript/controllers/toggle_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus";
2 |
3 | export default class extends Controller {
4 | static targets = ["toggleable"];
5 | static classes = ["toggle"]
6 |
7 | toggle() {
8 | this.toggleableTargets.forEach((t) => t.classList.toggle(this.toggleClass));
9 | }
10 |
11 | hide(event) {
12 | if (this.element.contains(event.target) === false) {
13 | this.toggleableTargets.forEach((t) => t.classList.add(this.toggleClass));
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/validators/username_validator.rb:
--------------------------------------------------------------------------------
1 | class UsernameValidator < ActiveModel::EachValidator
2 | USERNAME_REGEX = /\A([a-z0-9_-]+)\Z/i
3 |
4 | def validate_each(record, attribute, value)
5 | return if options[:allow_blank] && value.blank?
6 | return if options[:allow_nil] && value.nil?
7 | return if value =~ USERNAME_REGEX
8 |
9 | record.errors.add(attribute, (options[:message] || default_message))
10 | end
11 |
12 | private
13 |
14 | def default_message
15 | "is not a valid format"
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/policies/application_policy.rb:
--------------------------------------------------------------------------------
1 | class ApplicationPolicy
2 | attr_reader :user, :record
3 |
4 | def initialize(user, record)
5 | @user = user
6 | @record = record
7 | end
8 |
9 | def index?
10 | false
11 | end
12 |
13 | def show?
14 | false
15 | end
16 |
17 | def create?
18 | false
19 | end
20 |
21 | def new?
22 | create?
23 | end
24 |
25 | def update?
26 | false
27 | end
28 |
29 | def edit?
30 | update?
31 | end
32 |
33 | def destroy?
34 | false
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/app/policies/sample_policy.rb:
--------------------------------------------------------------------------------
1 | class SamplePolicy < ApplicationPolicy
2 | attr_reader :user, :sample
3 |
4 | def initialize(user, sample)
5 | @user = user
6 | @sample = sample
7 | end
8 |
9 | def show?
10 | public? || owner?
11 | end
12 |
13 | def create?
14 | true
15 | end
16 |
17 | def update?
18 | owner?
19 | end
20 |
21 | def destroy?
22 | owner?
23 | end
24 |
25 | private
26 |
27 | def public?
28 | @sample.status_public?
29 | end
30 |
31 | def owner?
32 | sample.user == user
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = "1.0"
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in the app/assets
11 | # folder are already added.
12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
13 |
--------------------------------------------------------------------------------
/app/models/file_tree.rb:
--------------------------------------------------------------------------------
1 | class FileTree
2 | def initialize(files)
3 | @files = files
4 | end
5 |
6 | def tree
7 | files.sort_by { |file| file.path }
8 | .map { |file| path_hash(file.path.split("/"), file) }
9 | .reduce({}) { |full, path| full.deep_merge(path) }
10 | end
11 |
12 | private
13 |
14 | attr_reader :files
15 |
16 | def path_hash(fragments, file)
17 | if fragments.size == 1
18 | { fragments[0] => file }
19 | else
20 | { fragments[0] => path_hash(fragments[1..-1], file) }
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/app/controllers/account/passwords_controller.rb:
--------------------------------------------------------------------------------
1 | class Account::PasswordsController < ApplicationController
2 | def update
3 | if current_user.update_with_password(password_params)
4 | bypass_sign_in(current_user)
5 | redirect_to account_account_path, notice: "Password updated"
6 | else
7 | render "account/accounts/show", status: :unprocessable_entity
8 | end
9 | end
10 |
11 | private
12 |
13 | def password_params
14 | params.require(:user).permit(
15 | :password, :password_confirmation, :current_password
16 | )
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ApplicationRecord
2 | # Include default devise modules. Others available are:
3 | # :lockable, :timeoutable, :trackable and :omniauthable
4 | devise :database_authenticatable, :confirmable, :registerable,
5 | :recoverable, :rememberable, :validatable
6 |
7 | has_many :samples
8 | has_one_attached :avatar
9 |
10 | validates :username, presence: true, username: true, uniqueness: true
11 |
12 | def active_for_authentication?
13 | super && active?
14 | end
15 |
16 | def active?
17 | deactivated_at.nil?
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] ||= "test"
2 | require_relative "../config/environment"
3 | require "rails/test_help"
4 |
5 | require "sidekiq/testing"
6 |
7 | class ActiveSupport::TestCase
8 | include Devise::Test::IntegrationHelpers
9 | include FactoryBot::Syntax::Methods
10 |
11 | # Run tests in parallel with specified workers
12 | parallelize(workers: :number_of_processors)
13 |
14 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
15 | fixtures :all
16 |
17 | # Add more helper methods to be used by all tests here...
18 | end
19 |
--------------------------------------------------------------------------------
/app/controllers/errors_controller.rb:
--------------------------------------------------------------------------------
1 | class ErrorsController < ApplicationController
2 | skip_before_action :authenticate_user!
3 |
4 | def not_found
5 | return_response status: 404
6 | end
7 |
8 | def unprocessable
9 | return_response status: 422
10 | end
11 |
12 | def internal_error
13 | return_response status: 500
14 | end
15 |
16 | private
17 |
18 | def return_response(status:)
19 | respond_to do |format|
20 | format.html { render status: status }
21 | format.all { head status, content_type: "text/html" }
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/views/categories/index.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Categories" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Categories") %>
6 |
7 | <% c.with_details do %>
8 | <% if policy(Category).new? %>
9 | <%= link_to "New", [:new, :category], class: "px-2 py-1 bg-[#950000] text-white text-sm rounded" %>
10 | <% end %>
11 | <% end %>
12 | <% end %>
13 | <% end %>
14 |
15 |
16 | <%= render @categories %>
17 |
18 |
--------------------------------------------------------------------------------
/app/views/users/show.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: @user.username %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_parent do %>
6 | <%= link_to "Users", [:users] %>
7 | <% end %>
8 |
9 | <% c.with_title do %>
10 | <%= image_tag user_avatar(@user), class: "h-12 w-12 bg-white border-2 border-white rounded-full mr-2" %>
11 | @<%= @user.username %>
12 | <% end %>
13 | <% end %>
14 | <% end %>
15 |
16 |
17 | <%= render @samples %>
18 |
19 |
--------------------------------------------------------------------------------
/app/views/categories/_category.html.erb:
--------------------------------------------------------------------------------
1 | <%= link_to category_path(category.slug), class: "bg-white rounded shadow-sm px-4 py-3" do %>
2 |
3 |
4 | <%= category.name %>
5 |
6 |
7 |
8 | <%= strip_markdown(category.description).truncate_words(50) %>
9 |
10 |
11 |
12 | <%= pluralize(category.samples.status_public.size, "sample") %>
13 |
14 |
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/app/models/sample_file.rb:
--------------------------------------------------------------------------------
1 | class SampleFile < ApplicationRecord
2 | belongs_to :sample
3 | has_one_attached :open_graph_image
4 |
5 | validates :path, presence: true, file_path: true
6 |
7 | def first_lines(number)
8 | return "" if contents.blank?
9 |
10 | contents.each_line.take(number)
11 | end
12 |
13 | def line_count_or_default
14 | return 1 if contents.blank?
15 |
16 | contents.lines.count
17 | end
18 |
19 | def update_open_graph_image
20 | self.open_graph_image.attach(io: CreateSampleFileImage.new(self).create, filename: "sample_file_#{id}.png")
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/controllers/turbo_controller.rb:
--------------------------------------------------------------------------------
1 | class TurboController < ApplicationController
2 | class Responder < ActionController::Responder
3 | def to_turbo_stream
4 | controller.render(options.merge(formats: :html))
5 | rescue ActionView::MissingTemplate => error
6 | if get?
7 | raise error
8 | elsif has_errors? && default_action
9 | render rendering_options.merge(formats: :html, status: :unprocessable_entity)
10 | else
11 | redirect_to navigation_location
12 | end
13 | end
14 | end
15 |
16 | self.responder = Responder
17 | respond_to :html, :turbo_stream
18 | end
19 |
--------------------------------------------------------------------------------
/app/views/account/samples/show.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Your Samples" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Your Samples") %>
6 |
7 | <% c.with_details do %>
8 | <% if policy(Sample).new? %>
9 | <%= link_to "New", [:new, :sample], class: "px-2 py-1 bg-[#950000] text-white text-sm rounded" %>
10 | <% end %>
11 | <% end %>
12 | <% end %>
13 | <% end %>
14 |
15 |
16 | <%= render collection: @samples, partial: "samples/sample" %>
17 |
18 |
--------------------------------------------------------------------------------
/app/views/users/_user.html.erb:
--------------------------------------------------------------------------------
1 | <%= link_to user_path(user.username), class: "bg-white rounded shadow-sm px-4 py-3" do %>
2 |
3 |
4 | <%= image_tag user_avatar(user), class: "h-12 w-12 bg-white border-2 border-white rounded-full" %>
5 |
6 |
7 | @<%= user.username %>
8 |
9 |
10 |
11 |
12 | <%= pluralize(user.samples.status_public.size, "sample") %>
13 |
14 |
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/app/services/create_sample_file_image.rb:
--------------------------------------------------------------------------------
1 | class CreateSampleFileImage
2 | def initialize(sample_file)
3 | @sample_file = sample_file
4 | end
5 |
6 | def create
7 | svg_string = ApplicationController.render(
8 | partial: "sample_files/sample_file", format: "svg", assigns: { sample_file: sample_file }
9 | )
10 |
11 | svg_file = Tempfile.new
12 | begin
13 | svg_file.write(svg_string)
14 |
15 | pipeline = ImageProcessing::Vips
16 | pipeline.source(svg_file).convert("png").call
17 | ensure
18 | svg_file.close
19 | svg_file.unlink
20 | end
21 | end
22 |
23 | private
24 |
25 | attr_reader :sample_file
26 | end
27 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: {
3 | content: [
4 | "./app/views/**/*.html.erb",
5 | "./app/helpers/**/*.rb",
6 | './app/components/**/*.erb',
7 | './app/components/**/*.rb',
8 | "./app/javascript/**/*.js",
9 | ]
10 | },
11 | theme: {
12 | fontFamily: {
13 | sans: ['Recursive', 'sans-serif'],
14 | },
15 | extend: {
16 | maxWidth: {
17 | '8xl': '88rem',
18 | '9xl': '96rem',
19 | }
20 | },
21 | },
22 | variants: {},
23 | plugins: [
24 | require('@tailwindcss/forms'),
25 | require('@tailwindcss/line-clamp'),
26 | require('@tailwindcss/typography'),
27 | ],
28 | }
29 |
--------------------------------------------------------------------------------
/app/controllers/account/avatars_controller.rb:
--------------------------------------------------------------------------------
1 | class Account::AvatarsController < ApplicationController
2 | def update
3 | if current_user.update(avatar_params)
4 | redirect_to account_account_path, notice: "Avatar uploaded"
5 | else
6 | render "account/accounts/show", status: :unprocessable_entity
7 | end
8 | end
9 |
10 | def destroy
11 | if current_user.avatar.purge
12 | redirect_to account_account_path, notice: "Avatar removed"
13 | else
14 | render "account/accounts/show", status: :unprocessable_entity
15 | end
16 | end
17 |
18 | private
19 |
20 | def avatar_params
21 | params.require(:user).permit(:avatar)
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, "\\1en"
8 | # inflect.singular /^(ox)en/i, "\\1"
9 | # inflect.irregular "person", "people"
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym "RESTful"
16 | # end
17 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | include Pundit::Authorization
3 | rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
4 |
5 | before_action :authenticate_user!
6 | before_action :configure_permitted_parameters, if: :devise_controller?
7 |
8 | def current_user
9 | super || GuestUser.new
10 | end
11 |
12 | def user_signed_in?
13 | super && !current_user.is_a?(GuestUser)
14 | end
15 |
16 | protected
17 |
18 | def configure_permitted_parameters
19 | devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
20 | end
21 |
22 | private
23 |
24 | def user_not_authorized
25 | redirect_back(fallback_location: root_path, alert: "You are not authorized to perform this action.")
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/views/devise/passwords/new.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Forgot Password?" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Forgot Password?") %>
6 | <% end %>
7 | <% end %>
8 |
9 |
10 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), builder: TailwindBuilder, html: { method: :post }) do |f| %>
11 | <%= render "devise/shared/error_messages", resource: resource %>
12 |
13 |
14 | <%= f.label :email %>
15 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
16 |
17 |
18 | <%= f.submit "Send password reset instructions", class: "px-3 py-2 text-white rounded bg-neutral-800 hover:bg-neutral-900 cursor-pointer" %>
19 | <% end %>
20 |
21 |
--------------------------------------------------------------------------------
/lib/svg_inline.rb:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*- #
2 | # frozen_string_literal: true
3 |
4 | module Rouge
5 | module Formatters
6 | class SVGInline < HTML
7 | tag 'svg_inline'
8 |
9 | def initialize(theme)
10 | if theme.is_a?(Class) && theme < Rouge::Theme
11 | @theme = theme.new
12 | elsif theme.is_a?(Rouge::Theme)
13 | @theme = theme
14 | elsif theme.is_a?(String)
15 | @theme = Rouge::Theme.find(theme).new
16 | else
17 | raise ArgumentError, "invalid theme: #{theme.inspect}"
18 | end
19 | end
20 |
21 | def safe_span(tok, safe_val)
22 | return safe_val if tok == Token::Tokens::Text
23 |
24 | style = @theme.style_for(tok)
25 |
26 | "#{safe_val} "
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/components/header_component.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% if parent? || actions? %>
4 |
5 |
6 | <%= parent %>
7 |
8 |
9 |
10 | <%= actions %>
11 |
12 |
13 | <% end %>
14 |
15 |
16 |
17 | <%= title %>
18 |
19 |
20 |
21 | <%= details %>
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/components/file_tree_component.html.erb:
--------------------------------------------------------------------------------
1 | <%= tag.ul(class: "ml-4") do %>
2 | <% @tree.map do |node| %>
3 | <%= tag.li do %>
4 | <% if node.last.is_a?(SampleFile) %>
5 | <%= link_to [node.last.sample, node.last], class: "flex items-center space-x-1 ml-5 mb-2 hover:text-blue-300" do %>
6 | <%= render "icons/heroicons/outline/document-text" %>
7 | <%= node.first %>
8 | <% end %>
9 | <% else %>
10 |
11 |
12 | <%= render "icons/heroicons/solid/chevron-down" %>
13 | <%= render "icons/heroicons/solid/folder" %>
14 |
15 |
16 |
<%= node.first %>
17 |
18 |
19 | <%= render FileTreeComponent.new(node.last) %>
20 | <% end %>
21 | <% end %>
22 | <% end %>
23 | <% end %>
24 |
--------------------------------------------------------------------------------
/app/views/devise/confirmations/new.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Resend Confirmation" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Resend Confirmation") %>
6 | <% end %>
7 | <% end %>
8 |
9 |
10 | <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), builder: TailwindBuilder, html: { method: :post }) do |f| %>
11 | <%= render "devise/shared/error_messages", resource: resource %>
12 |
13 |
14 | <%= f.label :email %>
15 | <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
16 |
17 |
18 | <%= f.submit "Resend confirmation instructions", class: "px-3 py-2 text-white rounded bg-neutral-800 hover:bg-neutral-900 cursor-pointer" %>
19 | <% end %>
20 |
21 |
--------------------------------------------------------------------------------
/app/views/samples/_sample_file_fields.html.erb:
--------------------------------------------------------------------------------
1 | <%= content_tag :div, class: "nested-form-wrapper bg-[#cecece] p-4 mb-6 rounded", data: { new_record: form.object.new_record? } do %>
2 |
3 | <%= form.label :path %>
4 |
5 |
Rails.root /
6 | <%= form.text_field :path, placeholder: "app/models/user.rb" %>
7 |
8 |
9 |
10 |
11 | <%= form.label :contents, "Code" %>
12 | <%= form.text_area :contents, rows: 8 %>
13 |
14 |
15 |
16 | <%= form.label :description, "Description" %>
17 | <%= form.text_area :description, rows: 6 %>
18 |
19 |
20 |
21 | Remove
22 |
23 |
24 | <%= form.hidden_field :_destroy %>
25 | <% end %>
26 |
--------------------------------------------------------------------------------
/app/views/sample_files/_top_menu.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | <% sample_files.each do |file| %>
14 | <%= link_to file.path, [file.sample, file], class: "block px-3 py-2 text-xs text-neutral-600 hover:bg-neutral-100", role: "menuitem" %>
15 | <% end %>
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/system/samples_test.rb:
--------------------------------------------------------------------------------
1 | require "application_system_test_case"
2 |
3 | class SamplesTest < ApplicationSystemTestCase
4 | def setup
5 | @user = create(:user)
6 | create(:category, name: "Default")
7 | end
8 |
9 | test "visiting the index" do
10 | sign_in @user
11 | visit new_sample_path
12 |
13 | fill_in "sample[title]", with: "Special Test"
14 | select "Default", from: "sample[category_id]"
15 | select "Public", from: "sample[status]"
16 | fill_in "sample[description]", with: "This is a special description."
17 |
18 | fill_in "sample[sample_files_attributes][0][path]", with: "app/models/special.rb"
19 | fill_in "sample[sample_files_attributes][0][contents]", with: "class Special\nend"
20 | fill_in "sample[sample_files_attributes][0][description]", with: "This is a special class."
21 |
22 | click_on "Save"
23 |
24 | assert_selector "h1", text: "Special Test"
25 | assert_selector "pre", text: "class Special\nend"
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t "hello"
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t("hello") %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # "true": "foo"
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/app/views/categories/show.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: @category.name, description: strip_markdown(@category.description) %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_parent do %>
6 | <%= link_to "Categories", [:categories] %>
7 | <% end %>
8 |
9 | <% c.with_actions do %>
10 | <% if policy(@category).edit? %>
11 | <%= link_to "Edit", [:edit, @category], class: "px-2 py-1 bg-[#950000] text-white text-sm rounded" %>
12 | <% end %>
13 | <% end %>
14 |
15 | <% c.with_title.with_content(@category.name) %>
16 | <% end %>
17 | <% end %>
18 |
19 | <% if @category.description.present? %>
20 | Description
21 |
22 |
23 | <%= markdown @category.description %>
24 |
25 | <% end %>
26 |
27 |
28 | <%= render @samples %>
29 |
30 |
--------------------------------------------------------------------------------
/app/views/categories/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with(model: category, builder: TailwindBuilder) do |form| %>
2 | <% if category.errors.any? %>
3 | Please review the problems below:
4 |
5 |
6 | <% category.errors.full_messages.each do |msg| %>
7 | <%= msg.html_safe %>
8 | <% end %>
9 |
10 | <% end %>
11 |
12 |
13 | <%= form.label :name %>
14 | <%= form.text_field :name %>
15 |
16 |
17 |
18 | <%= form.label :slug %>
19 | <%= form.text_field :slug %>
20 |
21 |
22 |
23 | <%= form.label :description, "Description" %>
24 | <%= form.text_area :description, rows: 6 %>
25 |
26 |
27 |
28 |
29 |
30 | <%= form.submit "Save", class: "px-3 py-2 text-white rounded bg-neutral-800 hover:bg-neutral-900 cursor-pointer" %>
31 |
32 | <% end %>
33 |
--------------------------------------------------------------------------------
/.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 | .env*
8 | !.env.example
9 |
10 | .DS_Store
11 |
12 | # Ignore bundler config.
13 | /.bundle
14 |
15 | # Ignore all logfiles and tempfiles.
16 | /log/*
17 | /tmp/*
18 | !/log/.keep
19 | !/tmp/.keep
20 |
21 | # Ignore pidfiles, but keep the directory.
22 | /tmp/pids/*
23 | !/tmp/pids/
24 | !/tmp/pids/.keep
25 |
26 | # Ignore uploaded files in development.
27 | /storage/*
28 | !/storage/.keep
29 | /tmp/storage/*
30 | !/tmp/storage/
31 | !/tmp/storage/.keep
32 |
33 | /public/assets
34 |
35 | # Ignore master key for decrypting credentials and more.
36 | /config/master.key
37 |
38 | /app/assets/builds/*
39 | !/app/assets/builds/.keep
40 |
41 | /node_modules
42 | /yarn-error.log
43 | yarn-debug.log*
44 | .yarn-integrity
45 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | ruby "3.1.2"
5 |
6 | gem "rails", "~> 7.0.3"
7 | gem "sprockets-rails"
8 | gem "pg", "~> 1.1"
9 | gem "puma", "~> 6.0"
10 | gem "turbo-rails"
11 | gem "stimulus-rails"
12 | gem "bootsnap", require: false
13 | gem "devise"
14 | gem "view_component"
15 | gem "pundit"
16 | gem "meta-tags"
17 | gem "image_processing", ">= 1.2"
18 | gem "lograge"
19 | gem "postmark-rails"
20 | gem "aws-sdk-s3", require: false
21 | gem "sentry-ruby"
22 | gem "sentry-rails"
23 | gem "jsbundling-rails"
24 | gem "cssbundling-rails"
25 | gem "redcarpet"
26 | gem "rouge"
27 | gem "sidekiq"
28 | gem "discard"
29 |
30 | group :development, :test do
31 | gem "debug", platforms: %i[ mri mingw x64_mingw ]
32 | gem "dotenv-rails"
33 | end
34 |
35 | group :development do
36 | gem "web-console"
37 | end
38 |
39 | group :test do
40 | gem "capybara"
41 | gem "factory_bot_rails"
42 | gem "selenium-webdriver"
43 | gem "shoulda"
44 | gem "webdrivers"
45 | end
46 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | require 'sidekiq/web'
2 |
3 | Rails.application.routes.draw do
4 | admin_constraint = lambda do |request|
5 | request.env["warden"].authenticate? && request.env["warden"].user.admin
6 | end
7 |
8 | mount Sidekiq::Web => "/admin/sidekiq", constraints: admin_constraint
9 |
10 | resources :categories
11 | resources :samples do
12 | resources :sample_files, path: :files, only: [:show]
13 | end
14 | resource :resize, only: [:update]
15 |
16 | resources :users, only: [:index]
17 | get "@:username", to: "users#show", as: :user
18 |
19 | namespace :account do
20 | resource :account, only: [:show, :update]
21 | resource :samples, only: [:show]
22 | resource :avatar, only: [:update, :destroy]
23 | resource :password, only: [:update]
24 |
25 | root to: "accounts#show"
26 | end
27 |
28 | devise_for :users
29 |
30 | root to: "home#index"
31 |
32 | get "/404", to: "errors#not_found"
33 | get "/422", to: "errors#unprocessable"
34 | get "/500", to: "errors#internal_error"
35 | end
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "railsinspire",
3 | "private": true,
4 | "author": "Jeremy Smith ",
5 | "license": "MIT",
6 | "dependencies": {
7 | "@hotwired/stimulus": "^3.2.1",
8 | "@hotwired/turbo-rails": "^7.1.3",
9 | "@rails/request.js": "^0.0.8",
10 | "@tailwindcss/forms": "^0.5.2",
11 | "@tailwindcss/line-clamp": "^0.4.2",
12 | "@tailwindcss/typography": "^0.5.4",
13 | "esbuild": "^0.15.5",
14 | "esbuild-rails": "^1.0.3",
15 | "postcss": "^8.4.16",
16 | "postcss-cli": "^10.0.0",
17 | "stimulus-rails-nested-form": "^4.0.0",
18 | "stimulus-use": "^0.50.0",
19 | "tailwindcss": "^3.1.8"
20 | },
21 | "version": "0.1.0",
22 | "scripts": {
23 | "build": "node esbuild.config.js",
24 | "build:css": "postcss ./app/assets/stylesheets/application.css -o ./app/assets/builds/application.css"
25 | },
26 | "devDependencies": {
27 | "postcss-flexbugs-fixes": "^5.0.2",
28 | "postcss-import": "^15.1.0",
29 | "postcss-preset-env": "^7.8.3"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | tests:
11 | name: Tests
12 | runs-on: ubuntu-22.04
13 | env:
14 | RAILS_ENV: test
15 | DATABASE_URL: postgres://railsinspire:password@localhost:5432/railsinspire_test
16 |
17 | services:
18 | postgres:
19 | image: postgres:14.6-alpine
20 | ports:
21 | - "5432:5432"
22 | env:
23 | POSTGRES_DB: railsinspire_test
24 | POSTGRES_USER: railsinspire
25 | POSTGRES_PASSWORD: password
26 |
27 | steps:
28 | - name: Checkout code
29 | uses: actions/checkout@v2
30 |
31 | - name: Setup Ruby
32 | uses: ruby/setup-ruby@v1
33 | with:
34 | bundler-cache: true
35 |
36 | - name: Setup database
37 | run: |
38 | bundle exec rails db:create
39 | bundle exec rails db:schema:load
40 |
41 | - name: Run tests
42 | run: bundle exec rails test:all
43 |
--------------------------------------------------------------------------------
/app/views/devise/passwords/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Change Password" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Change Password") %>
6 | <% end %>
7 | <% end %>
8 |
9 |
10 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), builder: TailwindBuilder, html: { method: :put }) do |f| %>
11 | <%= render "devise/shared/error_messages", resource: resource %>
12 | <%= f.hidden_field :reset_password_token %>
13 |
14 |
15 | <%= f.label :password, "New password" %>
16 | <%= f.password_field :password, autofocus: true, autocomplete: "new-password" %>
17 |
18 |
19 |
20 | <%= f.label :password_confirmation, "Confirm new password" %>
21 | <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
22 |
23 |
24 | <%= f.submit "Change password", class: "px-3 py-2 text-white rounded bg-neutral-800 hover:bg-neutral-900 cursor-pointer" %>
25 | <% end %>
26 |
27 |
--------------------------------------------------------------------------------
/app/views/home/index.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags reverse: false, title: "Curated code samples from Ruby on Rails projects", description: "RailsInspire is a curated collection of code samples from Ruby on Rails projects." %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("A curated collection of code samples from Ruby on Rails projects.") %>
6 |
7 | <% c.with_details do %>
8 | <% if !user_signed_in? %>
9 | Don’t have an account yet? <%= link_to "Sign up", new_registration_path(:user), class: "underline" %>.
10 | <% end %>
11 | <% end %>
12 | <% end %>
13 | <% end %>
14 |
15 | Latest Samples
16 |
17 |
18 | <%= render @samples %>
19 |
20 |
21 |
22 |
23 | Categories
24 |
25 |
26 | <%= render collection: @categories, partial: "categories/category_mini", as: :category %>
27 |
28 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path("..", __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts "== Installing dependencies =="
17 | system! "gem install bundler --conservative"
18 | system("bundle check") || system!("bundle install")
19 |
20 | # puts "\n== Copying sample files =="
21 | # unless File.exist?("config/database.yml")
22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml"
23 | # end
24 |
25 | puts "\n== Preparing database =="
26 | system! "bin/rails db:prepare"
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! "bin/rails log:clear tmp:clear"
30 |
31 | puts "\n== Restarting application server =="
32 | system! "bin/rails restart"
33 | end
34 |
--------------------------------------------------------------------------------
/app/javascript/controllers/resize_width_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus";
2 | import { FetchRequest } from "@rails/request.js";
3 | import { useResize, useDebounce } from "stimulus-use";
4 |
5 | export default class extends Controller {
6 | static values = { url: String, setting: String, currentWidth: Number };
7 | static debounces = ["resize"];
8 |
9 | connect() {
10 | useResize(this);
11 | useDebounce(this);
12 | }
13 |
14 | resize({ width }) {
15 | if (window.innerWidth < this.minBreakpointSize) return;
16 | if (width == this.currentWidthValue) return;
17 | if (width == 0) return;
18 |
19 | this.update(width);
20 | }
21 |
22 | async update(width) {
23 | let formData = new FormData();
24 | formData.append("width", width);
25 | formData.append("setting", this.settingValue);
26 |
27 | const request = new FetchRequest("patch", this.urlValue, { body: formData });
28 | const response = await request.perform();
29 | if (response.ok) {
30 | this.currentWidthValue = width;
31 | }
32 | }
33 |
34 | get minBreakpointSize() {
35 | return 768;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/javascript/controllers/resize_height_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus";
2 | import { FetchRequest } from "@rails/request.js";
3 | import { useResize, useDebounce } from "stimulus-use";
4 |
5 | export default class extends Controller {
6 | static values = { url: String, setting: String, currentHeight: String }
7 | static debounces = ["resize"];
8 |
9 | connect() {
10 | useResize(this);
11 | useDebounce(this);
12 | }
13 |
14 | resize({ height }) {
15 | if (window.innerWidth < this.minBreakpointSize) return;
16 | if (height == this.currentHeightValue) return;
17 | if (height == 0) return;
18 |
19 | this.update(height);
20 | }
21 |
22 | async update(height) {
23 | let formData = new FormData();
24 | formData.append("height", height);
25 | formData.append("setting", this.settingValue);
26 |
27 | const request = new FetchRequest("patch", this.urlValue, { body: formData });
28 | const response = await request.perform();
29 | if (response.ok) {
30 | this.currentHeightValue = height;
31 | }
32 | }
33 |
34 | get minBreakpointSize() {
35 | return 768;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy.
4 | # See the Securing Rails Applications Guide for more information:
5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header
6 |
7 | # Rails.application.configure do
8 | # config.content_security_policy do |policy|
9 | # policy.default_src :self, :https
10 | # policy.font_src :self, :https, :data
11 | # policy.img_src :self, :https, :data
12 | # policy.object_src :none
13 | # policy.script_src :self, :https
14 | # policy.style_src :self, :https
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 | #
19 | # # Generate session nonces for permitted importmap and inline scripts
20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
21 | # config.content_security_policy_nonce_directives = %w(script-src)
22 | #
23 | # # Report violations without enforcing the policy.
24 | # # config.content_security_policy_report_only = true
25 | # end
26 |
--------------------------------------------------------------------------------
/lib/one_dark.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Rouge
4 | module Themes
5 | class OneDark < CSSTheme
6 | name 'one-dark'
7 |
8 | palette faint: "#7F848E"
9 | palette purple: "#CD74E8"
10 | palette red: "#BE5046"
11 | palette salmon: "#EB6772"
12 | palette orange: "#DB9D63"
13 | palette blue: "#5CB3FA"
14 | palette white: "#AAB2BF"
15 | palette yellow: "#F0C678"
16 | palette pink: "#F8418E"
17 | palette green: "#98C379"
18 | palette aqua: "#5EBFCC"
19 |
20 | style Comment, fg: :faint, italic: true
21 | style Keyword, fg: :purple
22 | style Keyword::Pseudo, fg: :blue
23 | style Literal::Number, fg: :orange
24 | style Literal::String, fg: :green
25 | style Literal::String::Interpol, fg: :red
26 | style Literal::String::Symbol, fg: :aqua
27 | style Name::Attribute, fg: :orange
28 | style Name::Builtin, fg: :blue
29 | style Name::Class, fg: :yellow
30 | style Name::Constant, fg: :yellow
31 | style Name::Function, fg: :blue
32 | style Name::Tag, fg: :salmon
33 | style Operator, fg: :pink
34 | style Punctuation, fg: :white
35 | style Text, fg: :white
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails"
4 | # Pick the frameworks you want:
5 | require "active_model/railtie"
6 | require "active_job/railtie"
7 | require "active_record/railtie"
8 | require "active_storage/engine"
9 | require "action_controller/railtie"
10 | require "action_mailer/railtie"
11 | require "action_mailbox/engine"
12 | # require "action_text/engine"
13 | require "action_view/railtie"
14 | # require "action_cable/engine"
15 | require "rails/test_unit/railtie"
16 |
17 | # Require the gems listed in Gemfile, including any gems
18 | # you've limited to :test, :development, or :production.
19 | Bundler.require(*Rails.groups)
20 |
21 | module Railsinspire
22 | class Application < Rails::Application
23 | # Initialize configuration defaults for originally generated Rails version.
24 | config.load_defaults 7.0
25 | config.exceptions_app = self.routes
26 |
27 | # Configuration for the application, engines, and railties goes here.
28 | #
29 | # These settings can be overridden in specific environments using the files
30 | # in config/environments, which are processed later.
31 | #
32 | # config.time_zone = "Central Time (US & Canada)"
33 | # config.eager_load_paths << Rails.root.join("extras")
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/app/controllers/categories_controller.rb:
--------------------------------------------------------------------------------
1 | class CategoriesController < ApplicationController
2 | skip_before_action :authenticate_user!
3 |
4 | def index
5 | @categories = Category.all.order(:name)
6 | end
7 |
8 | def show
9 | @category = Category.find_by(slug: params[:id])
10 | authorize @category
11 | @samples = @category.samples.kept.status_public
12 | end
13 |
14 | def new
15 | @category = Category.new
16 | authorize @category
17 | end
18 |
19 | def create
20 | @category = Category.new(category_params)
21 | authorize @category
22 |
23 | if @category.save
24 | redirect_to category_path(@category.slug)
25 | else
26 | render :new, status: :unprocessable_entity
27 | end
28 | end
29 |
30 | def edit
31 | @category = Category.find(params[:id])
32 | authorize @category
33 | end
34 |
35 | def update
36 | @category = Category.find(params[:id])
37 | authorize @category
38 |
39 | if @category.update(category_params)
40 | redirect_to category_path(@category.slug)
41 | else
42 | render :edit, status: :unprocessable_entity
43 | end
44 | end
45 |
46 | private
47 |
48 | def category_params
49 | params.require(:category).permit(:name, :slug, :description)
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/app/views/samples/_sample.html.erb:
--------------------------------------------------------------------------------
1 | <%= link_to sample, class: "bg-white rounded shadow-sm px-4 py-3" do %>
2 |
3 |
4 |
5 | <%= sample.title %>
6 |
7 |
8 | <% if sample.status_private? %>
9 |
10 | <%= render "icons/heroicons/solid/lock-closed" %>
11 |
12 | <% end %>
13 |
14 |
15 |
16 | <%= strip_markdown(sample.description).truncate_words(50) %>
17 |
18 |
19 |
20 |
21 | <% if sample.sample_files&.first.present? %>
22 |
<%= syntax_higlight(sample.sample_files.first.first_lines(5).join, sample.sample_files.first.path).html_safe %>
23 | <% end %>
24 |
25 |
26 |
27 |
28 | Curated <%= relative_time sample.created_at %> ago by
29 | @<%= sample.user.username %>
30 |
31 |
32 | <% end %>
33 |
--------------------------------------------------------------------------------
/app/views/devise/sessions/new.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Log In" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Log In") %>
6 |
7 | <% c.with_details do %>
8 | Don’t have an account yet? <%= link_to "Sign up", new_registration_path(resource_name), class: "underline" %>.
9 | <% end %>
10 | <% end %>
11 | <% end %>
12 |
13 |
14 | <%= form_for(resource, as: resource_name, url: session_path(resource_name), builder: TailwindBuilder) do |f| %>
15 |
16 | <%= f.label :email %>
17 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
18 |
19 |
20 |
21 | <%= f.label :password %>
22 | <%= f.password_field :password, autocomplete: "current-password" %>
23 |
24 |
25 | <%= link_to "Forgot your password?", new_password_path(resource_name), class: "text-sm underline" %>
26 |
27 |
28 |
29 | <% if devise_mapping.rememberable? %>
30 |
31 | <%= f.check_box :remember_me %>
32 | <%= f.label :remember_me, class: "mb-0 ml-2" %>
33 |
34 | <% end %>
35 |
36 | <%= f.submit "Log in", class: "px-3 py-2 text-white rounded bg-neutral-800 hover:bg-neutral-900 cursor-pointer" %>
37 | <% end %>
38 |
39 |
--------------------------------------------------------------------------------
/app/views/devise/registrations/new.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Sign Up" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Sign Up") %>
6 |
7 | <% c.with_details do %>
8 | Already have an account? <%= link_to "Log in", new_session_path(resource_name), class: "underline" %>.
9 | <% end %>
10 | <% end %>
11 | <% end %>
12 |
13 |
14 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name), builder: TailwindBuilder) do |f| %>
15 | <%= render "devise/shared/error_messages", resource: resource %>
16 |
17 |
18 | <%= f.label :username %>
19 | <%= f.text_field :username, autofocus: true, autocomplete: "email" %>
20 |
21 |
22 |
23 | <%= f.label :email %>
24 | <%= f.email_field :email, autocomplete: "email" %>
25 |
26 |
27 |
28 | <%= f.label :password %>
29 | <%= f.password_field :password, autocomplete: "new-password" %>
30 |
31 |
32 |
33 | <%= f.label :password_confirmation %>
34 | <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
35 |
36 |
37 | <%= f.submit "Sign up", class: "px-3 py-2 text-white rounded bg-neutral-800 hover:bg-neutral-900 cursor-pointer" %>
38 | <% end %>
39 |
40 |
--------------------------------------------------------------------------------
/public/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/db/migrate/20220819023829_devise_create_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DeviseCreateUsers < ActiveRecord::Migration[7.0]
4 | def change
5 | create_table :users do |t|
6 | ## Database authenticatable
7 | t.string :email, null: false, default: ""
8 | t.string :encrypted_password, null: false, default: ""
9 |
10 | ## Recoverable
11 | t.string :reset_password_token
12 | t.datetime :reset_password_sent_at
13 |
14 | ## Rememberable
15 | t.datetime :remember_created_at
16 |
17 | ## Trackable
18 | # t.integer :sign_in_count, default: 0, null: false
19 | # t.datetime :current_sign_in_at
20 | # t.datetime :last_sign_in_at
21 | # t.string :current_sign_in_ip
22 | # t.string :last_sign_in_ip
23 |
24 | ## Confirmable
25 | t.string :confirmation_token
26 | t.datetime :confirmed_at
27 | t.datetime :confirmation_sent_at
28 | # t.string :unconfirmed_email # Only if using reconfirmable
29 |
30 | ## Lockable
31 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
32 | # t.string :unlock_token # Only if unlock strategy is :email or :both
33 | # t.datetime :locked_at
34 |
35 |
36 | t.timestamps null: false
37 | end
38 |
39 | add_index :users, :email, unique: true
40 | add_index :users, :reset_password_token, unique: true
41 | add_index :users, :confirmation_token, unique: true
42 | # add_index :users, :unlock_token, unique: true
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/app/helpers/tailwind_builder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class TailwindBuilder < ActionView::Helpers::FormBuilder
4 | def default_classes
5 | "w-full border-neutral-300 rounded disabled:bg-neutral-100 placeholder-neutral-300"
6 | end
7 |
8 | def label(method, text = nil, options={})
9 | super(method, text, options.merge(class: "#{options[:class]} block text-sm text-neutral-500 mb-2"))
10 | end
11 |
12 | def file_field(method, options = {})
13 | super(method, options.merge(class: "#{options[:class]} w-full p-2 bg-neutral-300 rounded disabled:bg-neutral-100"))
14 | end
15 |
16 | def text_field(method, options={})
17 | super(method, options.merge(class: "#{options[:class]} #{default_classes}"))
18 | end
19 |
20 | def email_field(method, options={})
21 | super(method, options.merge(class: "#{options[:class]} #{default_classes}"))
22 | end
23 |
24 | def password_field(method, options={})
25 | super(method, options.merge(class: "#{options[:class]} #{default_classes}"))
26 | end
27 |
28 | def text_area(method, options={})
29 | super(method, options.merge(class: "#{options[:class]} #{default_classes}"))
30 | end
31 |
32 | def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
33 | super(method, options.merge(class: "#{options[:class]} border-neutral-300 rounded disabled:bg-neutral-100"), checked_value, unchecked_value)
34 | end
35 |
36 | def select(method, choices = nil, options = {}, html_options = {}, &block)
37 | super(method, choices, options, html_options.merge(class: "#{html_options[:class]} #{default_classes}"), &block)
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= display_meta_tags(default_meta_tags) %>
5 |
6 |
7 |
8 | <%= csrf_meta_tags %>
9 | <%= csp_meta_tag %>
10 |
11 | <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
12 | <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | <%# render "layouts/analytics" if show_analytics? %>
24 |
25 |
26 |
27 | <%# render "layouts/analytics_noscript" if show_analytics? %>
28 |
29 |
30 | <%= render "layouts/navigation" %>
31 | <%= render "layouts/header" %>
32 |
33 |
34 |
35 | <%= render "layouts/flash" if flash.present? %>
36 |
37 | <%= yield %>
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/app/controllers/samples_controller.rb:
--------------------------------------------------------------------------------
1 | class SamplesController < ApplicationController
2 | skip_before_action :authenticate_user!, only: [:show]
3 |
4 | def show
5 | @sample = Sample.kept.find(params[:id])
6 | @sample_file = @sample.sample_files.first
7 | authorize @sample
8 |
9 | render "sample_files/show"
10 | end
11 |
12 | def new
13 | @sample = current_user.samples.new
14 | @sample.sample_files.new
15 | authorize @sample
16 | end
17 |
18 | def create
19 | @sample = current_user.samples.new(sample_params)
20 | authorize @sample
21 |
22 | if @sample.save
23 | ProcessSampleJob.perform_async(@sample.id)
24 | redirect_to @sample
25 | else
26 | render :new, status: :unprocessable_entity
27 | end
28 | end
29 |
30 | def edit
31 | @sample = current_user.samples.find(params[:id])
32 | authorize @sample
33 | end
34 |
35 | def update
36 | @sample = current_user.samples.find(params[:id])
37 | authorize @sample
38 |
39 | if @sample.update(sample_params)
40 | ProcessSampleJob.perform_async(@sample.id)
41 | redirect_to @sample
42 | else
43 | render :edit, status: :unprocessable_entity
44 | end
45 | end
46 |
47 | def destroy
48 | @sample = current_user.samples.find(params[:id])
49 | authorize @sample
50 |
51 | if @sample.discard
52 | redirect_to account_samples_path, notice: "Sample was removed"
53 | else
54 | redirect_to account_samples_path, notice: "Sample could not be removed"
55 | end
56 | end
57 |
58 | private
59 |
60 | def sample_params
61 | params.require(:sample).permit(
62 | :title, :description, :category_id, :status,
63 | sample_files_attributes: [:id, :_destroy, :path, :contents, :description]
64 | )
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/app/views/samples/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with(model: sample, builder: TailwindBuilder, data: { controller: "nested-form", nested_form_wrapper_selector_value: ".nested-form-wrapper" }) do |form| %>
2 | <% if sample.errors.any? %>
3 | Please review the problems below:
4 |
5 |
6 | <% sample.errors.full_messages.each do |msg| %>
7 | <%= msg.html_safe %>
8 | <% end %>
9 |
10 | <% end %>
11 |
12 |
13 | <%= form.label :title, "Title" %>
14 | <%= form.text_field :title %>
15 |
16 |
17 |
18 |
19 | <%= form.label :category_id %>
20 | <%= form.select :category_id, Category.all.map { |c| [c.name, c.id] } %>
21 |
22 |
23 |
24 | <%= form.label :status %>
25 | <%= form.select :status, Sample.statuses.map { |key, _| [key.titleize, key] } %>
26 |
27 |
28 |
29 |
30 | <%= form.label :description, "Description" %>
31 | <%= form.text_area :description, rows: 6 %>
32 |
33 |
34 |
35 |
36 |
37 | <%= form.fields_for :sample_files, SampleFile.new, child_index: "NEW_RECORD" do |sample_files| %>
38 | <%= render "sample_file_fields", form: sample_files %>
39 | <% end %>
40 |
41 |
42 | <%= form.fields_for :sample_files do |sample_file| %>
43 | <%= render "sample_file_fields", form: sample_file %>
44 | <% end %>
45 |
46 |
47 |
48 | Add File
49 |
50 |
51 |
52 |
53 | <%= form.submit "Save", class: "px-3 py-2 text-white rounded bg-neutral-800 hover:bg-neutral-900 cursor-pointer" %>
54 |
55 | <% end %>
56 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9 | threads min_threads_count, max_threads_count
10 |
11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
12 | # terminating a worker in development environments.
13 | #
14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
15 |
16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
17 | #
18 | port ENV.fetch("PORT") { 3000 }
19 |
20 | # Specifies the `environment` that Puma will run in.
21 | #
22 | environment ENV.fetch("RAILS_ENV") { "development" }
23 |
24 | # Specifies the `pidfile` that Puma will use.
25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
26 |
27 | # Specifies the number of `workers` to boot in clustered mode.
28 | # Workers are forked web server processes. If using threads and workers together
29 | # the concurrency of the application would be max `threads` * `workers`.
30 | # Workers do not work on JRuby or Windows (both of which do not support
31 | # processes).
32 |
33 | workers ENV.fetch("WEB_CONCURRENCY") { 1 }
34 |
35 | # Use the `preload_app!` method when specifying a `workers` number.
36 | # This directive tells Puma to first boot the application and load code
37 | # before forking the application. This takes advantage of Copy On Write
38 | # process behavior so workers use less memory.
39 |
40 | preload_app!
41 |
42 | x = nil
43 | on_worker_boot do
44 | x = Sidekiq.configure_embed do |config|
45 | # config.logger.level = Logger::DEBUG
46 | config.queues = %w[default]
47 | config.concurrency = 1
48 | end
49 | x.run
50 | end
51 |
52 | on_worker_shutdown do
53 | x&.stop
54 | end
55 |
56 | # Allow puma to be restarted by `bin/rails restart` command.
57 | plugin :tmp_restart
58 |
--------------------------------------------------------------------------------
/app/views/sample_files/show.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags(
2 | title: @sample.title,
3 | description: strip_markdown(@sample.description),
4 | og: {
5 | image: (rails_blob_url(@sample_file.open_graph_image) if @sample_file&.open_graph_image&.attached?)
6 | },
7 | twitter: {
8 | image: (rails_blob_url(@sample_file.open_graph_image) if @sample_file&.open_graph_image&.attached?)
9 | }
10 | ) %>
11 |
12 | <% content_for(:header) do %>
13 | <%= render HeaderComponent.new do |c| %>
14 | <% c.with_parent do %>
15 | <%= link_to @sample.category.name, category_path(@sample.category.slug) %>
16 | <% end %>
17 |
18 | <% c.with_actions do %>
19 | <% if policy(@sample).edit? %>
20 | <%= link_to "Edit", [:edit, @sample], class: "px-2 py-1 bg-[#950000] text-white text-sm rounded" %>
21 | <% end %>
22 |
23 | <% if policy(@sample).destroy? %>
24 | <%= button_to "Delete", @sample, method: :delete, form: { data: { turbo_confirm: "Delete this sample?" } }, class: "px-2 py-1 bg-[#950000] text-white text-sm rounded" %>
25 | <% end %>
26 | <% end %>
27 |
28 | <% c.with_title do %>
29 |
30 | <%= @sample.title %>
31 |
32 | <% if @sample.status_private? %>
33 |
34 | <%= render "icons/heroicons/solid/lock-closed" %>
35 |
Private
36 |
37 | <% end %>
38 |
39 | <% end %>
40 |
41 | <% c.with_details do %>
42 | Curated <%= relative_time @sample.created_at %> ago by
43 | <%= link_to user_path(@sample.user.username) do %>
44 | @<%= @sample.user.username %>
45 | <% end %>
46 | <% end %>
47 | <% end %>
48 | <% end %>
49 |
50 | <% if @sample.description.present? %>
51 | Description
52 |
53 |
54 | <%= markdown @sample.description %>
55 |
56 | <% end %>
57 |
58 | <%= turbo_frame_tag "editor", data: { turbo_action: "advance" } do %>
59 | <%= render "sample_files/editor", sample_files: @sample.sample_files, sample_file: @sample_file if @sample_file.present? %>
60 | <% end %>
61 |
--------------------------------------------------------------------------------
/app/views/sample_files/_editor.html.erb:
--------------------------------------------------------------------------------
1 |
16 |
17 | <%= tag.div id: "editor-frame", class: "bg-[#ccc] rounded shadow-sm flex flex-col lg:flex-row lg:items-stretch w-full mt-4 lg:min-h-[250px] lg:overflow-y-scroll lg:resize-y",
18 | data: { controller: "resize-height", resize_height_url_value: resize_path, resize_height_setting_value: "editor_height", resize_height_current_height_value: editor_height_or_default } do %>
19 | <%= tag.div id: "editor-top", class: "flex lg:hidden bg-[#333] rounded-t" do %>
20 | <%= tag.div class: "flex-1 px-6 py-4 text-xs text-[#D6D6D6]" do %>
21 | <%= render "sample_files/top_menu", sample_file: sample_file, sample_files: sample_files %>
22 | <% end %>
23 | <% end %>
24 |
25 | <%= tag.div id: "editor-menu", class: "hidden lg:flex bg-[#20252C] rounded-t-l resize-x min-w-[100px] max-w-[400px] lg:mb-3 overflow-y-scroll",
26 | data: { controller: "resize-width", resize_width_url_value: resize_path, resize_width_setting_value: "menu_width", resize_width_current_width_value: menu_width_or_default } do %>
27 | <%= tag.div class: "px-2 py-6 text-xs text-[#9DA5B6]" do %>
28 | <%= render "sample_files/left_menu", sample_files: sample_files %>
29 | <% end %>
30 | <% end %>
31 |
32 | <%= tag.div id: "editor-contents", class: "w-full lg:w-auto bg-[#272C35] lg:mb-3 lg:overflow-y-scroll resize-x lg:min-w-[100px] lg:max-w-[800px]",
33 | data: { controller: "resize-width", resize_width_url_value: resize_path, resize_width_setting_value: "contents_width", resize_width_current_width_value: contents_width_or_default } do %>
34 | <%= tag.div class: "p-6 text-sm leading-loose text-white font-recursive-mono" do %>
35 | <%= render "sample_files/contents", sample_file: sample_file %>
36 | <% end %>
37 | <% end %>
38 |
39 | <%= tag.div class: "lg:flex flex-1 bg-white rounded-b lg:rounded-none lg:mb-3 overflow-y-scroll rounded-t-r p-6 text-sm leading-relaxed" do %>
40 | <%= render "sample_files/description", sample_file: sample_file %>
41 | <% end %>
42 | <% end %>
43 |
--------------------------------------------------------------------------------
/db/migrate/20220904183125_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 | # Use Active Record's configured type for primary and foreign keys
5 | primary_key_type, foreign_key_type = primary_and_foreign_key_types
6 |
7 | create_table :active_storage_blobs, id: primary_key_type do |t|
8 | t.string :key, null: false
9 | t.string :filename, null: false
10 | t.string :content_type
11 | t.text :metadata
12 | t.string :service_name, null: false
13 | t.bigint :byte_size, null: false
14 | t.string :checksum
15 |
16 | if connection.supports_datetime_with_precision?
17 | t.datetime :created_at, precision: 6, null: false
18 | else
19 | t.datetime :created_at, null: false
20 | end
21 |
22 | t.index [ :key ], unique: true
23 | end
24 |
25 | create_table :active_storage_attachments, id: primary_key_type do |t|
26 | t.string :name, null: false
27 | t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
28 | t.references :blob, null: false, type: foreign_key_type
29 |
30 | if connection.supports_datetime_with_precision?
31 | t.datetime :created_at, precision: 6, null: false
32 | else
33 | t.datetime :created_at, null: false
34 | end
35 |
36 | t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
37 | t.foreign_key :active_storage_blobs, column: :blob_id
38 | end
39 |
40 | create_table :active_storage_variant_records, id: primary_key_type do |t|
41 | t.belongs_to :blob, null: false, index: false, type: foreign_key_type
42 | t.string :variation_digest, null: false
43 |
44 | t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
45 | t.foreign_key :active_storage_blobs, column: :blob_id
46 | end
47 | end
48 |
49 | private
50 | def primary_and_foreign_key_types
51 | config = Rails.configuration.generators
52 | setting = config.options[config.orm][:primary_key_type]
53 | primary_key_type = setting || :primary_key
54 | foreign_key_type = setting || :bigint
55 | [primary_key_type, foreign_key_type]
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | require 'default_renderer'
2 | require 'preview_renderer'
3 | require 'one_dark'
4 | require 'svg_inline'
5 |
6 | module ApplicationHelper
7 | def syntax_higlight(content, path, formatter = "html_inline")
8 | formatter = Rouge::Formatter.find(formatter).new("one-dark")
9 | lexer = Rouge::Lexer.guess(filename: path)
10 | formatter.format(lexer.lex(content))
11 | end
12 |
13 | def markdown(content)
14 | return "" if content.blank?
15 |
16 | sanitize Redcarpet::Markdown.new(
17 | DefaultRenderer.new(hard_wrap: true, with_toc_data: true),
18 | no_intra_emphasis: true, tables: true, autolink: true,
19 | gh_blockcode: true, fenced_code_blocks: true,
20 | disable_indented_code_blocks: true
21 | ).render(content)
22 | end
23 |
24 | def strip_markdown(content)
25 | return "" if content.blank?
26 |
27 | Redcarpet::Markdown.new(PreviewRenderer).render(content)
28 | end
29 |
30 | def user_avatar(user, size: 100)
31 | if user.avatar.attached?
32 | user.avatar.variant(resize_to_limit: [size, size])
33 | else
34 | "avatar.svg"
35 | end
36 | end
37 |
38 | def default_meta_tags
39 | {
40 | separator: "-",
41 | site: "RailsInspire",
42 | reverse: true,
43 | og: {
44 | site_name: :site,
45 | type: "website",
46 | title: :title,
47 | description: :description,
48 | image: image_url("opengraph.png")
49 | },
50 | twitter: {
51 | card: "photo",
52 | title: :title,
53 | description: :description,
54 | image: image_url("opengraph.png")
55 | }
56 | }
57 | end
58 |
59 | def flash_class(level)
60 | case level
61 | when "success" then "bg-green-700"
62 | when "notice" then "bg-blue-700"
63 | when "message" then "bg-blue-700"
64 | when "warning" then "bg-amber-700"
65 | when "error" then "bg-red-700"
66 | when "alert" then "bg-red-700"
67 | end
68 | end
69 |
70 | def relative_time(timestamp)
71 | return if timestamp.blank?
72 |
73 | content_tag(:time, time_ago_in_words(timestamp), datetime: timestamp)
74 | end
75 |
76 | def menu_width_or_default
77 | cookies[:menu_width].presence || 250
78 | end
79 |
80 | def contents_width_or_default
81 | cookies[:contents_width].presence || 650
82 | end
83 |
84 | def editor_height_or_default
85 | cookies[:editor_height].presence || 650
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
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 | # Turn false under Spring and add config.action_view.cache_template_loading = true.
12 | config.cache_classes = true
13 |
14 | # Eager loading loads your whole application. When running a single test locally,
15 | # this probably isn't necessary. It's a good idea to do in a continuous integration
16 | # system, or in some way before deploying your code.
17 | config.eager_load = ENV["CI"].present?
18 |
19 | # Configure public file server for tests with Cache-Control for performance.
20 | config.public_file_server.enabled = true
21 | config.public_file_server.headers = {
22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}"
23 | }
24 |
25 | # Show full error reports and disable caching.
26 | config.consider_all_requests_local = true
27 | config.action_controller.perform_caching = false
28 | config.cache_store = :null_store
29 |
30 | # Raise exceptions instead of rendering exception templates.
31 | config.action_dispatch.show_exceptions = false
32 |
33 | # Disable request forgery protection in test environment.
34 | config.action_controller.allow_forgery_protection = false
35 |
36 | # Store uploaded files on the local file system in a temporary directory.
37 | config.active_storage.service = :test
38 |
39 | config.action_mailer.perform_caching = false
40 |
41 | # Tell Action Mailer not to deliver emails to the real world.
42 | # The :test delivery method accumulates sent emails in the
43 | # ActionMailer::Base.deliveries array.
44 | config.action_mailer.delivery_method = :test
45 | config.action_mailer.default_url_options = { host: "localhost:3000" }
46 |
47 | # Print deprecation notices to the stderr.
48 | config.active_support.deprecation = :stderr
49 |
50 | # Raise exceptions for disallowed deprecations.
51 | config.active_support.disallowed_deprecation = :raise
52 |
53 | # Tell Active Support which deprecation messages to disallow.
54 | config.active_support.disallowed_deprecation_warnings = []
55 |
56 | # Raises error for missing translations.
57 | # config.i18n.raise_on_missing_translations = true
58 |
59 | # Annotate rendered view with file names.
60 | # config.action_view.annotate_rendered_view_with_filenames = true
61 | end
62 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # In the development environment your application's code is reloaded any time
7 | # it changes. This slows down response time but is perfect for development
8 | # since you don't have to restart the web server when you make code changes.
9 | config.cache_classes = false
10 |
11 | # Do not eager load code on boot.
12 | config.eager_load = false
13 |
14 | # Show full error reports.
15 | config.consider_all_requests_local = true
16 |
17 | # Enable server timing
18 | config.server_timing = true
19 |
20 | # Enable/disable caching. By default caching is disabled.
21 | # Run rails dev:cache to toggle caching.
22 | if Rails.root.join("tmp/caching-dev.txt").exist?
23 | config.action_controller.perform_caching = true
24 | config.action_controller.enable_fragment_cache_logging = true
25 |
26 | config.cache_store = :memory_store
27 | config.public_file_server.headers = {
28 | "Cache-Control" => "public, max-age=#{2.days.to_i}"
29 | }
30 | else
31 | config.action_controller.perform_caching = false
32 |
33 | config.cache_store = :null_store
34 | end
35 |
36 | # Store uploaded files on the local file system (see config/storage.yml for options).
37 | config.active_storage.service = :local
38 |
39 | # Don't care if the mailer can't send.
40 | config.action_mailer.raise_delivery_errors = false
41 |
42 | config.action_mailer.perform_caching = false
43 |
44 | config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
45 |
46 | # Print deprecation notices to the Rails logger.
47 | config.active_support.deprecation = :log
48 |
49 | # Raise exceptions for disallowed deprecations.
50 | config.active_support.disallowed_deprecation = :raise
51 |
52 | # Tell Active Support which deprecation messages to disallow.
53 | config.active_support.disallowed_deprecation_warnings = []
54 |
55 | # Raise an error on page load if there are pending migrations.
56 | config.active_record.migration_error = :page_load
57 |
58 | # Highlight code that triggered database queries in logs.
59 | config.active_record.verbose_query_logs = true
60 |
61 | # Suppress logger output for asset requests.
62 | config.assets.quiet = true
63 |
64 | # Raises error for missing translations.
65 | # config.i18n.raise_on_missing_translations = true
66 |
67 | # Annotate rendered view with file names.
68 | # config.action_view.annotate_rendered_view_with_filenames = true
69 |
70 | # Uncomment if you wish to allow Action Cable access from any origin.
71 | # config.action_cable.disable_request_forgery_protection = true
72 | end
73 |
--------------------------------------------------------------------------------
/app/views/account/accounts/show.html.erb:
--------------------------------------------------------------------------------
1 | <% set_meta_tags title: "Account Settings" %>
2 |
3 | <% content_for(:header) do %>
4 | <%= render HeaderComponent.new do |c| %>
5 | <% c.with_title.with_content("Account Settings") %>
6 | <% end %>
7 | <% end %>
8 |
9 |
10 | <%= form_for current_user, url: account_account_path do |f| %>
11 | <% if current_user.errors.any? %>
12 |
Please review the problems below:
13 |
14 |
15 | <% current_user.errors.full_messages.each do |msg| %>
16 | <%= msg.html_safe %>
17 | <% end %>
18 |
19 | <% end %>
20 | <% end %>
21 |
22 |
Avatar
23 |
24 |
25 |
26 | <%= image_tag user_avatar(current_user, size: 200), class: "w-full object-cover rounded-full" %>
27 |
28 |
29 | <% if current_user.avatar.attached? %>
30 | <%= button_to "Remove", account_avatar_path, method: :delete, form: { data: { turbo_confirm: "Remove this avatar?" } }, class: "px-2 py-1 text-sm text-white rounded bg-neutral-800 hover:bg-neutral-900 cursor-pointer" %>
31 | <% end %>
32 |
33 |
34 | <%= form_for current_user, url: account_avatar_path, builder: TailwindBuilder do |f| %>
35 |
36 | <%= f.file_field :avatar %>
37 |
38 |
39 | <%= f.submit "Upload Avatar", class: "px-3 py-2 text-white rounded bg-neutral-800 hover:bg-neutral-900 cursor-pointer" %>
40 | <% end %>
41 |
42 |
43 |
44 |
User Details
45 |
46 | <%= form_for current_user, url: account_account_path, builder: TailwindBuilder do |f| %>
47 |
48 | <%= f.label :email %>
49 | <%= f.email_field :email, input_html: { class: "max-w-sm" } %>
50 |
51 |
52 | <% if current_user.pending_reconfirmation? %>
53 |
Currently waiting for confirmation of: <%= current_user.unconfirmed_email %>
54 | <% end %>
55 |
56 |
57 | <%= f.label :username %>
58 | <%= f.text_field :username, input_html: { class: "max-w-xs" } %>
59 |
60 |
61 | <%= f.submit "Update Details", class: "px-3 py-2 text-white rounded bg-neutral-800 hover:bg-neutral-900 cursor-pointer" %>
62 | <% end %>
63 |
64 |
65 |
66 |
Password
67 |
68 | <%= form_for current_user, url: account_password_path, builder: TailwindBuilder do |f| %>
69 |
70 |
71 | <%= f.label :current_password %>
72 | <%= f.password_field :current_password %>
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | <%= f.label :password %>
82 | <%= f.password_field :password, label: "New password" %>
83 |
84 |
85 |
86 | <%= f.label :password_confirmation %>
87 | <%= f.password_field :password_confirmation, label: "Confirm new password" %>
88 |
89 |
90 |
91 | <%= f.submit "Update Password", class: "px-3 py-2 text-white rounded bg-neutral-800 hover:bg-neutral-900 cursor-pointer" %>
92 | <% end %>
93 |
94 |
--------------------------------------------------------------------------------
/config/locales/devise.en.yml:
--------------------------------------------------------------------------------
1 | # Additional translations at https://github.com/heartcombo/devise/wiki/I18n
2 |
3 | en:
4 | devise:
5 | confirmations:
6 | confirmed: "Your email address has been successfully confirmed."
7 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
9 | failure:
10 | already_authenticated: "You are already signed in."
11 | inactive: "Your account is not activated yet."
12 | invalid: "Invalid %{authentication_keys} or password."
13 | locked: "Your account is locked."
14 | last_attempt: "You have one more attempt before your account is locked."
15 | not_found_in_database: "Invalid %{authentication_keys} or password."
16 | timeout: "Your session expired. Please sign in again to continue."
17 | unauthenticated: "You need to sign in or sign up before continuing."
18 | unconfirmed: "You have to confirm your email address before continuing."
19 | mailer:
20 | confirmation_instructions:
21 | subject: "Confirmation instructions"
22 | reset_password_instructions:
23 | subject: "Reset password instructions"
24 | unlock_instructions:
25 | subject: "Unlock instructions"
26 | email_changed:
27 | subject: "Email Changed"
28 | password_change:
29 | subject: "Password Changed"
30 | omniauth_callbacks:
31 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
32 | success: "Successfully authenticated from %{kind} account."
33 | passwords:
34 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
35 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
36 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
37 | updated: "Your password has been changed successfully. You are now signed in."
38 | updated_not_active: "Your password has been changed successfully."
39 | registrations:
40 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
41 | signed_up: "Welcome! You have signed up successfully."
42 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
43 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
44 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
45 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address."
46 | updated: "Your account has been updated successfully."
47 | updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again."
48 | sessions:
49 | signed_in: "Signed in successfully."
50 | signed_out: "Signed out successfully."
51 | already_signed_out: "Signed out successfully."
52 | unlocks:
53 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
54 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
55 | unlocked: "Your account has been unlocked successfully. Please sign in to continue."
56 | errors:
57 | messages:
58 | already_confirmed: "was already confirmed, please try signing in"
59 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
60 | expired: "has expired, please request a new one"
61 | not_found: "not found"
62 | not_locked: "was not locked"
63 | not_saved:
64 | one: "1 error prohibited this %{resource} from being saved:"
65 | other: "%{count} errors prohibited this %{resource} from being saved:"
66 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # Code is not reloaded between requests.
7 | config.cache_classes = true
8 |
9 | # Eager load code on boot. This eager loads most of Rails and
10 | # your application in memory, allowing both threaded web servers
11 | # and those relying on copy on write to perform better.
12 | # Rake tasks automatically ignore this option for performance.
13 | config.eager_load = true
14 |
15 | # Full error reports are disabled and caching is turned on.
16 | config.consider_all_requests_local = false
17 | config.action_controller.perform_caching = true
18 |
19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
21 | # config.require_master_key = true
22 |
23 | # Disable serving static files from the `/public` folder by default since
24 | # Apache or NGINX already handles this.
25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
26 |
27 | # Compress CSS using a preprocessor.
28 | # config.assets.css_compressor = :sass
29 |
30 | # Do not fallback to assets pipeline if a precompiled asset is missed.
31 | config.assets.compile = false
32 |
33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
34 | # config.asset_host = "http://assets.example.com"
35 |
36 | # Specifies the header that your server uses for sending files.
37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
38 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
39 |
40 | # Store uploaded files on the local file system (see config/storage.yml for options).
41 | config.active_storage.service = :amazon
42 |
43 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
44 | config.force_ssl = true
45 |
46 | # Include generic and useful information about system operation, but avoid logging too much
47 | # information to avoid inadvertent exposure of personally identifiable information (PII).
48 | config.log_level = :info
49 |
50 | # Prepend all log lines with the following tags.
51 | config.log_tags = [:request_id, :remote_ip]
52 |
53 | # Use a different cache store in production.
54 | # config.cache_store = :mem_cache_store
55 |
56 | # Use a real queuing backend for Active Job (and separate queues per environment).
57 | # config.active_job.queue_adapter = :resque
58 | # config.active_job.queue_name_prefix = "railsinspire_production"
59 |
60 | config.action_mailer.delivery_method = :postmark
61 | config.action_mailer.postmark_settings = { api_token: ENV["POSTMARK_API_TOKEN"] }
62 | config.action_mailer.perform_deliveries = true
63 | config.action_mailer.raise_delivery_errors = true
64 | config.action_mailer.default_options = { from: 'no-reply@railsinspire.com' }
65 | config.action_mailer.default_url_options = { host: 'railsinspire.com' }
66 | config.action_mailer.perform_caching = false
67 |
68 | # Ignore bad email addresses and do not raise email delivery errors.
69 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
70 | # config.action_mailer.raise_delivery_errors = false
71 |
72 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
73 | # the I18n.default_locale when a translation cannot be found).
74 | config.i18n.fallbacks = true
75 |
76 | # Don't log any deprecations.
77 | config.active_support.report_deprecations = false
78 |
79 | # Use default logging formatter so that PID and timestamp are not suppressed.
80 | config.log_formatter = ::Logger::Formatter.new
81 |
82 | # Use a different logger for distributed setups.
83 | # require "syslog/logger"
84 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
85 |
86 | if ENV["RAILS_LOG_TO_STDOUT"].present?
87 | logger = ActiveSupport::Logger.new(STDOUT)
88 | logger.formatter = config.log_formatter
89 | config.logger = ActiveSupport::TaggedLogging.new(logger)
90 | end
91 |
92 | # Do not dump schema after migrations.
93 | config.active_record.dump_schema_after_migration = false
94 |
95 | config.lograge.enabled = true
96 | config.lograge.custom_options = lambda do |event|
97 | exceptions = %w[controller action format id]
98 | {
99 | params: event.payload[:params].except(*exceptions)
100 | }
101 | end
102 |
103 | config.lograge.custom_payload do |controller|
104 | {
105 | user_id: controller.current_user.try(:id)
106 | }
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/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[7.0].define(version: 2024_10_22_110917) do
14 | # These are extensions that must be enabled in order to support this database
15 | enable_extension "plpgsql"
16 |
17 | # Custom types defined in this database.
18 | # Note that some types may not work with other database engines. Be careful if changing database.
19 | create_enum "sample_status", ["private", "public"]
20 |
21 | create_table "active_storage_attachments", force: :cascade do |t|
22 | t.string "name", null: false
23 | t.string "record_type", null: false
24 | t.bigint "record_id", null: false
25 | t.bigint "blob_id", null: false
26 | t.datetime "created_at", null: false
27 | t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
28 | t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
29 | end
30 |
31 | create_table "active_storage_blobs", force: :cascade do |t|
32 | t.string "key", null: false
33 | t.string "filename", null: false
34 | t.string "content_type"
35 | t.text "metadata"
36 | t.string "service_name", null: false
37 | t.bigint "byte_size", null: false
38 | t.string "checksum"
39 | t.datetime "created_at", null: false
40 | t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
41 | end
42 |
43 | create_table "active_storage_variant_records", force: :cascade do |t|
44 | t.bigint "blob_id", null: false
45 | t.string "variation_digest", null: false
46 | t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
47 | end
48 |
49 | create_table "categories", force: :cascade do |t|
50 | t.string "name", null: false
51 | t.string "description"
52 | t.string "slug", null: false
53 | t.datetime "created_at", null: false
54 | t.datetime "updated_at", null: false
55 | end
56 |
57 | create_table "sample_files", force: :cascade do |t|
58 | t.bigint "sample_id", null: false
59 | t.string "path"
60 | t.text "contents"
61 | t.text "description"
62 | t.datetime "created_at", null: false
63 | t.datetime "updated_at", null: false
64 | t.index ["sample_id"], name: "index_sample_files_on_sample_id"
65 | end
66 |
67 | create_table "samples", force: :cascade do |t|
68 | t.bigint "category_id", null: false
69 | t.bigint "user_id", null: false
70 | t.string "title"
71 | t.text "description"
72 | t.datetime "created_at", null: false
73 | t.datetime "updated_at", null: false
74 | t.enum "status", default: "private", null: false, enum_type: "sample_status"
75 | t.datetime "discarded_at"
76 | t.index ["category_id"], name: "index_samples_on_category_id"
77 | t.index ["discarded_at"], name: "index_samples_on_discarded_at"
78 | t.index ["user_id"], name: "index_samples_on_user_id"
79 | end
80 |
81 | create_table "users", force: :cascade do |t|
82 | t.string "email", default: "", null: false
83 | t.string "encrypted_password", default: "", null: false
84 | t.string "reset_password_token"
85 | t.datetime "reset_password_sent_at"
86 | t.datetime "remember_created_at"
87 | t.string "confirmation_token"
88 | t.datetime "confirmed_at"
89 | t.datetime "confirmation_sent_at"
90 | t.datetime "created_at", null: false
91 | t.datetime "updated_at", null: false
92 | t.boolean "admin", default: false, null: false
93 | t.string "username", default: "", null: false
94 | t.string "unconfirmed_email"
95 | t.datetime "deactivated_at"
96 | t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
97 | t.index ["email"], name: "index_users_on_email", unique: true
98 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
99 | t.index ["username"], name: "index_users_on_username", unique: true
100 | end
101 |
102 | add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
103 | add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
104 | add_foreign_key "sample_files", "samples"
105 | add_foreign_key "samples", "categories"
106 | add_foreign_key "samples", "users"
107 | end
108 |
--------------------------------------------------------------------------------
/app/views/layouts/_navigation.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= link_to root_path, class: "text-xl font-semibold" do %>
5 |
Rails Inspire
6 | <% end %>
7 |
8 |
9 |
10 | <%= link_to "Users", users_path, class: "text-white text-sm px-2 py-1" %>
11 | <%= link_to "Categories", categories_path, class: "text-white text-sm px-2 py-1" %>
12 |
13 | <% if user_signed_in? %>
14 | <%= link_to [:new, :sample], class: "flex space-x-1 text-white text-sm px-2 py-1" do %>
15 | <%= render "icons/heroicons/solid/plus-sm" %>
16 | Add
17 | <% end %>
18 |
19 |
20 |
29 |
30 |
31 | <%= link_to "Account Settings", account_root_path, class: "block px-3 py-2 text-sm text-neutral-600 hover:bg-neutral-100", role: "menuitem" %>
32 |
33 | <%= link_to "Your Samples", account_samples_path, class: "block px-3 py-2 text-sm text-neutral-600 hover:bg-neutral-100", role: "menuitem" %>
34 |
35 | <%= button_to "Logout", destroy_user_session_path, method: :delete, class: "block w-full text-left px-3 py-2 text-sm text-neutral-600 hover:bg-neutral-100", role: "menuitem" %>
36 |
37 |
38 | <% else %>
39 | <%= link_to "Login", new_user_session_path, class: "text-white text-sm px-3 py-1" %>
40 | <% end %>
41 |
42 |
43 |
44 |
45 |
46 | Open main menu
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | <%= link_to "Users", users_path, class: "block px-3 py-2 text-sm text-neutral-600 hover:bg-neutral-100" %>
57 |
58 | <%= link_to "Categories", categories_path, class: "block px-3 py-2 text-sm text-neutral-600 hover:bg-neutral-100" %>
59 |
60 | <% if user_signed_in? %>
61 | <%= link_to [:new, :sample], class: "flex space-x-1 px-3 py-2 text-sm text-neutral-600 hover:bg-neutral-100" do %>
62 | <%= render "icons/heroicons/solid/plus-sm" %>
63 | Add
64 | <% end %>
65 | <% end %>
66 |
67 |
68 | <% if user_signed_in? %>
69 |
70 |
71 | <%= image_tag user_avatar(current_user), class: "h-8 w-8 rounded-full" %>
72 |
73 |
74 |
75 |
@<%= current_user.username %>
76 |
77 |
78 |
79 | <%= link_to "Account Settings", account_root_path, class: "block px-3 py-2 text-sm text-neutral-600 hover:bg-neutral-100" %>
80 |
81 | <%= link_to "Your Samples", account_samples_path, class: "block px-3 py-2 text-sm text-neutral-600 hover:bg-neutral-100", role: "menuitem" %>
82 |
83 | <%= link_to "Logout", destroy_user_session_path, method: :delete, class: "block px-3 py-2 text-sm text-neutral-600 hover:bg-neutral-100" %>
84 |
85 | <% else %>
86 |
87 | <%= link_to "Login", new_user_session_path, class: "block px-3 py-2 text-sm text-neutral-600 hover:bg-neutral-100" %>
88 |
89 | <% end %>
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actioncable (7.0.4)
5 | actionpack (= 7.0.4)
6 | activesupport (= 7.0.4)
7 | nio4r (~> 2.0)
8 | websocket-driver (>= 0.6.1)
9 | actionmailbox (7.0.4)
10 | actionpack (= 7.0.4)
11 | activejob (= 7.0.4)
12 | activerecord (= 7.0.4)
13 | activestorage (= 7.0.4)
14 | activesupport (= 7.0.4)
15 | mail (>= 2.7.1)
16 | net-imap
17 | net-pop
18 | net-smtp
19 | actionmailer (7.0.4)
20 | actionpack (= 7.0.4)
21 | actionview (= 7.0.4)
22 | activejob (= 7.0.4)
23 | activesupport (= 7.0.4)
24 | mail (~> 2.5, >= 2.5.4)
25 | net-imap
26 | net-pop
27 | net-smtp
28 | rails-dom-testing (~> 2.0)
29 | actionpack (7.0.4)
30 | actionview (= 7.0.4)
31 | activesupport (= 7.0.4)
32 | rack (~> 2.0, >= 2.2.0)
33 | rack-test (>= 0.6.3)
34 | rails-dom-testing (~> 2.0)
35 | rails-html-sanitizer (~> 1.0, >= 1.2.0)
36 | actiontext (7.0.4)
37 | actionpack (= 7.0.4)
38 | activerecord (= 7.0.4)
39 | activestorage (= 7.0.4)
40 | activesupport (= 7.0.4)
41 | globalid (>= 0.6.0)
42 | nokogiri (>= 1.8.5)
43 | actionview (7.0.4)
44 | activesupport (= 7.0.4)
45 | builder (~> 3.1)
46 | erubi (~> 1.4)
47 | rails-dom-testing (~> 2.0)
48 | rails-html-sanitizer (~> 1.1, >= 1.2.0)
49 | activejob (7.0.4)
50 | activesupport (= 7.0.4)
51 | globalid (>= 0.3.6)
52 | activemodel (7.0.4)
53 | activesupport (= 7.0.4)
54 | activerecord (7.0.4)
55 | activemodel (= 7.0.4)
56 | activesupport (= 7.0.4)
57 | activestorage (7.0.4)
58 | actionpack (= 7.0.4)
59 | activejob (= 7.0.4)
60 | activerecord (= 7.0.4)
61 | activesupport (= 7.0.4)
62 | marcel (~> 1.0)
63 | mini_mime (>= 1.1.0)
64 | activesupport (7.0.4)
65 | concurrent-ruby (~> 1.0, >= 1.0.2)
66 | i18n (>= 1.6, < 2)
67 | minitest (>= 5.1)
68 | tzinfo (~> 2.0)
69 | addressable (2.8.7)
70 | public_suffix (>= 2.0.2, < 7.0)
71 | aws-eventstream (1.2.0)
72 | aws-partitions (1.628.0)
73 | aws-sdk-core (3.144.0)
74 | aws-eventstream (~> 1, >= 1.0.2)
75 | aws-partitions (~> 1, >= 1.525.0)
76 | aws-sigv4 (~> 1.1)
77 | jmespath (~> 1, >= 1.6.1)
78 | aws-sdk-kms (1.58.0)
79 | aws-sdk-core (~> 3, >= 3.127.0)
80 | aws-sigv4 (~> 1.1)
81 | aws-sdk-s3 (1.114.0)
82 | aws-sdk-core (~> 3, >= 3.127.0)
83 | aws-sdk-kms (~> 1)
84 | aws-sigv4 (~> 1.4)
85 | aws-sigv4 (1.5.1)
86 | aws-eventstream (~> 1, >= 1.0.2)
87 | bcrypt (3.1.18)
88 | bindex (0.8.1)
89 | bootsnap (1.13.0)
90 | msgpack (~> 1.2)
91 | builder (3.2.4)
92 | capybara (3.40.0)
93 | addressable
94 | matrix
95 | mini_mime (>= 0.1.3)
96 | nokogiri (~> 1.11)
97 | rack (>= 1.6.0)
98 | rack-test (>= 0.6.3)
99 | regexp_parser (>= 1.5, < 3.0)
100 | xpath (~> 3.2)
101 | concurrent-ruby (1.1.10)
102 | connection_pool (2.3.0)
103 | crass (1.0.6)
104 | cssbundling-rails (1.1.1)
105 | railties (>= 6.0.0)
106 | date (3.3.3)
107 | debug (1.7.1)
108 | irb (>= 1.5.0)
109 | reline (>= 0.3.1)
110 | devise (4.8.1)
111 | bcrypt (~> 3.0)
112 | orm_adapter (~> 0.1)
113 | railties (>= 4.1.0)
114 | responders
115 | warden (~> 1.2.3)
116 | discard (1.3.0)
117 | activerecord (>= 4.2, < 8)
118 | dotenv (2.8.1)
119 | dotenv-rails (2.8.1)
120 | dotenv (= 2.8.1)
121 | railties (>= 3.2)
122 | erubi (1.12.0)
123 | factory_bot (6.2.1)
124 | activesupport (>= 5.0.0)
125 | factory_bot_rails (6.2.0)
126 | factory_bot (~> 6.2.0)
127 | railties (>= 5.0.0)
128 | ffi (1.15.5)
129 | globalid (1.0.0)
130 | activesupport (>= 5.0)
131 | i18n (1.12.0)
132 | concurrent-ruby (~> 1.0)
133 | image_processing (1.12.2)
134 | mini_magick (>= 4.9.5, < 5)
135 | ruby-vips (>= 2.0.17, < 3)
136 | io-console (0.6.0)
137 | irb (1.6.2)
138 | reline (>= 0.3.0)
139 | jmespath (1.6.1)
140 | jsbundling-rails (1.0.3)
141 | railties (>= 6.0.0)
142 | json (2.6.2)
143 | lograge (0.12.0)
144 | actionpack (>= 4)
145 | activesupport (>= 4)
146 | railties (>= 4)
147 | request_store (~> 1.0)
148 | loofah (2.19.1)
149 | crass (~> 1.0.2)
150 | nokogiri (>= 1.5.9)
151 | mail (2.8.0.1)
152 | mini_mime (>= 0.1.1)
153 | net-imap
154 | net-pop
155 | net-smtp
156 | marcel (1.0.2)
157 | matrix (0.4.2)
158 | meta-tags (2.17.0)
159 | actionpack (>= 3.2.0, < 7.1)
160 | method_source (1.0.0)
161 | mini_magick (4.11.0)
162 | mini_mime (1.1.5)
163 | mini_portile2 (2.8.7)
164 | minitest (5.17.0)
165 | msgpack (1.5.4)
166 | net-imap (0.3.4)
167 | date
168 | net-protocol
169 | net-pop (0.1.2)
170 | net-protocol
171 | net-protocol (0.2.1)
172 | timeout
173 | net-smtp (0.3.3)
174 | net-protocol
175 | nio4r (2.5.8)
176 | nokogiri (1.16.7)
177 | mini_portile2 (~> 2.8.2)
178 | racc (~> 1.4)
179 | nokogiri (1.16.7-x86_64-darwin)
180 | racc (~> 1.4)
181 | nokogiri (1.16.7-x86_64-linux)
182 | racc (~> 1.4)
183 | orm_adapter (0.5.0)
184 | pg (1.4.3)
185 | postmark (1.22.1)
186 | json
187 | postmark-rails (0.22.1)
188 | actionmailer (>= 3.0.0)
189 | postmark (>= 1.21.3, < 2.0)
190 | public_suffix (6.0.1)
191 | puma (6.0.2)
192 | nio4r (~> 2.0)
193 | pundit (2.2.0)
194 | activesupport (>= 3.0.0)
195 | racc (1.8.1)
196 | rack (2.2.10)
197 | rack-test (2.1.0)
198 | rack (>= 1.3)
199 | rails (7.0.4)
200 | actioncable (= 7.0.4)
201 | actionmailbox (= 7.0.4)
202 | actionmailer (= 7.0.4)
203 | actionpack (= 7.0.4)
204 | actiontext (= 7.0.4)
205 | actionview (= 7.0.4)
206 | activejob (= 7.0.4)
207 | activemodel (= 7.0.4)
208 | activerecord (= 7.0.4)
209 | activestorage (= 7.0.4)
210 | activesupport (= 7.0.4)
211 | bundler (>= 1.15.0)
212 | railties (= 7.0.4)
213 | rails-dom-testing (2.0.3)
214 | activesupport (>= 4.2.0)
215 | nokogiri (>= 1.6)
216 | rails-html-sanitizer (1.4.4)
217 | loofah (~> 2.19, >= 2.19.1)
218 | railties (7.0.4)
219 | actionpack (= 7.0.4)
220 | activesupport (= 7.0.4)
221 | method_source
222 | rake (>= 12.2)
223 | thor (~> 1.0)
224 | zeitwerk (~> 2.5)
225 | rake (13.0.6)
226 | redcarpet (3.5.1)
227 | redis-client (0.12.0)
228 | connection_pool
229 | regexp_parser (2.9.2)
230 | reline (0.3.2)
231 | io-console (~> 0.5)
232 | request_store (1.5.1)
233 | rack (>= 1.4)
234 | responders (3.0.1)
235 | actionpack (>= 5.0)
236 | railties (>= 5.0)
237 | rexml (3.3.8)
238 | rouge (4.0.0)
239 | ruby-vips (2.1.4)
240 | ffi (~> 1.12)
241 | rubyzip (2.3.2)
242 | selenium-webdriver (4.10.0)
243 | rexml (~> 3.2, >= 3.2.5)
244 | rubyzip (>= 1.2.2, < 3.0)
245 | websocket (~> 1.0)
246 | sentry-rails (5.4.2)
247 | railties (>= 5.0)
248 | sentry-ruby (~> 5.4.2)
249 | sentry-ruby (5.4.2)
250 | concurrent-ruby (~> 1.0, >= 1.0.2)
251 | shoulda (4.0.0)
252 | shoulda-context (~> 2.0)
253 | shoulda-matchers (~> 4.0)
254 | shoulda-context (2.0.0)
255 | shoulda-matchers (4.5.1)
256 | activesupport (>= 4.2.0)
257 | sidekiq (7.0.3)
258 | concurrent-ruby (< 2)
259 | connection_pool (>= 2.3.0)
260 | rack (>= 2.2.4)
261 | redis-client (>= 0.11.0)
262 | sprockets (4.1.1)
263 | concurrent-ruby (~> 1.0)
264 | rack (> 1, < 3)
265 | sprockets-rails (3.4.2)
266 | actionpack (>= 5.2)
267 | activesupport (>= 5.2)
268 | sprockets (>= 3.0.0)
269 | stimulus-rails (1.1.0)
270 | railties (>= 6.0.0)
271 | thor (1.2.1)
272 | timeout (0.3.1)
273 | turbo-rails (1.1.1)
274 | actionpack (>= 6.0.0)
275 | activejob (>= 6.0.0)
276 | railties (>= 6.0.0)
277 | tzinfo (2.0.5)
278 | concurrent-ruby (~> 1.0)
279 | view_component (2.69.0)
280 | activesupport (>= 5.0.0, < 8.0)
281 | concurrent-ruby (~> 1.0)
282 | method_source (~> 1.0)
283 | warden (1.2.9)
284 | rack (>= 2.0.9)
285 | web-console (4.2.0)
286 | actionview (>= 6.0.0)
287 | activemodel (>= 6.0.0)
288 | bindex (>= 0.4.0)
289 | railties (>= 6.0.0)
290 | webdrivers (5.3.1)
291 | nokogiri (~> 1.6)
292 | rubyzip (>= 1.3.0)
293 | selenium-webdriver (~> 4.0, < 4.11)
294 | websocket (1.2.11)
295 | websocket-driver (0.7.5)
296 | websocket-extensions (>= 0.1.0)
297 | websocket-extensions (0.1.5)
298 | xpath (3.2.0)
299 | nokogiri (~> 1.8)
300 | zeitwerk (2.6.6)
301 |
302 | PLATFORMS
303 | ruby
304 | x86_64-darwin-21
305 | x86_64-linux
306 |
307 | DEPENDENCIES
308 | aws-sdk-s3
309 | bootsnap
310 | capybara
311 | cssbundling-rails
312 | debug
313 | devise
314 | discard
315 | dotenv-rails
316 | factory_bot_rails
317 | image_processing (>= 1.2)
318 | jsbundling-rails
319 | lograge
320 | meta-tags
321 | pg (~> 1.1)
322 | postmark-rails
323 | puma (~> 6.0)
324 | pundit
325 | rails (~> 7.0.3)
326 | redcarpet
327 | rouge
328 | selenium-webdriver
329 | sentry-rails
330 | sentry-ruby
331 | shoulda
332 | sidekiq
333 | sprockets-rails
334 | stimulus-rails
335 | turbo-rails
336 | view_component
337 | web-console
338 | webdrivers
339 |
340 | RUBY VERSION
341 | ruby 3.1.2p20
342 |
343 | BUNDLED WITH
344 | 2.3.16
345 |
--------------------------------------------------------------------------------
/config/initializers/devise.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class TurboFailureApp < Devise::FailureApp
4 | def respond
5 | if request_format == :turbo_stream
6 | redirect
7 | else
8 | super
9 | end
10 | end
11 |
12 | def skip_format?
13 | %w(html turbo_stream */*).include? request_format.to_s
14 | end
15 | end
16 |
17 | # Assuming you have not yet modified this file, each configuration option below
18 | # is set to its default value. Note that some are commented out while others
19 | # are not: uncommented lines are intended to protect your configuration from
20 | # breaking changes in upgrades (i.e., in the event that future versions of
21 | # Devise change the default values for those options).
22 | #
23 | # Use this hook to configure devise mailer, warden hooks and so forth.
24 | # Many of these configuration options can be set straight in your model.
25 | Devise.setup do |config|
26 | # The secret key used by Devise. Devise uses this key to generate
27 | # random tokens. Changing this key will render invalid all existing
28 | # confirmation, reset password and unlock tokens in the database.
29 | # Devise will use the `secret_key_base` as its `secret_key`
30 | # by default. You can change it below and use your own secret key.
31 | # config.secret_key = '71919d3385e67f86a683fcbde6c3508c71892a13f52ff540b53ca184a0488b1b3f6e7f894922e4c34311bccc65f790d52962a7683a1bb56e40d154276bd88403'
32 |
33 | # ==> Controller configuration
34 | # Configure the parent class to the devise controllers.
35 | config.parent_controller = 'TurboController'
36 |
37 | # ==> Mailer Configuration
38 | # Configure the e-mail address which will be shown in Devise::Mailer,
39 | # note that it will be overwritten if you use your own mailer class
40 | # with default "from" parameter.
41 | config.mailer_sender = 'hi@railsinspire.com'
42 |
43 | # Configure the class responsible to send e-mails.
44 | # config.mailer = 'Devise::Mailer'
45 |
46 | # Configure the parent class responsible to send e-mails.
47 | # config.parent_mailer = 'ActionMailer::Base'
48 |
49 | # ==> ORM configuration
50 | # Load and configure the ORM. Supports :active_record (default) and
51 | # :mongoid (bson_ext recommended) by default. Other ORMs may be
52 | # available as additional gems.
53 | require 'devise/orm/active_record'
54 |
55 | # ==> Configuration for any authentication mechanism
56 | # Configure which keys are used when authenticating a user. The default is
57 | # just :email. You can configure it to use [:username, :subdomain], so for
58 | # authenticating a user, both parameters are required. Remember that those
59 | # parameters are used only when authenticating and not when retrieving from
60 | # session. If you need permissions, you should implement that in a before filter.
61 | # You can also supply a hash where the value is a boolean determining whether
62 | # or not authentication should be aborted when the value is not present.
63 | # config.authentication_keys = [:email]
64 |
65 | # Configure parameters from the request object used for authentication. Each entry
66 | # given should be a request method and it will automatically be passed to the
67 | # find_for_authentication method and considered in your model lookup. For instance,
68 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication.
69 | # The same considerations mentioned for authentication_keys also apply to request_keys.
70 | # config.request_keys = []
71 |
72 | # Configure which authentication keys should be case-insensitive.
73 | # These keys will be downcased upon creating or modifying a user and when used
74 | # to authenticate or find a user. Default is :email.
75 | config.case_insensitive_keys = [:email]
76 |
77 | # Configure which authentication keys should have whitespace stripped.
78 | # These keys will have whitespace before and after removed upon creating or
79 | # modifying a user and when used to authenticate or find a user. Default is :email.
80 | config.strip_whitespace_keys = [:email]
81 |
82 | # Tell if authentication through request.params is enabled. True by default.
83 | # It can be set to an array that will enable params authentication only for the
84 | # given strategies, for example, `config.params_authenticatable = [:database]` will
85 | # enable it only for database (email + password) authentication.
86 | # config.params_authenticatable = true
87 |
88 | # Tell if authentication through HTTP Auth is enabled. False by default.
89 | # It can be set to an array that will enable http authentication only for the
90 | # given strategies, for example, `config.http_authenticatable = [:database]` will
91 | # enable it only for database authentication.
92 | # For API-only applications to support authentication "out-of-the-box", you will likely want to
93 | # enable this with :database unless you are using a custom strategy.
94 | # The supported strategies are:
95 | # :database = Support basic authentication with authentication key + password
96 | # config.http_authenticatable = false
97 |
98 | # If 401 status code should be returned for AJAX requests. True by default.
99 | # config.http_authenticatable_on_xhr = true
100 |
101 | # The realm used in Http Basic Authentication. 'Application' by default.
102 | # config.http_authentication_realm = 'Application'
103 |
104 | # It will change confirmation, password recovery and other workflows
105 | # to behave the same regardless if the e-mail provided was right or wrong.
106 | # Does not affect registerable.
107 | # config.paranoid = true
108 |
109 | # By default Devise will store the user in session. You can skip storage for
110 | # particular strategies by setting this option.
111 | # Notice that if you are skipping storage for all authentication paths, you
112 | # may want to disable generating routes to Devise's sessions controller by
113 | # passing skip: :sessions to `devise_for` in your config/routes.rb
114 | config.skip_session_storage = [:http_auth]
115 |
116 | # By default, Devise cleans up the CSRF token on authentication to
117 | # avoid CSRF token fixation attacks. This means that, when using AJAX
118 | # requests for sign in and sign up, you need to get a new CSRF token
119 | # from the server. You can disable this option at your own risk.
120 | # config.clean_up_csrf_token_on_authentication = true
121 |
122 | # When false, Devise will not attempt to reload routes on eager load.
123 | # This can reduce the time taken to boot the app but if your application
124 | # requires the Devise mappings to be loaded during boot time the application
125 | # won't boot properly.
126 | # config.reload_routes = true
127 |
128 | # ==> Configuration for :database_authenticatable
129 | # For bcrypt, this is the cost for hashing the password and defaults to 12. If
130 | # using other algorithms, it sets how many times you want the password to be hashed.
131 | # The number of stretches used for generating the hashed password are stored
132 | # with the hashed password. This allows you to change the stretches without
133 | # invalidating existing passwords.
134 | #
135 | # Limiting the stretches to just one in testing will increase the performance of
136 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use
137 | # a value less than 10 in other environments. Note that, for bcrypt (the default
138 | # algorithm), the cost increases exponentially with the number of stretches (e.g.
139 | # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation).
140 | config.stretches = Rails.env.test? ? 1 : 12
141 |
142 | # Set up a pepper to generate the hashed password.
143 | # config.pepper = '77c442dcc2d655bad3d75de953fc18f529aa632d7f3515e31c2559e832f5a340cf1f72433df06090e52a3d4a22feff61c101291f2d6633b96a63d9614a9023cf'
144 |
145 | # Send a notification to the original email when the user's email is changed.
146 | # config.send_email_changed_notification = false
147 |
148 | # Send a notification email when the user's password is changed.
149 | # config.send_password_change_notification = false
150 |
151 | # ==> Configuration for :confirmable
152 | # A period that the user is allowed to access the website even without
153 | # confirming their account. For instance, if set to 2.days, the user will be
154 | # able to access the website for two days without confirming their account,
155 | # access will be blocked just in the third day.
156 | # You can also set it to nil, which will allow the user to access the website
157 | # without confirming their account.
158 | # Default is 0.days, meaning the user cannot access the website without
159 | # confirming their account.
160 | # config.allow_unconfirmed_access_for = 2.days
161 |
162 | # A period that the user is allowed to confirm their account before their
163 | # token becomes invalid. For example, if set to 3.days, the user can confirm
164 | # their account within 3 days after the mail was sent, but on the fourth day
165 | # their account can't be confirmed with the token any more.
166 | # Default is nil, meaning there is no restriction on how long a user can take
167 | # before confirming their account.
168 | # config.confirm_within = 3.days
169 |
170 | # If true, requires any email changes to be confirmed (exactly the same way as
171 | # initial account confirmation) to be applied. Requires additional unconfirmed_email
172 | # db field (see migrations). Until confirmed, new email is stored in
173 | # unconfirmed_email column, and copied to email column on successful confirmation.
174 | config.reconfirmable = true
175 |
176 | # Defines which key will be used when confirming an account
177 | # config.confirmation_keys = [:email]
178 |
179 | # ==> Configuration for :rememberable
180 | # The time the user will be remembered without asking for credentials again.
181 | # config.remember_for = 2.weeks
182 |
183 | # Invalidates all the remember me tokens when the user signs out.
184 | config.expire_all_remember_me_on_sign_out = true
185 |
186 | # If true, extends the user's remember period when remembered via cookie.
187 | # config.extend_remember_period = false
188 |
189 | # Options to be passed to the created cookie. For instance, you can set
190 | # secure: true in order to force SSL only cookies.
191 | # config.rememberable_options = {}
192 |
193 | # ==> Configuration for :validatable
194 | # Range for password length.
195 | config.password_length = 6..128
196 |
197 | # Email regex used to validate email formats. It simply asserts that
198 | # one (and only one) @ exists in the given string. This is mainly
199 | # to give user feedback and not to assert the e-mail validity.
200 | config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
201 |
202 | # ==> Configuration for :timeoutable
203 | # The time you want to timeout the user session without activity. After this
204 | # time the user will be asked for credentials again. Default is 30 minutes.
205 | # config.timeout_in = 30.minutes
206 |
207 | # ==> Configuration for :lockable
208 | # Defines which strategy will be used to lock an account.
209 | # :failed_attempts = Locks an account after a number of failed attempts to sign in.
210 | # :none = No lock strategy. You should handle locking by yourself.
211 | # config.lock_strategy = :failed_attempts
212 |
213 | # Defines which key will be used when locking and unlocking an account
214 | # config.unlock_keys = [:email]
215 |
216 | # Defines which strategy will be used to unlock an account.
217 | # :email = Sends an unlock link to the user email
218 | # :time = Re-enables login after a certain amount of time (see :unlock_in below)
219 | # :both = Enables both strategies
220 | # :none = No unlock strategy. You should handle unlocking by yourself.
221 | # config.unlock_strategy = :both
222 |
223 | # Number of authentication tries before locking an account if lock_strategy
224 | # is failed attempts.
225 | # config.maximum_attempts = 20
226 |
227 | # Time interval to unlock the account if :time is enabled as unlock_strategy.
228 | # config.unlock_in = 1.hour
229 |
230 | # Warn on the last attempt before the account is locked.
231 | # config.last_attempt_warning = true
232 |
233 | # ==> Configuration for :recoverable
234 | #
235 | # Defines which key will be used when recovering the password for an account
236 | # config.reset_password_keys = [:email]
237 |
238 | # Time interval you can reset your password with a reset password key.
239 | # Don't put a too small interval or your users won't have the time to
240 | # change their passwords.
241 | config.reset_password_within = 6.hours
242 |
243 | # When set to false, does not sign a user in automatically after their password is
244 | # reset. Defaults to true, so a user is signed in automatically after a reset.
245 | # config.sign_in_after_reset_password = true
246 |
247 | # ==> Configuration for :encryptable
248 | # Allow you to use another hashing or encryption algorithm besides bcrypt (default).
249 | # You can use :sha1, :sha512 or algorithms from others authentication tools as
250 | # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20
251 | # for default behavior) and :restful_authentication_sha1 (then you should set
252 | # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper).
253 | #
254 | # Require the `devise-encryptable` gem when using anything other than bcrypt
255 | # config.encryptor = :sha512
256 |
257 | # ==> Scopes configuration
258 | # Turn scoped views on. Before rendering "sessions/new", it will first check for
259 | # "users/sessions/new". It's turned off by default because it's slower if you
260 | # are using only default views.
261 | # config.scoped_views = false
262 |
263 | # Configure the default scope given to Warden. By default it's the first
264 | # devise role declared in your routes (usually :user).
265 | # config.default_scope = :user
266 |
267 | # Set this configuration to false if you want /users/sign_out to sign out
268 | # only the current scope. By default, Devise signs out all scopes.
269 | # config.sign_out_all_scopes = true
270 |
271 | # ==> Navigation configuration
272 | # Lists the formats that should be treated as navigational. Formats like
273 | # :html, should redirect to the sign in page when the user does not have
274 | # access, but formats like :xml or :json, should return 401.
275 | #
276 | # If you have any extra navigational formats, like :iphone or :mobile, you
277 | # should add them to the navigational formats lists.
278 | #
279 | # The "*/*" below is required to match Internet Explorer requests.
280 | config.navigational_formats = ['*/*', :html, :turbo_stream]
281 |
282 | # The default HTTP method used to sign out a resource. Default is :delete.
283 | config.sign_out_via = :delete
284 |
285 | # ==> OmniAuth
286 | # Add a new OmniAuth provider. Check the wiki for more information on setting
287 | # up on your models and hooks.
288 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
289 |
290 | # ==> Warden configuration
291 | # If you want to use other strategies, that are not supported by Devise, or
292 | # change the failure app, you can configure them inside the config.warden block.
293 | #
294 | config.warden do |manager|
295 | manager.failure_app = TurboFailureApp
296 | end
297 |
298 | # ==> Mountable engine configurations
299 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine
300 | # is mountable, there are some extra configurations to be taken into account.
301 | # The following options are available, assuming the engine is mounted as:
302 | #
303 | # mount MyEngine, at: '/my_engine'
304 | #
305 | # The router that invoked `devise_for`, in the example above, would be:
306 | # config.router_name = :my_engine
307 | #
308 | # When using OmniAuth, Devise cannot automatically set OmniAuth path,
309 | # so you need to do it manually. For the users scope, it would be:
310 | # config.omniauth_path_prefix = '/my_engine/users/auth'
311 |
312 | # ==> Turbolinks configuration
313 | # If your app is using Turbolinks, Turbolinks::Controller needs to be included to make redirection work correctly:
314 | #
315 | # ActiveSupport.on_load(:devise_failure_app) do
316 | # include Turbolinks::Controller
317 | # end
318 |
319 | # ==> Configuration for :registerable
320 |
321 | # When set to false, does not sign a user in automatically after their password is
322 | # changed. Defaults to true, so a user is signed in automatically after changing a password.
323 | # config.sign_in_after_change_password = true
324 | end
325 |
--------------------------------------------------------------------------------
/app/views/sample_files/_sample_file.svg.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= @sample_file.sample.title %>
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | <%= @sample_file.sample.title %>
20 |
21 |
22 |
23 | <%= strip_markdown(@sample_file.sample.description).truncate(75) %>
24 |
25 |
26 |
27 |
28 | <% syntax_higlight(@sample_file.contents, @sample_file.path, "svg_inline").each_line.with_index do |line, i| %>
29 | <%= line.html_safe %>
30 | <% end %>
31 |
32 |
33 |
34 | <% (1..@sample_file.line_count_or_default).each do |line| %>
35 | <%= line %>
36 | <% end %>
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------