add entry requests, invite new users
This commit is contained in:
@@ -12,6 +12,7 @@ class Admin::DashboardController < Admin::BaseController
|
||||
@pending_suggestions_count = SuggestedMeaning.pending.count
|
||||
@accepted_suggestions_count = SuggestedMeaning.accepted.count
|
||||
@rejected_suggestions_count = SuggestedMeaning.rejected.count
|
||||
@requested_entries_count = Entry.requested.count
|
||||
|
||||
@comment_count = Comment.count
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
class Admin::RequestsController < Admin::BaseController
|
||||
def index
|
||||
@requested_entries = Entry.requested
|
||||
.includes(:requested_by)
|
||||
.order(created_at: :desc)
|
||||
|
||||
@approved_entries = Entry.approved
|
||||
.includes(:requested_by)
|
||||
.order(updated_at: :desc)
|
||||
end
|
||||
|
||||
def show
|
||||
@entry = Entry.find(params[:id])
|
||||
end
|
||||
|
||||
def edit
|
||||
@entry = Entry.find(params[:id])
|
||||
end
|
||||
|
||||
def update
|
||||
@entry = Entry.find(params[:id])
|
||||
|
||||
if @entry.update(entry_params)
|
||||
redirect_to admin_request_path(@entry), notice: "Request updated successfully."
|
||||
else
|
||||
flash.now[:alert] = "Error updating request."
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def approve
|
||||
@entry = Entry.find(params[:id])
|
||||
@user = @entry.requested_by
|
||||
|
||||
@user.update!(
|
||||
invitation_token: SecureRandom.urlsafe_base64(32),
|
||||
invitation_sent_at: Time.current,
|
||||
invited_by: current_user
|
||||
)
|
||||
|
||||
@entry.update!(status: :approved)
|
||||
InvitationMailer.invite(@user, approved_entry: @entry).deliver_later
|
||||
|
||||
redirect_to admin_requests_path, notice: "Request approved and invitation sent to #{@user.email}."
|
||||
end
|
||||
|
||||
def reject
|
||||
@entry = Entry.find(params[:id])
|
||||
@user = @entry.requested_by
|
||||
|
||||
entry_preview = [@entry.fi, @entry.en, @entry.sv, @entry.no, @entry.ru, @entry.de].compact.first || "Entry"
|
||||
|
||||
@entry.destroy!
|
||||
@user.destroy! if @user.requested_entries.count.zero?
|
||||
|
||||
redirect_to admin_requests_path, notice: "Request '#{entry_preview}' has been rejected and deleted."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def entry_params
|
||||
params.require(:entry).permit(:category, :fi, :en, :sv, :no, :ru, :de, :notes)
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,7 @@ class EntriesController < ApplicationController
|
||||
@page = [ params[:page].to_i, 1 ].max
|
||||
@per_page = 25
|
||||
|
||||
entries_scope = Entry.all
|
||||
entries_scope = Entry.active_entries
|
||||
entries_scope = entries_scope.with_category(@category)
|
||||
entries_scope = entries_scope.search(@query, language_code: @language_code)
|
||||
entries_scope = entries_scope.starts_with(@starts_with, language_code: @language_code) if @starts_with.present?
|
||||
@@ -20,17 +20,18 @@ class EntriesController < ApplicationController
|
||||
@total_pages = (@total_entries.to_f / @per_page).ceil
|
||||
@entries = entries_scope.offset((@page - 1) * @per_page).limit(@per_page)
|
||||
|
||||
@entry_count = Entry.count
|
||||
@verified_count = Entry.where(verified: true).count
|
||||
@entry_count = Entry.active_entries.count
|
||||
@requested_count = Entry.requested.count
|
||||
@verified_count = Entry.active_entries.where(verified: true).count
|
||||
@needs_review_count = @entry_count - @verified_count
|
||||
@complete_entries_count = supported_languages.reduce(Entry.all) do |scope, language|
|
||||
@complete_entries_count = supported_languages.reduce(Entry.active_entries) do |scope, language|
|
||||
scope.where.not(language.code => [ nil, "" ])
|
||||
end.count
|
||||
@missing_entries_count = @entry_count - @complete_entries_count
|
||||
@language_completion = supported_languages.index_with do |language|
|
||||
next 0 if @entry_count.zero?
|
||||
|
||||
(Entry.where.not(language.code => [ nil, "" ]).count * 100.0 / @entry_count).round
|
||||
(Entry.active_entries.where.not(language.code => [ nil, "" ]).count * 100.0 / @entry_count).round
|
||||
end
|
||||
|
||||
if @language_code.present?
|
||||
@@ -61,7 +62,7 @@ class EntriesController < ApplicationController
|
||||
end
|
||||
|
||||
def download
|
||||
@entries = Entry.order(:id)
|
||||
@entries = Entry.active_entries.order(:id)
|
||||
respond_to do |format|
|
||||
format.xlsx do
|
||||
filename = "sanasto-entries-#{Time.zone.today}.xlsx"
|
||||
|
||||
@@ -21,6 +21,9 @@ class InvitationsController < ApplicationController
|
||||
invitation_token: nil
|
||||
)
|
||||
|
||||
# Activate approved entries by this user
|
||||
Entry.where(requested_by: @user, status: :approved).update_all(status: :active)
|
||||
|
||||
session[:user_id] = @user.id
|
||||
redirect_to admin? ? admin_root_path : root_path, notice: "Welcome to Sanasto Wiki, #{@user.name}!"
|
||||
else
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
class RequestsController < ApplicationController
|
||||
def new
|
||||
@entry = Entry.new
|
||||
|
||||
if current_user
|
||||
@pending_count = current_user.requested_entries.where(status: [ :requested, :approved ]).count
|
||||
elsif params[:email].present?
|
||||
@pending_count = User.find_by(email: params[:email])&.requested_entries&.where(status: [ :requested, :approved ])&.count || 0
|
||||
else
|
||||
@pending_count = 0
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
# If user is logged in, use their account
|
||||
if current_user
|
||||
@user = current_user
|
||||
else
|
||||
# Anonymous submission - need to find or create user
|
||||
email = request_params[:email]
|
||||
existing_user = User.find_by(email: email)
|
||||
|
||||
# Check if user has already accepted an invitation
|
||||
if existing_user&.invitation_accepted_at.present?
|
||||
redirect_to login_path, alert: "An account with this email already exists. Please log in."
|
||||
return
|
||||
end
|
||||
|
||||
# Use existing pending user or create new one
|
||||
@user = existing_user || User.new(
|
||||
name: request_params[:name],
|
||||
email: email,
|
||||
password: SecureRandom.alphanumeric(32),
|
||||
role: :contributor
|
||||
)
|
||||
end
|
||||
|
||||
# Create entry in a transaction
|
||||
ActiveRecord::Base.transaction do
|
||||
# Save user only if it's a new record
|
||||
if @user.new_record? && !@user.save
|
||||
@pending_count = 0
|
||||
@entry = Entry.new(entry_params)
|
||||
flash.now[:alert] = "There was an error submitting your request. Please check the form."
|
||||
render :new, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
return
|
||||
end
|
||||
|
||||
# Create entry
|
||||
@entry = Entry.new(entry_params)
|
||||
@entry.status = :requested
|
||||
@entry.requested_by = @user
|
||||
|
||||
if @entry.save
|
||||
redirect_to root_path, notice: "Thank you for your request! We'll review it and get back to you soon."
|
||||
else
|
||||
@pending_count = 0
|
||||
flash.now[:alert] = "There was an error submitting your request. Please check the form."
|
||||
render :new, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def request_params
|
||||
params.require(:entry).permit(:name, :email, :category, :fi, :en, :sv, :no, :ru, :de, :notes)
|
||||
end
|
||||
|
||||
def entry_params
|
||||
request_params.except(:name, :email)
|
||||
end
|
||||
end
|
||||
@@ -28,4 +28,15 @@ module EntriesHelper
|
||||
def format_entry_category(entry)
|
||||
entry.category.to_s.tr("_", " ").capitalize
|
||||
end
|
||||
|
||||
def format_entry_status(entry)
|
||||
case entry.status
|
||||
when "requested"
|
||||
content_tag(:span, "Requested", class: "px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800")
|
||||
when "approved"
|
||||
content_tag(:span, "Approved", class: "px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800")
|
||||
when "active"
|
||||
content_tag(:span, "Active", class: "px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
class InvitationMailer < ApplicationMailer
|
||||
def invite(user)
|
||||
def invite(user, approved_entry: nil)
|
||||
@user = user
|
||||
@approved_entry = approved_entry
|
||||
@invitation_url = invitation_url(@user.invitation_token)
|
||||
@expires_at = @user.invitation_sent_at + User::INVITATION_TOKEN_EXPIRY
|
||||
|
||||
subject = if @approved_entry
|
||||
"Your entry request has been approved - Join Sanasto Wiki"
|
||||
else
|
||||
"You've been invited to join Sanasto Wiki"
|
||||
end
|
||||
|
||||
mail(
|
||||
to: @user.email,
|
||||
subject: "You've been invited to join Sanasto Wiki"
|
||||
subject: subject
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
class Entry < ApplicationRecord
|
||||
belongs_to :created_by, class_name: "User", optional: true
|
||||
belongs_to :updated_by, class_name: "User", optional: true
|
||||
belongs_to :requested_by, class_name: "User", optional: true
|
||||
|
||||
has_many :suggested_meanings, dependent: :destroy
|
||||
has_many :comments, as: :commentable, dependent: :destroy
|
||||
|
||||
enum :category, %i[word phrase proper_name title reference other]
|
||||
enum :status, %i[requested approved active], default: :active
|
||||
|
||||
validates :category, presence: true
|
||||
validate :at_least_one_translation
|
||||
|
||||
scope :with_category, ->(cat) { cat.present? ? where(category: cat) : all }
|
||||
scope :requested, -> { where(status: :requested) }
|
||||
scope :approved, -> { where(status: :approved) }
|
||||
scope :active_entries, -> { where(status: :active) }
|
||||
|
||||
def self.search(query, language_code: nil)
|
||||
return all if query.blank?
|
||||
@@ -43,4 +49,10 @@ class Entry < ApplicationRecord
|
||||
def self.valid_lang?(code)
|
||||
SupportedLanguage.valid_codes.include?(code.to_s)
|
||||
end
|
||||
|
||||
def at_least_one_translation
|
||||
if [fi, en, sv, no, ru, de].all?(&:blank?)
|
||||
errors.add(:base, "At least one language translation is required")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,7 @@ class User < ApplicationRecord
|
||||
|
||||
has_many :created_entries, class_name: "Entry", foreign_key: :created_by_id, dependent: :nullify
|
||||
has_many :updated_entries, class_name: "Entry", foreign_key: :updated_by_id, dependent: :nullify
|
||||
has_many :requested_entries, class_name: "Entry", foreign_key: :requested_by_id, dependent: :nullify
|
||||
has_many :submitted_suggested_meanings,
|
||||
class_name: "SuggestedMeaning",
|
||||
foreign_key: :submitted_by_id,
|
||||
|
||||
@@ -18,10 +18,12 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Total Users</dt>
|
||||
<dd class="text-3xl font-semibold text-gray-900"><%= @user_count %></dd>
|
||||
</dl>
|
||||
<%= link_to admin_users_path do %>
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Total Users</dt>
|
||||
<dd class="text-3xl font-semibold text-gray-900"><%= @user_count %></dd>
|
||||
</dl>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,10 +46,12 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Total Entries</dt>
|
||||
<dd class="text-3xl font-semibold text-gray-900"><%= @entry_count %></dd>
|
||||
</dl>
|
||||
<%= link_to root_path do %>
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Total Entries</dt>
|
||||
<dd class="text-3xl font-semibold text-gray-900"><%= @entry_count %></dd>
|
||||
</dl>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,10 +73,12 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Suggestions</dt>
|
||||
<dd class="text-3xl font-semibold text-gray-900"><%= @pending_suggestions_count %></dd>
|
||||
</dl>
|
||||
<%= link_to admin_requests_path do %>
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Suggestions / Requests</dt>
|
||||
<dd class="text-3xl font-semibold text-gray-900"><%= @pending_suggestions_count %> / <%= @requested_entries_count %></dd>
|
||||
</dl>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,10 +100,12 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Pending Invites</dt>
|
||||
<dd class="text-3xl font-semibold text-gray-900"><%= @pending_invitations %></dd>
|
||||
</dl>
|
||||
<%= link_to admin_invitations_path do %>
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Pending Invites</dt>
|
||||
<dd class="text-3xl font-semibold text-gray-900"><%= @pending_invitations %></dd>
|
||||
</dl>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Edit Entry Request</h1>
|
||||
<p class="text-gray-600 mt-2">Modify the entry details before approval.</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<%= form_with model: @entry, url: admin_request_path(@entry), method: :patch, class: "space-y-6" do |f| %>
|
||||
<% if @entry.errors.any? %>
|
||||
<div class="mx-6 mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<h3 class="font-semibold text-red-800 mb-2">Please fix the following errors:</h3>
|
||||
<ul class="list-disc list-inside text-red-700 text-sm space-y-1">
|
||||
<% @entry.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="px-6 pt-6 space-y-4">
|
||||
<div>
|
||||
<%= f.label :category, "Category", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<%= f.select :category, Entry.categories.keys.map { |cat| [cat.humanize, cat] }, {}, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Translations</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<%= f.label :fi, "🇫🇮 Finnish", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= f.text_field :fi, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :en, "🇬🇧 English", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= f.text_field :en, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :sv, "🇸🇪 Swedish", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= f.text_field :sv, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :no, "🇳🇴 Norwegian", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= f.text_field :no, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :ru, "🇷🇺 Russian", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= f.text_field :ru, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :de, "🇩🇪 German", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= f.text_field :de, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :notes, "Additional Notes", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<%= f.text_area :notes, rows: 4, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition", placeholder: "Any additional context or information about this entry..." %>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">Requester (Read-only)</h3>
|
||||
<div class="bg-gray-50 rounded-lg p-4 space-y-1 text-sm">
|
||||
<div><span class="font-medium">Name:</span> <%= @entry.requested_by&.name %></div>
|
||||
<div><span class="font-medium">Email:</span> <%= @entry.requested_by&.email %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 px-6 py-4 bg-gray-50 flex gap-3">
|
||||
<%= f.submit "Save Changes", class: "px-6 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-lg transition" %>
|
||||
<%= link_to "Cancel", admin_request_path(@entry), class: "px-6 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 font-semibold rounded-lg transition" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,126 @@
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Entry Requests</h1>
|
||||
<p class="text-gray-600 mt-2">Review and manage entry requests from public users.</p>
|
||||
</div>
|
||||
|
||||
<!-- Requested Entries Section -->
|
||||
<div class="mb-12">
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div class="bg-yellow-50 border-b border-yellow-100 px-6 py-4">
|
||||
<h2 class="text-xl font-bold text-yellow-900 flex items-center gap-2">
|
||||
<span>⏳</span> Pending Review
|
||||
<span class="text-sm font-normal text-yellow-700">(<%= @requested_entries.count %> total)</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<% if @requested_entries.any? %>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Entry</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Category</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Requester</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<% @requested_entries.each do |entry| %>
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
<%= [entry.fi, entry.en, entry.sv, entry.no, entry.ru, entry.de].compact.first || "(empty)" %>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-indigo-100 text-indigo-800">
|
||||
<%= entry.category.humanize %>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-900"><%= entry.requested_by&.name %></div>
|
||||
<div class="text-xs text-gray-500"><%= entry.requested_by&.email %></div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<%= entry.created_at.strftime("%b %d, %Y") %>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
||||
<%= link_to "View", admin_request_path(entry), class: "text-indigo-600 hover:text-indigo-900" %>
|
||||
<%= link_to "Edit", edit_admin_request_path(entry), class: "text-blue-600 hover:text-blue-900" %>
|
||||
<%= button_to "Approve", approve_admin_request_path(entry), method: :post, class: "inline text-green-600 hover:text-green-900", form: { data: { turbo_confirm: "Send invitation to #{entry.requested_by&.email}?" } } %>
|
||||
<%= button_to "Reject", reject_admin_request_path(entry), method: :delete, class: "inline text-red-600 hover:text-red-900", form: { data: { turbo_confirm: "Delete this request?" } } %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="px-6 py-12 text-center text-gray-500">
|
||||
No pending requests at the moment.
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Approved Entries Section -->
|
||||
<div>
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div class="bg-blue-50 border-b border-blue-100 px-6 py-4">
|
||||
<h2 class="text-xl font-bold text-blue-900 flex items-center gap-2">
|
||||
<span>✅</span> Approved (Awaiting User Acceptance)
|
||||
<span class="text-sm font-normal text-blue-700">(<%= @approved_entries.count %> total)</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<% if @approved_entries.any? %>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Entry</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Category</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Requester</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Approved</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<% @approved_entries.each do |entry| %>
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
<%= [entry.fi, entry.en, entry.sv, entry.no, entry.ru, entry.de].compact.first || "(empty)" %>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full bg-indigo-100 text-indigo-800">
|
||||
<%= entry.category.humanize %>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm text-gray-900"><%= entry.requested_by&.name %></div>
|
||||
<div class="text-xs text-gray-500"><%= entry.requested_by&.email %></div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<%= entry.updated_at.strftime("%b %d, %Y") %>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
||||
<%= link_to "View", admin_request_path(entry), class: "text-indigo-600 hover:text-indigo-900" %>
|
||||
<%= button_to "Reject", reject_admin_request_path(entry), method: :delete, class: "inline text-red-600 hover:text-red-900", form: { data: { turbo_confirm: "Delete this approved request?" } } %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="px-6 py-12 text-center text-gray-500">
|
||||
No approved entries awaiting user acceptance.
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,96 @@
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Entry Request Details</h1>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div class="border-b border-gray-200 px-6 py-4 bg-gray-50">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold text-gray-900">Entry Information</h2>
|
||||
<%= content_tag(:span, @entry.status.titleize, class: "px-3 py-1 text-sm font-semibold rounded-full #{@entry.requested? ? 'bg-yellow-100 text-yellow-800' : 'bg-blue-100 text-blue-800'}") %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-6 space-y-6">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">Category</h3>
|
||||
<span class="px-3 py-1 text-sm font-semibold rounded-full bg-indigo-100 text-indigo-800">
|
||||
<%= @entry.category.humanize %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Translations</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<div class="text-sm font-medium text-gray-700 mb-1">🇫🇮 Finnish</div>
|
||||
<div class="text-gray-900"><%= @entry.fi.presence || "—" %></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<div class="text-sm font-medium text-gray-700 mb-1">🇬🇧 English</div>
|
||||
<div class="text-gray-900"><%= @entry.en.presence || "—" %></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<div class="text-sm font-medium text-gray-700 mb-1">🇸🇪 Swedish</div>
|
||||
<div class="text-gray-900"><%= @entry.sv.presence || "—" %></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<div class="text-sm font-medium text-gray-700 mb-1">🇳🇴 Norwegian</div>
|
||||
<div class="text-gray-900"><%= @entry.no.presence || "—" %></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<div class="text-sm font-medium text-gray-700 mb-1">🇷🇺 Russian</div>
|
||||
<div class="text-gray-900"><%= @entry.ru.presence || "—" %></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<div class="text-sm font-medium text-gray-700 mb-1">🇩🇪 German</div>
|
||||
<div class="text-gray-900"><%= @entry.de.presence || "—" %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @entry.notes.present? %>
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">Notes</h3>
|
||||
<div class="bg-gray-50 rounded-lg p-4 text-gray-900 whitespace-pre-wrap">
|
||||
<%= @entry.notes %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Requester Information</h3>
|
||||
<div class="bg-gray-50 rounded-lg p-4 space-y-2">
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-700">Name:</span>
|
||||
<span class="text-gray-900 ml-2"><%= @entry.requested_by&.name %></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-700">Email:</span>
|
||||
<span class="text-gray-900 ml-2"><%= @entry.requested_by&.email %></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-700">Submitted:</span>
|
||||
<span class="text-gray-900 ml-2"><%= @entry.created_at.strftime("%B %d, %Y at %I:%M %p") %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 px-6 py-4 bg-gray-50 flex flex-wrap gap-3">
|
||||
<%= link_to "← Back to Requests", admin_requests_path, class: "px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 font-semibold rounded-lg transition" %>
|
||||
|
||||
<% if @entry.requested? %>
|
||||
<%= link_to "Edit", edit_admin_request_path(@entry), class: "px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition" %>
|
||||
<%= button_to "Approve & Send Invitation", approve_admin_request_path(@entry), method: :post, class: "px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition", form: { data: { turbo_confirm: "Send invitation to #{@entry.requested_by&.email}?" } } %>
|
||||
<% end %>
|
||||
|
||||
<%= button_to "Reject", reject_admin_request_path(@entry), method: :delete, class: "px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-semibold rounded-lg transition ml-auto", form: { data: { turbo_confirm: "Are you sure you want to delete this request?" } } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,6 +42,9 @@
|
||||
<tr>
|
||||
<td colspan="<%= table_languages.size + 1 %>" class="px-6 py-6 text-slate-500">
|
||||
No entries matched your filters.
|
||||
<br><br>
|
||||
<%= link_to "Request a new entry", new_request_path,
|
||||
class: "text-indigo-600 font-semibold hover:text-indigo-800 underline" %>
|
||||
</td>
|
||||
</tr>
|
||||
<% else %>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
<span class="text-xl font-light text-slate-400">Wiki</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<%= link_to "Request Entry", new_request_path,
|
||||
class: "text-xs font-bold text-emerald-700 px-3 py-2 rounded-md border border-emerald-200 bg-emerald-50 hover:bg-emerald-100 transition" %>
|
||||
<%= link_to "Download XLSX", download_entries_path(format: :xlsx),
|
||||
class: "text-xs font-bold text-indigo-700 px-3 py-2 rounded-md border border-indigo-200 bg-indigo-50 hover:bg-indigo-100 transition" %>
|
||||
<% if admin? %>
|
||||
@@ -21,6 +23,21 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Flash messages -->
|
||||
<% if flash.any? %>
|
||||
<div class="max-w-7xl mx-auto px-4 mt-4 w-full">
|
||||
<% flash.each do |type, message| %>
|
||||
<div class="<%= type == 'notice' ? 'bg-green-50 border border-green-200 text-green-700' : 'bg-red-50 border border-red-200 text-red-700' %> px-4 py-3 rounded-lg mb-4 relative" role="alert">
|
||||
<span class="block sm:inline pr-8"><%= message %></span>
|
||||
<button type="button" class="absolute top-0 right-0 mt-3 mr-3 text-current opacity-50 hover:opacity-100 transition" onclick="this.parentElement.remove()">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="flex-1 flex flex-col">
|
||||
<section id="search">
|
||||
|
||||
@@ -79,6 +79,32 @@
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.entry-box {
|
||||
background: #f0fdf4;
|
||||
border-left: 4px solid #10b981;
|
||||
padding: 16px;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.entry-box h3 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #065f46;
|
||||
font-size: 16px;
|
||||
}
|
||||
.entry-translations {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.entry-translations dt {
|
||||
font-weight: 600;
|
||||
color: #064e3b;
|
||||
}
|
||||
.entry-translations dd {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -90,11 +116,53 @@
|
||||
<div class="content">
|
||||
<p class="greeting">Hello <%= @user.name %>,</p>
|
||||
|
||||
<p>
|
||||
The <strong>Sanasto Wiki</strong> let you search and compare, or download, translations across languages used all over the living Christianity.
|
||||
</p>
|
||||
<% if @approved_entry %>
|
||||
<p>
|
||||
Great news! Your entry request has been <strong>approved</strong> and is ready to be published.
|
||||
</p>
|
||||
|
||||
<p>With a login account, you can contribute to this work.</p>
|
||||
<div class="entry-box">
|
||||
<h3>✓ Your Approved Entry</h3>
|
||||
<p style="margin: 0 0 4px 0;"><strong>Category:</strong> <%= @approved_entry.category.to_s.humanize %></p>
|
||||
|
||||
<dl class="entry-translations">
|
||||
<% if @approved_entry.fi.present? %>
|
||||
<dt>🇫🇮 Finnish:</dt>
|
||||
<dd><%= @approved_entry.fi %></dd>
|
||||
<% end %>
|
||||
<% if @approved_entry.en.present? %>
|
||||
<dt>🇬🇧 English:</dt>
|
||||
<dd><%= @approved_entry.en %></dd>
|
||||
<% end %>
|
||||
<% if @approved_entry.sv.present? %>
|
||||
<dt>🇸🇪 Swedish:</dt>
|
||||
<dd><%= @approved_entry.sv %></dd>
|
||||
<% end %>
|
||||
<% if @approved_entry.no.present? %>
|
||||
<dt>🇳🇴 Norwegian:</dt>
|
||||
<dd><%= @approved_entry.no %></dd>
|
||||
<% end %>
|
||||
<% if @approved_entry.ru.present? %>
|
||||
<dt>🇷🇺 Russian:</dt>
|
||||
<dd><%= @approved_entry.ru %></dd>
|
||||
<% end %>
|
||||
<% if @approved_entry.de.present? %>
|
||||
<dt>🇩🇪 German:</dt>
|
||||
<dd><%= @approved_entry.de %></dd>
|
||||
<% end %>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
To complete the process and publish your entry, please accept this invitation to create your account on <strong>Sanasto Wiki</strong>.
|
||||
</p>
|
||||
<% else %>
|
||||
<p>
|
||||
The <strong>Sanasto Wiki</strong> let you search and compare, or download, translations across languages used all over the living Christianity.
|
||||
</p>
|
||||
|
||||
<p>With a login account, you can contribute to this work.</p>
|
||||
<% end %>
|
||||
|
||||
<div class="info-box">
|
||||
<p style="margin: 0;"><strong>Your Account Details:</strong></p>
|
||||
|
||||
@@ -4,9 +4,28 @@ SANASTO WIKI - INVITATION
|
||||
|
||||
Hello <%= @user.name %>,
|
||||
|
||||
<% if @approved_entry %>
|
||||
Great news! Your entry request has been APPROVED and is ready to be published.
|
||||
|
||||
YOUR APPROVED ENTRY
|
||||
-------------------
|
||||
Category: <%= @approved_entry.category.to_s.humanize %>
|
||||
|
||||
Translations:
|
||||
<% if @approved_entry.fi.present? %> • Finnish: <%= @approved_entry.fi %>
|
||||
<% end %><% if @approved_entry.en.present? %> • English: <%= @approved_entry.en %>
|
||||
<% end %><% if @approved_entry.sv.present? %> • Swedish: <%= @approved_entry.sv %>
|
||||
<% end %><% if @approved_entry.no.present? %> • Norwegian: <%= @approved_entry.no %>
|
||||
<% end %><% if @approved_entry.ru.present? %> • Russian: <%= @approved_entry.ru %>
|
||||
<% end %><% if @approved_entry.de.present? %> • German: <%= @approved_entry.de %>
|
||||
<% end %>
|
||||
|
||||
To complete the process and publish your entry, please accept this invitation to create your account on Sanasto Wiki.
|
||||
<% else %>
|
||||
The Sanasto Wiki let you search and compare, or download, translations across languages used all over the living Christianity.
|
||||
|
||||
With a login account, you can contribute to this work.
|
||||
<% end %>
|
||||
|
||||
YOUR ACCOUNT DETAILS
|
||||
--------------------
|
||||
|
||||
@@ -24,6 +24,16 @@
|
||||
<%= link_to "Dashboard", admin_dashboard_path, class: "text-sm font-medium text-slate-600 hover:text-indigo-600 transition" %>
|
||||
<%= link_to "Users", admin_users_path, class: "text-sm font-medium text-slate-600 hover:text-indigo-600 transition" %>
|
||||
<%= link_to "Invitations", admin_invitations_path, class: "text-sm font-medium text-slate-600 hover:text-indigo-600 transition" %>
|
||||
<% requested_count = Entry.requested.count %>
|
||||
<% gap = requested_count.zero? ? '' : 'pr-4' %>
|
||||
<%= link_to admin_requests_path, class: "text-sm font-medium text-slate-600 hover:text-indigo-600 transition relative #{gap}" do %>
|
||||
Requests
|
||||
<% if requested_count > 0 %>
|
||||
<span class="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold rounded-full h-5 w-5 flex items-center justify-center">
|
||||
<%= requested_count %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= link_to "Back to Site", root_path, class: "text-sm font-medium text-slate-600 hover:text-indigo-600 transition" %>
|
||||
<%= button_to "Log Out", logout_path, method: :delete, form: { data: { turbo: false }, style: "display: inline-block;" }, class: "bg-indigo-600 text-white px-4 py-2 rounded-lg text-sm font-semibold hover:bg-indigo-700 transition cursor-pointer" %>
|
||||
</nav>
|
||||
@@ -35,8 +45,13 @@
|
||||
<% if flash.any? %>
|
||||
<div class="max-w-7xl mx-auto px-4 mt-4">
|
||||
<% flash.each do |type, message| %>
|
||||
<div class="<%= type == 'notice' ? 'bg-green-50 border border-green-200 text-green-700' : 'bg-red-50 border border-red-200 text-red-700' %> px-4 py-3 rounded-lg mb-4" role="alert">
|
||||
<span class="block sm:inline"><%= message %></span>
|
||||
<div class="<%= type == 'notice' ? 'bg-green-50 border border-green-200 text-green-700' : 'bg-red-50 border border-red-200 text-red-700' %> px-4 py-3 rounded-lg mb-4 relative" role="alert">
|
||||
<span class="block sm:inline pr-8"><%= message %></span>
|
||||
<button type="button" class="absolute top-0 right-0 mt-3 mr-3 text-current opacity-50 hover:opacity-100 transition" onclick="this.parentElement.remove()">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<% content_for :title, "Request a New Entry" %>
|
||||
|
||||
<div class="min-h-screen flex flex-col">
|
||||
<header class="bg-white border-b border-slate-200">
|
||||
<div class="max-w-7xl mx-auto px-4">
|
||||
<div class="h-16 flex items-center justify-between">
|
||||
<%= link_to root_path, class: "flex items-center gap-2" do %>
|
||||
<span class="text-xl font-bold tracking-tight text-indigo-600">Sanasto</span>
|
||||
<span class="text-xl font-light text-slate-400">Wiki</span>
|
||||
<% end %>
|
||||
<div class="flex items-center gap-3">
|
||||
<%= link_to "Sign In", login_path, class: "text-sm font-medium text-slate-600 hover:text-indigo-600 transition" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 bg-gradient-to-br from-indigo-50 via-white to-purple-50 flex items-center justify-center px-4 py-12">
|
||||
<div class="max-w-2xl w-full">
|
||||
<div class="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">Request a New Entry</h1>
|
||||
<p class="text-gray-600">Is there a word you would like to see in this glossary?</p>
|
||||
</div>
|
||||
|
||||
<% if flash[:alert] %>
|
||||
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-800">
|
||||
<%= flash[:alert] %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @pending_count && @pending_count > 0 %>
|
||||
<div class="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg text-blue-800">
|
||||
You have <%= @pending_count %> pending <%= "request".pluralize(@pending_count) %> being reviewed.
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= form_with model: @entry, url: requests_path, class: "space-y-6", data: { turbo: false } do |f| %>
|
||||
<% if @entry.errors.any? %>
|
||||
<div class="p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<h3 class="font-semibold text-red-800 mb-2">Please fix the following errors:</h3>
|
||||
<ul class="list-disc list-inside text-red-700 text-sm space-y-1">
|
||||
<% @entry.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-4">
|
||||
<% if current_user %>
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
|
||||
<p class="text-sm text-blue-900">
|
||||
<span class="font-semibold">Submitting as:</span> <%= current_user.name %> (<%= current_user.email %>)
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div>
|
||||
<%= f.label :name, "Your Name", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<%= f.text_field :name, required: true, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :email, "Your Email", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<%= f.email_field :email, required: true, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= f.label :category, "Category", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<%= f.select :category, Entry.categories.keys.map { |cat| [cat.humanize, cat] }, { prompt: "Select a category" }, { required: true, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Translations (at least one required)</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<%= f.label :fi, "🇫🇮 Finnish", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= f.text_field :fi, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :en, "🇬🇧 English", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= f.text_field :en, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :sv, "🇸🇪 Swedish", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= f.text_field :sv, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :no, "🇳🇴 Norwegian", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= f.text_field :no, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :ru, "🇷🇺 Russian", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= f.text_field :ru, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :de, "🇩🇪 German", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
||||
<%= f.text_field :de, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= f.label :notes, "Additional Notes (optional)", class: "block text-sm font-semibold text-gray-700 mb-2" %>
|
||||
<%= f.text_area :notes, rows: 4, class: "w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition", placeholder: "Any additional context or information about this entry..." %>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 pt-4">
|
||||
<%= f.submit "Submit Request", class: "flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-3 px-6 rounded-lg transition shadow-md hover:shadow-lg" %>
|
||||
<%= link_to "Cancel", root_path, class: "flex-1 text-center bg-gray-100 hover:bg-gray-200 text-gray-800 font-semibold py-3 px-6 rounded-lg transition" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless current_user %>
|
||||
<div class="mt-6 text-center text-sm text-gray-600">
|
||||
Already have an account? <%= link_to "Sign in", login_path, class: "text-indigo-600 hover:text-indigo-800 font-semibold" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user