chore: init commit

in worker.../repo/GITFOLDER.zip is the .git folder.
This commit is contained in:
2026-08-11 14:44:09 -04:00
parent 0012380fd3
commit 2854619bc9
782 changed files with 65944 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
# frozen_string_literal: true
module Admin
module FormHelper
# Creates a form using the Admin::FormBuilder
# Usage: admin_form_for @resource do |f|
# f.text_field :name
# f.email_field :email
# f.submit_button
# end
def admin_form_for(record, options = {}, &)
options[:builder] = Admin::FormBuilder
options[:html] ||= {}
options[:html][:class] = "space-y-6 #{options[:html][:class]}"
form_with(model: record, scope: model_scope(record), url: form_url(record), **options, &)
end
private
# Determines the model scope for the form
def model_scope(record)
return record.first.model_name.param_key if record.is_a?(Array)
record.model_name.param_key
end
# Determines the form URL based on the record (handles both new and existing records)
def form_url(record)
if record.is_a?(Array)
resource = record.last
if resource.persisted?
[:admin, resource]
else
record.first.is_a?(Symbol) ? [record.first, :admin, resource] : [:admin, resource]
end
else
resource = record
resource.persisted? ? [:admin, resource] : [:admin, resource.class.model_name.collection]
end
end
end
end

View File

@@ -0,0 +1,250 @@
# frozen_string_literal: true
# rubocop:disable Metrics/ModuleLength
module AdminHelper
# Renders a table for index pages with sortable columns
# @param collection [ActiveRecord::Relation] The records to display
# @param columns [Array<Hash>] Column definitions with :attribute, :label, :sortable keys
# @param options [Hash] Additional options for the table
def admin_table(collection, columns: [], **options)
title = options.delete(:title)
actions = options.delete(:actions)
render "admin/shared/table",
collection: collection,
columns: columns,
options: options,
title: title,
actions: actions
end
# Renders attribute rows for show pages
# @param resource [ActiveRecord::Base] The record to display
# @param attributes [Array<Symbol>] Attributes to display
def admin_show_attributes(resource, attributes: [])
render "admin/shared/show_attributes", resource: resource, attributes: attributes
end
# Renders breadcrumbs for navigation
# @param breadcrumbs [Array<Hash>] Breadcrumb items with :label and :path keys
def admin_breadcrumbs(breadcrumbs = [])
render "admin/shared/breadcrumbs", breadcrumbs: breadcrumbs
end
# Renders action buttons (Edit, Delete, Custom)
# @param resource [ActiveRecord::Base] The record for actions
# @param actions [Array<Symbol>] Actions to include (:edit, :delete, :custom)
# @param custom_actions [Array<Hash>] Custom action definitions
def admin_actions(resource, actions: %i[edit delete], custom_actions: [])
render "admin/shared/actions", resource: resource, actions: actions, custom_actions: custom_actions
end
# Formats an attribute value for display
# @param resource [ActiveRecord::Base] The record
# @param attribute [Symbol] The attribute name
# @return [String] Formatted value
def format_attribute(resource, attribute)
value = resource.send(attribute)
case value
when TrueClass, FalseClass
boolean_badge(value)
when Time, DateTime, Date
value.strftime("%B %d, %Y")
when ActiveRecord::Base
format_association(value)
when nil
content_tag(:span, "—", class: "text-gray-400")
else
value.to_s
end
end
# Renders a boolean badge
# @param value [Boolean] The boolean value
# @return [String] HTML badge
def boolean_badge(value)
if value
content_tag(
:span, "Yes",
class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"
)
else
content_tag(
:span, "No",
class: "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800"
)
end
end
# Generates a sort link for table headers
# @param column [Symbol] The column name
# @param label [String] The display label
# @return [String] HTML link
def sort_link(column, label)
direction = params[:sort] == column.to_s && params[:direction] == "asc" ? "desc" : "asc"
icon = sort_icon(column)
# Build URL with query parameters
url = url_for(sort: column, direction: direction, only_path: true)
link_to url, class: "group inline-flex items-center" do
safe_join(
[
label,
content_tag(
:span, icon,
class: "ml-2 flex-none rounded text-gray-900 group-hover:text-gray-900"
)
]
)
end
end
# Returns the sort icon for a column
# @param column [Symbol] The column name
# @return [String] Icon HTML
def sort_icon(column)
if params[:sort] == column.to_s
params[:direction] == "asc" ? "↑" : "↓"
else
"⇅"
end
end
# Renders search and filter form
# @param filters [Array<Hash>] Filter definitions with :name, :label, :options keys
# @param search_placeholder [String] Placeholder text for search field
def admin_search_filter(filters: [], search_placeholder: "Search...")
render "admin/shared/search_filter", filters: filters, search_placeholder: search_placeholder
end
# Determines the current discard filter state based on query parameters
# @return [Symbol] :active, :discarded, or :all
def current_discard_filter
if params[:discarded].present?
:discarded
elsif params[:all].present?
:all
else
:active
end
end
# Returns the correct model for routing purposes
# Handles STI (Single Table Inheritance) by returning the base class
# @param record [ActiveRecord::Base] The record
# @return [ActiveRecord::Base] The record or its base class for routing
def route_model(record)
if record.class.base_class == record.class
record
else
record.becomes(record.class.base_class)
end
end
# Renders the archive/activate toggle button for a classroom
# @param classroom [Classroom] The classroom record
# @return [String] HTML button
def classroom_archive_toggle_button(classroom)
if classroom.archived?
activate_button(classroom)
else
archive_button(classroom)
end
end
# Renders the discard/restore action button for a soft-deletable resource
# @param resource [ActiveRecord::Base] The resource record (must respond_to :discarded?)
# @return [String] HTML button or link
def discard_restore_action(resource)
if resource.discarded?
restore_button(resource)
else
discard_link(resource)
end
end
# Returns the appropriate show path for a user based on their type
# @param user [User] The user record
# @return [String] Path to the type-specific show page
def user_show_path(user)
case user.type
when "Student"
admin_student_path(user)
when "Teacher"
admin_teacher_path(user)
else
admin_user_path(user)
end
end
private
def format_association(value)
# Use presenter if available, otherwise fall back to to_s
presenter_class = "#{value.class.name}Presenter".safe_constantize
display_value = if presenter_class
presenter_class.new(value).display_name
else
value.to_s
end
# Try to link to the resource, but fall back to text if route doesn't exist
begin
link_to display_value, [:admin, route_model(value)]
rescue NoMethodError, ActionController::UrlGenerationError
display_value
end
end
def restore_button(resource)
resource_name = resource.class.name.underscore
restore_path = send("restore_admin_#{resource_name}_path", resource)
button_to "Restore", restore_path,
method: :patch,
data: { turbo_confirm: "Are you sure you want to restore this #{resource_name.humanize.downcase}?" },
class: "text-green-600 hover:text-green-800",
form: { style: "display: inline;" }
end
def discard_link(resource)
resource_name = resource.class.name.underscore
resource_path = send("admin_#{resource_name}_path", resource)
link_to "Archive", resource_path,
data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to archive this #{resource_name.humanize.downcase}?" }, # rubocop:disable Layout/LineLength
class: "text-red-600 hover:text-red-800"
end
def activate_button(classroom)
button_class = "inline-flex items-center px-4 py-2 border border-green-300 shadow-sm " \
"text-sm font-medium rounded-md text-green-700 bg-white hover:bg-green-50"
link_to toggle_archive_admin_classroom_path(classroom),
data: { turbo_method: :patch, turbo_confirm: "Are you sure you want to activate this classroom?" },
class: button_class do
safe_join(
[
content_tag(:i, "", class: "fas fa-check-circle -ml-1 mr-2 h-5 w-5 text-green-500"),
"Activate"
]
)
end
end
def archive_button(classroom)
button_class = "inline-flex items-center px-4 py-2 border border-yellow-300 shadow-sm " \
"text-sm font-medium rounded-md text-yellow-700 bg-white hover:bg-yellow-50"
link_to toggle_archive_admin_classroom_path(classroom),
data: { turbo_method: :patch, turbo_confirm: "Are you sure you want to archive this classroom?" },
class: button_class do
safe_join(
[
content_tag(:i, "", class: "fas fa-archive -ml-1 mr-2 h-5 w-5 text-yellow-500"),
"Archive"
]
)
end
end
end
# rubocop:enable Metrics/ModuleLength

View File

@@ -0,0 +1,53 @@
# frozen_string_literal: true
module ApplicationHelper
def ticker_stocks
Stock.active.order(:ticker)
end
def format_money(cents)
format("$%.2f", cents / 100.0)
end
def safe_url(url)
uri = URI.parse(url)
%w[http https].include?(uri.scheme) ? url : nil
rescue URI::InvalidURIError
nil
end
# Generates a sort link for table headers
# @param column [Symbol] The column name
# @param label [String] The display label
# @return [String] HTML link
def sort_link(column, label)
direction = params[:sort] == column.to_s && params[:direction] == "asc" ? "desc" : "asc"
icon = sort_icon(column)
# Build URL with query parameters
url = url_for(sort: column, direction: direction, only_path: true)
link_to url, class: "group inline-flex items-center" do
safe_join(
[
label,
content_tag(
:span, icon,
class: "ml-2 flex-none rounded text-gray-900 group-hover:text-gray-900"
)
]
)
end
end
# Returns the sort icon for a column
# @param column [Symbol] The column name
# @return [String] Icon HTML
def sort_icon(column)
if params[:sort] == column.to_s
params[:direction] == "asc" ? "↑" : "↓"
else
"⇅"
end
end
end

View File

@@ -0,0 +1,4 @@
# frozen_string_literal: true
module ClassroomsHelper
end

View File

@@ -0,0 +1,35 @@
# frozen_string_literal: true
module Components
module ButtonHelper
def render_button(label = "", text: nil, variant: :default, as: :button, href: nil, data: {}, **options, &block)
base_classes = base_class_styling
variant_classes = case variant.to_sym
when :default
" bg-primary text-primary-foreground hover:bg-primary/90 "
when :secondary
" bg-secondary text-secondary-foreground hover:bg-secondary/80 "
when :error, :danger, :alert, :destructive
" bg-destructive text-destructive-foreground hover:bg-destructive/90 "
when :outline
" border border-input bg-background hover:bg-accent hover:text-accent-foreground"
when :ghost
" hover:bg-accent hover:text-accent-foreground "
end
button_classes = "#{base_classes} #{variant_classes} #{options[:class]}"
button_classes = tw(button_classes)
text = label if label.present?
text = capture(&block) if block
render "components/ui/button", text:, button_classes:, as:, href:, data:, **options
end
private
def base_class_styling
" inline-flex items-center justify-center rounded-md text-sm font-medium " \
"ring-offset-background transition-colors focus-visible:outline-hidden " \
"focus-visible:ring-2 focus-visible:ring-3 focus-visible:ring-offset-2 " \
"disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2 "
end
end
end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module Components
module CheckboxHelper
def render_checkbox(label:, name:, **options)
render "components/ui/checkbox", name: name, label: label, options: options
end
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module Components
module FormsHelper
def render_form_with(**, &)
form_with(**, builder: Shadcn::FormBuilder, &)
end
def render_form_for(obj, **opts, &)
form_for(obj, **opts, builder: Shadcn::FormBuilder, html: opts, &)
end
end
end

View File

@@ -0,0 +1,55 @@
# frozen_string_literal: true
module Components
module InputHelper
def render_input(name:, label: false, id: nil, type: :text, value: nil, **options)
options[:class] = border_styling(options[:class])
options[:class] << case options[:variant]
when :borderless
borderless_variant_styling
else
border_variant_default_styling
end
options[:class] = tw(options[:class])
options.reverse_merge!(
label: options[:lable] || false,
required: options[:required] || false,
disabled: options[:disabled] || false,
readonly: options[:readonly] || false,
placeholder: options[:placeholder] || "",
autocomplete: options[:autocomplete] || "",
autocapitalize: options[:autocapitalize] || nil,
autocorrect: options[:autocorrect] || nil
)
render partial: "components/ui/input", locals: {
type:,
label:,
name:,
value:,
id:,
options: options
}
end
private
def border_styling(options_class)
"flex h-10 w-full rounded-md border border-input bg-background px-3 " \
"py-2 text-sm transition-colors ring-offset-background file:border-0 " \
"file:bg-transparent file:text-sm file:font-medium " \
"placeholder:text-muted-foreground disabled:cursor-not-allowed " \
"disabled:opacity-50 #{options_class} "
end
def borderless_variant_styling
" border-0 focus-visible:outline-hidden focus-visible:shadow-none focus-visible:ring-transparent"
end
def border_variant_default_styling
"shadow-xs focus-visible:outline-hidden focus-visible:ring-2a " \
"focus-visible:ring-3 focus-visible:ring-offset-2 focus-visible:border-muted"
end
end
end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module Components
module LabelHelper
def render_label(name:, label:, **options)
render "components/ui/label", name: name, label: label, options: options
end
end
end

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
module Components
module TextareaHelper
def render_textarea(name:, label: false, id: nil, value: nil, **options)
options.reverse_merge!(
rows: 3, required: false, disabled: false,
readonly: false, class: "", label: false, placeholder: "Type here..."
)
render partial: "components/ui/textarea", locals: {
label:,
name:,
value:,
id:,
options: options
}
end
end
end

View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
require "tailwind_merge"
module ComponentsHelper
def tw(*classes)
TailwindMerge::Merger.new.merge(classes.join(" "))
end
PRIMARY_CLASSES = " bg-primary text-primary-foreground hover:bg-primary/80 "
SECONDARY_CLASSES = " bg-secondary text-secondary-foreground hover:bg-secondary/80 "
OUTLINE_CLASSES = " border border-input bg-background hover:bg-accent hover:text-accent-foreground "
GHOST_CLASSES = " hover:bg-accent hover:text-accent-foreground "
DESTRUCTIVE_CLASSES = " bg-destructive text-destructive-foreground hover:bg-destructive/90 "
module Button
PRIMARY = ComponentsHelper::PRIMARY_CLASSES
SECONDARY = ComponentsHelper::SECONDARY_CLASSES
OUTLINE = ComponentsHelper::OUTLINE_CLASSES
GHOST = ComponentsHelper::GHOST_CLASSES
DESTRUCTIVE = ComponentsHelper::DESTRUCTIVE_CLASSES
end
module Alert
end
end

View File

@@ -0,0 +1,4 @@
# frozen_string_literal: true
module OrdersHelper
end

View File

@@ -0,0 +1,4 @@
# frozen_string_literal: true
module SchoolsHelper
end