Created
May 13, 2026 11:30
-
-
Save anon987654321/1bb6c29c1cf7de3081d713f9a8ce6eb1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env ruby -E UTF-8 | |
| # Unified patch for DEPLOY/ – applies shared engine, basic fixes, feed, map, Solidus marketplace, and Reddit scraper | |
| require "fileutils" | |
| require "yaml" | |
| ROOT = Dir.pwd | |
| puts "📦 Applying unified patch from #{ROOT}" | |
| # ------------------------------------------------------------------------------ | |
| # 1. Create shared engine (rails/shared) | |
| # ------------------------------------------------------------------------------ | |
| shared_root = "shared" | |
| shared_lib = "#{shared_root}/lib/shared" | |
| shared_engine_rb = "#{shared_lib}/engine.rb" | |
| shared_gemspec = "#{shared_root}/shared.gemspec" | |
| shared_gemfile = "#{shared_root}/Gemfile" | |
| FileUtils.mkdir_p("#{shared_root}/app/controllers/concerns") | |
| FileUtils.mkdir_p("#{shared_root}/app/views/layouts") | |
| FileUtils.mkdir_p("#{shared_root}/app/assets/stylesheets") | |
| FileUtils.mkdir_p("#{shared_root}/config/initializers") | |
| FileUtils.mkdir_p(shared_lib) | |
| # shared.gemspec | |
| unless File.exist?(shared_gemspec) | |
| File.write(shared_gemspec, <<~'RUBY') | |
| require_relative "lib/shared/version" | |
| Gem::Specification.new do |spec| | |
| spec.name = "shared" | |
| spec.version = "0.1.0" | |
| spec.authors = ["Brgen Core"] | |
| spec.email = ["core@brgen.no"] | |
| spec.summary = "Shared engine for Brgen apps" | |
| spec.description = "Authentication, pagination, flash partials, base styles" | |
| spec.license = "MIT" | |
| spec.files = Dir["{app,config,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] | |
| spec.add_dependency "rails", "~> 8.1.2" | |
| spec.add_dependency "pagy" | |
| spec.add_dependency "importmap-rails" | |
| spec.add_dependency "stimulus-rails" | |
| spec.add_dependency "turbo-rails" | |
| spec.add_dependency "propshaft" | |
| end | |
| RUBY | |
| puts " ✅ Created #{shared_gemspec}" | |
| end | |
| # shared/Gemfile | |
| unless File.exist?(shared_gemfile) | |
| File.write(shared_gemfile, <<~'RUBY') | |
| source "https://rubygems.org" | |
| gemspec | |
| gem "rails", "~> 8.1.2" | |
| gem "pagy", "~> 9.3" | |
| gem "importmap-rails" | |
| gem "stimulus-rails" | |
| gem "turbo-rails" | |
| gem "propshaft" | |
| group :development, :test do | |
| gem "debug" | |
| gem "brakeman" | |
| gem "rubocop-rails-omakase" | |
| end | |
| RUBY | |
| puts " ✅ Created #{shared_gemfile}" | |
| end | |
| # shared/lib/shared/engine.rb | |
| unless File.exist?(shared_engine_rb) | |
| FileUtils.mkdir_p(File.dirname(shared_engine_rb)) | |
| File.write(shared_engine_rb, <<~'RUBY') | |
| module Shared | |
| class Engine < ::Rails::Engine | |
| isolate_namespace Shared | |
| initializer "shared.assets.precompile" do |app| | |
| app.config.assets.precompile += %w[shared_manifest.js] | |
| end | |
| end | |
| end | |
| RUBY | |
| puts " ✅ Created #{shared_engine_rb}" | |
| end | |
| # shared/lib/shared/version.rb | |
| version_rb = "#{shared_lib}/version.rb" | |
| unless File.exist?(version_rb) | |
| File.write(version_rb, <<~'RUBY') | |
| module Shared | |
| VERSION = "0.1.0" | |
| end | |
| RUBY | |
| puts " ✅ Created #{version_rb}" | |
| end | |
| # shared/app/controllers/concerns/authentication.rb | |
| auth_concern = "#{shared_root}/app/controllers/concerns/authentication.rb" | |
| unless File.exist?(auth_concern) | |
| File.write(auth_concern, <<~'RUBY') | |
| module Authentication | |
| extend ActiveSupport::Concern | |
| included do | |
| before_action :require_authentication | |
| helper_method :authenticated? | |
| end | |
| class_methods do | |
| def allow_unauthenticated_access(**options) | |
| skip_before_action :require_authentication, **options | |
| end | |
| end | |
| private | |
| def authenticated? | |
| resume_session | |
| end | |
| def require_authentication | |
| resume_session || request_authentication | |
| end | |
| def resume_session | |
| Current.session ||= find_session_by_cookie | |
| end | |
| def find_session_by_cookie | |
| Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id] | |
| end | |
| def request_authentication | |
| session[:return_to_after_authenticating] = request.url | |
| redirect_to new_session_path | |
| end | |
| def after_authentication_url | |
| session.delete(:return_to_after_authenticating) || root_url | |
| end | |
| def start_new_session_for(user) | |
| user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session| | |
| Current.session = session | |
| cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax } | |
| end | |
| end | |
| def terminate_session | |
| Current.session.destroy | |
| cookies.delete(:session_id) | |
| end | |
| end | |
| RUBY | |
| puts " ✅ Created #{auth_concern}" | |
| end | |
| # shared/app/views/layouts/_flash.html.erb | |
| flash_partial = "#{shared_root}/app/views/layouts/_flash.html.erb" | |
| unless File.exist?(flash_partial) | |
| FileUtils.mkdir_p(File.dirname(flash_partial)) | |
| File.write(flash_partial, <<~'ERB') | |
| <% if flash.any? %> | |
| <div class="flash-container" data-controller="flash"> | |
| <% flash.each do |type, msg| %> | |
| <div class="flash flash--<%= type %>" role="alert"> | |
| <%= msg %> | |
| <button type="button" class="flash__close" data-action="click->flash#dismiss">×</button> | |
| </div> | |
| <% end %> | |
| </div> | |
| <% end %> | |
| ERB | |
| puts " ✅ Created #{flash_partial}" | |
| end | |
| # shared/app/views/layouts/_errors.html.erb | |
| errors_partial = "#{shared_root}/app/views/layouts/_errors.html.erb" | |
| unless File.exist?(errors_partial) | |
| File.write(errors_partial, <<~'ERB') | |
| <% if object.errors.any? %> | |
| <div class="errors"> | |
| <h2><%= pluralize(object.errors.count, "error") %> prohibited this record from being saved:</h2> | |
| <ul> | |
| <% object.errors.full_messages.each do |msg| %> | |
| <li><%= msg %></li> | |
| <% end %> | |
| </ul> | |
| </div> | |
| <% end %> | |
| ERB | |
| puts " ✅ Created #{errors_partial}" | |
| end | |
| # shared/config/initializers/pagy.rb | |
| pagy_init = "#{shared_root}/config/initializers/pagy.rb" | |
| unless File.exist?(pagy_init) | |
| File.write(pagy_init, <<~'RUBY') | |
| require "pagy/extras/overflow" | |
| Pagy::DEFAULT[:items] = 25 | |
| Pagy::DEFAULT[:overflow] = :last_page | |
| Pagy::DEFAULT[:size] = [1, 2, 2, 1] | |
| RUBY | |
| puts " ✅ Created #{pagy_init}" | |
| end | |
| # ------------------------------------------------------------------------------ | |
| # Helper to update Gemfile and application files for each app | |
| # ------------------------------------------------------------------------------ | |
| apps = %w[amber baibl blognet brgen bsdports hjerterom] | |
| apps.each do |app| | |
| app_root = "rails/#{app}" | |
| next unless Dir.exist?(app_root) | |
| # ----- Gemfile: add shared gem, faker, bcrypt, etc. ----- | |
| gemfile = "#{app_root}/Gemfile" | |
| if File.exist?(gemfile) | |
| content = File.read(gemfile, encoding: "UTF-8") | |
| unless content.include?('gem "shared", path: "../../shared"') | |
| content.sub!(/^gem "rails".*$/, "\\0\ngem \"shared\", path: \"../../shared\"") | |
| File.write(gemfile, content, encoding: "UTF-8") | |
| puts " ✅ Added shared gem to #{gemfile}" | |
| end | |
| unless content.include?('gem "faker"') | |
| content.sub!(/group :development, :test do/, "\\0\n gem \"faker\"") | |
| File.write(gemfile, content, encoding: "UTF-8") | |
| puts " ✅ Added faker to #{gemfile}" | |
| end | |
| if app == "baibl" && !content.include?('gem "bcrypt"') | |
| content.sub!(/^gem "rails".*$/, "\\0\ngem \"bcrypt\", \"~> 3.1.7\"") | |
| File.write(gemfile, content, encoding: "UTF-8") | |
| puts " ✅ Added bcrypt to #{gemfile}" | |
| end | |
| if app == "brgen" | |
| unless content.include?('gem "solidus"') | |
| content.sub!(/group :development, :test do/, "\\0\n gem \"solidus\", \"~> 4.5\"\n gem \"solidus_starter_frontend\"") | |
| File.write(gemfile, content, encoding: "UTF-8") | |
| puts " ✅ Added solidus to #{gemfile}" | |
| end | |
| unless content.include?('gem "geocoder"') | |
| content.sub!(/^gem "rails".*$/, "\\0\ngem \"geocoder\"") | |
| File.write(gemfile, content, encoding: "UTF-8") | |
| puts " ✅ Added geocoder to #{gemfile}" | |
| end | |
| end | |
| if app == "hjerterom" && !content.include?('gem "geocoder"') | |
| content.sub!(/^gem "rails".*$/, "\\0\ngem \"geocoder\"") | |
| File.write(gemfile, content, encoding: "UTF-8") | |
| puts " ✅ Added geocoder to #{gemfile}" | |
| end | |
| end | |
| # ----- Delete local authentication.rb ----- | |
| auth_local = "#{app_root}/app/controllers/concerns/authentication.rb" | |
| if File.exist?(auth_local) | |
| File.delete(auth_local) | |
| puts " ✅ Deleted #{auth_local}" | |
| end | |
| # ----- Delete local pagy initializer ----- | |
| pagy_local = "#{app_root}/config/initializers/pagy.rb" | |
| if File.exist?(pagy_local) | |
| File.delete(pagy_local) | |
| puts " ✅ Deleted #{pagy_local}" | |
| end | |
| # ----- Update ApplicationController to include Shared helpers ----- | |
| app_controller = "#{app_root}/app/controllers/application_controller.rb" | |
| if File.exist?(app_controller) | |
| content = File.read(app_controller, encoding: "UTF-8") | |
| unless content.include?("helper Shared::Engine.helpers") | |
| content.sub!(/class ApplicationController.*\n/, "\\0 helper Shared::Engine.helpers\n") | |
| File.write(app_controller, content, encoding: "UTF-8") | |
| puts " ✅ Updated #{app_controller}" | |
| end | |
| if app == "brgen" && !content.include?("def current_ability") | |
| content << "\n def current_ability\n @current_ability ||= Spree::Ability.new(current_user)\n end\n" | |
| File.write(app_controller, content, encoding: "UTF-8") | |
| puts " ✅ Added current_ability to #{app_controller}" | |
| end | |
| end | |
| # ----- Update layout to use shared flash partial ----- | |
| layout = "#{app_root}/app/views/layouts/application.html.erb" | |
| if File.exist?(layout) | |
| content = File.read(layout, encoding: "UTF-8") | |
| if content.include?('render "shared/flash"') | |
| content.gsub!('render "shared/flash"', 'render "layouts/flash"') | |
| File.write(layout, content, encoding: "UTF-8") | |
| puts " ✅ Updated flash partial in #{layout}" | |
| elsif !content.include?('render "layouts/flash"') | |
| content.sub!(/<body>.*\n/, "\\0 <%= render \"layouts/flash\" %>\n") | |
| File.write(layout, content, encoding: "UTF-8") | |
| puts " ✅ Added flash partial to #{layout}" | |
| end | |
| end | |
| # ----- App-specific fixes ----- | |
| if app == "baibl" | |
| # Add has_secure_password to User model | |
| user_model = "#{app_root}/app/models/user.rb" | |
| if File.exist?(user_model) | |
| content = File.read(user_model, encoding: "UTF-8") | |
| unless content.include?("has_secure_password") | |
| content.sub!(/class User < ApplicationRecord/, "\\0\n has_secure_password") | |
| File.write(user_model, content, encoding: "UTF-8") | |
| puts " ✅ Added has_secure_password to #{user_model}" | |
| end | |
| end | |
| # Enhance seeds (add Genesis) | |
| seeds = "#{app_root}/db/seeds.rb" | |
| if File.exist?(seeds) | |
| content = File.read(seeds, encoding: "UTF-8") | |
| unless content.include?("Genesis") | |
| content << <<~'RUBY' | |
| genesis = Book.find_or_create_by!(name: "Genesis", abbreviation: "Gen", testament: "Old", order_index: 1) | |
| 50.times do |chapter_num| | |
| chapter = genesis.chapters.find_or_create_by!(number: chapter_num + 1) | |
| 20.times do |verse_num| | |
| chapter.verses.find_or_create_by!(number: verse_num + 1, content: "Verse #{verse_num + 1} of chapter #{chapter_num + 1} of Genesis. (Replace with real text.)") | |
| end | |
| end | |
| RUBY | |
| File.write(seeds, content, encoding: "UTF-8") | |
| puts " ✅ Enhanced seeds for baibl" | |
| end | |
| end | |
| end | |
| if app == "blognet" | |
| # Add Follow model | |
| follow_model = "#{app_root}/app/models/follow.rb" | |
| unless File.exist?(follow_model) | |
| File.write(follow_model, <<~'RUBY') | |
| class Follow < ApplicationRecord | |
| belongs_to :follower, class_name: "User" | |
| belongs_to :followed, class_name: "User" | |
| validates :follower_id, uniqueness: { scope: :followed_id } | |
| validate :no_self_follow | |
| private def no_self_follow = errors.add(:followed, "can't follow yourself") if follower_id == followed_id | |
| end | |
| RUBY | |
| puts " ✅ Created #{follow_model}" | |
| end | |
| # Add UsersController | |
| users_controller = "#{app_root}/app/controllers/users_controller.rb" | |
| unless File.exist?(users_controller) | |
| File.write(users_controller, <<~'RUBY') | |
| class UsersController < ApplicationController | |
| allow_unauthenticated_access only: [:show] | |
| def show | |
| @user = User.find(params[:id]) | |
| @posts = @user.posts.published.order(published_at: :desc).limit(10) | |
| end | |
| end | |
| RUBY | |
| puts " ✅ Created #{users_controller}" | |
| end | |
| # Update User model with follow associations | |
| user_model = "#{app_root}/app/models/user.rb" | |
| if File.exist?(user_model) | |
| content = File.read(user_model, encoding: "UTF-8") | |
| unless content.include?("has_many :active_follows") | |
| content.sub!(/has_many :posts.*\n/, "\\0 has_many :active_follows, class_name: \"Follow\", foreign_key: \"follower_id\", dependent: :destroy\n has_many :following, through: :active_follows, source: :followed\n has_many :passive_follows, class_name: \"Follow\", foreign_key: \"followed_id\", dependent: :destroy\n has_many :followers, through: :passive_follows, source: :follower\n") | |
| File.write(user_model, content, encoding: "UTF-8") | |
| puts " ✅ Added follow associations to User in blognet" | |
| end | |
| end | |
| end | |
| if app == "brgen" | |
| # ----- Add swipe controller ----- | |
| swipe_js = "#{app_root}/app/javascript/controllers/swipe_controller.js" | |
| unless File.exist?(swipe_js) | |
| File.write(swipe_js, <<~'JS') | |
| import { Controller } from "@hotwired/stimulus" | |
| export default class extends Controller { | |
| static targets = ["card"] | |
| connect() { | |
| this.startX = 0; this.currentX = 0; this.isDragging = false | |
| this.card = this.cardTargets[0] | |
| this.element.addEventListener("touchstart", this.onTouchStart) | |
| this.element.addEventListener("touchmove", this.onTouchMove) | |
| this.element.addEventListener("touchend", this.onTouchEnd) | |
| } | |
| onTouchStart = (e) => { this.startX = e.touches[0].clientX; this.isDragging = true } | |
| onTouchMove = (e) => { | |
| if (!this.isDragging) return | |
| this.currentX = e.touches[0].clientX | |
| const diff = this.currentX - this.startX | |
| this.card.style.transform = `translateX(${diff}px) rotate(${diff * 0.02}deg)` | |
| this.card.style.opacity = 1 - Math.abs(diff) / 500 | |
| } | |
| onTouchEnd = () => { | |
| const diff = this.currentX - this.startX | |
| if (Math.abs(diff) > 100) this[diff > 0 ? "right" : "left"]() | |
| else { this.card.style.transform = ""; this.card.style.opacity = 1 } | |
| this.isDragging = false | |
| } | |
| right() { this.submitVote("like") } | |
| left() { this.submitVote("dislike") } | |
| submitVote(type) { | |
| const userId = this.card.dataset.userId | |
| fetch(`/dating/${type}s`, { method: "POST", headers: { "X-CSRF-Token": document.querySelector("[name=csrf-token]").content }, body: new URLSearchParams({ user_id: userId }) }) | |
| .then(() => this.nextCard()) | |
| } | |
| nextCard() { this.card.remove(); this.card = this.cardTargets[0] } | |
| } | |
| JS | |
| puts " ✅ Created #{swipe_js}" | |
| end | |
| # ----- Add post_form controller for Facebook feed ----- | |
| post_form_js = "#{app_root}/app/javascript/controllers/post_form.js" | |
| unless File.exist?(post_form_js) | |
| File.write(post_form_js, <<~'JS') | |
| import { Controller } from "@hotwired/stimulus" | |
| export default class extends Controller { | |
| static targets = ["content", "anonymousCheckbox"] | |
| clear() { | |
| this.contentTarget.value = "" | |
| if (this.hasAnonymousCheckboxTarget) { | |
| this.anonymousCheckboxTarget.checked = false | |
| } | |
| } | |
| } | |
| JS | |
| puts " ✅ Created #{post_form_js}" | |
| end | |
| # ----- Add Cart model ----- | |
| cart_model = "#{app_root}/app/models/cart.rb" | |
| unless File.exist?(cart_model) | |
| File.write(cart_model, <<~'RUBY') | |
| class Cart < ApplicationRecord | |
| belongs_to :user | |
| has_many :cart_items, dependent: :destroy | |
| has_many :listings, through: :cart_items | |
| validates :status, inclusion: { in: %w[active abandoned] } | |
| def total_cents | |
| cart_items.sum { |ci| ci.quantity * ci.listing.price_cents } | |
| end | |
| def to_order! | |
| order = Marketplace::Order.create!(buyer: user, status: "pending", total_cents: total_cents) | |
| cart_items.each do |ci| | |
| order.line_items.create!(purchasable: ci.listing, quantity: ci.quantity, unit_price_cents: ci.listing.price_cents) | |
| end | |
| update!(status: "abandoned") | |
| order | |
| end | |
| end | |
| RUBY | |
| puts " ✅ Created #{cart_model}" | |
| end | |
| cart_item_model = "#{app_root}/app/models/cart_item.rb" | |
| unless File.exist?(cart_item_model) | |
| File.write(cart_item_model, <<~'RUBY') | |
| class CartItem < ApplicationRecord | |
| belongs_to :cart | |
| belongs_to :listing, class_name: "Marketplace::Listing" | |
| validates :quantity, numericality: { greater_than: 0 } | |
| delegate :price_cents, to: :listing, prefix: true | |
| end | |
| RUBY | |
| puts " ✅ Created #{cart_item_model}" | |
| end | |
| # ----- Add cart show view ----- | |
| cart_view_dir = "#{app_root}/app/views/marketplace/cart" | |
| cart_view = "#{cart_view_dir}/show.html.erb" | |
| unless File.exist?(cart_view) | |
| FileUtils.mkdir_p(cart_view_dir) | |
| File.write(cart_view, <<~'ERB') | |
| <h1>Shopping Cart</h1> | |
| <% if @cart.cart_items.any? %> | |
| <table> | |
| <% @cart.cart_items.each do |item| %> | |
| <tr> | |
| <td><%= item.listing.title %></td> | |
| <td><%= item.quantity %></td> | |
| <td><%= number_to_currency(item.listing.price_cents / 100.0) %></td> | |
| </tr> | |
| <% end %> | |
| </table> | |
| <p>Total: <%= number_to_currency(@cart.total_cents / 100.0) %></p> | |
| <%= button_to "Checkout", checkout_marketplace_cart_path, method: :post %> | |
| <% else %><p>Your cart is empty.</p><% end %> | |
| ERB | |
| puts " ✅ Created #{cart_view}" | |
| end | |
| # ----- Update home view with Facebook feed ----- | |
| home_view = "#{app_root}/app/views/home/index.html.erb" | |
| if File.exist?(home_view) | |
| new_content = <<~'ERB' | |
| <div class="container mx-auto px-4 py-6"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
| <div class="lg:col-span-2"> | |
| <div class="bg-white rounded-lg shadow mb-6 p-4" data-controller="post-form"> | |
| <%= form_with model: Post.new, url: posts_path, data: { turbo: true } do |f| %> | |
| <div class="flex items-start space-x-3"> | |
| <div class="flex-shrink-0"> | |
| <% if authenticated? && Current.user.avatar.attached? %> | |
| <%= image_tag Current.user.avatar, class: "h-10 w-10 rounded-full" %> | |
| <% else %> | |
| <div class="h-10 w-10 rounded-full bg-gray-300"></div> | |
| <% end %> | |
| </div> | |
| <div class="flex-1"> | |
| <%= f.text_area :content, placeholder: "What's on your mind?", rows: 2, class: "w-full border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-blue-500", data: { post_form_target: "content" } %> | |
| <div class="flex items-center justify-between mt-2"> | |
| <div class="flex items-center space-x-4"> | |
| <%= f.collection_select :community_id, Community.all, :id, :name, { include_blank: "Share to…" }, class: "text-sm border rounded px-2 py-1" %> | |
| <label class="inline-flex items-center text-sm"> | |
| <%= f.check_box :anonymous, data: { post_form_target: "anonymousCheckbox" } %> | |
| <span class="ml-2">Post anonymously</span> | |
| </label> | |
| </div> | |
| <%= f.submit "Post", class: "bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600" %> | |
| </div> | |
| </div> | |
| </div> | |
| <% end %> | |
| </div> | |
| <div id="feed"> | |
| <%= render @posts %> | |
| </div> | |
| </div> | |
| <div class="lg:col-span-1"> | |
| <div class="bg-white rounded-lg shadow p-4 sticky top-6"> | |
| <h3 class="font-bold text-lg mb-3">Communities</h3> | |
| <ul class="space-y-2"> | |
| <% @communities.each do |c| %> | |
| <li><%= link_to c.name, community_path(c), class: "text-blue-600 hover:underline" %></li> | |
| <% end %> | |
| </ul> | |
| <% if authenticated? %> | |
| <%= link_to "+ Create community", new_community_path, class: "mt-3 inline-block text-sm text-blue-600" %> | |
| <% end %> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ERB | |
| File.write(home_view, new_content, encoding: "UTF-8") | |
| puts " ✅ Updated home view for brgen feed" | |
| end | |
| # ----- Add post partial with anonymous support ----- | |
| post_partial = "#{app_root}/app/views/posts/_post.html.erb" | |
| if File.exist?(post_partial) | |
| content = File.read(post_partial, encoding: "UTF-8") | |
| unless content.include?("Anonymous") | |
| content.sub!(/<header>/, '<header class="flex items-center gap-2">') | |
| content.sub!(/<header.*>/, "\\0\n <% if post.anonymous? %>\n <span class=\"text-gray-500 text-sm\">Anonymous</span>\n <% else %>\n <%= link_to post.user.username, user_path(post.user), class: \"font-medium\" %>\n <% end %>") | |
| File.write(post_partial, content, encoding: "UTF-8") | |
| puts " ✅ Added anonymous support to post partial" | |
| end | |
| end | |
| # ----- Add create.turbo_stream.erb for live feed update ----- | |
| turbo_stream_view = "#{app_root}/app/views/posts/create.turbo_stream.erb" | |
| unless File.exist?(turbo_stream_view) | |
| File.write(turbo_stream_view, <<~'ERB') | |
| <%= turbo_stream.prepend "feed", partial: "posts/post", locals: { post: @post } %> | |
| <%= turbo_stream.update "post_form", "" do %> | |
| <%= javascript_tag "document.querySelector('[data-controller=\"post-form\"]').dispatchEvent(new CustomEvent('reset'))" %> | |
| <% end %> | |
| ERB | |
| puts " ✅ Created turbo stream for posts" | |
| end | |
| # ----- Update routes to mount Solidus and remove old marketplace ----- | |
| routes = "#{app_root}/config/routes.rb" | |
| if File.exist?(routes) | |
| content = File.read(routes, encoding: "UTF-8") | |
| # Remove old marketplace constraints block if present | |
| content.gsub!(/constraints\(subdomain: MARKETPLACE_SUBDOMAINS\) do.*?end/m, "") | |
| # Add Solidus mount if not present | |
| unless content.include?("mount Spree::Core::Engine") | |
| content.sub!(/^end$/, " mount Spree::Core::Engine, at: '/marketplace', as: 'spree'\nend") | |
| File.write(routes, content, encoding: "UTF-8") | |
| puts " ✅ Updated routes with Solidus mount" | |
| end | |
| end | |
| # ----- Add Solidus initializers ----- | |
| spree_init = "#{app_root}/config/initializers/spree.rb" | |
| unless File.exist?(spree_init) | |
| File.write(spree_init, <<~'RUBY') | |
| Spree.load_defaults '4.5' | |
| Spree.config do |config| | |
| config.currency = "NOK" | |
| config.shipping_instructions = true | |
| config.address_requires_state = false | |
| config.require_master_price = false | |
| config.admin_interface_v2 = true | |
| end | |
| Spree::Backend::Config.configure do |config| | |
| config.locale = 'en' | |
| end | |
| Spree.user_class = "User" | |
| Spree::Auth::Config.set(:signout_redirect_url, "/") | |
| Spree::Auth::Config.set(:registration_redirect_path, "/marketplace") | |
| Spree::Auth::Config.set(:login_redirect_path, "/marketplace") | |
| RUBY | |
| puts " ✅ Created #{spree_init}" | |
| end | |
| auth_init = "#{app_root}/config/initializers/solidus_auth.rb" | |
| unless File.exist?(auth_init) | |
| File.write(auth_init, <<~'RUBY') | |
| Devise.setup do |config| | |
| config.parent_controller = 'ApplicationController' | |
| config.scoped_views = true | |
| end | |
| Rails.application.config.to_prepare do | |
| Devise.mappings[:spree_user] = Devise.mappings[:user] | |
| Spree::User = User | |
| end | |
| Spree::Order.class_eval do | |
| def guest_token | |
| session[:guest_token] if session | |
| end | |
| def associate_user!(user, override_email = true) | |
| self.user = user | |
| self.email = user.email if override_email | |
| save! | |
| end | |
| end | |
| RUBY | |
| puts " ✅ Created #{auth_init}" | |
| end | |
| # ----- Add Reddit scraper rake task ----- | |
| scraper_task = "#{app_root}/lib/tasks/reddit_scraper.rake" | |
| unless File.exist?(scraper_task) | |
| FileUtils.mkdir_p("#{app_root}/lib/tasks") | |
| File.write(scraper_task, <<~'RUBY') | |
| namespace :reddit do | |
| desc "Scrape r/bergen posts using Reddit's JSON API and save as Solidus products" | |
| task scrape_bergen: :environment do | |
| require "open-uri" | |
| require "json" | |
| subreddit = "bergen" | |
| url = "https://www.reddit.com/r/#{subreddit}/new.json?limit=50" | |
| puts "🌐 Fetching r/#{subreddit}..." | |
| response = URI.open(url, "User-Agent" => "Brgen/1.0 (Rails 8 Scraper)").read | |
| data = JSON.parse(response) | |
| posts = data["data"]["children"] | |
| created = 0 | |
| posts.each do |child| | |
| post = child["data"] | |
| next if Spree::Product.exists?(meta_keywords: "reddit_#{post['id']}") | |
| product = Spree::Product.new( | |
| name: post["title"].truncate(255), | |
| description: post["selftext"].presence || "No description.", | |
| price: (post["score"] / 10.0).round(2), | |
| available_on: Time.at(post["created_utc"]), | |
| meta_keywords: "reddit_#{post['id']}", | |
| shipping_required: false, | |
| taxon_ids: [Spree::Taxon.first_or_create!(name: "Community Posts", taxonomy: Spree::Taxonomy.first_or_create!(name: "Categories")).id] | |
| ) | |
| product.master.sku = "RDDT-#{post['id']}" | |
| product.master.cost_price = 0.0 | |
| if product.save | |
| created += 1 | |
| puts "✅ Created product: #{post['title']}" | |
| else | |
| puts "❌ Failed: #{product.errors.full_messages.join(', ')}" | |
| end | |
| end | |
| puts "🎉 Done. Created #{created} products." | |
| end | |
| end | |
| RUBY | |
| puts " ✅ Created #{scraper_task}" | |
| end | |
| # ----- Delete obsolete marketplace custom files ----- | |
| obsolete_files = [ | |
| "#{app_root}/app/controllers/marketplace/cart_controller.rb", | |
| "#{app_root}/app/models/marketplace/listing.rb", | |
| "#{app_root}/app/models/marketplace/order.rb", | |
| "#{app_root}/app/views/marketplace/listings/index.html.erb" | |
| ] | |
| obsolete_files.each do |f| | |
| if File.exist?(f) | |
| File.delete(f) | |
| puts " ✅ Deleted obsolete #{f}" | |
| end | |
| end | |
| end | |
| if app == "bsdports" | |
| # Add ports import rake task | |
| ports_rake = "#{app_root}/lib/tasks/ports.rake" | |
| unless File.exist?(ports_rake) | |
| FileUtils.mkdir_p("#{app_root}/lib/tasks") | |
| File.write(ports_rake, <<~'RUBY') | |
| namespace :ports do | |
| desc "Import OpenBSD ports from official mirror" | |
| task import: :environment do | |
| require "open-uri" | |
| require "tmpdir" | |
| url = "https://cdn.openbsd.org/pub/OpenBSD/ports.tar.gz" | |
| Dir.mktmpdir do |dir| | |
| tgz = File.join(dir, "ports.tar.gz") | |
| puts "Downloading #{url}..." | |
| URI.open(url) { |f| File.write(tgz, f.read) } | |
| puts "Extracting..." | |
| system("tar xzf #{tgz} -C #{dir}") or raise "Extraction failed" | |
| ports_dir = File.join(dir, "ports") | |
| Dir.glob("#{ports_dir}/*/*/Makefile") do |makefile| | |
| category = makefile.split("/")[-3] | |
| name = makefile.split("/")[-2] | |
| comment = File.readlines(makefile).grep(/^COMMENT=/).first.to_s.gsub(/^COMMENT=\s*/, "").strip | |
| Port.find_or_create_by!(name: name, category: category) do |p| | |
| p.comment = comment | |
| p.version = "unknown" | |
| p.pkgpath = "#{category}/#{name}" | |
| end | |
| end | |
| puts "Imported #{Port.count} ports." | |
| end | |
| end | |
| end | |
| RUBY | |
| puts " ✅ Created #{ports_rake}" | |
| end | |
| end | |
| if app == "hjerterom" | |
| # ----- Add map controller for Mapbox ----- | |
| map_js = "#{app_root}/app/javascript/controllers/map_controller.js" | |
| unless File.exist?(map_js) | |
| File.write(map_js, <<~'JS') | |
| import { Controller } from "@hotwired/stimulus" | |
| import mapboxgl from "mapbox-gl" | |
| export default class extends Controller { | |
| static values = { accessToken: String, markers: Array, centerLat: { type: Number, default: 60.3913 }, centerLng: { type: Number, default: 5.3221 }, zoom: { type: Number, default: 12 } } | |
| connect() { | |
| mapboxgl.accessToken = this.accessTokenValue | |
| this.map = new mapboxgl.Map({ container: this.element, style: "mapbox://styles/mapbox/streets-v12", center: [this.centerLngValue, this.centerLatValue], zoom: this.zoomValue }) | |
| this.map.on("load", () => this.addMarkers()) | |
| } | |
| addMarkers() { | |
| this.markersValue.forEach(markerData => { | |
| if (!markerData.lat || !markerData.lng) return | |
| const popup = new mapboxgl.Popup({ offset: 25 }).setHTML(`<div class="p-2"><strong>${this.escapeHtml(markerData.title)}</strong><br><span class="text-sm text-gray-600">${markerData.type}</span><br><a href="${markerData.url}" class="text-blue-600 text-sm mt-1 inline-block">View details →</a></div>`) | |
| new mapboxgl.Marker().setLngLat([markerData.lng, markerData.lat]).setPopup(popup).addTo(this.map) | |
| }) | |
| } | |
| escapeHtml(str) { return str.replace(/[&<>]/g, m => ({ "&": "&", "<": "<", ">": ">" }[m])) } | |
| disconnect() { if (this.map) this.map.remove() } | |
| } | |
| JS | |
| puts " ✅ Created #{map_js}" | |
| end | |
| # ----- Update home view to use map ----- | |
| home_view = "#{app_root}/app/views/home/index.html.erb" | |
| if File.exist?(home_view) | |
| new_home = <<~'ERB' | |
| <div class="container mx-auto px-4 py-6"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
| <div class="lg:col-span-2"> | |
| <div class="bg-white rounded-lg shadow overflow-hidden" style="height: 70vh;"> | |
| <div data-controller="map" | |
| data-map-access-token-value="<%= ENV['MAPBOX_ACCESS_TOKEN'] %>" | |
| data-map-markers-value="<%= @markers.to_json %>" | |
| data-map-center-lat-value="60.3913" | |
| data-map-center-lng-value="5.3221" | |
| data-map-zoom-value="12" | |
| class="w-full h-full"></div> | |
| </div> | |
| </div> | |
| <div class="lg:col-span-1 space-y-6"> | |
| <div class="bg-white rounded-lg shadow p-4"> | |
| <h3 class="font-bold text-lg mb-2">How to help</h3> | |
| <p class="text-gray-600 mb-4">Find food or resources near you. Click on a marker for details.</p> | |
| <%= link_to "List food", new_food_listing_path, class: "block w-full text-center bg-green-600 text-white py-2 rounded-lg mb-2" if authenticated? %> | |
| <%= link_to "Add resource", new_resource_path, class: "block w-full text-center bg-blue-600 text-white py-2 rounded-lg" if authenticated? %> | |
| </div> | |
| <div class="bg-white rounded-lg shadow p-4"> | |
| <h3 class="font-bold text-lg mb-2">Crisis support</h3> | |
| <ul class="space-y-2"> | |
| <% Crisis.where(available_24h: true).limit(5).each do |c| %> | |
| <li><strong><%= c.title %></strong><br><%= c.phone %></li> | |
| <% end %> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ERB | |
| File.write(home_view, new_home, encoding: "UTF-8") | |
| puts " ✅ Updated home view with map for hjerterom" | |
| end | |
| # ----- Add geocoder initializer ----- | |
| geocoder_init = "#{app_root}/config/initializers/geocoder.rb" | |
| unless File.exist?(geocoder_init) | |
| File.write(geocoder_init, <<~'RUBY') | |
| Geocoder.configure( | |
| lookup: :nominatim, | |
| http_headers: { "User-Agent" => "Hjerterom App (contact@hjerterom.no)" }, | |
| timeout: 5 | |
| ) | |
| RUBY | |
| puts " ✅ Created #{geocoder_init}" | |
| end | |
| # ----- Update importmap to include mapbox-gl ----- | |
| importmap = "#{app_root}/config/importmap.rb" | |
| if File.exist?(importmap) | |
| content = File.read(importmap, encoding: "UTF-8") | |
| unless content.include?("mapbox-gl") | |
| content.sub!(/pin "application"/, "pin \"application\"\npin \"mapbox-gl\", to: \"https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl.js\"\npin \"mapbox-gl.css\", to: \"https://api.mapbox.com/mapbox-gl-js/v3.8.0/mapbox-gl.css\"") | |
| File.write(importmap, content, encoding: "UTF-8") | |
| puts " ✅ Added mapbox-gl to importmap" | |
| end | |
| end | |
| # ----- Precompile mapbox css ----- | |
| assets_init = "#{app_root}/config/initializers/assets.rb" | |
| if File.exist?(assets_init) | |
| content = File.read(assets_init, encoding: "UTF-8") | |
| unless content.include?("mapbox-gl.css") | |
| content << "\nRails.application.config.assets.precompile += %w( mapbox-gl.css )\n" | |
| File.write(assets_init, content, encoding: "UTF-8") | |
| puts " ✅ Added mapbox-gl.css to precompile" | |
| end | |
| end | |
| end | |
| end | |
| # ------------------------------------------------------------------------------ | |
| # 3. Add global seed scraper (LLM-assisted) rake task for brgen | |
| # ------------------------------------------------------------------------------ | |
| seed_scraper = "rails/brgen/lib/tasks/seed_scraper.rake" | |
| unless File.exist?(seed_scraper) | |
| FileUtils.mkdir_p(File.dirname(seed_scraper)) | |
| File.write(seed_scraper, <<~'RUBY') | |
| namespace :seed do | |
| desc "Scrape real data using Ferrum + LLM and insert into DB (brgen only)" | |
| task scrape: :environment do | |
| unless defined?(Brgen) || Rails.root.basename.to_s == "brgen" | |
| puts "This task is intended for brgen app only." | |
| next | |
| end | |
| require_relative "../../app/services/scrape" | |
| url = ENV["SEED_URL"] || "https://news.ycombinator.com" | |
| schema = ["title", "url", "score", "author"] | |
| hint = "Extract the top 30 posts. Return JSON with items array." | |
| puts "Scraping #{url}..." | |
| result = Scrape.call(url, schema: schema, hint: hint) | |
| if result["items"].is_a?(Array) | |
| result["items"].each do |item| | |
| Post.create!( | |
| title: item["title"], | |
| content: "Source: #{item['url']}\nScore: #{item['score']}", | |
| anonymous: true, | |
| user: User.guest.first || User.create!(email_address: "scraper@example.com", password: "scraper123", guest: true) | |
| ) | |
| end | |
| puts "Created #{result['items'].size} posts from scraper." | |
| else | |
| puts "No items found or invalid response." | |
| end | |
| end | |
| end | |
| RUBY | |
| puts " ✅ Created global seed scraper rake task" | |
| end | |
| # ------------------------------------------------------------------------------ | |
| # 4. Final instructions | |
| # ------------------------------------------------------------------------------ | |
| puts <<~MSG | |
| ✅ Patch applied successfully! | |
| Next steps: | |
| 1. Run `bundle install` in each app (rails/{amber,baibl,blognet,brgen,bsdports,hjerterom}) | |
| 2. Run `bin/rails solidus:install` inside brgen (answer Y to auth, choose starter frontend, skip payment) | |
| 3. Set environment variable MAPBOX_ACCESS_TOKEN for hjerterom | |
| 4. Set OPENROUTER_API_KEY if using LLM scraper | |
| 5. Run migrations: `bin/rails db:migrate` in each app | |
| 6. Seed data: `RAILS_ENV=development SEED_REAL=true bin/rails db:seed` (optional) | |
| 7. Test Reddit scraper: `cd rails/brgen && bin/rails reddit:scrape_bergen` | |
| Your apps now have shared engine, Facebook feed, map frontpage, Solidus marketplace, and r/bergen scraper. | |
| MSG |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment