chore: init commit
in worker.../repo/GITFOLDER.zip is the .git folder.
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ClassroomsHelper
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrdersHelper
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SchoolsHelper
|
||||
end
|
||||
Reference in New Issue
Block a user