Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ DATABASE_PASSWORD=password
DATABASE_NAME=r_solutions_development
DATABASE_PORT=5432
REDIS_URL=redis://solutions_redis:6379/12
APP_HOST=localhost
APP_HOST=localhost:5250
RECAPTCHA_SITE_KEY=foocvi0PSRc_AyhjP7jcN
RECAPTCHA_SECRET_KEY=bar8saabKym-6u5KGh
VIOLET_SERVICE_TIMEOUT=500
Expand Down
6 changes: 6 additions & 0 deletions app/controllers/comfy/admin/sales_collateral_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ def dashboard
@website = host
@email = email_resolved_for_apex
end

def generate
subdomain = Subdomain.current
subdomain.generate_default_sales_assets
redirect_to sales_collateral_path
end
end
175 changes: 175 additions & 0 deletions app/models/comfy/cms/page.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# frozen_string_literal: true

class Comfy::Cms::Page < ActiveRecord::Base

self.table_name = "comfy_cms_pages"

include Comfy::Cms::WithFragments
include Comfy::Cms::WithCategories

cms_acts_as_tree counter_cache: :children_count, order: :position
cms_has_revisions_for :fragments_attributes

attr_accessor :content

# -- Relationships -----------------------------------------------------------
belongs_to :site
belongs_to :target_page,
class_name: "Comfy::Cms::Page",
optional: true

has_many :translations,
dependent: :destroy

# -- Callbacks ---------------------------------------------------------------
before_validation :assigns_label,
:assign_parent,
:escape_slug,
:assign_full_path
before_create :assign_position
after_save :sync_child_full_paths!
after_find :unescape_slug_and_path

# -- Validations -------------------------------------------------------------
validates :label,
presence: true
validates :slug,
presence: true,
uniqueness: { scope: :parent_id },
unless: ->(p) {
p.site && (p.site.pages.count.zero? || p.site.pages.root == self)
}
validate :validate_target_page
validate :validate_format_of_unescaped_slug

# -- Scopes ------------------------------------------------------------------
scope :published, -> { where(is_published: true) }

# -- Violet customizations ------
has_one_attached :og_image
has_one_attached :story_post
has_one_attached :landscape_post

# -- Class Methods -----------------------------------------------------------
# Tree-like structure for pages
def self.options_for_select(site:, current_page: nil, exclude_self: false)
options = []

options_for_page = ->(page, depth = 0) do
return if page.nil?
return if exclude_self && page == current_page

options << ["#{'. . ' * depth}#{page.label}", page.id]

page.children.order(:position).each do |child_page|
options_for_page.call(child_page, depth + 1)
end
end

options_for_page.call(site.pages.root)

options
end

# -- Instance Methods --------------------------------------------------------
# For previewing purposes sometimes we need to have full_path set. This
# full path take care of the pages and its childs but not of the site path
def full_path
read_attribute(:full_path) || assign_full_path
end

# Somewhat unique method of identifying a page that is not a full_path
def identifier
parent_id.blank? ? "index" : full_path[1..-1].parameterize
end

# Full url for a page
def url(relative: false)
[site.url(relative: relative), full_path].compact.join
end

# This method will mutate page object by transfering attributes from translation
# for a given locale.
def translate!
# If site locale same as page's or there's no translastions, we do nothing
if site.locale == I18n.locale.to_s || translations.blank?
return
end

translation = translations.published.find_by!(locale: I18n.locale)
self.layout = translation.layout
self.label = translation.label
self.content_cache = translation.content_cache

# We can't just assign fragments as it's a relation and will write to DB
# This has odd side-effect of preserving page's fragments and just replacing
# them from the translation. Not an issue if all fragments match.
self.fragments_attributes = translation.fragments_attributes
readonly!

self
end

protected

def assigns_label
self.label = label.blank? ? slug.try(:titleize) : label
end

def assign_parent
return unless site
self.parent ||= site.pages.root unless self == site.pages.root || site.pages.count.zero?
end

def assign_full_path
self.full_path =
if self.parent
[CGI.escape(self.parent.full_path).gsub("%2F", "/"), slug].join("/").squeeze("/")
else
"/"
end
end

def assign_position
return unless self.parent
return if position.to_i.positive?
max = self.parent.children.maximum(:position)
self.position = max ? max + 1 : 0
end

def validate_target_page
return unless target_page
p = self
while p.target_page
if (p = p.target_page) == self
return errors.add(:target_page_id, "Invalid Redirect")
end
end
end

def validate_format_of_unescaped_slug
return unless slug.present?
unescaped_slug = CGI.unescape(slug)
errors.add(:slug, :invalid) unless unescaped_slug =~ %r{^\p{Alnum}[\.\p{Alnum}\p{Mark}_-]*$}i
end

# Forcing re-saves for child pages so they can update full_paths
def sync_child_full_paths!
return unless full_path_previously_changed?
children.each do |p|
p.update_attribute(:full_path, p.send(:assign_full_path))
end
end

# Escape slug unless it's nonexistent (root)
def escape_slug
self.slug = CGI.escape(slug) unless slug.nil?
end

# Unescape the slug and full path back into their original forms unless they're nonexistent
def unescape_slug_and_path
self.slug = CGI.unescape(slug) unless slug.nil?
self.full_path = CGI.unescape(full_path) unless full_path.nil?
end

end
72 changes: 72 additions & 0 deletions app/models/subdomain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ class Subdomain < ApplicationRecord
has_one_attached :favicon
has_one_attached :og_image
has_one_attached :qr_code
has_one_attached :story_post
has_one_attached :landscape_post
has_one_attached :square_post


has_rich_text :email_signature


Expand Down Expand Up @@ -219,6 +224,73 @@ def puppeteer_capture
end
end

def generate_default_sales_assets
# Get the host
subdomain = Apartment::Tenant.current
subdomain_resolved_for_apex = subdomain == 'public' || subdomain == 'root' ? "http://#{ENV['APP_HOST']}" : "http://#{subdomain}.#{ENV['APP_HOST']}"
url = subdomain_resolved_for_apex
Puppeteer.launch(headless: true, args: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage']) do |browser|
page = browser.new_page
page.goto(url)
page.viewport = Puppeteer::Viewport.new(width: 1080, height: 1920)
screenshot_settings = {
type: 'jpeg',
quality: ENV["PUPPETEER_SCREENSHOT_QUALITY"] ? ENV["PUPPETEER_SCREENSHOT_QUALITY"].to_i : 50
}
# optioanlly remove Violet Cookie banner
dimensions = page.evaluate(<<~JAVASCRIPT)
() => {

// Source - https://stackoverflow.com/a
// Posted by Johan Dettmar, modified by community. See post 'Timeline' for change history
// Retrieved 2025-11-12, License - CC BY-SA 4.0

Element.prototype.remove = function() {
this.parentElement.removeChild(this);
}
NodeList.prototype.remove = HTMLCollection.prototype.remove = function() {
for(var i = this.length - 1; i >= 0; i--) {
if(this[i] && this[i].parentElement) {
this[i].parentElement.removeChild(this[i]);
}
}
}
document.getElementById("cookie-consent-wrapper").remove();
}
JAVASCRIPT

screenshot = page.screenshot(**screenshot_settings)
binary_output = screenshot
self.story_post.attach(
io: StringIO.new(binary_output),
filename: "story.jpeg",
content_type: "image/jpeg",
identify: false
)

page.viewport = Puppeteer::Viewport.new(width: 1920, height: 1080)
screenshot = page.screenshot(**screenshot_settings)
binary_output = screenshot
self.landscape_post.attach(
io: StringIO.new(binary_output),
filename: "landscape.jpeg",
content_type: "image/jpeg",
identify: false
)

page.viewport = Puppeteer::Viewport.new(width: 1080, height: 1080)
screenshot = page.screenshot(**screenshot_settings)
binary_output = screenshot
self.square_post.attach(
io: StringIO.new(binary_output),
filename: "square.jpeg",
content_type: "image/jpeg",
identify: false
)
browser.close
end
end

private

def change_2fa_setting
Expand Down
49 changes: 48 additions & 1 deletion app/views/comfy/admin/sales_collateral/dashboard.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
.d-flex.my-2.justify-content-between.align-items-center
.h2.mb-0
Sales Collateral
= link_to 'Sales Collateral Builder - coming soon', sales_collateral_path, role: 'button', class: 'btn btn-primary'
.d-flex
= link_to 'Sales Collateral Builder - coming soon', sales_collateral_path, role: 'button', class: 'btn btn-primary m-2'
= link_to 'Refresh', sales_collateral_generate_path, role: 'button', class: 'btn btn-secondary m-2'

.card
.card-header
Expand Down Expand Up @@ -72,6 +74,51 @@
.m-2
= image_tag(Subdomain.current.qr_code)



.card.my-5
.card-header
%strong
Outbound
.card-body
%p.m-2
Apex
#accordion
.card
#headingOne.card-header
%h5.mb-0
%button.btn.btn-link{"aria-controls" => "collapseOne", "aria-expanded" => "false", "data-target" => "#collapseOne", "data-toggle" => "collapse"}
Square Post:
#collapseOne.collapse.show{"aria-labelledby" => "headingOne", "data-parent" => "#accordion"}
.card-body
.container
.img-fluid
= image_tag(Subdomain.current.square_post) if Subdomain.current.square_post.attached?
.card
#headingTwo.card-header
%h5.mb-0
%button.btn.btn-link.collapsed{"aria-controls" => "collapseTwo", "aria-expanded" => "false", "data-target" => "#collapseTwo", "data-toggle" => "collapse"}
Story Post:
#collapseTwo.collapse{"aria-labelledby" => "headingTwo", "data-parent" => "#accordion"}
.card-body
.container
.img-fluid
= image_tag(Subdomain.current.story_post) if Subdomain.current.story_post.attached?
.card
#headingThree.card-header
%h5.mb-0
%button.btn.btn-link.collapsed{"aria-controls" => "collapseThree", "aria-expanded" => "false", "data-target" => "#collapseThree", "data-toggle" => "collapse"}
Landscape Post:
#collapseThree.collapse{"aria-labelledby" => "headingThree", "data-parent" => "#accordion"}
.card-body
.container
.img-fluid
= image_tag(Subdomain.current.landscape_post) if Subdomain.current.landscape_post.attached?
%p.m-2
CMS Pages
- Comfy::Cms::Page.all.each do |page|
= page.full_path

.card.my-5
.card-header
%strong
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def self.matches?(request)
get 'v2/dashboard', to: 'comfy/admin/v2/dashboard#dashboard'

get 'sales_collateral', to: 'comfy/admin/sales_collateral#dashboard'
get 'sales_collateral/generate', to: 'comfy/admin/sales_collateral#generate'
get "/sales_collateral_builder", to: redirect("/motionity/src/index.html")

# video calling, lock down new/create actions-- and allow public show action
get 'rooms', to: 'comfy/admin/rooms#new'
Expand Down
2 changes: 2 additions & 0 deletions public/motionity/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
21 changes: 21 additions & 0 deletions public/motionity/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Alyssa X

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading