From 8453801820d8425222badba73f623fcb093df6b3 Mon Sep 17 00:00:00 2001 From: Runar Ingebrigtsen Date: Thu, 22 Jan 2026 19:29:25 +0100 Subject: [PATCH] gemini suggestion front page --- Gemfile | 2 + Gemfile.lock | 14 +++ app/controllers/entries_controller.rb | 43 +++++++ app/helpers/entries_helper.rb | 16 +++ app/views/entries/download.xlsx.axlsx | 33 +++++ app/views/entries/index.html.erb | 164 +++++++++++++++++++++++++ app/views/entries/show.html.erb | 54 ++++++++ app/views/layouts/application.html.erb | 3 +- config/routes.rb | 8 +- 9 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 app/controllers/entries_controller.rb create mode 100644 app/helpers/entries_helper.rb create mode 100644 app/views/entries/download.xlsx.axlsx create mode 100644 app/views/entries/index.html.erb create mode 100644 app/views/entries/show.html.erb diff --git a/Gemfile b/Gemfile index 11f447a..80384f8 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,8 @@ gem "turbo-rails" gem "stimulus-rails" # Build JSON APIs with ease [https://github.com/rails/jbuilder] gem "jbuilder" +gem "caxlsx" +gem "caxlsx_rails" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] gem "bcrypt", "~> 3.1.7" diff --git a/Gemfile.lock b/Gemfile.lock index 969c472..7c8ff79 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,6 +100,14 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + caxlsx (4.4.1) + htmlentities (~> 4.3, >= 4.3.4) + marcel (~> 1.0) + nokogiri (~> 1.10, >= 1.10.4) + rubyzip (>= 2.4, < 4) + caxlsx_rails (0.6.4) + actionpack (>= 3.1) + caxlsx (>= 3.0) concurrent-ruby (1.3.6) connection_pool (3.0.2) crass (1.0.6) @@ -120,6 +128,7 @@ GEM raabro (~> 1.4) globalid (1.3.0) activesupport (>= 6.1) + htmlentities (4.4.2) i18n (1.14.8) concurrent-ruby (~> 1.0) image_processing (1.14.0) @@ -362,6 +371,8 @@ DEPENDENCIES brakeman bundler-audit capybara + caxlsx + caxlsx_rails debug image_processing (~> 1.2) importmap-rails @@ -408,6 +419,8 @@ CHECKSUMS builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9 capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef + caxlsx (4.4.1) sha256=c23e756a73737250d1571b51ed5c91a830178c60444da2728d345cdc7c9f272f + caxlsx_rails (0.6.4) sha256=27e1ebb4617473f49a7956214e352b485beb080d590769ef5a826c410c7d2276 concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d @@ -422,6 +435,7 @@ CHECKSUMS ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68 globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11 + htmlentities (4.4.2) sha256=bbafbdf69f2eca9262be4efef7e43e6a1de54c95eb600f26984f71d2fe96c5c3 i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a diff --git a/app/controllers/entries_controller.rb b/app/controllers/entries_controller.rb new file mode 100644 index 0000000..c36b028 --- /dev/null +++ b/app/controllers/entries_controller.rb @@ -0,0 +1,43 @@ +class EntriesController < ApplicationController + before_action :set_entry, only: [:show] + + def index + @supported_languages = SupportedLanguage.where(active: true).order(:sort_order, :name) + @language_code = params[:language].presence + @category = params[:category].presence + @query = params[:q].to_s.strip + @starts_with = params[:starts_with].presence + + entries_scope = Entry.all + 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? + 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) + @entry_count = Entry.count + @verified_count = Entry.where(verified: true).count + @recent_entries = Entry.order(created_at: :desc).limit(6) + end + + def show + @supported_languages = SupportedLanguage.where(active: true).order(:sort_order, :name) + end + + def download + @entries = Entry.order(:id) + respond_to do |format| + format.xlsx do + filename = "sanasto-entries-#{Time.zone.today}.xlsx" + response.headers["Content-Disposition"] = "attachment; filename=\"#{filename}\"" + end + end + end + + private + + def set_entry + @entry = Entry.find(params[:id]) + end +end diff --git a/app/helpers/entries_helper.rb b/app/helpers/entries_helper.rb new file mode 100644 index 0000000..c5d868d --- /dev/null +++ b/app/helpers/entries_helper.rb @@ -0,0 +1,16 @@ +module EntriesHelper + def alphabet_letters + ("A".."Z").to_a + end + + def entry_translation_for(entry, language_code) + language_column = language_code.to_s + return unless entry.has_attribute?(language_column) + + entry.public_send(language_column) + end + + def format_entry_category(entry) + entry.category.to_s.tr("_", " ").capitalize + end +end diff --git a/app/views/entries/download.xlsx.axlsx b/app/views/entries/download.xlsx.axlsx new file mode 100644 index 0000000..c9b1c90 --- /dev/null +++ b/app/views/entries/download.xlsx.axlsx @@ -0,0 +1,33 @@ +workbook = xlsx_package.workbook + +workbook.add_worksheet(name: "Entries") do |sheet| + sheet.add_row [ + "Category", + "Finnish (fi)", + "English (en)", + "Swedish (sv)", + "Norwegian (no)", + "Russian (ru)", + "German (de)", + "Notes", + "Verified", + "Created At", + "Updated At" + ] + + @entries.find_each do |entry| + sheet.add_row [ + entry.category, + entry.fi, + entry.en, + entry.sv, + entry.no, + entry.ru, + entry.de, + entry.notes, + entry.verified? ? "Yes" : "No", + entry.created_at&.to_date, + entry.updated_at&.to_date + ] + end +end diff --git a/app/views/entries/index.html.erb b/app/views/entries/index.html.erb new file mode 100644 index 0000000..b81a8b2 --- /dev/null +++ b/app/views/entries/index.html.erb @@ -0,0 +1,164 @@ +<% 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| %> +
+
+ + + +
+ <%= 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'}" %> + <% end %> +
+ <% end %> +
+ +
+
+
Total Entries
+
<%= number_with_delimiter(@entry_count) %>
+
+
+
Verified
+
<%= number_with_delimiter(@verified_count) %>
+
+
+
Recent Additions
+
Updated with the latest translations
+
+
+ +
+
+

Entries

+
<%= @entries.size %> shown
+
+ + <% 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" %> +
+
+ <% 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 %> +
+
+
diff --git a/app/views/entries/show.html.erb b/app/views/entries/show.html.erb new file mode 100644 index 0000000..cc6364a --- /dev/null +++ b/app/views/entries/show.html.erb @@ -0,0 +1,54 @@ +<% content_for :title, "Entry" %> + + + +
+
+ <%= link_to "← Back to search", entries_path, class: "text-sm text-slate-500 hover:text-indigo-600" %> +
+ +
+
+ <%= 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 %> +
+
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index eb65e8a..20c0fe1 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -20,9 +20,10 @@ <%# Includes all stylesheet files in app/assets/stylesheets %> <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + - + <%= yield %> diff --git a/config/routes.rb b/config/routes.rb index b345f22..561dca5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,9 +10,13 @@ Rails.application.routes.draw do # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker # Defines the root path route ("/") - # root "posts#index" + root "entries#index" - resources :entries + resources :entries do + collection do + get :download + end + end resources :suggested_meanings resources :comments, only: [:create, :update, :destroy] resources :entry_versions, only: [:index, :show]