diff --git a/app/controllers/resources_controller.rb b/app/controllers/resources_controller.rb
index 2b42dfce5..92a9003e0 100644
--- a/app/controllers/resources_controller.rb
+++ b/app/controllers/resources_controller.rb
@@ -79,7 +79,7 @@ def create
end
if success
- redirect_to resources_path
+ redirect_to resource_path(@resource)
else
@resource = @resource.decorate
set_form_variables
@@ -106,7 +106,7 @@ def update
if success
flash[:notice] = "Resource updated."
- redirect_to resources_path
+ redirect_to resource_path(@resource)
else
set_form_variables
flash[:alert] = "Failed to update Resource."
diff --git a/app/frontend/javascript/controllers/file_preview_controller.js b/app/frontend/javascript/controllers/file_preview_controller.js
index 42bbaccdf..064db5f68 100644
--- a/app/frontend/javascript/controllers/file_preview_controller.js
+++ b/app/frontend/javascript/controllers/file_preview_controller.js
@@ -1,30 +1,109 @@
import { Controller } from "@hotwired/stimulus"
+const FILE_TYPE_ICONS = {
+ "application/pdf": "fa-regular fa-file-pdf",
+ "application/zip": "fa-regular fa-file-zipper",
+ "application/msword": "fa-regular fa-file-word",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "fa-regular fa-file-word",
+ "application/vnd.oasis.opendocument.text": "fa-regular fa-file-lines",
+ "text/html": "fa-regular fa-file-code",
+}
+
export default class extends Controller {
static targets = ["input", "preview", "placeholder", "filename"]
+ connect() {
+ this.handleUploadError = this.onUploadError.bind(this)
+ this.handleUploadEnd = this.onUploadEnd.bind(this)
+ this.element.addEventListener("direct-upload:error", this.handleUploadError)
+ this.element.addEventListener("direct-upload:end", this.handleUploadEnd)
+ }
+
+ disconnect() {
+ this.element.removeEventListener("direct-upload:error", this.handleUploadError)
+ this.element.removeEventListener("direct-upload:end", this.handleUploadEnd)
+ }
+
update(event) {
const file = event.target.files[0]
if (!file) return
+ this.clearError()
+
// Update filename
if (this.hasFilenameTarget) {
this.filenameTarget.textContent = file.name
}
- // Update preview image (if you have one)
- if (this.hasPreviewTarget) {
- const reader = new FileReader()
- reader.onload = e => {
- this.previewTarget.src = e.target.result
- this.previewTarget.classList.remove("hidden")
- }
- reader.readAsDataURL(file)
+ if (file.type.startsWith("image/")) {
+ this.showImagePreview(file)
+ } else {
+ this.showFileIcon(file.type)
}
+ }
+
+ // Ensure a placeholder div exists (server may not render one when a persisted file exists)
+ ensurePlaceholder() {
+ if (this.hasPlaceholderTarget) return this.placeholderTarget
+
+ const wrapper = this.hasPreviewTarget
+ ? this.previewTarget.parentElement
+ : this.element.querySelector(".flex.flex-col")
+
+ const placeholder = document.createElement("div")
+ placeholder.dataset.filePreviewTarget = "placeholder"
+ placeholder.className = "absolute inset-0 flex items-center justify-center border border-gray-300 shadow-sm bg-gray-50 text-gray-400 text-6xl"
+ wrapper.appendChild(placeholder)
+ return placeholder
+ }
+
+ showImagePreview(file) {
+ if (!this.hasPreviewTarget) return
+
+ const reader = new FileReader()
+ reader.onload = e => {
+ this.previewTarget.src = e.target.result
+ this.previewTarget.classList.remove("hidden")
+ }
+ reader.readAsDataURL(file)
- // Hide placeholder if present
if (this.hasPlaceholderTarget) {
this.placeholderTarget.classList.add("hidden")
}
}
+
+ showFileIcon(mimeType) {
+ const placeholder = this.ensurePlaceholder()
+ const iconClass = FILE_TYPE_ICONS[mimeType] || "fa-regular fa-file"
+ placeholder.innerHTML = ``
+ placeholder.classList.remove("hidden")
+
+ if (this.hasPreviewTarget) {
+ this.previewTarget.classList.add("hidden")
+ }
+ }
+
+ onUploadError(event) {
+ event.preventDefault()
+ const { error } = event.detail
+ this.showError(`Upload failed: ${error}`)
+ }
+
+ onUploadEnd(event) {
+ this.clearError()
+ }
+
+ showError(message) {
+ this.clearError()
+ const errorEl = document.createElement("p")
+ errorEl.className = "text-red-500 text-sm mt-1"
+ errorEl.dataset.filePreviewTarget = "error"
+ errorEl.textContent = message
+ this.element.appendChild(errorEl)
+ }
+
+ clearError() {
+ const existing = this.element.querySelector('[data-file-preview-target="error"]')
+ if (existing) existing.remove()
+ }
}
diff --git a/app/models/asset.rb b/app/models/asset.rb
index c2d0acc9e..056f89dcd 100644
--- a/app/models/asset.rb
+++ b/app/models/asset.rb
@@ -5,6 +5,9 @@ class Asset < ApplicationRecord
"image/jpeg",
"image/png",
"image/gif",
+ "image/webp",
+ "image/heic",
+ "image/heif",
"application/pdf",
"application/zip",
"application/msword", # Word .doc
@@ -39,6 +42,29 @@ def self.allowed_types_for_owner(owner)
end
end
+ CONTENT_TYPE_LABELS = {
+ "image/jpeg" => "JPG",
+ "image/png" => "PNG",
+ "image/gif" => "GIF",
+ "image/webp" => "WebP",
+ "image/heic" => "HEIC",
+ "image/heif" => "HEIF",
+ "application/pdf" => "PDF",
+ "application/zip" => "ZIP",
+ "application/msword" => "DOC",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => "DOCX",
+ "application/vnd.oasis.opendocument.text" => "ODT",
+ "text/html" => "HTML"
+ }.freeze
+
+ def self.accept_attribute
+ self::ACCEPTED_CONTENT_TYPES.join(",")
+ end
+
+ def self.accepted_types_label
+ self::ACCEPTED_CONTENT_TYPES.map { |ct| CONTENT_TYPE_LABELS[ct] || ct }.join(", ")
+ end
+
belongs_to :owner, polymorphic: true, optional: true, touch: true
belongs_to :report, optional: true
@@ -55,16 +81,10 @@ def self.allowed_types_for_owner(owner)
def file_type
return unless file.attached?
- allowed_types =
- case type
- when "PrimaryAsset"
- PrimaryAsset::ACCEPTED_CONTENT_TYPES
- else
- ACCEPTED_CONTENT_TYPES
- end
+ allowed_types = self.class::ACCEPTED_CONTENT_TYPES
unless allowed_types.include?(file.content_type)
- errors.add(:file, "type is not allowed for #{type.underscore.humanize}")
+ errors.add(:file, "type not accepted")
end
end
end
diff --git a/app/models/downloadable_asset.rb b/app/models/downloadable_asset.rb
index 558f55bce..1fa2c8b85 100644
--- a/app/models/downloadable_asset.rb
+++ b/app/models/downloadable_asset.rb
@@ -3,6 +3,9 @@ class DownloadableAsset < Asset
"image/jpeg",
"image/png",
"image/gif",
+ "image/webp",
+ "image/heic",
+ "image/heif",
"application/pdf",
"application/zip",
"application/msword", # Word .doc
diff --git a/app/models/primary_asset.rb b/app/models/primary_asset.rb
index 66d2ce6e8..5e74e62cb 100644
--- a/app/models/primary_asset.rb
+++ b/app/models/primary_asset.rb
@@ -1,3 +1,9 @@
class PrimaryAsset < Asset
- ACCEPTED_CONTENT_TYPES = [ "image/jpeg", "image/png" ].freeze
+ ACCEPTED_CONTENT_TYPES = [
+ "image/jpeg",
+ "image/png",
+ "image/webp",
+ "image/heic",
+ "image/heif"
+ ].freeze
end
diff --git a/app/models/rich_text_asset.rb b/app/models/rich_text_asset.rb
index 4694dcebd..e0069a023 100644
--- a/app/models/rich_text_asset.rb
+++ b/app/models/rich_text_asset.rb
@@ -5,6 +5,9 @@ class RichTextAsset < Asset
"image/jpeg",
"image/png",
"image/gif",
+ "image/webp",
+ "image/heic",
+ "image/heif",
"application/pdf",
"application/zip",
"application/msword", # Word .doc
diff --git a/app/views/assets/_display_assets.html.erb b/app/views/assets/_display_assets.html.erb
index 76d95bc4d..a0cbabb96 100644
--- a/app/views/assets/_display_assets.html.erb
+++ b/app/views/assets/_display_assets.html.erb
@@ -1,4 +1,4 @@
- <%= render "assets/display_image", resource: resource, file: (defined?(file) ? file : nil), variant: (defined?(variant) ? variant : :hero) %>
- <%= render "assets/display_gallery_media", resource: resource %>
+ <%= render "assets/display_image", resource: resource, file: (defined?(file) ? file : nil), variant: (defined?(variant) ? variant : :hero), link: (defined?(link) ? link : nil) %>
+ <%= render "assets/display_gallery_media", resource: resource, link: (defined?(link) ? link : nil) %>
diff --git a/app/views/assets/_display_gallery_media.html.erb b/app/views/assets/_display_gallery_media.html.erb
index e7786cfe9..4a91c2dad 100644
--- a/app/views/assets/_display_gallery_media.html.erb
+++ b/app/views/assets/_display_gallery_media.html.erb
@@ -4,7 +4,7 @@
<% gallery_assets.each_with_index do |gallery_assets, idx| %>
- <%= render "assets/display_image", item: gallery_assets, idx: idx, variant: :gallery %>
+ <%= render "assets/display_image", item: gallery_assets, idx: idx, variant: :gallery, link: (defined?(link) ? link : nil) %>
<% end %>
diff --git a/app/views/assets/_display_image.html.erb b/app/views/assets/_display_image.html.erb
index a2f8332cf..20ca894cc 100644
--- a/app/views/assets/_display_image.html.erb
+++ b/app/views/assets/_display_image.html.erb
@@ -4,7 +4,7 @@
<% file ||= item&.file || resource&.try(field_name)&.file || file %>
<% variant ||= :gallery %>
<% link_to_object ||= false %>
-<% link ||= link_to_object %>
+<% link = (defined?(link) && !link.nil?) ? link : link_to_object %>
<% display_open_pdf_link ||= false %>
<% idx ||= 0 %>
<% item_type ||= "PrimaryAsset" %>
diff --git a/app/views/community_news/show.html.erb b/app/views/community_news/show.html.erb
index 7de94edee..a219cbfb0 100644
--- a/app/views/community_news/show.html.erb
+++ b/app/views/community_news/show.html.erb
@@ -34,7 +34,7 @@
- <%= render "assets/display_assets", resource: @community_news %>
+ <%= render "assets/display_assets", resource: @community_news, link: true %>
diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb
index 2dc5a111c..8b45eb41f 100644
--- a/app/views/events/show.html.erb
+++ b/app/views/events/show.html.erb
@@ -60,7 +60,7 @@
- <%= render "assets/display_assets", resource: @event %>
+ <%= render "assets/display_assets", resource: @event, link: true %>
diff --git a/app/views/resources/_resource_card.html.erb b/app/views/resources/_resource_card.html.erb
index c21a1c24b..b2b59504b 100644
--- a/app/views/resources/_resource_card.html.erb
+++ b/app/views/resources/_resource_card.html.erb
@@ -52,7 +52,11 @@
<% end %>
-
+
+ <% if allowed_to?(:edit?, resource) %>
+
<%= resource.created_at.strftime("%b %-d, %Y") %>
+ <% end %>
+
<% if resource.downloadable_asset&.file&.attached? %>
<%= link_to(
content_tag(:span, "", class: "fa fa-download"),
@@ -69,6 +73,7 @@
data: {turbo_frame: "_top", turbo_prefetch: false }
) %>
<% end %>
+
diff --git a/app/views/resources/show.html.erb b/app/views/resources/show.html.erb
index c426a94c3..1dac56d35 100644
--- a/app/views/resources/show.html.erb
+++ b/app/views/resources/show.html.erb
@@ -52,7 +52,7 @@
- <%= render "assets/display_assets", resource: @resource, file: @resource.display_image, variant: :hero %>
+ <%= render "assets/display_assets", resource: @resource, file: @resource.display_image, variant: :hero, link: true %>
diff --git a/app/views/shared/_form_image_field.html.erb b/app/views/shared/_form_image_field.html.erb
index 08e7efbb0..2bcb8a5e8 100644
--- a/app/views/shared/_form_image_field.html.erb
+++ b/app/views/shared/_form_image_field.html.erb
@@ -20,6 +20,8 @@
<% use_profile_placeholder ||= false %>
<% show_file_field = show_file_field.to_s.present? ? show_file_field : true %>
<% show_existing = show_existing.to_s.present? ? show_existing : true %>
+<% accept_attr = asset.class.respond_to?(:accept_attribute) ? asset.class.accept_attribute : nil %>
+<% accepted_label = asset.class.respond_to?(:accepted_types_label) ? asset.class.accepted_types_label : nil %>
<% file =
if file.present?
file
@@ -92,6 +94,7 @@
<%= f.file_field field_name,
+ accept: accept_attr,
data: {
file_preview_target: "input",
action: "change->file-preview#update"
@@ -118,9 +121,20 @@
<% end %>
+
+ <% if accepted_label.present? %>
+ Accepts: <%= accepted_label %>
+ <% end %>
<% end %>
-<%= f.error :file, wrap_with: { tag: :p, class: "text-red-500 text-sm mt-1" } %>
+<% if asset.errors[:file].any? %>
+
+
+
+ File type not accepted
+
+
+<% end %>
diff --git a/app/views/stories/show.html.erb b/app/views/stories/show.html.erb
index 526304ab3..582d0e78d 100644
--- a/app/views/stories/show.html.erb
+++ b/app/views/stories/show.html.erb
@@ -56,7 +56,7 @@
- <%= render "assets/display_assets", resource: @story %>
+ <%= render "assets/display_assets", resource: @story, link: true %>
diff --git a/app/views/story_ideas/show.html.erb b/app/views/story_ideas/show.html.erb
index 9879ff5bf..54c6fd695 100644
--- a/app/views/story_ideas/show.html.erb
+++ b/app/views/story_ideas/show.html.erb
@@ -35,7 +35,7 @@
- <%= render "assets/display_assets", resource: @story_idea %>
+ <%= render "assets/display_assets", resource: @story_idea, link: true %>
diff --git a/app/views/story_shares/show.html.erb b/app/views/story_shares/show.html.erb
index 2e5aff078..7a83307ba 100644
--- a/app/views/story_shares/show.html.erb
+++ b/app/views/story_shares/show.html.erb
@@ -8,7 +8,8 @@
<%= render "assets/display_image",
resource: @story,
- variant: :hero %>
+ variant: :hero,
+ link: true %>
@@ -81,7 +82,7 @@
<% if @story.assets.count > 1 %>
- <%= render "assets/display_assets", resource: @story %>
+ <%= render "assets/display_assets", resource: @story, link: true %>
<% end %>
diff --git a/app/views/tutorials/show.html.erb b/app/views/tutorials/show.html.erb
index a06951984..017db120e 100644
--- a/app/views/tutorials/show.html.erb
+++ b/app/views/tutorials/show.html.erb
@@ -36,7 +36,7 @@
<% end %>
- <%= render "assets/display_assets", resource: @tutorial %>
+ <%= render "assets/display_assets", resource: @tutorial, link: true %>
diff --git a/app/views/workshop_ideas/show.html.erb b/app/views/workshop_ideas/show.html.erb
index 040ef44ce..931dff7ce 100644
--- a/app/views/workshop_ideas/show.html.erb
+++ b/app/views/workshop_ideas/show.html.erb
@@ -101,6 +101,6 @@
- <%= render "assets/display_assets", resource: @workshop_idea %>
+ <%= render "assets/display_assets", resource: @workshop_idea, link: true %>
diff --git a/app/views/workshop_variation_ideas/show.html.erb b/app/views/workshop_variation_ideas/show.html.erb
index 648dac667..bad75f3f3 100644
--- a/app/views/workshop_variation_ideas/show.html.erb
+++ b/app/views/workshop_variation_ideas/show.html.erb
@@ -47,6 +47,6 @@
- <%= render "assets/display_assets", resource: @workshop_variation_idea %>
+ <%= render "assets/display_assets", resource: @workshop_variation_idea, link: true %>
diff --git a/app/views/workshop_variations/show.html.erb b/app/views/workshop_variations/show.html.erb
index 4d9b9bfcc..9337971ab 100644
--- a/app/views/workshop_variations/show.html.erb
+++ b/app/views/workshop_variations/show.html.erb
@@ -46,7 +46,7 @@
- <%= render "assets/display_assets", resource: @workshop_variation %>
+ <%= render "assets/display_assets", resource: @workshop_variation, link: true %>
<% if @workshop_variation.youtube_url.present? %>
diff --git a/package-lock.json b/package-lock.json
index fd72a7c2c..5da9f8fc6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4,6 +4,7 @@
"requires": true,
"packages": {
"": {
+ "name": "awbw",
"dependencies": {
"@fortawesome/fontawesome-free": "^7.0.1",
"@hotwired/stimulus": "^3.2.2",
@@ -1092,6 +1093,60 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.4.5",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.0.4",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.4.5",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.0.4",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "0.2.12",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.4.3",
+ "@emnapi/runtime": "^1.4.3",
+ "@tybys/wasm-util": "^0.10.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
+ "version": "0.10.0",
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
+ "version": "2.8.0",
+ "inBundle": true,
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz",
diff --git a/package.json b/package.json
index a3275fc94..34b9e6b99 100644
--- a/package.json
+++ b/package.json
@@ -1,4 +1,5 @@
{
+ "name": "awbw",
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"rollup-plugin-visualizer": "^6.0.5",
diff --git a/spec/requests/resources_spec.rb b/spec/requests/resources_spec.rb
index 8fb7bfb24..ddb603fbc 100644
--- a/spec/requests/resources_spec.rb
+++ b/spec/requests/resources_spec.rb
@@ -78,10 +78,10 @@
}.to change(Resource, :count).by(1)
end
- it "redirects to the resources index" do
+ it "redirects to the created resource" do
post resources_url, params: { resource: valid_attributes }
- expect(response).to redirect_to(resources_url)
+ expect(response).to redirect_to(resource_url(Resource.last))
end
end
@@ -114,11 +114,11 @@
expect(resource.title).to eq("Updated Resource Title")
end
- it "redirects to the resources index" do
+ it "redirects to the updated resource" do
resource = Resource.create! valid_attributes
patch resource_url(resource), params: { resource: new_attributes }
- expect(response).to redirect_to(resources_url)
+ expect(response).to redirect_to(resource_url(resource))
end
end
diff --git a/spec/views/resources/show.html.erb_spec.rb b/spec/views/resources/show.html.erb_spec.rb
new file mode 100644
index 000000000..b51caea2f
--- /dev/null
+++ b/spec/views/resources/show.html.erb_spec.rb
@@ -0,0 +1,46 @@
+require "rails_helper"
+
+RSpec.describe "resources/show", type: :view do
+ let(:admin) { create(:user, :admin) }
+ let(:resource) { create(:resource, :published, user: admin) }
+
+ before do
+ sign_in admin
+ allow(view).to receive(:current_user).and_return(admin)
+ allow(view).to receive(:allowed_to?).and_return(true)
+ assign(:resource, resource.decorate)
+ assign(:mentions, [])
+ end
+
+ context "when resource has an attached primary image" do
+ before do
+ create(:primary_asset, :with_file, owner: resource)
+ resource.reload
+ render
+ end
+
+ it "wraps the hero image in a link that opens in a new tab" do
+ expect(rendered).to have_css("a.display-image-link[target='_blank'][rel='noopener noreferrer']")
+ end
+ end
+
+ context "when resource has an attached gallery image" do
+ before do
+ create(:gallery_asset, :with_file, owner: resource)
+ resource.reload
+ render
+ end
+
+ it "wraps the gallery image in a link that opens in a new tab" do
+ expect(rendered).to have_css("a.display-image-link[target='_blank'][rel='noopener noreferrer']")
+ end
+ end
+
+ context "when resource has no attached image" do
+ before { render }
+
+ it "renders without errors" do
+ expect(rendered).to have_text(resource.title)
+ end
+ end
+end