From 95324a97cbbb040817b161120bcb73268d9fd9b9 Mon Sep 17 00:00:00 2001 From: Runar Ingebrigtsen Date: Thu, 22 Jan 2026 20:35:45 +0100 Subject: [PATCH] tabular front page interface --- DESIGN.md | 29 +++ app/controllers/entries_controller.rb | 25 +- app/views/entries/index.html.erb | 324 ++++++++++++++------------ 3 files changed, 227 insertions(+), 151 deletions(-) create mode 100644 DESIGN.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..0f471c9 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,29 @@ +draft: tmp/translation-page.html + +Layout + +Full-height design with the table taking up most of the viewport +Sticky header row and sticky key column for easy navigation when scrolling +Horizontal scroll for many languages, vertical scroll for many keys + +Columns + +Language columns: English (source), Norwegian, German, French, Spanish +Each language shows completion percentage in the header + +Visual indicators + +Green: source language +Blue: high completion (85%+) +Amber: moderate completion / needs review +Red row highlighting: missing translations + +Features + +Inline editing (click any cell to edit) +Search bar for finding anything +Stats bar showing translated/review/missing counts +Pagination for large translation sets +"All changes saved" status indicator + +This should be a solid starting point for your translation work. Let me know if you want to adjust the languages, add interactivity (like saving to an API), or modify the styling. diff --git a/app/controllers/entries_controller.rb b/app/controllers/entries_controller.rb index c36b028..0d74df7 100644 --- a/app/controllers/entries_controller.rb +++ b/app/controllers/entries_controller.rb @@ -7,6 +7,8 @@ class EntriesController < ApplicationController @category = params[:category].presence @query = params[:q].to_s.strip @starts_with = params[:starts_with].presence + @page = [params[:page].to_i, 1].max + @per_page = 25 entries_scope = Entry.all entries_scope = entries_scope.with_category(@category) @@ -15,10 +17,29 @@ class EntriesController < ApplicationController entries_scope = entries_scope.alphabetical_for(@language_code) if @query.blank? && @starts_with.blank? && @language_code.present? entries_scope = entries_scope.order(created_at: :desc) if entries_scope.order_values.empty? - @entries = entries_scope.limit(50) + @total_entries = entries_scope.count + @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 - @recent_entries = Entry.order(created_at: :desc).limit(6) + @needs_review_count = @entry_count - @verified_count + @complete_entries_count = @supported_languages.reduce(Entry.all) 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 + end + + if @language_code.present? + primary_language, other_languages = @supported_languages.partition { |language| language.code == @language_code } + @display_languages = primary_language + other_languages + else + @display_languages = @supported_languages + end end def show diff --git a/app/views/entries/index.html.erb b/app/views/entries/index.html.erb index b81a8b2..81df9b0 100644 --- a/app/views/entries/index.html.erb +++ b/app/views/entries/index.html.erb @@ -1,164 +1,190 @@ <% content_for :title, "Sanasto Wiki" %> - - -
- <% base_params = { q: @query.presence, category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence }.compact %> - -
- <%= form_with url: entries_path, method: :get, local: true, class: "space-y-4" do |form| %> -
-
- - - +
+
+
+
+
+ Sanasto + Wiki +
+
+ Read-only public view + <%= 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" %> + <%= link_to "Sign In", "#", class: "bg-indigo-600 text-white px-4 py-2 rounded-lg text-sm font-semibold hover:bg-indigo-700 transition" %>
- <%= form.text_field :q, - value: @query, - placeholder: "Search words, phrases, or biblical terms...", - class: "block w-full pl-11 pr-4 py-4 bg-white border border-slate-200 rounded-2xl shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" %>
-
- <%= form.submit "Search", class: "bg-indigo-600 text-white px-4 py-2 rounded-lg text-sm font-semibold hover:bg-indigo-700 transition" %> - <%= link_to "Download XLSX", - download_entries_path(format: :xlsx), - class: "text-sm font-semibold text-indigo-700 px-4 py-2 rounded-lg border border-indigo-200 bg-indigo-50 hover:bg-indigo-100 transition" %> -
- <% end %> - -
- <% all_category_params = base_params.except(:category) %> - <%= link_to "All", - entries_path(all_category_params), - class: "px-4 py-1.5 rounded-full #{@category.blank? ? 'bg-indigo-100 text-indigo-700' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'} text-xs font-bold uppercase tracking-wider" %> - <% Entry.categories.keys.each do |category_name| %> - <%= link_to category_name.tr('_', ' ').capitalize, - entries_path(base_params.merge(category: category_name)), - class: "px-4 py-1.5 rounded-full #{@category == category_name ? 'bg-indigo-100 text-indigo-700' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'} text-xs font-bold uppercase tracking-wider" %> - <% end %> -
- -
- <% all_language_params = base_params.except(:language, :starts_with) %> - <%= link_to "All Languages", - entries_path(all_language_params), - class: "px-3 py-1.5 rounded-full #{@language_code.blank? ? 'bg-slate-900 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-slate-300'} text-xs font-semibold uppercase tracking-wider" %> - <% @supported_languages.each do |language| %> - <%= link_to "#{language.name} (#{language.code.upcase})", - entries_path(all_language_params.merge(language: language.code)), - class: "px-3 py-1.5 rounded-full #{@language_code == language.code ? 'bg-slate-900 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-slate-300'} text-xs font-semibold uppercase tracking-wider" %> - <% end %> -
- - <% if @language_code.present? %> -
- <% alphabet_params = base_params.merge(language: @language_code).except(:starts_with) %> - <%= link_to "All", - entries_path(alphabet_params), - class: "px-2.5 py-1 rounded-md #{@starts_with.blank? ? 'bg-indigo-600 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'}" %> - <% alphabet_letters.each do |letter| %> - <%= link_to letter, - entries_path(alphabet_params.merge(starts_with: letter)), - class: "px-2.5 py-1 rounded-md #{@starts_with == letter ? 'bg-indigo-600 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'}" %> +
+ <%= form_with url: entries_path, method: :get, local: true do |form| %> +
+
+ + + +
+ <%= form.text_field :q, + value: @query, + placeholder: "Search words, phrases, or biblical terms...", + class: "block w-full pl-11 pr-4 py-4 bg-white border border-slate-200 rounded-2xl shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" %> +
<% end %> -
- <% end %> -
-
-
-
Total Entries
-
<%= number_with_delimiter(@entry_count) %>
-
-
-
Verified
-
<%= number_with_delimiter(@verified_count) %>
-
-
-
Recent Additions
-
Updated with the latest translations
-
-
+
+ <% base_params = { q: @query.presence, category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence }.compact %> + <% all_category_params = base_params.except(:category) %> + <%= link_to "All", + entries_path(all_category_params), + class: "px-4 py-1.5 rounded-full #{@category.blank? ? 'bg-indigo-100 text-indigo-700' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'} text-xs font-bold uppercase tracking-wider" %> + <% Entry.categories.keys.each do |category_name| %> + <%= link_to category_name.tr('_', ' ').capitalize, + entries_path(base_params.merge(category: category_name)), + class: "px-4 py-1.5 rounded-full #{@category == category_name ? 'bg-indigo-100 text-indigo-700' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'} text-xs font-bold uppercase tracking-wider" %> + <% end %> +
-
-
-

Entries

-
<%= @entries.size %> shown
-
+
+ <% all_language_params = base_params.except(:language, :starts_with, :page) %> + <%= link_to "All Languages", + entries_path(all_language_params), + class: "px-3 py-1.5 rounded-full #{@language_code.blank? ? 'bg-slate-900 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-slate-300'} text-xs font-semibold uppercase tracking-wider" %> + <% @supported_languages.each do |language| %> + <%= link_to "#{language.name} (#{language.code.upcase})", + entries_path(all_language_params.merge(language: language.code)), + class: "px-3 py-1.5 rounded-full #{@language_code == language.code ? 'bg-slate-900 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-slate-300'} text-xs font-semibold uppercase tracking-wider" %> + <% end %> +
- <% if @entries.empty? %> -
- No entries matched your filters. Try a different search or remove some filters. -
- <% else %> -
- <% @entries.each do |entry| %> -
-
- <%= format_entry_category(entry) %> - <% if entry.verified? %> -
- - Verified -
- <% else %> - Unverified - <% end %> -
- -
-
- <% @supported_languages.each do |language| %> - <% translation = entry_translation_for(entry, language.code) %> - <% next if translation.blank? %> -
- <%= "#{language.name} (#{language.code.upcase})" %> -

<%= translation %>

-
- <% end %> -
- - <% if entry.notes.present? %> -
-

Context & Notes

-

<%= entry.notes %>

-
- <% end %> -
- -
- <%= link_to "View Entry", - entry_path(entry), - class: "text-xs font-bold text-indigo-600 px-3 py-2 rounded-md border border-indigo-200 bg-indigo-50 hover:bg-indigo-100 transition ml-auto" %> -
+ <% if @language_code.present? %> +
+ <% alphabet_params = base_params.merge(language: @language_code).except(:starts_with, :page) %> + <%= link_to "All", + entries_path(alphabet_params), + class: "px-2.5 py-1 rounded-md #{@starts_with.blank? ? 'bg-indigo-600 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'}" %> + <% alphabet_letters.each do |letter| %> + <%= link_to letter, + entries_path(alphabet_params.merge(starts_with: letter)), + class: "px-2.5 py-1 rounded-md #{@starts_with == letter ? 'bg-indigo-600 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-indigo-300'}" %> + <% end %>
<% end %>
- <% end %> -
+ + -
-

Recent Entries

-
- <% @recent_entries.each do |entry| %> - <%= link_to entry_path(entry), class: "block p-4 rounded-lg border border-slate-200 hover:border-indigo-300 transition" do %> -
<%= format_entry_category(entry) %>
-
<%= entry.fi.presence || entry.en.presence || entry.sv.presence || entry.no.presence || entry.ru.presence || entry.de.presence || "Untitled" %>
-
<%= entry.created_at.strftime("%B %-d, %Y") %>
- <% end %> - <% end %> +
+
+
+
+
Fully Translated
+
<%= number_with_delimiter(@complete_entries_count) %>
+
+
+
Needs Review
+
<%= number_with_delimiter(@needs_review_count) %>
+
+
+
Missing Translations
+
<%= number_with_delimiter(@missing_entries_count) %>
+
+
-
+ +
+
+
+
+

Translation Table

+

Public glossary entries with verified status and language coverage.

+
+
<%= @total_entries %> entries
+
+ +
+
+ + + + + <% @display_languages.each do |language| %> + + <% end %> + + + + <% if @entries.empty? %> + + + + <% else %> + <% @entries.each do |entry| %> + <% translation_values = @display_languages.map { |language| entry.public_send(language.code) } %> + <% missing_any = translation_values.any?(&:blank?) %> + + + <% @display_languages.each do |language| %> + <% translation = entry.public_send(language.code) %> + + <% end %> + + <% end %> + <% end %> + +
+
Entry
+
Category / Status
+
+
<%= language.name %>
+
<%= language.code.upcase %>
+
+ <%= @language_completion.fetch(language, 0) %>% complete +
+
+ No entries matched your filters. +
+ <% primary_text = if @language_code.present? + entry.public_send(@language_code) + else + translation_values.find(&:present?) + end %> + <% primary_text = primary_text.presence || "Untitled" %> +
<%= primary_text %>
+
+ <%= format_entry_category(entry) %> + <% if entry.verified? %> + Verified + <% else %> + Unverified + <% end %> + <%= link_to "View", + entry_path(entry), + class: "text-indigo-600 font-semibold hover:underline" %> +
+
+ <% if translation.present? %> + <%= translation %> + <% else %> + + <% end %> +
+
+
+ +
+
Page <%= @page %> of <%= [@total_pages, 1].max %>
+
+ <% previous_page = @page > 1 ? @page - 1 : nil %> + <% next_page = @page < @total_pages ? @page + 1 : nil %> + <% pagination_params = { q: @query.presence, category: @category.presence, language: @language_code.presence, starts_with: @starts_with.presence }.compact %> + <%= link_to "Previous", previous_page ? entries_path(pagination_params.merge(page: previous_page)) : "#", + class: "px-3 py-1.5 rounded-md border border-slate-200 #{previous_page ? 'hover:border-indigo-300' : 'text-slate-300 cursor-not-allowed'}" %> + <%= link_to "Next", next_page ? entries_path(pagination_params.merge(page: next_page)) : "#", + class: "px-3 py-1.5 rounded-md border border-slate-200 #{next_page ? 'hover:border-indigo-300' : 'text-slate-300 cursor-not-allowed'}" %> +
+
+
+
+