iron-cms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +174 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/builds/iron.css +2 -0
  6. data/app/assets/config/iron_manifest.js +1 -0
  7. data/app/assets/images/iron/icons/sprite.svg +9425 -0
  8. data/app/assets/tailwind/iron/actiontext.css +550 -0
  9. data/app/assets/tailwind/iron/application.css +66 -0
  10. data/app/assets/tailwind/iron/components/button.css +74 -0
  11. data/app/assets/tailwind/iron/components/dropdown.css +42 -0
  12. data/app/assets/tailwind/iron/components/fieldset.css +18 -0
  13. data/app/assets/tailwind/iron/components/form.css +14 -0
  14. data/app/assets/tailwind/iron/components/icon-picker.css +9 -0
  15. data/app/assets/tailwind/iron/components/input.css +44 -0
  16. data/app/assets/tailwind/iron/components/page.css +7 -0
  17. data/app/assets/tailwind/iron/components/select.css +36 -0
  18. data/app/assets/tailwind/iron/components/sidebar.css +21 -0
  19. data/app/assets/tailwind/iron/components/textarea.css +27 -0
  20. data/app/controllers/concerns/iron/authentication.rb +66 -0
  21. data/app/controllers/concerns/iron/authorization.rb +11 -0
  22. data/app/controllers/concerns/iron/locale_aware.rb +20 -0
  23. data/app/controllers/concerns/iron/web_page.rb +18 -0
  24. data/app/controllers/iron/application_controller.rb +7 -0
  25. data/app/controllers/iron/block_definitions/field_definitions_controller.rb +8 -0
  26. data/app/controllers/iron/block_definitions_controller.rb +52 -0
  27. data/app/controllers/iron/blocks_controller.rb +12 -0
  28. data/app/controllers/iron/content_types/field_definitions_controller.rb +8 -0
  29. data/app/controllers/iron/content_types_controller.rb +52 -0
  30. data/app/controllers/iron/entries_controller.rb +103 -0
  31. data/app/controllers/iron/field_definitions_controller.rb +55 -0
  32. data/app/controllers/iron/first_runs_controller.rb +29 -0
  33. data/app/controllers/iron/home_controller.rb +6 -0
  34. data/app/controllers/iron/icons_controller.rb +10 -0
  35. data/app/controllers/iron/locales_controller.rb +52 -0
  36. data/app/controllers/iron/passwords_controller.rb +36 -0
  37. data/app/controllers/iron/references_controller.rb +11 -0
  38. data/app/controllers/iron/schemas_controller.rb +32 -0
  39. data/app/controllers/iron/sessions_controller.rb +32 -0
  40. data/app/controllers/iron/settings_controller.rb +23 -0
  41. data/app/controllers/iron/users_controller.rb +66 -0
  42. data/app/helpers/iron/application_helper.rb +25 -0
  43. data/app/helpers/iron/avatar_helper.rb +35 -0
  44. data/app/helpers/iron/block_helper.rb +4 -0
  45. data/app/helpers/iron/buttons_helper.rb +14 -0
  46. data/app/helpers/iron/components/badge_helper.rb +70 -0
  47. data/app/helpers/iron/components/dropdown_helper.rb +161 -0
  48. data/app/helpers/iron/content_types_helper.rb +11 -0
  49. data/app/helpers/iron/entries_helper.rb +13 -0
  50. data/app/helpers/iron/field_definitions_helper.rb +5 -0
  51. data/app/helpers/iron/form_builder.rb +43 -0
  52. data/app/helpers/iron/icons_helper.rb +30 -0
  53. data/app/helpers/iron/image_helper.rb +50 -0
  54. data/app/javascript/iron/application.js +6 -0
  55. data/app/javascript/iron/controllers/application.js +9 -0
  56. data/app/javascript/iron/controllers/element_controller.js +11 -0
  57. data/app/javascript/iron/controllers/file_upload_controller.js +18 -0
  58. data/app/javascript/iron/controllers/form_controller.js +7 -0
  59. data/app/javascript/iron/controllers/handle_controller.js +27 -0
  60. data/app/javascript/iron/controllers/icon_picker_controller.js +48 -0
  61. data/app/javascript/iron/controllers/index.js +4 -0
  62. data/app/javascript/iron/controllers/sortable_list_controller.js +125 -0
  63. data/app/javascript/iron/controllers/toggle_controller.js +14 -0
  64. data/app/javascript/iron/controllers/trix_controller.js +19 -0
  65. data/app/javascript/iron/lib/lexorank.js +51 -0
  66. data/app/jobs/iron/application_job.rb +4 -0
  67. data/app/jobs/iron/generate_entry_routes_job.rb +15 -0
  68. data/app/mailers/iron/application_mailer.rb +6 -0
  69. data/app/mailers/passwords_mailer.rb +6 -0
  70. data/app/models/concerns/iron/csv_serializable.rb +28 -0
  71. data/app/models/iron/account/joinable.rb +16 -0
  72. data/app/models/iron/account.rb +13 -0
  73. data/app/models/iron/application_record.rb +5 -0
  74. data/app/models/iron/block_definition/portable.rb +20 -0
  75. data/app/models/iron/block_definition.rb +10 -0
  76. data/app/models/iron/content_type/field_queryable.rb +101 -0
  77. data/app/models/iron/content_type/portable.rb +30 -0
  78. data/app/models/iron/content_type/titlable.rb +13 -0
  79. data/app/models/iron/content_type/web_publishable.rb +47 -0
  80. data/app/models/iron/content_type.rb +39 -0
  81. data/app/models/iron/content_types/collection.rb +6 -0
  82. data/app/models/iron/content_types/single.rb +9 -0
  83. data/app/models/iron/current.rb +12 -0
  84. data/app/models/iron/entry/deep_validation.rb +32 -0
  85. data/app/models/iron/entry/presentable.rb +40 -0
  86. data/app/models/iron/entry/schemable.rb +16 -0
  87. data/app/models/iron/entry/titlable.rb +35 -0
  88. data/app/models/iron/entry/web_publishable.rb +85 -0
  89. data/app/models/iron/entry.rb +16 -0
  90. data/app/models/iron/field/belongs_to_entry.rb +29 -0
  91. data/app/models/iron/field.rb +20 -0
  92. data/app/models/iron/field_definition/portable.rb +36 -0
  93. data/app/models/iron/field_definition.rb +42 -0
  94. data/app/models/iron/field_definitions/block.rb +23 -0
  95. data/app/models/iron/field_definitions/block_list.rb +8 -0
  96. data/app/models/iron/field_definitions/boolean.rb +7 -0
  97. data/app/models/iron/field_definitions/date.rb +7 -0
  98. data/app/models/iron/field_definitions/file.rb +67 -0
  99. data/app/models/iron/field_definitions/number.rb +7 -0
  100. data/app/models/iron/field_definitions/reference.rb +8 -0
  101. data/app/models/iron/field_definitions/reference_list.rb +8 -0
  102. data/app/models/iron/field_definitions/rich_text_area.rb +4 -0
  103. data/app/models/iron/field_definitions/text_area.rb +7 -0
  104. data/app/models/iron/field_definitions/text_field.rb +25 -0
  105. data/app/models/iron/fields/block.rb +40 -0
  106. data/app/models/iron/fields/block_list.rb +16 -0
  107. data/app/models/iron/fields/boolean.rb +7 -0
  108. data/app/models/iron/fields/date.rb +7 -0
  109. data/app/models/iron/fields/file.rb +29 -0
  110. data/app/models/iron/fields/number.rb +7 -0
  111. data/app/models/iron/fields/reference.rb +14 -0
  112. data/app/models/iron/fields/reference_list.rb +17 -0
  113. data/app/models/iron/fields/rich_text_area.rb +9 -0
  114. data/app/models/iron/fields/text_area.rb +7 -0
  115. data/app/models/iron/fields/text_field.rb +19 -0
  116. data/app/models/iron/first_run.rb +13 -0
  117. data/app/models/iron/icon_catalog.rb +45 -0
  118. data/app/models/iron/locale.rb +30 -0
  119. data/app/models/iron/reference.rb +8 -0
  120. data/app/models/iron/schema_archive.rb +71 -0
  121. data/app/models/iron/schema_exporter.rb +15 -0
  122. data/app/models/iron/schema_importer.rb +278 -0
  123. data/app/models/iron/session.rb +5 -0
  124. data/app/models/iron/user/role.rb +21 -0
  125. data/app/models/iron/user.rb +37 -0
  126. data/app/views/active_storage/blobs/_blob.html.erb +14 -0
  127. data/app/views/iron/block_definitions/_block_definition.html.erb +21 -0
  128. data/app/views/iron/block_definitions/_form.html.erb +46 -0
  129. data/app/views/iron/block_definitions/edit.html.erb +8 -0
  130. data/app/views/iron/block_definitions/index.html.erb +18 -0
  131. data/app/views/iron/block_definitions/new.html.erb +8 -0
  132. data/app/views/iron/block_definitions/show.html.erb +34 -0
  133. data/app/views/iron/blocks/new.turbo_stream.erb +6 -0
  134. data/app/views/iron/content_types/_content_type.html.erb +21 -0
  135. data/app/views/iron/content_types/_form.html.erb +137 -0
  136. data/app/views/iron/content_types/edit.html.erb +8 -0
  137. data/app/views/iron/content_types/index.html.erb +28 -0
  138. data/app/views/iron/content_types/new.html.erb +8 -0
  139. data/app/views/iron/content_types/show.html.erb +49 -0
  140. data/app/views/iron/entries/_entry.html.erb +17 -0
  141. data/app/views/iron/entries/_entry_option.html.erb +19 -0
  142. data/app/views/iron/entries/_form.html.erb +63 -0
  143. data/app/views/iron/entries/edit.html.erb +52 -0
  144. data/app/views/iron/entries/entry.html.erb +21 -0
  145. data/app/views/iron/entries/fields/_block.html.erb +92 -0
  146. data/app/views/iron/entries/fields/_block_list.html.erb +45 -0
  147. data/app/views/iron/entries/fields/_boolean.html.erb +13 -0
  148. data/app/views/iron/entries/fields/_date.html.erb +11 -0
  149. data/app/views/iron/entries/fields/_file.html.erb +60 -0
  150. data/app/views/iron/entries/fields/_number.html.erb +11 -0
  151. data/app/views/iron/entries/fields/_reference.html.erb +12 -0
  152. data/app/views/iron/entries/fields/_reference_item.html.erb +28 -0
  153. data/app/views/iron/entries/fields/_reference_list.html.erb +44 -0
  154. data/app/views/iron/entries/fields/_rich_text_area.html.erb +13 -0
  155. data/app/views/iron/entries/fields/_text_area.html.erb +11 -0
  156. data/app/views/iron/entries/fields/_text_field.html.erb +28 -0
  157. data/app/views/iron/entries/index.html.erb +21 -0
  158. data/app/views/iron/entries/new.html.erb +8 -0
  159. data/app/views/iron/entries/search.html.erb +33 -0
  160. data/app/views/iron/field_definitions/_field_definition.html.erb +37 -0
  161. data/app/views/iron/field_definitions/block/_form.html.erb +16 -0
  162. data/app/views/iron/field_definitions/block_list/_form.html.erb +13 -0
  163. data/app/views/iron/field_definitions/boolean/_form.html.erb +2 -0
  164. data/app/views/iron/field_definitions/date/_form.html.erb +2 -0
  165. data/app/views/iron/field_definitions/edit.html.erb +16 -0
  166. data/app/views/iron/field_definitions/file/_form.html.erb +40 -0
  167. data/app/views/iron/field_definitions/index.html.erb +14 -0
  168. data/app/views/iron/field_definitions/layouts/_form.html.erb +47 -0
  169. data/app/views/iron/field_definitions/new.html.erb +22 -0
  170. data/app/views/iron/field_definitions/number/_form.html.erb +2 -0
  171. data/app/views/iron/field_definitions/reference/_form.html.erb +13 -0
  172. data/app/views/iron/field_definitions/reference_list/_form.html.erb +13 -0
  173. data/app/views/iron/field_definitions/rich_text_area/_form.html.erb +2 -0
  174. data/app/views/iron/field_definitions/text_area/_form.html.erb +2 -0
  175. data/app/views/iron/field_definitions/text_field/_form.html.erb +17 -0
  176. data/app/views/iron/first_runs/show.html.erb +44 -0
  177. data/app/views/iron/home/show.html.erb +2 -0
  178. data/app/views/iron/icons/index.html.erb +21 -0
  179. data/app/views/iron/locales/_form.html.erb +24 -0
  180. data/app/views/iron/locales/_locale.html.erb +15 -0
  181. data/app/views/iron/locales/edit.html.erb +8 -0
  182. data/app/views/iron/locales/index.html.erb +12 -0
  183. data/app/views/iron/locales/new.html.erb +8 -0
  184. data/app/views/iron/passwords/edit.html.erb +7 -0
  185. data/app/views/iron/passwords/new.html.erb +26 -0
  186. data/app/views/iron/passwords_mailer/reset.html.erb +4 -0
  187. data/app/views/iron/passwords_mailer/reset.text.erb +2 -0
  188. data/app/views/iron/published_pages/show.html.erb +10 -0
  189. data/app/views/iron/references/new.turbo_stream.erb +6 -0
  190. data/app/views/iron/schemas/new.html.erb +57 -0
  191. data/app/views/iron/sessions/new.html.erb +36 -0
  192. data/app/views/iron/settings/show.html.erb +38 -0
  193. data/app/views/iron/shared/_icon_picker.html.erb +60 -0
  194. data/app/views/iron/users/_form.html.erb +21 -0
  195. data/app/views/iron/users/_user.html.erb +27 -0
  196. data/app/views/iron/users/edit.html.erb +12 -0
  197. data/app/views/iron/users/index.html.erb +21 -0
  198. data/app/views/iron/users/new.html.erb +29 -0
  199. data/app/views/iron/users/show.html.erb +21 -0
  200. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  201. data/app/views/layouts/iron/_sidebar.html.erb +83 -0
  202. data/app/views/layouts/iron/_sidebar_item.html.erb +14 -0
  203. data/app/views/layouts/iron/_toast.html.erb +29 -0
  204. data/app/views/layouts/iron/application.html.erb +46 -0
  205. data/app/views/layouts/iron/authentication.html.erb +41 -0
  206. data/config/importmap.rb +9 -0
  207. data/config/routes.rb +40 -0
  208. data/db/migrate/20250422131656_initial_iron_schema.rb +105 -0
  209. data/db/migrate/20250427100754_create_iron_accounts.rb +10 -0
  210. data/db/migrate/20250427104020_add_role_to_iron_users.rb +5 -0
  211. data/db/migrate/20250428094923_add_default_iron_locale_to_iron_account.rb +5 -0
  212. data/db/migrate/20250504144731_create_join_table_content_types_field_definitions.rb +25 -0
  213. data/db/migrate/20250505090716_add_title_field_definition_to_iron_content_types.rb +5 -0
  214. data/db/migrate/20250512210352_create_iron_references.rb +11 -0
  215. data/db/migrate/20250524220355_add_referenced_entry_id_to_iron_fields.rb +5 -0
  216. data/db/migrate/20250529211703_add_metadata_to_iron_field_definitions.rb +5 -0
  217. data/db/migrate/20250529211704_add_value_boolean_to_iron_fields.rb +5 -0
  218. data/db/migrate/20250531172837_add_type_to_iron_content_types.rb +5 -0
  219. data/db/migrate/20250601080146_add_icon_to_iron_content_types.rb +5 -0
  220. data/db/migrate/20250609091605_add_web_publishing_to_iron_content_types.rb +7 -0
  221. data/db/migrate/20250609091813_add_route_to_iron_entries.rb +6 -0
  222. data/lib/generators/iron/pages/pages_generator.rb +34 -0
  223. data/lib/generators/iron/pages/templates/pages_controller.rb +14 -0
  224. data/lib/generators/iron/pages/templates/show.html.erb +9 -0
  225. data/lib/generators/iron/template/template_generator.rb +35 -0
  226. data/lib/generators/iron/template/templates/content_type.html.erb +5 -0
  227. data/lib/iron/cva.rb +69 -0
  228. data/lib/iron/engine.rb +42 -0
  229. data/lib/iron/image_analyzer.rb +26 -0
  230. data/lib/iron/lexorank/exceptions.rb +4 -0
  231. data/lib/iron/lexorank/rankable.rb +185 -0
  232. data/lib/iron/lexorank/rebalance_rank_job.rb +10 -0
  233. data/lib/iron/lexorank/utils.rb +77 -0
  234. data/lib/iron/lexorank.rb +7 -0
  235. data/lib/iron/routing.rb +66 -0
  236. data/lib/iron/sdk.rb +19 -0
  237. data/lib/iron/version.rb +3 -0
  238. data/lib/iron.rb +8 -0
  239. data/lib/puma/plugin/iron_tailwindcss.rb +78 -0
  240. metadata +458 -0
@@ -0,0 +1,29 @@
1
+ module Iron
2
+ class FirstRunsController < ApplicationController
3
+ layout "iron/authentication"
4
+
5
+ allow_unauthenticated_access
6
+
7
+ before_action :prevent_running_after_setup
8
+
9
+ def show
10
+ @user = User.new
11
+ end
12
+
13
+ def create
14
+ user = FirstRun.create!(user_params)
15
+ start_new_session_for user
16
+
17
+ redirect_to root_url
18
+ end
19
+
20
+ private
21
+ def prevent_running_after_setup
22
+ redirect_to root_url if User.any?
23
+ end
24
+
25
+ def user_params
26
+ params.require(:user).permit(:email_address, :password)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ module Iron
2
+ class HomeController < ApplicationController
3
+ def show
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,10 @@
1
+ module Iron
2
+ class IconsController < ApplicationController
3
+ def index
4
+ @selected = params[:selected]
5
+ @icons = IconCatalog.all
6
+
7
+ render layout: false
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,52 @@
1
+ module Iron
2
+ class LocalesController < ApplicationController
3
+ before_action :set_locale, only: %i[ edit update destroy ]
4
+ before_action :ensure_can_administer
5
+
6
+ def index
7
+ @locales = Locale.all
8
+ end
9
+
10
+ def new
11
+ @locale = Locale.new
12
+ end
13
+
14
+ def edit
15
+ end
16
+
17
+ def create
18
+ @locale = Locale.new(locale_params)
19
+
20
+ if @locale.save
21
+ redirect_to locales_path, notice: "Locale was successfully created."
22
+ else
23
+ render :new, status: :unprocessable_entity
24
+ end
25
+ end
26
+
27
+ def update
28
+ if @locale.update(locale_params)
29
+ redirect_to locales_path, notice: "Locale was successfully updated.", status: :see_other
30
+ else
31
+ render :edit, status: :unprocessable_entity
32
+ end
33
+ end
34
+
35
+ def destroy
36
+ if @locale.destroy
37
+ redirect_to locales_path, notice: "Locale was successfully destroyed.", status: :see_other
38
+ else
39
+ redirect_back fallback_location: locales_path, alert: @locale.errors.full_messages.to_sentence, status: :see_other
40
+ end
41
+ end
42
+
43
+ private
44
+ def set_locale
45
+ @locale = Locale.find(params.expect(:id))
46
+ end
47
+
48
+ def locale_params
49
+ params.expect(locale: [ :code, :name ])
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,36 @@
1
+ module Iron
2
+ class PasswordsController < ApplicationController
3
+ layout "iron/authentication"
4
+ allow_unauthenticated_access
5
+ before_action :set_user_by_token, only: %i[ edit update ]
6
+
7
+ def new
8
+ end
9
+
10
+ def create
11
+ if user = User.find_by(email_address: params[:email_address])
12
+ PasswordsMailer.reset(user).deliver_later
13
+ end
14
+
15
+ redirect_to new_session_url, notice: "Password reset instructions sent (if user with that email address exists)."
16
+ end
17
+
18
+ def edit
19
+ end
20
+
21
+ def update
22
+ if @user.update(params.permit(:password, :password_confirmation))
23
+ redirect_to new_session_url, notice: "Password has been reset."
24
+ else
25
+ redirect_to edit_password_url(params[:token]), alert: "Passwords did not match."
26
+ end
27
+ end
28
+
29
+ private
30
+ def set_user_by_token
31
+ @user = User.find_by_password_reset_token!(params[:token])
32
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
33
+ redirect_to new_password_url, alert: "Password reset link is invalid or has expired."
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ module Iron
2
+ class ReferencesController < ApplicationController
3
+ def new
4
+ @form_field_name = params[:form_field_name]
5
+ @reference_list_dom_id = params[:reference_list_dom_id]
6
+
7
+ @entry = Entry.find(params[:entry_id])
8
+ @reference = Reference.build(entry: @entry)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ module Iron
2
+ class SchemasController < ApplicationController
3
+ def export
4
+ send_data SchemaExporter.export,
5
+ filename: "iron-schema-#{Date.current}.tar",
6
+ type: "application/x-tar",
7
+ disposition: "attachment"
8
+ end
9
+
10
+ def new
11
+ @import_modes = SchemaImporter::IMPORT_MODES
12
+ end
13
+
14
+ def import
15
+ file = params[:schema_file]
16
+ mode = params[:import_mode]
17
+
18
+ unless file.present?
19
+ redirect_to schema_path, alert: "Please select a file to import"
20
+ return
21
+ end
22
+
23
+ result = SchemaImporter.import(file, mode:)
24
+
25
+ if result.success?
26
+ redirect_to content_types_path, notice: "Schema imported successfully"
27
+ else
28
+ redirect_to schema_path, alert: result.errors.join(". ")
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ module Iron
2
+ class SessionsController < ApplicationController
3
+ layout "iron/authentication"
4
+ allow_unauthenticated_access only: %i[ new create ]
5
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to sign_in_url, alert: "Try again later." }
6
+
7
+ before_action :ensure_user_exists, only: :new
8
+
9
+ def new
10
+ end
11
+
12
+ def create
13
+ if user = User.authenticate_by(params.permit(:email_address, :password))
14
+ start_new_session_for user
15
+ redirect_to after_authentication_url
16
+ else
17
+ redirect_to sign_in_url, alert: "Try another email address or password."
18
+ end
19
+ end
20
+
21
+ def destroy
22
+ terminate_session
23
+ redirect_to sign_in_url
24
+ end
25
+
26
+ private
27
+
28
+ def ensure_user_exists
29
+ redirect_to first_run_url if User.none?
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ module Iron
2
+ class SettingsController < ApplicationController
3
+ before_action :ensure_can_administer
4
+
5
+ def show
6
+ @account = Current.account
7
+ @locales = Locale.all
8
+ end
9
+
10
+ def update
11
+ if Current.account.update(account_params)
12
+ redirect_back fallback_location: settings_path, notice: "Settings updated successfully."
13
+ else
14
+ render :edit, status: :unprocessable_entity
15
+ end
16
+ end
17
+
18
+ private
19
+ def account_params
20
+ params.require(:account).permit(:name, :default_locale_id)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,66 @@
1
+ module Iron
2
+ class UsersController < ApplicationController
3
+ layout "iron/authentication", only: %i[ new create ]
4
+ require_unauthenticated_access only: %i[ new create ]
5
+
6
+ before_action :set_user, only: %i[ show update destroy ]
7
+ before_action :verify_join_code, only: %i[ new create ]
8
+ before_action :ensure_can_administer, except: %i[ new create ]
9
+
10
+ def index
11
+ @users = User.all
12
+ end
13
+
14
+ def new
15
+ @user = User.new
16
+ end
17
+
18
+ def show
19
+ end
20
+
21
+ def create
22
+ @user = User.new(join_params)
23
+ if @user.save
24
+ start_new_session_for @user
25
+ redirect_to root_url
26
+ else
27
+ render :new, status: :unprocessable_entity
28
+ end
29
+ rescue ActiveRecord::RecordNotUnique
30
+ redirect_to sign_in_url(email_address: join_params[:email_address])
31
+ end
32
+
33
+ def update
34
+ if @user.update(update_params)
35
+ redirect_back fallback_location: users_path, notice: "User was successfully updated.", status: :see_other
36
+ else
37
+ redirect_back fallback_location: users_path, alert: @user.errors.full_messages.to_sentence, status: :see_other
38
+ end
39
+ end
40
+
41
+ def destroy
42
+ if @user.destroy
43
+ redirect_back fallback_location: users_path, notice: "User was successfully removed.", status: :see_other
44
+ else
45
+ redirect_back fallback_location: users_path, alert: @user.errors.full_messages.to_sentence, status: :see_other
46
+ end
47
+ end
48
+
49
+ private
50
+ def set_user
51
+ @user = User.find(params.expect(:id))
52
+ end
53
+
54
+ def join_params
55
+ params.expect(user: [ :email_address, :password, :password_confirmation ])
56
+ end
57
+
58
+ def update_params
59
+ params.expect(user: [ :email_address, :role ])
60
+ end
61
+
62
+ def verify_join_code
63
+ head :not_found if Current.account.join_code != params[:join_code]
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,25 @@
1
+ require "tailwind_merge"
2
+ module Iron
3
+ module ApplicationHelper
4
+ include IconsHelper, AvatarHelper, ButtonsHelper, EntriesHelper, ContentTypesHelper
5
+ include Components::DropdownHelper, Components::BadgeHelper
6
+
7
+ def tw(*classes)
8
+ TailwindMerge::Merger.new.merge(classes.reject(&:blank?).compact.join(" "))
9
+ end
10
+
11
+ def cva(base_classes = "", options = {})
12
+ Cva::VariantBuilder.new(
13
+ base_classes: base_classes,
14
+ variants: options[:variants] || {},
15
+ defaults: options[:defaults] || {},
16
+ compound_variants: options[:compound_variants] || [],
17
+ required: options[:required] || []
18
+ )
19
+ end
20
+
21
+ def unique_id
22
+ rand(10000000..100000000)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,35 @@
1
+ module Iron
2
+ module AvatarHelper
3
+ def avatar(user, options = {})
4
+ variant = options.delete(:variant) || "circle"
5
+ alt = options.delete(:alt) || ""
6
+ css_class = tw(
7
+ options.delete(:class),
8
+ # Basic layout
9
+ "inline-grid shrink-0 align-middle [--avatar-radius:20%] [--ring-opacity:20%] *:col-start-1 *:row-start-1",
10
+ "outline outline-1 -outline-offset-1 outline-black/(--ring-opacity) dark:outline-white/(--ring-opacity)",
11
+ # Add the correct border radius
12
+ variant == "square" ? "rounded-(--avatar-radius) *:rounded-(--avatar-radius)" : "rounded-full *:rounded-full"
13
+ )
14
+
15
+ tag.span class: css_class, data: { slot: "avatar" } do
16
+ tag.svg class: "size-full select-none fill-current p-[5%] text-[48px] font-medium uppercase",
17
+ viewbox: "0 0 100 100",
18
+ "aria-hidden": alt.present? ? nil : true do
19
+ safe_join([
20
+ alt.present? ? tag.title(alt) : nil,
21
+ tag.text(
22
+ user.initials,
23
+ x: "50%",
24
+ y: "50%",
25
+ "alignment-baseline": "middle",
26
+ "dominant-baseline": "middle",
27
+ "text-anchor": "middle",
28
+ dy: ".125em",
29
+ )
30
+ ])
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,4 @@
1
+ module Iron
2
+ module BlockHelper
3
+ end
4
+ end
@@ -0,0 +1,14 @@
1
+ module Iron::ButtonsHelper
2
+ def back_button_to(title, url, options = {}, &)
3
+ link_to url,
4
+ class: tw(
5
+ "inline-flex items-center gap-2 text-sm/6 text-stone-500 dark:text-stone-400",
6
+ options.delete(:class)
7
+ ) do
8
+ safe_join([
9
+ icon("chevron-left", class: "size-4"),
10
+ tag.span(title)
11
+ ])
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,70 @@
1
+ module Iron
2
+ module Components::BadgeHelper
3
+ def badge_class(**options)
4
+ @badge_variants ||= cva(
5
+ # Base classes
6
+ "inline-flex items-center gap-x-1.5 rounded-md px-1.5 py-0.5 text-sm/5 font-medium sm:text-xs/5 forced-colors:outline",
7
+ variants: {
8
+ color: {
9
+ red: "bg-red-500/15 text-red-700 group-hover:bg-red-500/25 dark:bg-red-500/10 dark:text-red-400 dark:group-hover:bg-red-500/20",
10
+ orange: "bg-orange-500/15 text-orange-700 group-hover:bg-orange-500/25 dark:bg-orange-500/10 dark:text-orange-400 dark:group-hover:bg-orange-500/20",
11
+ amber: "bg-amber-400/20 text-amber-700 group-hover:bg-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400 dark:group-hover:bg-amber-400/15",
12
+ yellow: "bg-yellow-400/20 text-yellow-700 group-hover:bg-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:group-hover:bg-yellow-400/15",
13
+ lime: "bg-lime-400/20 text-lime-700 group-hover:bg-lime-400/30 dark:bg-lime-400/10 dark:text-lime-300 dark:group-hover:bg-lime-400/15",
14
+ green: "bg-green-500/15 text-green-700 group-hover:bg-green-500/25 dark:bg-green-500/10 dark:text-green-400 dark:group-hover:bg-green-500/20",
15
+ emerald: "bg-emerald-500/15 text-emerald-700 group-hover:bg-emerald-500/25 dark:bg-emerald-500/10 dark:text-emerald-400 dark:group-hover:bg-emerald-500/20",
16
+ teal: "bg-teal-500/15 text-teal-700 group-hover:bg-teal-500/25 dark:bg-teal-500/10 dark:text-teal-300 dark:group-hover:bg-teal-500/20",
17
+ cyan: "bg-cyan-400/20 text-cyan-700 group-hover:bg-cyan-400/30 dark:bg-cyan-400/10 dark:text-cyan-300 dark:group-hover:bg-cyan-400/15",
18
+ sky: "bg-sky-500/15 text-sky-700 group-hover:bg-sky-500/25 dark:bg-sky-500/10 dark:text-sky-300 dark:group-hover:bg-sky-500/20",
19
+ blue: "bg-blue-500/15 text-blue-700 group-hover:bg-blue-500/25 dark:text-blue-400 dark:group-hover:bg-blue-500/25",
20
+ indigo: "bg-indigo-500/15 text-indigo-700 group-hover:bg-indigo-500/25 dark:text-indigo-400 dark:group-hover:bg-indigo-500/20",
21
+ violet: "bg-violet-500/15 text-violet-700 group-hover:bg-violet-500/25 dark:text-violet-400 dark:group-hover:bg-violet-500/20",
22
+ purple: "bg-purple-500/15 text-purple-700 group-hover:bg-purple-500/25 dark:text-purple-400 dark:group-hover:bg-purple-500/20",
23
+ fuchsia: "bg-fuchsia-400/15 text-fuchsia-700 group-hover:bg-fuchsia-400/25 dark:bg-fuchsia-400/10 dark:text-fuchsia-400 dark:group-hover:bg-fuchsia-400/20",
24
+ pink: "bg-pink-400/15 text-pink-700 group-hover:bg-pink-400/25 dark:bg-pink-400/10 dark:text-pink-400 dark:group-hover:bg-pink-400/20",
25
+ rose: "bg-rose-400/15 text-rose-700 group-hover:bg-rose-400/25 dark:bg-rose-400/10 dark:text-rose-400 dark:group-hover:bg-rose-400/20",
26
+ stone: "bg-stone-600/10 text-stone-700 group-hover:bg-stone-600/20 dark:bg-white/5 dark:text-stone-400 dark:group-hover:bg-white/10"
27
+ }
28
+ },
29
+ defaults: {
30
+ color: :stone
31
+ }
32
+ )
33
+ @badge_variants.call(options)
34
+ end
35
+
36
+ def badge(content = nil, **options, &block)
37
+ css_class = options.delete(:class)
38
+
39
+ tag.span class: tw(
40
+ badge_class(**options),
41
+ css_class
42
+ ), **options do
43
+ block_given? ? capture(&block) : content
44
+ end
45
+ end
46
+
47
+ def badge_button_to(content = nil, url = nil, **options, &block)
48
+ css_class = options.delete(:class)
49
+
50
+ base_class = tw(
51
+ "group relative inline-flex rounded-md focus:outline-hidden focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500",
52
+ css_class
53
+ )
54
+
55
+ if url.nil?
56
+ button_tag class: base_class, **options do
57
+ tag.div class: "btn-touch-target" do
58
+ badge(content || capture(&block), **options)
59
+ end
60
+ end
61
+ else
62
+ link_to url, class: base_class, **options do
63
+ tag.div class: "btn-touch-target" do
64
+ badge(content || capture(&block), **options)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,161 @@
1
+ # module Iron
2
+ # module Components::DropdownHelper
3
+ # def dropdown_divider
4
+ # tag.div role: "separator", class: "col-span-full mx-3.5 my-1 h-px border-0 bg-stone-950/5 sm:mx-3 dark:bg-white/10 forced-colors:bg-[CanvasText]"
5
+ # end
6
+ # end
7
+ # end
8
+
9
+ module Iron
10
+ module Components::DropdownHelper
11
+ # Main dropdown container
12
+ def dropdown_menu(options = {}, &block)
13
+ # Initialize storage for different parts of the dropdown
14
+ @dropdown_id = options[:id] || "dropdown-#{SecureRandom.hex(4)}"
15
+ @dropdown_trigger = nil
16
+ @dropdown_content = nil
17
+
18
+ # Capture the nested content
19
+ capture(&block)
20
+
21
+ # Build the complete dropdown
22
+ content_tag(:div, class: options[:class]) do
23
+ safe_join([
24
+ @dropdown_trigger,
25
+ @dropdown_content
26
+ ].compact)
27
+ end
28
+ end
29
+
30
+ # Button that toggles the dropdown
31
+ def dropdown_menu_trigger(options = {}, &block)
32
+ css_class = options.delete(:class)
33
+ trigger_content = capture(&block)
34
+
35
+ @dropdown_trigger = content_tag(:button,
36
+ trigger_content,
37
+ id: "#{@dropdown_id}-trigger",
38
+ popovertarget: @dropdown_id,
39
+ class: tw("inline-flex items-center justify-center gap-2", css_class),
40
+ style: "anchor-name: --#{@dropdown_id}-trigger;",
41
+ **options
42
+ )
43
+
44
+ nil # Return nil to avoid content being rendered twice
45
+ end
46
+
47
+ def dropdown_menu_content(options = {}, &block)
48
+ css_class = options.delete(:class)
49
+ width = options[:width] || "16rem"
50
+ align = options[:align] || "end" # Default to end alignment
51
+ side = options[:side]
52
+
53
+ positioning = "position-anchor: --#{@dropdown_id}-trigger;"
54
+
55
+ # Add alignment to positioning
56
+ positioning += " position-align: #{align};" if align.present?
57
+
58
+ # Add side to positioning if provided
59
+ positioning += " position-side: #{side};" if side.present?
60
+
61
+ content = capture(&block)
62
+
63
+ @dropdown_content = content_tag(:div,
64
+ content,
65
+ id: @dropdown_id,
66
+ popover: "",
67
+ class: tw("dropdown", css_class),
68
+ style: "#{positioning} --popover-width: #{width};",
69
+ **options
70
+ )
71
+
72
+ nil # Return nil to avoid content being rendered twice
73
+ end
74
+
75
+ # Individual dropdown menu item
76
+ def dropdown_menu_item(*args, &block)
77
+ options = args.extract_options!
78
+
79
+ # Determine what the remaining args are
80
+ text = args[0]
81
+ url = args[1]
82
+
83
+ # Extract common options
84
+ css_class = options.delete(:class)
85
+ icon_name = options.delete(:icon)
86
+ method = options.delete(:method)
87
+
88
+ item_content = if block_given?
89
+ capture(&block)
90
+ elsif icon_name.present?
91
+ safe_join([
92
+ icon(icon_name, variant: :micro, data: { slot: "icon" }),
93
+ content_tag(:div, text, data: { slot: "label" })
94
+ ])
95
+ else
96
+ text
97
+ end
98
+
99
+ if url.nil? && method.present?
100
+ # Button form
101
+ button_tag(item_content,
102
+ type: "button",
103
+ class: tw("dropdown-item w-full text-left", css_class),
104
+ **options
105
+ )
106
+ elsif method.present?
107
+ # Button_to form
108
+ button_to(url.to_s,
109
+ method: method,
110
+ class: tw("dropdown-item w-full", css_class),
111
+ **options
112
+ ) { item_content }
113
+ elsif url.present?
114
+ # Link form
115
+ link_to(url.to_s,
116
+ class: tw("dropdown-item", css_class),
117
+ **options
118
+ ) { item_content }
119
+ else
120
+ # Plain item
121
+ content_tag(:div, item_content,
122
+ class: tw("dropdown-item", css_class),
123
+ **options
124
+ )
125
+ end
126
+ end
127
+
128
+ # Label for dropdown sections
129
+ def dropdown_menu_label(text = nil, options = {}, &block)
130
+ css_class = options.delete(:class)
131
+
132
+ content = block_given? ? capture(&block) : text
133
+
134
+ content_tag(:div, content,
135
+ class: tw("px-3.5 py-2 text-sm font-semibold text-stone-950 dark:text-white sm:px-3", css_class),
136
+ **options
137
+ )
138
+ end
139
+
140
+ # Divider between dropdown items
141
+ def dropdown_menu_separator(options = {})
142
+ css_class = options.delete(:class)
143
+
144
+ content_tag(:div, nil,
145
+ role: "separator",
146
+ class: tw("dropdown-divider", css_class),
147
+ **options
148
+ )
149
+ end
150
+
151
+ # Group of related dropdown items
152
+ def dropdown_menu_group(options = {}, &block)
153
+ css_class = options.delete(:class)
154
+
155
+ content_tag(:div, capture(&block),
156
+ class: tw("p-1", css_class),
157
+ **options
158
+ )
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,11 @@
1
+ module Iron
2
+ module ContentTypesHelper
3
+ def content_type_sidebar_url(content_type)
4
+ if content_type.single?
5
+ entry_content_type_path(content_type)
6
+ else
7
+ content_type_entries_path(content_type)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module Iron::EntriesHelper
2
+ def entry_fields_for(field, prefix, &)
3
+ fields_for "#{prefix}[fields_attributes][#{field.id || unique_id}]", field, &
4
+ end
5
+
6
+ def current_content_page?(content_type)
7
+ request.path.start_with?(content_type_entries_path(content_type)) || (
8
+ controller_name == "entries" &&
9
+ %w[show edit].include?(action_name) &&
10
+ Iron::Entry.find(params[:id]).content_type == content_type
11
+ )
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module Iron::FieldDefinitionsHelper
2
+ def field_definition_type_options
3
+ Iron::FieldDefinition::TYPES.map { |type| [ type.humanize, type ] }
4
+ end
5
+ end