Skip to content
Snippets Groups Projects
Commit 375f17e8 authored by Teal's avatar Teal
Browse files

Merge branch 'admin-improvements-and-styling-fixes' into 'main'

Admin improvements, RBAC and styling fixes

See merge request !26
parents 8698eee8 5f4e3c6c
No related branches found
No related tags found
1 merge request!26Admin improvements, RBAC and styling fixes
Pipeline #38307 passed
Showing
with 865 additions and 376 deletions
......@@ -2,20 +2,59 @@
@tailwind components;
@tailwind utilities;
/*
@custom-variant dark (&:where(.dark, .dark *));
@layer components {
/* Button Base Styles */
.btn {
@apply py-2 px-4 font-medium rounded inline-block text-center no-underline border border-transparent cursor-pointer;
}
/* Primary Button */
.btn-primary {
@apply py-2 px-4 bg-blue-200;
@apply bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800;
}
/* Secondary Button */
.btn-secondary {
@apply bg-gray-200 text-gray-800 border-gray-300 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:border-gray-700 dark:hover:bg-gray-700;
}
/* Tertiary Button */
.btn-tertiary {
@apply bg-gray-600 text-white hover:bg-gray-700 dark:bg-gray-700 dark:hover:bg-gray-800;
}
/* Danger Button */
.btn-danger {
@apply bg-red-600 text-white hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800;
}
/* Success Button */
.btn-success {
@apply bg-green-600 text-white hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-800;
}
/* Info Button */
.btn-info {
@apply bg-indigo-600 text-white hover:bg-indigo-700 dark:bg-indigo-700 dark:hover:bg-indigo-800;
}
/* Light Info Button */
.btn-info-light {
@apply bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800;
}
}
*/
/* Light Success Button */
.btn-success-light {
@apply bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900 dark:text-green-200 dark:hover:bg-green-800;
}
}
h1, h2, h3, h4, h5, h6 {
@apply font-bold;
}
input[type=submit] {
{{/* input[type=submit] {
@apply border bg-gray-400 rounded-md px-2 py-1;
&.primary {
@apply bg-teal-800 text-teal-50 border-teal-600;
......@@ -37,7 +76,7 @@ select, [type=text], [type=password] {
@apply bg-white/60;
}
}
}
} */}}
.session-holder {
@apply w-full;
......@@ -79,11 +118,6 @@ select, [type=text], [type=password] {
}
}
}
.main-nav {
a {
@apply underline hover:text-blue-600 hover:border-blue-600;
}
}
.past {
@apply opacity-50 hover:opacity-100;
}
......
class Admin::BaseController < ApplicationController
before_action :authenticate_user!
layout "admin"
protected
def authorize_admin
unless current_user&.has_permission?("manage_users") || current_user&.has_permission?("manage_conferences")
redirect_to root_path, alert: "You are not authorized to access this section."
end
end
end
module Admin
class ConferencesController < ApplicationController
before_action :authenticate_user!
class ConferencesController < Admin::BaseController
before_action :authorize_permission
before_action :set_conference, except: [ :index, :new, :create ]
......
module Admin
class DashboardController < Admin::BaseController
def index
@conferences_count = Conference.count
@users_count = User.count
@sessions_count = Session.count
@assignments_count = Assignment.count
@recent_imports = ImportHistory.order(created_at: :desc).limit(5)
@admin_users = Role.find_by(name: "admin")&.users || []
@shift_coordinators = Role.find_by(name: "shift_coordinator")&.users || []
@events_admins = Role.find_by(name: "events_admin")&.users || []
end
end
end
module Admin
class RolesController < ApplicationController
before_action :authenticate_user!
class RolesController < Admin::BaseController
before_action :authorize_role
before_action :set_role, only: [ :edit, :update ]
......
class Admin::UsersController < Admin::BaseController
before_action :authorize_admin
before_action :set_user, only: [ :show, :edit, :update, :destroy ]
def index
@users = User.all.order(:name)
end
def show
end
def new
@user = User.new
end
def edit
@available_roles = Role.all
end
def create
@user = User.new(user_params)
if @user.save
update_user_roles
redirect_to admin_users_path, notice: "User was successfully created."
else
render :new, status: :unprocessable_entity
end
end
def update
if @user.update(user_params)
update_user_roles
redirect_to admin_users_path, notice: "User was successfully updated."
else
@available_roles = Role.all
render :edit, status: :unprocessable_entity
end
end
def destroy
@user.destroy
redirect_to admin_users_path, notice: "User was successfully deleted."
end
private
def set_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation, :telegram_username, :languages_from, :languages_to)
end
def update_user_roles
# Clear existing roles
@user.roles.clear
# Add selected roles
if params[:user][:role_ids].present?
params[:user][:role_ids].each do |role_id|
next if role_id.blank?
role = Role.find(role_id)
@user.roles << role
end
end
end
def authorize_admin
unless current_user&.has_permission?("manage_users")
redirect_to root_path, alert: "You are not authorized to access this section."
end
end
end
module ButtonHelper
def button_primary_class
"px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800"
end
def button_secondary_class
"px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
end
def button_tertiary_class
"px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700 dark:bg-gray-700 dark:hover:bg-gray-800"
end
def button_danger_class
"px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800"
end
def button_success_class
"px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-800"
end
def button_info_class
"px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700 dark:bg-indigo-700 dark:hover:bg-indigo-800"
end
def button_light_info_class
"px-4 py-2 bg-indigo-100 text-indigo-700 rounded hover:bg-indigo-200 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800"
end
def button_light_success_class
"px-4 py-2 bg-green-100 text-green-700 rounded hover:bg-green-200 dark:bg-green-900 dark:text-green-200 dark:hover:bg-green-800"
end
end
class ImportHistory < ApplicationRecord
belongs_to :conference
enum :status, started: 0, completed: 1, failed: 2
enum :status, {
started: "started",
completed: "completed",
failed: "failed"
}
def duration
return nil unless started_at
......
<%= form_with(model: [:admin, conference], url: url, method: method, class: "space-y-6", data: { controller: "conference-form" }) do |form| %>
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<% if conference.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<h2 class="font-bold"><%= pluralize(conference.errors.count, "error") %> prohibited this conference from being saved:</h2>
<ul class="list-disc list-inside mt-2">
<% conference.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="space-y-4">
<div>
<%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :slug, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :slug, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Used in URLs. Should contain only lowercase letters, numbers, and hyphens.</p>
</div>
<div>
<%= form.label :url, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.url_field :url, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :time_zone, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.time_zone_select :time_zone, nil, { include_blank: "Select Time Zone" }, { class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" } %>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<%= form.label :starts_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.datetime_local_field :starts_at, value: conference.starts_at&.strftime('%Y-%m-%dT%H:%M'), class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :ends_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.datetime_local_field :ends_at, value: conference.ends_at&.strftime('%Y-%m-%dT%H:%M'), class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
</div>
<div>
<%= form.label :import_job_class, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.select :import_job_class,
[["Select Import Job Class", ""]] +
ConferencesController.available_import_job_classes.map { |class_name, display_name| [display_name, class_name] },
{},
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white",
data: { conference_form_target: "importJobClass", action: "change->conference-form#importJobClassChanged" } } %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Select the import job class to see required data fields</p>
</div>
<!-- Add hidden inputs to preserve all data values -->
<% if conference.data.present? %>
<% conference.data.each do |key, value| %>
<input type="hidden" name="data[<%= key %>]" value="<%= value %>" id="hidden_data_<%= key %>">
<% end %>
<% end %>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Required Data Fields</legend>
<div class="space-y-4" data-conference-form-target="requiredFields">
<% if conference.import_job_class.present? %>
<% begin %>
<% klass = conference.import_job_class.constantize %>
<% conference.required_data_fields.each do |field| %>
<% metadata = klass.respond_to?(:field_metadata) ? (klass.field_metadata[field] || {}) : {} %>
<div class="mb-4">
<label for="data_<%= field %>" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
<%= metadata[:title] || field.humanize %><%= metadata[:required] ? ' <span class="text-red-500">*</span>'.html_safe : '' %>
</label>
<% if metadata[:description].present? %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400"><%= metadata[:description] %></p>
<% end %>
<input
type="text"
id="data_<%= field %>"
name="data[<%= field %>]"
value="<%= conference.data&.dig(field) %>"
placeholder="<%= metadata[:placeholder] %>"
<%= 'required' if metadata[:required] %>
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<% end %>
<% rescue => e %>
<div class="text-red-500">Error loading field metadata: <%= e.message %></div>
<% end %>
<% else %>
<p class="text-sm text-gray-500 dark:text-gray-400">Select an import job class to see required fields</p>
<% end %>
</div>
</fieldset>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Custom Data Fields</legend>
<div class="space-y-4" data-conference-form-target="customFields">
<% if conference.data.present? %>
<% conference.data.except(*conference.required_data_fields).each do |key, value| %>
<div class="flex items-center space-x-2 custom-field-row">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Key</label>
<input type="text" name="custom_field_keys[]" value="<%= key %>" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Value</label>
<input type="text" name="custom_field_values[]" value="<%= value %>" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex items-end">
<button type="button" data-action="click->conference-form#removeCustomField" class="mt-1 p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
<% end %>
<% end %>
</div>
<template data-conference-form-target="customTemplate">
<div class="flex items-center space-x-2 custom-field-row">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Key</label>
<input type="text" name="custom_field_keys[]" value="KEY_PLACEHOLDER" placeholder="Enter key" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Value</label>
<input type="text" name="custom_field_values[]" value="VALUE_PLACEHOLDER" placeholder="Enter value" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex items-end">
<button type="button" data-action="click->conference-form#removeCustomField" class="mt-1 p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</template>
<div class="mt-4">
<button type="button" data-action="click->conference-form#addCustomField" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800">
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-0.5 mr-2 h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
Add Custom Field
</button>
</div>
</fieldset>
</div>
</div>
<div class="flex justify-between pt-6">
<div class="space-x-2">
<%= link_to "Cancel", admin_conferences_path, class: "btn btn-secondary" %>
<% if conference.persisted? %>
<%= link_to "View Import History", import_history_admin_conference_path(conference), class: "btn btn-info-light" %>
<% end %>
</div>
<div class="space-x-2">
<% if conference.persisted? %>
<%= link_to "Manage Relevant Stages", select_relevant_stages_admin_conference_path(conference), class: "btn btn-success-light" %>
<% end %>
<%= form.submit submit_text, class: "btn btn-primary" %>
</div>
</div>
<% end %>
<% if conference.persisted? %>
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold mb-4 dark:text-gray-200">Danger Zone</h2>
<div class="bg-red-50 border border-red-300 rounded-md p-4 dark:bg-red-900/20 dark:border-red-800">
<h3 class="text-lg font-medium text-red-800 dark:text-red-300">Delete This Conference</h3>
<p class="mt-1 text-sm text-red-700 dark:text-red-400">Once you delete a conference, there is no going back. This will delete all associated data including sessions, speakers, and stages.</p>
<div class="mt-4">
<%= button_to "Delete Conference", admin_conference_path(slug: conference.slug), method: :delete,
data: { confirm: "Are you sure you want to delete this conference? This action cannot be undone." },
class: "btn btn-danger" %>
</div>
</div>
</div>
<% end %>
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6 dark:text-gray-200">Admin: Edit Conference: <%= @conference.name %></h1>
<%= form_with(model: [:admin, @conference], url: admin_conference_path(slug: @conference.slug), method: :patch, class: "space-y-6", data: { controller: "conference-form" }) do |form| %>
<% if @conference.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<h2 class="font-bold"><%= pluralize(@conference.errors.count, "error") %> prohibited this conference from being saved:</h2>
<ul class="list-disc list-inside mt-2">
<% @conference.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="space-y-4">
<div>
<%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :slug, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :slug, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Used in URLs. Should contain only lowercase letters, numbers, and hyphens.</p>
</div>
<div>
<%= form.label :url, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.url_field :url, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :time_zone, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.time_zone_select :time_zone, nil, {}, { class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" } %>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<%= form.label :starts_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.datetime_local_field :starts_at, value: @conference.starts_at&.strftime('%Y-%m-%dT%H:%M'), class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :ends_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.datetime_local_field :ends_at, value: @conference.ends_at&.strftime('%Y-%m-%dT%H:%M'), class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
</div>
<div>
<%= form.label :import_job_class, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.select :import_job_class,
[["Select Import Job Class", ""]] +
ConferencesController.available_import_job_classes.map { |class_name, display_name| [display_name, class_name] },
{},
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white",
data: { conference_form_target: "importJobClass", action: "change->conference-form#importJobClassChanged" } } %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Select the import job class to see required data fields</p>
</div>
<!-- Add hidden inputs to preserve all data values -->
<% if @conference.data.present? %>
<% @conference.data.each do |key, value| %>
<input type="hidden" name="data[<%= key %>]" value="<%= value %>" id="hidden_data_<%= key %>">
<% end %>
<% end %>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Required Data Fields</legend>
<div class="space-y-4" data-conference-form-target="requiredFields">
<% if @conference.import_job_class.present? %>
<% begin %>
<% klass = @conference.import_job_class.constantize %>
<% @conference.required_data_fields.each do |field| %>
<% metadata = klass.respond_to?(:field_metadata) ? (klass.field_metadata[field] || {}) : {} %>
<div class="mb-4">
<label for="data_<%= field %>" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
<%= metadata[:title] || field.humanize %><%= metadata[:required] ? ' <span class="text-red-500">*</span>'.html_safe : '' %>
</label>
<% if metadata[:description].present? %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400"><%= metadata[:description] %></p>
<% end %>
<input
type="text"
id="data_<%= field %>"
name="data[<%= field %>]"
value="<%= @conference.data&.dig(field) %>"
placeholder="<%= metadata[:placeholder] %>"
<%= 'required' if metadata[:required] %>
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<% end %>
<% rescue => e %>
<div class="text-red-500">Error loading field metadata: <%= e.message %></div>
<% end %>
<% else %>
<p class="text-sm text-gray-500 dark:text-gray-400">Select an import job class to see required fields</p>
<% end %>
</div>
</fieldset>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Custom Data Fields</legend>
<div class="space-y-4" data-conference-form-target="customFields">
<% if @conference.data.present? %>
<% @conference.data.except(*@conference.required_data_fields).each do |key, value| %>
<div class="flex items-center space-x-2 custom-field-row">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Key</label>
<input type="text" name="custom_field_keys[]" value="<%= key %>" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Value</label>
<input type="text" name="custom_field_values[]" value="<%= value %>" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex items-end">
<button type="button" data-action="click->conference-form#removeCustomField" class="mt-1 p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
<% end %>
<% end %>
</div>
<template data-conference-form-target="customTemplate">
<div class="flex items-center space-x-2 custom-field-row">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Key</label>
<input type="text" name="custom_field_keys[]" value="KEY_PLACEHOLDER" placeholder="Enter key" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Value</label>
<input type="text" name="custom_field_values[]" value="VALUE_PLACEHOLDER" placeholder="Enter value" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex items-end">
<button type="button" data-action="click->conference-form#removeCustomField" class="mt-1 p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</template>
<div class="mt-4">
<button type="button" data-action="click->conference-form#addCustomField" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800">
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-0.5 mr-2 h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
Add Custom Field
</button>
</div>
</fieldset>
</div>
<div class="flex justify-between pt-6">
<div class="space-x-2">
<%= link_to "Cancel", admin_conferences_path, class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" %>
<% if @conference.persisted? %>
<%= link_to "View Import History", import_history_admin_conference_path(@conference), class: "px-4 py-2 text-sm font-medium text-indigo-700 bg-indigo-100 border border-transparent rounded-md shadow-sm hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800" %>
<% end %>
</div>
<div class="space-x-2">
<% if @conference.persisted? %>
<%= link_to "Manage Relevant Stages", select_relevant_stages_admin_conference_path(@conference), class: "px-4 py-2 text-sm font-medium text-green-700 bg-green-100 border border-transparent rounded-md shadow-sm hover:bg-green-200 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 dark:bg-green-900 dark:text-green-200 dark:hover:bg-green-800" %>
<% end %>
<%= form.submit "Update Conference", class: "inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-700 dark:hover:bg-blue-800" %>
</div>
</div>
<% end %>
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold mb-4 dark:text-gray-200">Danger Zone</h2>
<div class="bg-red-50 border border-red-300 rounded-md p-4 dark:bg-red-900/20 dark:border-red-800">
<h3 class="text-lg font-medium text-red-800 dark:text-red-300">Delete This Conference</h3>
<p class="mt-1 text-sm text-red-700 dark:text-red-400">Once you delete a conference, there is no going back. This will delete all associated data including sessions, speakers, and stages.</p>
<div class="mt-4">
<%= button_to "Delete Conference", admin_conference_path(slug: @conference.slug), method: :delete,
data: { confirm: "Are you sure you want to delete this conference? This action cannot be undone." },
class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 dark:bg-red-700 dark:hover:bg-red-800" %>
</div>
</div>
</div>
<%= render "form", conference: @conference, url: admin_conference_path(slug: @conference.slug), method: :patch, submit_text: "Update Conference" %>
</div>
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6">Import History for <%= @conference.name %></h1>
<div class="mb-6">
<%= link_to "Back to Conference", edit_admin_conference_path(@conference), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %>
<span class="mx-2">|</span>
<%= button_to "Run Import Now", retry_import_admin_conference_path(@conference), method: :post, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-blue-700 dark:hover:bg-blue-800" %>
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold dark:text-gray-200">Import History for <%= @conference.name %></h1>
<div class="flex space-x-2">
<%= button_to "Run Import Now", retry_import_admin_conference_path(@conference), method: :post, class: "btn btn-primary" %>
<%= link_to "Back to Conference", admin_conference_path(@conference), class: "btn btn-tertiary" %>
</div>
</div>
<div class="bg-white shadow overflow-hidden sm:rounded-lg dark:bg-gray-800">
......@@ -35,11 +35,11 @@
<%= history.created_at.strftime("%Y-%m-%d %H:%M:%S") %>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<% if history.status == 'completed' %>
<% if history.completed? %>
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Completed
</span>
<% elsif history.status == 'failed' %>
<% elsif history.failed? %>
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
Failed
</span>
......
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold dark:text-gray-200">Admin: Conferences</h1>
<%= link_to "New Conference", new_admin_conference_path, class: "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800" %>
<%= link_to "New Conference", new_admin_conference_path, class: "btn btn-primary" %>
</div>
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
......@@ -28,7 +28,9 @@
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= conference.import_job_class %></td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex space-x-2">
<%= link_to "View", admin_conference_path(slug: conference.slug), class: "text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300" %>
<%= link_to "Edit", edit_admin_conference_path(slug: conference.slug), class: "text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300" %>
<%= link_to "Import History", import_history_admin_conference_path(slug: conference.slug), class: "text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300" %>
<%= button_to "Delete", admin_conference_path(slug: conference.slug), method: :delete,
data: { confirm: "Are you sure you want to delete this conference? This action cannot be undone." },
class: "text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 bg-transparent border-none cursor-pointer" %>
......
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6 dark:text-gray-200">Admin: New Conference</h1>
<%= form_with(model: [:admin, @conference], class: "space-y-6", data: { controller: "conference-form" }) do |form| %>
<% if @conference.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<h2 class="font-bold"><%= pluralize(@conference.errors.count, "error") %> prohibited this conference from being saved:</h2>
<ul class="list-disc list-inside mt-2">
<% @conference.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="space-y-4">
<div>
<%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :slug, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :slug, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Used in URLs. Should contain only lowercase letters, numbers, and hyphens.</p>
</div>
<div>
<%= form.label :url, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.url_field :url, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :time_zone, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.time_zone_select :time_zone, nil, { include_blank: "Select Time Zone" }, { class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" } %>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<%= form.label :starts_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.datetime_local_field :starts_at, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :ends_at, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.datetime_local_field :ends_at, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
</div>
<div>
<%= form.label :import_job_class, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.select :import_job_class,
[["Select Import Job Class", ""]] +
ConferencesController.available_import_job_classes.map { |class_name, display_name| [display_name, class_name] },
{},
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white",
data: { conference_form_target: "importJobClass", action: "change->conference-form#importJobClassChanged" } } %>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Select the import job class to see required data fields</p>
</div>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Required Data Fields</legend>
<div class="space-y-4" data-conference-form-target="requiredFields">
<p class="text-sm text-gray-500 dark:text-gray-400">Select an import job class to see required fields</p>
</div>
</fieldset>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Custom Data Fields</legend>
<div class="space-y-4" data-conference-form-target="customFields">
<!-- Custom fields will be added here -->
</div>
<template data-conference-form-target="customTemplate">
<div class="flex items-center space-x-2 custom-field-row">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Key</label>
<input type="text" name="custom_field_keys[]" value="KEY_PLACEHOLDER" placeholder="Enter key" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Value</label>
<input type="text" name="custom_field_values[]" value="VALUE_PLACEHOLDER" placeholder="Enter value" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="flex items-end">
<button type="button" data-action="click->conference-form#removeCustomField" class="mt-1 p-2 text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</template>
<div class="mt-4">
<button type="button" data-action="click->conference-form#addCustomField" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-indigo-900 dark:text-indigo-200 dark:hover:bg-indigo-800">
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-0.5 mr-2 h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
Add Custom Field
</button>
</div>
</fieldset>
</div>
<div class="flex justify-between pt-6">
<%= link_to "Cancel", admin_conferences_path, class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" %>
<%= form.submit "Create Conference", class: "inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-700 dark:hover:bg-blue-800" %>
</div>
<% end %>
<%= render "form", conference: @conference, url: admin_conferences_path, method: :post, submit_text: "Create Conference" %>
</div>
......@@ -21,8 +21,8 @@
<% end %>
<div class="mt-4 flex space-x-4">
<%= button_to "Retry Import", retry_import_admin_conference_path(@conference), method: :post, class: "px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700" %>
<%= link_to "View Error Details", import_error_admin_conference_path(@conference), class: "px-4 py-2 border border-red-600 text-red-600 rounded hover:bg-red-50", data: { action: "view-error" } %>
<%= button_to "Retry Import", retry_import_admin_conference_path(@conference), method: :post, class: "btn btn-danger" %>
<%= link_to "View Error Details", import_error_admin_conference_path(@conference), class: "btn btn-secondary", data: { action: "view-error" } %>
</div>
</div>
......@@ -55,7 +55,7 @@
<% if @conference.last_import_completed_at %>
<p class="mt-2">Last import completed at <%= @conference.last_import_completed_at.strftime("%Y-%m-%d %H:%M:%S") %></p>
<% end %>
<%= button_to "Run Import", retry_import_admin_conference_path(@conference), method: :post, class: "mt-2 bg-yellow-500 hover:bg-yellow-700 text-white font-bold py-2 px-4 rounded" %>
<%= button_to "Run Import", retry_import_admin_conference_path(@conference), method: :post, class: "mt-2 btn btn-primary" %>
</div>
<% else %>
<div class="flex justify-between items-center mb-6">
......@@ -65,7 +65,7 @@
<% end %>
</div>
<%= button_to "Run Import Again", retry_import_admin_conference_path(@conference), method: :post, class: "px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600" %>
<%= button_to "Run Import Again", retry_import_admin_conference_path(@conference), method: :post, class: "btn btn-secondary" %>
</div>
<%= form_with(model: [:admin, @conference], url: update_relevant_stages_admin_conference_path(@conference), method: :patch, class: "space-y-6") do |form| %>
......@@ -85,8 +85,8 @@
</div>
<div class="flex justify-between pt-6">
<%= link_to "Back to Conferences", admin_conferences_path, class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" %>
<%= form.submit "Save Relevant Stages", class: "inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-700 dark:hover:bg-blue-800" %>
<%= link_to "Back to Conferences", admin_conferences_path, class: "btn btn-tertiary" %>
<%= form.submit "Save Relevant Stages", class: "btn btn-primary" %>
</div>
<% end %>
<% end %>
......
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold dark:text-gray-200">Conference: <%= @conference.name %></h1>
<div class="flex space-x-2">
<%= link_to "Edit", edit_admin_conference_path(@conference), class: "btn btn-primary" %>
<%= link_to "Back to List", admin_conferences_path, class: "btn btn-tertiary" %>
</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4">Details</h2>
<div class="space-y-4">
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Name</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200"><%= @conference.name %></p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Slug</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200"><%= @conference.slug %></p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">URL</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200">
<a href="<%= @conference.url %>" target="_blank" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
<%= @conference.url %>
</a>
</p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Time Zone</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200"><%= @conference.time_zone || 'Not set' %></p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Dates</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200">
<%= @conference.starts_at&.strftime('%Y-%m-%d %H:%M') %> to <%= @conference.ends_at&.strftime('%Y-%m-%d %H:%M') %>
</p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Import Job Class</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200"><%= @conference.import_job_class || 'Not set' %></p>
</div>
</div>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4">Statistics</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Stages</h3>
<p class="mt-1 text-xl font-semibold text-gray-900 dark:text-gray-200"><%= @conference.stages.count %></p>
</div>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Sessions</h3>
<p class="mt-1 text-xl font-semibold text-gray-900 dark:text-gray-200"><%= @conference.sessions.count %></p>
</div>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Speakers</h3>
<p class="mt-1 text-xl font-semibold text-gray-900 dark:text-gray-200"><%= @conference.speakers.count %></p>
</div>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Assignments</h3>
<p class="mt-1 text-xl font-semibold text-gray-900 dark:text-gray-200"><%= @conference.sessions.joins(:assignments).count %></p>
</div>
</div>
<div class="mt-6">
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4">Import Status</h2>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Last Import</h3>
<p class="mt-1 text-sm text-gray-900 dark:text-gray-200">
<% if @conference.last_import_completed_at %>
<%= @conference.last_import_completed_at.strftime('%Y-%m-%d %H:%M:%S') %>
<% else %>
Never completed
<% end %>
</p>
</div>
<div class="flex space-x-2">
<%= link_to "Import History", import_history_admin_conference_path(@conference), class: "btn btn-info" %>
<%= button_to "Run Import Now", retry_import_admin_conference_path(@conference), method: :post, class: "btn btn-success" %>
</div>
</div>
</div>
</div>
</div>
</div>
<% if @conference.data.present? %>
<div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4">Data</h2>
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
<pre class="text-xs text-gray-800 dark:text-gray-200 overflow-auto max-h-64"><%= JSON.pretty_generate(@conference.data) %></pre>
</div>
</div>
<% end %>
</div>
</div>
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold dark:text-gray-200 mb-6">Admin Dashboard</h1>
<!-- Stats Overview -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Conferences</dt>
<dd class="mt-1 text-3xl font-semibold text-indigo-600 dark:text-indigo-400">
<%= link_to @conferences_count, admin_conferences_path, class: "hover:underline" %>
</dd>
</dl>
</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Sessions</dt>
<dd class="mt-1 text-3xl font-semibold text-indigo-600 dark:text-indigo-400">
<%= @sessions_count %>
</dd>
</dl>
</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Users</dt>
<dd class="mt-1 text-3xl font-semibold text-indigo-600 dark:text-indigo-400">
<%= link_to @users_count, admin_users_path, class: "hover:underline" %>
</dd>
</dl>
</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Assignments</dt>
<dd class="mt-1 text-3xl font-semibold text-indigo-600 dark:text-indigo-400">
<%= @assignments_count %>
</dd>
</dl>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Recent Imports -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Recent Import Jobs</h3>
</div>
<div class="px-4 py-5 sm:p-6">
<% if @recent_imports.any? %>
<div class="flow-root">
<ul class="-my-5 divide-y divide-gray-200 dark:divide-gray-700">
<% @recent_imports.each do |import| %>
<li class="py-4">
<div class="flex items-center space-x-4">
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
<%= link_to import.conference.name, admin_conference_path(import.conference), class: "text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300" %>
<% status_class = case import.status
when 'completed' then 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
when 'failed' then 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
else 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
end %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= status_class %>">
<%= import.status %>
</span>
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
Started <%= import.started_at.strftime("%Y-%m-%d %H:%M") %>
<% if import.completed_at %>
Completed <%= import.completed_at.strftime("%Y-%m-%d %H:%M") %>
<% end %>
</p>
</div>
<div>
<% if import.conference %>
<%= link_to "Details", import_history_admin_conference_path(import.conference), class: "text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300" %>
<% end %>
</div>
</div>
</li>
<% end %>
</ul>
</div>
<% else %>
<p class="text-gray-500 dark:text-gray-400">No recent imports found.</p>
<% end %>
</div>
</div>
<!-- User Roles -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Users with Special Roles</h3>
</div>
<div class="px-4 py-5 sm:p-6">
<div class="space-y-6">
<div>
<h4 class="font-medium text-gray-900 dark:text-white">Admins</h4>
<div class="mt-2">
<% if @admin_users.any? %>
<div class="flex flex-wrap gap-2">
<% @admin_users.each do |user| %>
<%= link_to admin_user_path(user), class: "inline-flex items-center px-3 py-1 rounded-full text-sm bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200" do %>
<div class="w-5 h-5 rounded-full mr-2" style="background-color: <%= user.avatar_color %>"></div>
<%= user.name %>
<% end %>
<% end %>
</div>
<% else %>
<p class="text-gray-500 dark:text-gray-400">No admin users found.</p>
<% end %>
</div>
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-white">Events Admins</h4>
<div class="mt-2">
<% if @events_admins.any? %>
<div class="flex flex-wrap gap-2">
<% @events_admins.each do |user| %>
<%= link_to admin_user_path(user), class: "inline-flex items-center px-3 py-1 rounded-full text-sm bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200" do %>
<div class="w-5 h-5 rounded-full mr-2" style="background-color: <%= user.avatar_color %>"></div>
<%= user.name %>
<% end %>
<% end %>
</div>
<% else %>
<p class="text-gray-500 dark:text-gray-400">No events admin users found.</p>
<% end %>
</div>
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-white">Shift Coordinators</h4>
<div class="mt-2">
<% if @shift_coordinators.any? %>
<div class="flex flex-wrap gap-2">
<% @shift_coordinators.each do |user| %>
<%= link_to admin_user_path(user), class: "inline-flex items-center px-3 py-1 rounded-full text-sm bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200" do %>
<div class="w-5 h-5 rounded-full mr-2" style="background-color: <%= user.avatar_color %>"></div>
<%= user.name %>
<% end %>
<% end %>
</div>
<% else %>
<p class="text-gray-500 dark:text-gray-400">No shift coordinator users found.</p>
<% end %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%= form_with(model: [:admin, role], class: "space-y-6") do |form| %>
<% if role.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<h2 class="font-bold"><%= pluralize(role.errors.count, "error") %> prohibited this role from being saved:</h2>
<ul class="list-disc list-inside mt-2">
<% role.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="columns-1 md:columns-2 gap-6 space-y-6">
<!-- Role Information -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg break-inside-avoid mb-6">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Role Information</h3>
</div>
<div class="px-4 py-5 sm:p-6 space-y-4">
<div>
<%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :description, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_area :description, rows: 3, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
</div>
</div>
<!-- Permissions -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg break-inside-avoid mb-6">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Permissions</h3>
</div>
<div class="px-4 py-5 sm:p-6">
<div class="space-y-2">
<% permissions.each do |permission| %>
<div class="flex items-center">
<%= check_box_tag "role[permission_ids][]", permission.id, role.permissions.include?(permission), id: "permission_#{permission.id}", class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600" %>
<%= label_tag "permission_#{permission.id}", permission.name, class: "ml-2 block text-sm text-gray-900 dark:text-gray-300" %>
<% if permission.description.present? %>
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400"><%= permission.description %></span>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
</div>
<div class="flex justify-between pt-6">
<%= link_to "Cancel", admin_roles_path, class: "btn btn-secondary" %>
<%= form.submit submit_text, class: "btn btn-primary" %>
</div>
<% end %>
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6 dark:text-gray-200">Admin: Edit Role: <%= @role.name %></h1>
<%= form_with(model: [:admin, @role], class: "space-y-6") do |form| %>
<% if @role.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<h2 class="font-bold"><%= pluralize(@role.errors.count, "error") %> prohibited this role from being saved:</h2>
<ul class="list-disc list-inside mt-2">
<% @role.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="space-y-4">
<div>
<%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :description, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_area :description, rows: 3, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<fieldset class="mt-6 border border-gray-300 rounded-md p-4 dark:border-gray-600">
<legend class="px-2 text-sm font-medium text-gray-700 dark:text-gray-300">Permissions</legend>
<div class="space-y-2 mt-2">
<% @permissions.each do |permission| %>
<div class="flex items-center">
<%= check_box_tag "role[permission_ids][]", permission.id, @role.permissions.include?(permission), id: "permission_#{permission.id}", class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600" %>
<%= label_tag "permission_#{permission.id}", permission.name, class: "ml-2 block text-sm text-gray-900 dark:text-gray-300" %>
<% if permission.description.present? %>
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400"><%= permission.description %></span>
<% end %>
</div>
<% end %>
</div>
</fieldset>
</div>
<div class="flex justify-between pt-6">
<%= link_to "Cancel", admin_roles_path, class: "px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-600" %>
<%= form.submit "Update Role", class: "inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-700 dark:hover:bg-blue-800" %>
</div>
<% end %>
<%= render "form", role: @role, permissions: @permissions, submit_text: "Update Role" %>
</div>
<%= form_with(model: [:admin, user], class: "space-y-6") do |form| %>
<% if user.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4 dark:bg-red-900 dark:border-red-700 dark:text-red-300">
<h2 class="font-bold"><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul class="list-disc list-inside mt-2">
<% user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="columns-1 md:columns-2 gap-6 space-y-6">
<!-- Account Information -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg break-inside-avoid mb-6">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Account Information</h3>
</div>
<div class="px-4 py-5 sm:p-6 space-y-4">
<div>
<%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :email, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.email_field :email, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<div>
<%= form.label :password, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.password_field :password, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
<% if user.persisted? %>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Leave blank if you don't want to change it</p>
<% end %>
</div>
<div>
<%= form.label :password_confirmation, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.password_field :password_confirmation, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
<% if user.new_record? %>
<div>
<%= form.label :invitation_token, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :invitation_token, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white", value: "gargamel" %>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Default invitation token</p>
</div>
<% end %>
</div>
</div>
<!-- Communication Channels -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg break-inside-avoid mb-6">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Communication Channels</h3>
</div>
<div class="px-4 py-5 sm:p-6 space-y-4">
<div>
<%= form.label :telegram_username, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :telegram_username, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
</div>
</div>
</div>
<!-- Languages -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg break-inside-avoid mb-6">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Languages</h3>
</div>
<div class="px-4 py-5 sm:p-6 space-y-4">
<div>
<%= form.label :languages_from, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :languages_from, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Comma-separated two-letter codes (e.g., en,de,fr)</p>
</div>
<div>
<%= form.label :languages_to, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :languages_to, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" %>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Comma-separated two-letter codes (e.g., en,de,fr)</p>
</div>
</div>
</div>
<!-- Roles -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg break-inside-avoid mb-6">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Roles</h3>
</div>
<div class="px-4 py-5 sm:p-6">
<div class="space-y-4">
<% Role.all.each do |role| %>
<div class="flex items-start">
<div class="flex items-center h-5">
<%= check_box_tag "user[role_ids][]", role.id, user.roles.include?(role), id: "user_role_#{role.id}", class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600" %>
</div>
<div class="ml-3 text-sm">
<%= label_tag "user_role_#{role.id}", role.name, class: "font-medium text-gray-700 dark:text-gray-300" %>
<p class="text-gray-500 dark:text-gray-400"><%= role.description %></p>
</div>
</div>
<% end %>
</div>
<%= hidden_field_tag "user[role_ids][]", "" %>
</div>
</div>
</div>
<% end %>
<div class="flex justify-between pt-6">
<div>
<%= link_to "Cancel", admin_users_path, class: "btn btn-secondary" %>
</div>
<div>
<%= submit_tag "Save User", class: "btn btn-primary" %>
</div>
</div>
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold dark:text-gray-200">Edit User</h1>
<div class="flex space-x-2">
<%= link_to "View User", admin_user_path(@user), class: "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800" %>
<%= link_to "Back to Users", admin_users_path, class: "px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700 dark:bg-gray-700 dark:hover:bg-gray-800" %>
</div>
</div>
<%= render 'form', user: @user %>
</div>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment