Skip to content

Commit

Permalink
Add organization structured data fields to Spree stores for SEO
Browse files Browse the repository at this point in the history
- Added new fields to spree_stores to support organization structured
data for SEO.
- Fields include legal_name, contact_email, contact_phone, description,
vat_id, tax_id, address1, address2, city, zipcode, state_name,
country_id, and state_id.
- This update enhances SEO by allowing structured data markup for better
search engine visibility.

Enhance store API schema with additional attributes

Updated the API schema for store-related endpoints to include additional
store attributes.

Update admin view for store new and edit form

- Added component for rendering the store edit and new views.
- Added address partial as a separate component within the store form.
- Added the test cases for new, edit and address form component
  • Loading branch information
rahulsingh321 committed Feb 27, 2025
1 parent cc2f7cb commit bbff7bc
Show file tree
Hide file tree
Showing 24 changed files with 919 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<fieldset class="<%= stimulus_id %>"
data-controller="<%= stimulus_id %>"
>
<div class="<%= stimulus_id %>--address-form flex flex-wrap gap-4 pb-4">
<%= render component("ui/forms/field").text_field(@name, :legal_name, object: @store) %>
<%= render component("ui/forms/field").text_field(@name, :address1, object: @store) %>
<%= render component("ui/forms/field").text_field(@name, :address2, object: @store) %>
<div class="flex gap-4 w-full">
<%= render component("ui/forms/field").text_field(@name, :city, object: @store) %>
<%= render component("ui/forms/field").text_field(@name, :zipcode, object: @store) %>
</div>

<%= render component("ui/forms/field").select(
@name,
:country_id,
Spree::Country.pluck(:name, :id),
object: @store,
value: @store.country_id,
"data-#{stimulus_id}-target": "country",
"data-action": "change->#{stimulus_id}#loadStates"
) %>

<%= content_tag :div,
class: "flex flex-col gap-2 w-full #{'hidden' unless @store.country&.states_required}",
data: { "#{stimulus_id}-target": "stateNameWrapper" } do %>
<%= render component("ui/forms/field").text_field(
@name, :state_name,
object: @store,
value: @store.state_name,
data: { "#{stimulus_id}-target": "stateName" }
) %>
<% end %>

<input autocomplete="off" type="hidden" name=<%= "#{@name}[state_id]" %>>

<%= content_tag :div,
class: "flex flex-col gap-2 w-full #{'hidden' if @store.country&.states_required}",
data: { "#{stimulus_id}-target": "stateWrapper" } do %>
<%= render component("ui/forms/field").select(
@name, :state_id,
state_options,
object: @store,
value: @store.state_id,
data: { "#{stimulus_id}-target": "state" }
) %>
<% end %>

<%= render component("ui/forms/field").text_field(@name, :contact_phone, object: @store) %>
</div>
</fieldset>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["country", "state", "stateName", "stateWrapper", "stateNameWrapper"]

loadStates() {
const countryId = this.countryTarget.value

fetch(`/admin/countries/${countryId}/states`)
.then((response) => response.json())
.then((data) => {
this.updateStateOptions(data)
})
}

updateStateOptions(states) {
if (states.length === 0) {
this.toggleStateFields(false)
} else {
this.toggleStateFields(true)
this.populateStateSelect(states)
}
}

toggleStateFields(showSelect) {
const stateWrapper = this.stateWrapperTarget
const stateNameWrapper = this.stateNameWrapperTarget
const stateSelect = this.stateTarget
const stateName = this.stateNameTarget

if (showSelect) {
// Show state select dropdown.
stateSelect.disabled = false
stateName.value = ""
stateWrapper.classList.remove("hidden")
stateNameWrapper.classList.add("hidden")
} else {
// Show state name text input if no states to choose from.
stateSelect.disabled = true
stateWrapper.classList.add("hidden")
stateNameWrapper.classList.remove("hidden")
}
}

populateStateSelect(states) {
const stateSelect = this.stateTarget
stateSelect.innerHTML = ""

states.forEach((state) => {
const option = document.createElement("option")
option.value = state.id
option.innerText = state.name
stateSelect.appendChild(option)
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class SolidusAdmin::Stores::AddressForm::Component < SolidusAdmin::BaseComponent
def initialize(store:)
@name = "store"
@store = store
end

def state_options
country = @store.country
return [] unless country && country.states_required

country.states.pluck(:name, :id)
end
end
65 changes: 65 additions & 0 deletions admin/app/components/solidus_admin/stores/edit/component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<%= page do %>
<%= page_header do %>
<%= page_header_back(solidus_admin.stores_path) %>
<%= page_header_title(t(".title", store: @store&.name)) %>
<% end %>

<%= form_for @store, url: solidus_admin.store_path(@store), html: { id: form_id } do |f| %>
<%= page_with_sidebar do %>
<%= page_with_sidebar_main do %>
<%= render component("ui/panel").new(title: t(".store_details")) do %>
<div class="flex flex-wrap gap-4 pb-4">
<%= render component("ui/forms/field").text_field(f, :name, required: true) %>
<%= render component("ui/forms/field").text_field(f, :code, required: true) %>
<%= render component("ui/forms/field").text_field(f, :seo_title) %>
<%= render component("ui/forms/field").text_field(f, :meta_keywords) %>
<%= render component("ui/forms/field").text_area(f, :meta_description) %>
<%= render component("ui/forms/field").text_field(f, :tax_id) %>
<%= render component("ui/forms/field").text_field(f, :vat_id) %>
<%= render component("ui/forms/field").text_field(f, :url, required: true) %>
<%= render component("ui/forms/field").text_field(f, :mail_from_address, required: true) %>
<%= render component("ui/forms/field").text_field(f, :bcc_email) %>
<%= render component("ui/forms/field").select(
f,
:default_currency,
currency_options,
include_blank: true
) %>
<%= render component("ui/forms/field").select(
f,
:cart_tax_country_iso,
cart_tax_country_options,
include_blank: t(".no_cart_tax_country")
) %>
<%= render component("ui/forms/field").select(
f,
:available_locales,
localization_options,
multiple: true,
class: "select2",
name: "store[available_locales][]"
) %>
</div>
<%= render component("ui/forms/field").text_area(f, :description) %>
<% end %>

<%= render component("ui/panel").new(title: t(".address")) do %>
<div class="js-addresses-form">
<%= render component("stores/address_form").new(
store: @store,
) %>
</div>
<% end %>
<% end %>
<% end %>
<% end %>

<%= page_footer do %>
<%= page_footer_actions do %>
<div class="py-1.5 text-center">
<%= render component("ui/button").new(tag: :button, text: t(".update"), form: form_id) %>
<%= render component("ui/button").new(tag: :a, text: t(".cancel"), href: solidus_admin.edit_store_path(@store), scheme: :secondary) %>
</div>
<% end %>
<% end %>
<% end %>
62 changes: 62 additions & 0 deletions admin/app/components/solidus_admin/stores/edit/component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

class SolidusAdmin::Stores::Edit::Component < SolidusAdmin::BaseComponent
include SolidusAdmin::Layout::PageHelpers

# Define the necessary attributes for the component
attr_reader :store, :available_countries

# Initialize the component with required data
def initialize(store:)
@store = store
@available_countries = fetch_available_countries
end

def form_id
@form_id ||= "#{stimulus_id}--form-#{@store.id}"
end

def currency_options
Spree::Config.available_currencies.map(&:iso_code)
end

# Generates options for cart tax countries
def cart_tax_country_options
fetch_available_countries(restrict_to_zone: Spree::Config[:checkout_zone]).map do |country|
[country.name, country.iso]
end
end

# Generates available locales
def localization_options
Spree.i18n_available_locales.map do |locale|
[
I18n.t('spree.i18n.this_file_language', locale: locale, default: locale.to_s),
locale
]
end
end

# Fetch countries for the address form
def available_country_options
Spree::Country.order(:name).map { |country| [country.name, country.id] }
end

private

# Fetch the available countries for the localization section
def fetch_available_countries(restrict_to_zone: Spree::Config[:checkout_zone])
countries = Spree::Country.available(restrict_to_zone:)

country_names = Carmen::Country.all.map do |country|
[country.code, country.name]
end.to_h

country_names.update I18n.t('spree.country_names', default: {}).stringify_keys

countries.collect do |country|
country.name = country_names.fetch(country.iso, country.name)
country
end.sort_by { |country| country.name.parameterize }
end
end
6 changes: 6 additions & 0 deletions admin/app/components/solidus_admin/stores/edit/component.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
en:
title: "%{store}"
store_details: Store Details
address: Address
update: Update
cancel: Cancel
65 changes: 65 additions & 0 deletions admin/app/components/solidus_admin/stores/new/component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<%= page do %>
<%= page_header do %>
<%= page_header_back(solidus_admin.stores_path) %>
<%= page_header_title(t(".title")) %>
<% end %>

<%= form_for @store, url: solidus_admin.stores_path, html: { id: form_id } do |f| %>
<%= page_with_sidebar do %>
<%= page_with_sidebar_main do %>
<%= render component("ui/panel").new(title: t(".store_details")) do %>
<div class="flex flex-wrap gap-4 pb-4">
<%= render component("ui/forms/field").text_field(f, :name, required: true) %>
<%= render component("ui/forms/field").text_field(f, :code, required: true) %>
<%= render component("ui/forms/field").text_field(f, :seo_title) %>
<%= render component("ui/forms/field").text_field(f, :meta_keywords) %>
<%= render component("ui/forms/field").text_area(f, :meta_description) %>
<%= render component("ui/forms/field").text_field(f, :tax_id) %>
<%= render component("ui/forms/field").text_field(f, :vat_id) %>
<%= render component("ui/forms/field").text_field(f, :url, required: true) %>
<%= render component("ui/forms/field").text_field(f, :mail_from_address, required: true) %>
<%= render component("ui/forms/field").text_field(f, :bcc_email) %>
<%= render component("ui/forms/field").select(
f,
:default_currency,
currency_options,
include_blank: true
) %>
<%= render component("ui/forms/field").select(
f,
:cart_tax_country_iso,
cart_tax_country_options,
include_blank: t(".no_cart_tax_country")
) %>
<%= render component("ui/forms/field").select(
f,
:available_locales,
localization_options,
multiple: true,
class: "select2",
name: "store[available_locales][]"
) %>
</div>
<%= render component("ui/forms/field").text_area(f, :description) %>
<% end %>

<%= render component("ui/panel").new(title: t(".address")) do %>
<div class="js-addresses-form">
<%= render component("stores/address_form").new(
store: @store,
) %>
</div>
<% end %>
<% end %>
<% end %>
<% end %>

<%= page_footer do %>
<%= page_footer_actions do %>
<div class="py-1.5 text-center">
<%= render component("ui/button").new(tag: :button, text: t(".save"), form: form_id) %>
<%= render component("ui/button").new(tag: :a, text: t(".cancel"), href: solidus_admin.new_store_path, scheme: :secondary) %>
</div>
<% end %>
<% end %>
<% end %>
62 changes: 62 additions & 0 deletions admin/app/components/solidus_admin/stores/new/component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

class SolidusAdmin::Stores::New::Component < SolidusAdmin::BaseComponent
include SolidusAdmin::Layout::PageHelpers

# Define the necessary attributes for the component
attr_reader :store, :available_countries

# Initialize the component with required data
def initialize(store:)
@store = store
@available_countries = fetch_available_countries
end

def form_id
@form_id ||= "#{stimulus_id}--form-#{@store.id}"
end

def currency_options
Spree::Config.available_currencies.map(&:iso_code)
end

# Generates options for cart tax countries
def cart_tax_country_options
fetch_available_countries(restrict_to_zone: Spree::Config[:checkout_zone]).map do |country|
[country.name, country.iso]
end
end

# Generates available locales
def localization_options
Spree.i18n_available_locales.map do |locale|
[
I18n.t('spree.i18n.this_file_language', locale: locale, default: locale.to_s),
locale
]
end
end

# Fetch countries for the address form
def available_country_options
Spree::Country.order(:name).map { |country| [country.name, country.id] }
end

private

# Fetch the available countries for the localization section
def fetch_available_countries(restrict_to_zone: Spree::Config[:checkout_zone])
countries = Spree::Country.available(restrict_to_zone:)

country_names = Carmen::Country.all.map do |country|
[country.code, country.name]
end.to_h

country_names.update I18n.t('spree.country_names', default: {}).stringify_keys

countries.collect do |country|
country.name = country_names.fetch(country.iso, country.name)
country
end.sort_by { |country| country.name.parameterize }
end
end
6 changes: 6 additions & 0 deletions admin/app/components/solidus_admin/stores/new/component.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
en:
title: "New Store"
save: Save
store_details: Store Details
address: Address
cancel: Cancel
Loading

0 comments on commit bbff7bc

Please sign in to comment.