diff --git a/app/assets/stylesheets/admin.bootstrap.scss b/app/assets/stylesheets/admin.bootstrap.scss index b8fe9689..871486a2 100644 --- a/app/assets/stylesheets/admin.bootstrap.scss +++ b/app/assets/stylesheets/admin.bootstrap.scss @@ -1,4 +1,3 @@ - @import 'admin/components/colors.scss'; @import 'admin/components/buttons.scss'; @@ -8,3 +7,7 @@ // Custom @import 'admin/header.scss'; + +// Slim Select custom +@import 'slim-select/dist/slimselect.css'; +@import 'admin/custom_slim_select.scss'; diff --git a/app/assets/stylesheets/admin/custom_slim_select.scss b/app/assets/stylesheets/admin/custom_slim_select.scss new file mode 100644 index 00000000..ef87cb4c --- /dev/null +++ b/app/assets/stylesheets/admin/custom_slim_select.scss @@ -0,0 +1,36 @@ +.ss-content { + padding-right: 0.75rem; + background-image: none; + + .ss-list { + height: 150px; + + .ss-option { + color: var(--bs-body-color); + + &:not(.ss-disabled) { + &.ss-selected { + background-color: $primary; + } + + &:first-child { + color: var(--bs-secondary-color); + } + } + + &:hover { + color: var(--bs-body-color); + background-color: $primary; + } + &:first-child { + &.ss-selected { + background-color: $primary-bg-subtle; + } + } + } + } + + .ss-search input:focus { + box-shadow: $box-shadow-sm; + } +} diff --git a/app/controllers/admin/events_controller.rb b/app/controllers/admin/events_controller.rb index 1f612adf..cb7dcade 100644 --- a/app/controllers/admin/events_controller.rb +++ b/app/controllers/admin/events_controller.rb @@ -16,9 +16,9 @@ def create @event = Event.create(event_params) if @event.save - redirect_to admin_events_path, notice: 'Event has been created successfully' + redirect_to edit_admin_event_path(@event), notice: 'Event was successfully created' else - render :new, alert: 'Please review' + render :new, status: :unprocessable_entity end end @@ -30,9 +30,9 @@ def update render_not_found unless @event if @event.update(event_params) - redirect_to admin_events_path + redirect_to admin_events_path, notice: 'Event was successfully updated' else - render :edit + render :edit, status: :unprocessable_entity, notice: 'Error updating event' end end @@ -46,6 +46,34 @@ def destroy end end + #GET /admin/events/set_talk/1 + def set_talk + @talk = Talk.new + @url = generate_talk_admin_events_path(talk_id: 'create_talk') + + return unless params[:talk_id] != 'new_talk' + talk_id = params[:talk_id] + @talk = Talk.new(event_params['talks_attributes'][talk_id].except(:_destroy)) + @url = generate_talk_admin_events_path(talk_id: talk_id) + + end + + #POST /admin/events/get_talk/1 + def generate_talk + @talk = Talk.new(talk_params) + talk_id = @talk.object_id + + talk_id = params[:talk_id] if params[:talk_id] != 'create_talk' + @talk.id = params[:talk_id] if params[:talk_id] != 'create_talk' + + if @talk.valid? + render 'admin/events/form/_card_talk_new', locals: { talk: @talk, talk_id: talk_id } + else + @url = generate_talk_admin_events_path(talk_id: talk_id) + render :set_talk, status: :bad_request + end + end + private def authorize_event @@ -53,17 +81,30 @@ def authorize_event end def set_event - @event = Event.includes(:talks).find_by(id: params[:id]) + @event = Event.includes(talks: :speaker).find_by(id: params[:id]) end def event_params params.require(:event).permit( + :id, :title, :location, :description, :date, :type, :panel_video_link, + talks_attributes: Talk.attribute_names.map(&:to_sym).push(:_destroy), + ) + end + + def talk_params + params.require(:talk).permit( + :talk_description, + :talk_title, + :talk_video_link, + :speaker_id, + :event_id, + :_destroy, ) end end diff --git a/app/controllers/admin/speakers_controller.rb b/app/controllers/admin/speakers_controller.rb index abcb7e11..596c930a 100644 --- a/app/controllers/admin/speakers_controller.rb +++ b/app/controllers/admin/speakers_controller.rb @@ -6,7 +6,7 @@ class SpeakersController < AdminController # GET /admin/speakers def index - @speakers = Speaker.ordered_by_name + @speakers = Speaker.all end # GET /admin/speakers/1 diff --git a/app/javascript/admin.js b/app/javascript/admin.js index b895ce1b..188d979f 100644 --- a/app/javascript/admin.js +++ b/app/javascript/admin.js @@ -3,3 +3,6 @@ import './controllers'; import * as bootstrap from 'bootstrap'; import '@hotwired/turbo-rails'; +import './turbo_streams'; + +window.bootstrap = bootstrap; diff --git a/app/javascript/controllers/bs_modal_controller.js b/app/javascript/controllers/bs_modal_controller.js new file mode 100644 index 00000000..736fec66 --- /dev/null +++ b/app/javascript/controllers/bs_modal_controller.js @@ -0,0 +1,15 @@ +import { Controller } from '@hotwired/stimulus'; + +// Connects to data-controller="bs-modal" +export default class extends Controller { + connect() { + // eslint-disable-next-line no-undef + this.modal = new bootstrap.Modal(this.element); + + this.modal.show(); + } + + disconnect() { + this.modal.hide(); + } +} diff --git a/app/javascript/controllers/event_form_remove_talk_controller.js b/app/javascript/controllers/event_form_remove_talk_controller.js new file mode 100644 index 00000000..202b49d6 --- /dev/null +++ b/app/javascript/controllers/event_form_remove_talk_controller.js @@ -0,0 +1,20 @@ +import { Controller } from '@hotwired/stimulus'; + +// Connects to data-controller="event-form-remove-talk" +export default class extends Controller { + removeRecord(event) { + let talkId = event.currentTarget.dataset.talkId; + let talkPersisted = event.currentTarget.dataset.persisted === 'true'; + let talk = document.getElementById(`talk-${talkId}`); + console.log(talkPersisted); + + if (talkPersisted) { + console.log('to be hidden'); + let removeRecordElement = talk.querySelector('.remove_record'); + removeRecordElement.value = '1'; + talk.className += ' d-none'; + } else { + talk.remove(); + } + } +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 0c92444d..7ce7e345 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -4,5 +4,17 @@ import { application } from './application'; +import BsModalController from './bs_modal_controller'; +application.register('bs-modal', BsModalController); + import HelloController from './hello_controller'; application.register('hello', HelloController); + +import EventFormRemoveTalkController from './event_form_remove_talk_controller'; +application.register('event-form-remove-talk', EventFormRemoveTalkController); + +import SlimSelectController from './slim_select_controller'; +application.register('slim-select', SlimSelectController); + +import TurboController from './turbo_controller'; +application.register('turbo', TurboController); diff --git a/app/javascript/controllers/slim_select_controller.js b/app/javascript/controllers/slim_select_controller.js new file mode 100644 index 00000000..9a23977a --- /dev/null +++ b/app/javascript/controllers/slim_select_controller.js @@ -0,0 +1,19 @@ +import { Controller } from '@hotwired/stimulus'; +import SlimSelect from 'slim-select'; + +// Connects to data-controller="slim-select" +export default class extends Controller { + connect() { + this.select = new SlimSelect({ + select: this.element, + settings: { + openPosition: 'up', + }, + }); + document.querySelector('svg.ss-arrow').remove(); + } + + disconnect() { + this.select.destroy(); + } +} diff --git a/app/javascript/controllers/turbo_controller.js b/app/javascript/controllers/turbo_controller.js new file mode 100644 index 00000000..bbd59ccc --- /dev/null +++ b/app/javascript/controllers/turbo_controller.js @@ -0,0 +1,36 @@ +import { Controller } from '@hotwired/stimulus'; +import { Turbo } from '@hotwired/turbo-rails'; +// Connects to data-controller="turbo" +export default class extends Controller { + initialize() { + this.element.setAttribute('data-action', 'click->turbo#click'); + } + click(e) { + e.preventDefault(); + + if (this.element.hasAttribute('href')) { + // If href is present, get its value + this.url = this.element.getAttribute('href'); + } else if (this.element.hasAttribute('data-href')) { + // If href is not present, check for data-href attribute + this.url = this.element.getAttribute('data-href'); + } else if (this.element.hasAttribute('formaction')) { + // If href is not present, check for formaction attribute + // breakpoint here + + this.url = this.element.getAttribute('formaction'); + } + if (this.url) { + fetch(this.url, { + headers: { + Accept: 'text/vnd.turbo-stream.html', + }, + }) + .then((r) => r.text()) + .then((html) => Turbo.renderStreamMessage(html)) + .catch((err) => console.log(err)); + } else { + console.log('No URL found'); + } + } +} diff --git a/app/javascript/turbo_streams/index.js b/app/javascript/turbo_streams/index.js new file mode 100644 index 00000000..33230825 --- /dev/null +++ b/app/javascript/turbo_streams/index.js @@ -0,0 +1 @@ +import './update_children_or_append'; diff --git a/app/javascript/turbo_streams/update_children_or_append.js b/app/javascript/turbo_streams/update_children_or_append.js new file mode 100644 index 00000000..c4eb3ffd --- /dev/null +++ b/app/javascript/turbo_streams/update_children_or_append.js @@ -0,0 +1,34 @@ +import { StreamActions } from '@hotwired/turbo'; + +StreamActions.update_children_or_append = function () { + function duplicateChildren(existingChildren, newChildrenIds) { + return existingChildren + .filter((existingChild) => newChildrenIds.includes(existingChild.id)) + .map((existingChild) => { + const templateChild = this.templateContent.querySelector(`#${existingChild.id}`); + return { targetChild: existingChild, templateChild }; + }); + } + + if (this.duplicateChildren.length > 0) { + const existingChildren = this.targetElements + .flatMap((e) => [...e.children]) + .filter((c) => !!c.id); + + const newChildrenIds = [...(this.templateContent?.children || [])] + .filter((c) => !!c.id) + .map((c) => c.id); + + const duplicatedChildren = duplicateChildren.call(this, existingChildren, newChildrenIds); + + duplicatedChildren.forEach(({ targetChild, templateChild }) => { + if (targetChild && templateChild) { + const newElement = templateChild.cloneNode(true); + templateChild.remove(); + targetChild.replaceWith(newElement); + } + }); + } else { + this.targetElements.forEach((e) => e.append(this.templateContent)); + } +}; diff --git a/app/models/event.rb b/app/models/event.rb index f4a58a53..779f1f1c 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -18,5 +18,7 @@ class Event < ApplicationRecord has_many :talks, dependent: :destroy has_many :speakers, through: :talks + accepts_nested_attributes_for :talks, allow_destroy: true + validates :title, :location, :date, presence: true end diff --git a/app/models/speaker.rb b/app/models/speaker.rb index ff681899..36d6d42e 100644 --- a/app/models/speaker.rb +++ b/app/models/speaker.rb @@ -28,7 +28,7 @@ class Speaker < ApplicationRecord before_validation :format_links - scope :ordered_by_name, -> { order(:name) } + default_scope { order(name: :asc) } def validate_social_media_brand return if links.blank? || links.keys.all? { |key| SOCIAL_MEDIA_LINKS.include?(key) } diff --git a/app/models/talk.rb b/app/models/talk.rb index 301c537b..dbcaa40d 100644 --- a/app/models/talk.rb +++ b/app/models/talk.rb @@ -25,5 +25,9 @@ # class Talk < ApplicationRecord belongs_to :speaker - belongs_to :event + belongs_to :event, optional: true + + validates :talk_title, :talk_description, presence: true + + default_scope { order(id: :asc) } end diff --git a/app/policies/event_policy.rb b/app/policies/event_policy.rb index 58e2518d..9c79f9dd 100644 --- a/app/policies/event_policy.rb +++ b/app/policies/event_policy.rb @@ -23,4 +23,12 @@ def update? def destroy? user.admin? end + + def set_talk? + create? + end + + def generate_talk? + create? + end end diff --git a/app/views/admin/events/_form.html.erb b/app/views/admin/events/_form.html.erb index 74105c8e..c8565ed4 100644 --- a/app/views/admin/events/_form.html.erb +++ b/app/views/admin/events/_form.html.erb @@ -1,31 +1,51 @@ - <%= form_for [:admin, event.becomes(Event)] do |f| %> - <%= render 'layouts/admin/form_errors', object: f.object %> -
- <%= f.text_field :title, class: "form-control" %> - <%= f.label :title %> +<%= form_with model: [:admin, event.becomes(Event)] do |f| %> + <%= render 'layouts/admin/form_errors', object: f.object %> +
+ <%= f.text_field :title, class: "form-control shadow-sm" %> + <%= f.label :title %> +
+
+ <%= f.text_field :location, class: "form-control shadow-sm" %> + <%= f.label :location %> +
+
+ <%= f.text_area :description, rows: 40, class: "form-control shadow-sm", style: "height: 100px" %> + <%= f.label :description %> +
+
+ <%= f.label :date, class: "d-block form-label" %> + <%= f.datetime_select :date, {}, { class: "form-select shadow-sm d-inline-block w-auto" } %> +
+
+ <%= f.select :type, ["Meetup", "Panel"], {}, { class: "form-select shadow-sm" } %> + <%= f.label :type, "Type" %> +
+
+ <%= f.text_field :panel_video_link, class: "form-control shadow-sm" %> + <%= f.label :panel_video_link, "Video link" %> +
+
+ Talks +
+
+
+ <%=button_tag type: "button", class: "btn btn-outline-primary fw-semibold h-100 p-3", data: { controller: "turbo", href: set_talk_admin_events_path(talk_id: "new_talk") } do %> + + + + Add talk + <% end %> +
+
+ <% if f.object.talks.any? %> + <% event.talks.each do |talk| %> + <%= render "admin/events/form/card_talk", talk: talk, talk_id: talk.id %> + <% end %> + <% end %>
-
- <%= f.text_field :location, class: "form-control" %> - <%= f.label :location %> -
-
- <%= f.text_area :description, rows: 40, class: "form-control", style: "height: 100px" %> - <%= f.label :description %> -
-
- <%= f.label :date, class: "d-block form-label" %> - <%= f.datetime_select :date, {}, { class: "form-select d-inline-block w-auto" } %> -
-
- <%= f.select :type, ['Meetup', 'Panel'], {}, { class: "form-select" } %> - <%= f.label :type, "Type" %> -
-
- <%= f.text_field :panel_video_link, class: "form-control" %> - <%= f.label :panel_video_link, "Video link" %> -
-
- <%= f.submit "Save", class: "btn btn-primary btn-lg" %> - <%=link_to "Cancel", admin_events_path, class: "btn btn-outline-primary btn-lg" %> -
- <% end %> \ No newline at end of file +
+
+ <%=f.submit "Save", class: "btn btn-primary btn-lg shadow-sm" %> + <%=link_to "Cancel", admin_events_path, class: "btn btn-outline-primary btn-lg shadow-sm" %> +
+<% end %> diff --git a/app/views/admin/events/edit.html.erb b/app/views/admin/events/edit.html.erb index 3b8fdfd0..637b04ed 100644 --- a/app/views/admin/events/edit.html.erb +++ b/app/views/admin/events/edit.html.erb @@ -1,6 +1,6 @@
-
+

Edit event

- <%=render 'form', event: @event %> + <%= render "form", event: @event %>
diff --git a/app/views/admin/events/form/_card_talk.html.erb b/app/views/admin/events/form/_card_talk.html.erb new file mode 100644 index 00000000..a25eae65 --- /dev/null +++ b/app/views/admin/events/form/_card_talk.html.erb @@ -0,0 +1,69 @@ +<%= tag.div id: "talk-#{talk_id}", class: "col-12 col-md-6" do %> +
+
+
+
+ <%= fields_for "event[talks_attributes][#{talk_id}]", talk do |ff| %> + <%= ff.hidden_field :talk_title %> + <%= ff.hidden_field :talk_description %> + <%= ff.hidden_field :talk_video_link %> + <%= ff.hidden_field :speaker_id %> + <% if talk.persisted? %> + <%= ff.hidden_field :_destroy, { class: "remove_record" } %> + <% end %> + <% end %> +
+
+
+
+ <%=image_tag talk.speaker.image_url, class:"rounded-circle object-fit-cover border border-3 border-primary shadow", height: 53, width: 53, alt:"Profile photo for #{talk.speaker.name}" %> +
+
+

<%= talk.talk_title %>

+

<%= talk.speaker.name %>

+
+
+
+
+

<%= talk.talk_description %>

+
+
+
+
+
+
+ + <% if talk.talk_video_link.present? %> + Video: + <%= link_to "Youtube", talk.talk_video_link, class:"text-nowrap", target: "_blank", rel: "noopener noreferrer" %> + <% else %> + (No video provided) + <% end %> + +
+
+ <%= button_tag type: :submit, formmethod: "get", formaction: set_talk_admin_events_path(talk_id: talk_id), class: "btn btn-info", data: { turbo_stream: "true" } do %> + + <% end %> + <%=button_tag type: "button", class: "btn btn-outline-secondary", data: { action: "event-form-remove-talk#removeRecord", talk_id: talk_id, persisted: talk.persisted? } do %> + + <% end %> +
+
+
+
+
+ +
+<% end %> +<% if talk.persisted? %> + +<% end %> diff --git a/app/views/admin/events/form/_card_talk_new.turbo_stream.erb b/app/views/admin/events/form/_card_talk_new.turbo_stream.erb new file mode 100644 index 00000000..f2cd240f --- /dev/null +++ b/app/views/admin/events/form/_card_talk_new.turbo_stream.erb @@ -0,0 +1,8 @@ + + + diff --git a/app/views/admin/events/index.html.erb b/app/views/admin/events/index.html.erb index 52cba037..f148d7d7 100644 --- a/app/views/admin/events/index.html.erb +++ b/app/views/admin/events/index.html.erb @@ -25,7 +25,6 @@ <% @events.each do |event| %> - <%= event.id %> <%= event.title %> @@ -36,7 +35,7 @@ <%= truncate_html(event.description) %> <%= link_to 'Edit', edit_admin_event_path(event) %> - <%= button_to "Delete", admin_event_url(event), form_class: "", class: "link-primary border-0 bg-transparent text-decoration-underline", method: :delete %> + <%= button_to "Delete", admin_event_url(event), form_class: "", class: "link-primary border-0 bg-transparent text-decoration-underline", method: :delete, data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %> <% end %> diff --git a/app/views/admin/events/new.html.erb b/app/views/admin/events/new.html.erb index 4f4c1d0a..f9c18747 100644 --- a/app/views/admin/events/new.html.erb +++ b/app/views/admin/events/new.html.erb @@ -1,6 +1,6 @@
-
+

New Event

- <%= render 'form', event: @event %> + <%= render "form", event: @event %>
diff --git a/app/views/admin/events/set_talk.turbo_stream.erb b/app/views/admin/events/set_talk.turbo_stream.erb new file mode 100644 index 00000000..fc250cd7 --- /dev/null +++ b/app/views/admin/events/set_talk.turbo_stream.erb @@ -0,0 +1,38 @@ +<%= turbo_stream.append "remote_modal" do %> + <%= turbo_frame_tag :remote_modal, target: :_top do %> + + <% end %> +<% end %> diff --git a/app/views/admin/speakers/_form.html.erb b/app/views/admin/speakers/_form.html.erb index 5592d23d..1941b348 100644 --- a/app/views/admin/speakers/_form.html.erb +++ b/app/views/admin/speakers/_form.html.erb @@ -1,19 +1,19 @@ <%= form_for [:admin, speaker] do |f| %> <%= render "layouts/admin/form_errors", object: f.object %>
- <%= f.text_field :name, class: "form-control" %> + <%= f.text_field :name, class: "form-control shadow-sm" %> <%= f.label :name %>
- <%= f.text_area :bio, rows: 40, class: "form-control", style: "height: 200px" %> + <%= f.text_area :bio, rows: 40, class: "form-control shadow-sm", style: "height: 200px" %> <%= f.label :bio %>
- <%= f.text_field :tagline, class: "form-control" %> + <%= f.text_field :tagline, class: "form-control shadow-sm" %> <%= f.label :tagline %>
- <%= f.url_field :image_url, class: "form-control" %> + <%= f.url_field :image_url, class: "form-control shadow-sm" %> <%= f.label :image_url, "Image link" %>
@@ -30,7 +30,7 @@
<%= f.label link.to_sym, class: "input-group-text" %> - <%= f.text_field link.to_sym, class: "form-control", placeholder: "https://socialmedia.url", aria: { label: link.capitalize, describedby: link } %> + <%= f.text_field link.to_sym, class: "form-control shadow-sm", placeholder: "https://socialmedia.url", aria: { label: link.capitalize, describedby: link } %>
<% end %> diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 6632583b..69e716f3 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -18,11 +18,12 @@ <%= stylesheet_link_tag "admin" %> <%= favicon_link_tag asset_path('favicon.ico') %> - + <%= render "layouts/admin/header" %> <%= render "layouts/admin/flashes" %>
<%= yield %>
+ <%= turbo_frame_tag "remote_modal" %> diff --git a/app/views/layouts/admin/_flashes.html.erb b/app/views/layouts/admin/_flashes.html.erb index d6958919..464860e7 100644 --- a/app/views/layouts/admin/_flashes.html.erb +++ b/app/views/layouts/admin/_flashes.html.erb @@ -1,5 +1,5 @@ <% if flash.any? %> -