Shopify themes, liquid, logos, and UX
Hi everyone,
I'm using the Dawn 2.0 theme.
wortheattt.myshopify.com
I'm using the “multicolumn” section and in the mobile version its slider looks like this:
I want it to change it to dots like this:
How can I change it?
Thanks
Solved! Go to the solution
This is an accepted solution.
Hi @Ben1000,
Please follow the steps below:
- Step 1: Please go to multicolumn.liquid file and change code here:
before
after
Code:
<div class="slider-counter caption">
<div class="slideshow__control-wrapper">
{%- for block in section.blocks -%}
<button class="slider-counter__link slider-counter__link--dots link" aria-label="{{ 'sections.slideshow.load_slide' | t }} {{ forloop.index }} {{ 'general.slider.of' | t }} {{ forloop.length }}" aria-controls="Slider-{{ section.id }}">
<span class="dot"></span>
</button>
{%- endfor -%}
</div>
</div>
- Step 2: Go to global.js file, find 'class SliderComponent' and change code here:
Code:
class SliderComponent extends HTMLElement {
constructor() {
super();
this.slider = this.querySelector('[id^="Slider-"]');
this.sliderItems = this.querySelectorAll('[id^="Slide-"]');
this.enableSliderLooping = false;
this.currentPageElement = this.querySelector('.slider-counter--current');
this.pageTotalElement = this.querySelector('.slider-counter--total');
this.prevButton = this.querySelector('button[name="previous"]');
this.nextButton = this.querySelector('button[name="next"]');
if (!this.slider || !this.nextButton) return;
this.initPages();
const resizeObserver = new ResizeObserver(entries => this.initPages());
resizeObserver.observe(this.slider);
this.slider.addEventListener('scroll', this.update.bind(this));
this.prevButton.addEventListener('click', this.onButtonClick.bind(this));
this.nextButton.addEventListener('click', this.onButtonClick.bind(this));
this.sliderControlWrapper = this.querySelector('.slider-buttons');
if (!this.sliderControlWrapper) return;
this.sliderFirstItemNode = this.slider.querySelector('.slideshow__slide');
this.sliderControlLinksArray = Array.from(this.sliderControlWrapper.querySelectorAll('.slider-counter__link'));
this.sliderControlLinksArray.forEach(link => link.addEventListener('click', this.linkToSlide.bind(this)));
}
initPages() {
this.sliderItemsToShow = Array.from(this.sliderItems).filter(element => element.clientWidth > 0);
if (this.sliderItemsToShow.length < 2) return;
this.sliderItemOffset = this.sliderItemsToShow[1].offsetLeft - this.sliderItemsToShow[0].offsetLeft;
this.slidesPerPage = Math.floor(this.slider.clientWidth / this.sliderItemOffset);
this.totalPages = this.sliderItemsToShow.length - this.slidesPerPage + 1;
this.update();
}
resetPages() {
this.sliderItems = this.querySelectorAll('[id^="Slide-"]');
this.initPages();
}
update() {
const previousPage = this.currentPage;
this.currentPage = Math.round(this.slider.scrollLeft / this.sliderItemOffset) + 1;
if (this.currentPageElement && this.pageTotalElement) {
this.currentPageElement.textContent = this.currentPage;
this.pageTotalElement.textContent = this.totalPages;
}
if (this.currentPage != previousPage) {
this.dispatchEvent(new CustomEvent('slideChanged', { detail: {
currentPage: this.currentPage,
currentElement: this.sliderItemsToShow[this.currentPage - 1]
}}));
}
if (this.enableSliderLooping) return;
if (this.isSlideVisible(this.sliderItemsToShow[0])) {
this.prevButton.setAttribute('disabled', 'disabled');
} else {
this.prevButton.removeAttribute('disabled');
}
if (this.isSlideVisible(this.sliderItemsToShow[this.sliderItemsToShow.length - 1])) {
this.nextButton.setAttribute('disabled', 'disabled');
} else {
this.nextButton.removeAttribute('disabled');
}
this.sliderControlButtons = this.querySelectorAll('.slider-counter__link');
this.prevButton.removeAttribute('disabled');
if (!this.sliderControlButtons.length) return;
this.sliderControlButtons.forEach(link => {
link.classList.remove('slider-counter__link--active');
link.removeAttribute('aria-current');
});
this.sliderControlButtons[this.currentPage - 1].classList.add('slider-counter__link--active');
this.sliderControlButtons[this.currentPage - 1].setAttribute('aria-current', true);
}
isSlideVisible(element, offset = 0) {
const lastVisibleSlide = this.slider.clientWidth + this.slider.scrollLeft - offset;
return (element.offsetLeft + element.clientWidth) <= lastVisibleSlide && element.offsetLeft >= this.slider.scrollLeft;
}
onButtonClick(event) {
event.preventDefault();
const step = event.currentTarget.dataset.step || 1;
this.slideScrollPosition = event.currentTarget.name === 'next' ? this.slider.scrollLeft + (step * this.sliderItemOffset) : this.slider.scrollLeft - (step * this.sliderItemOffset);
this.slider.scrollTo({
left: this.slideScrollPosition
});
}
linkToSlide(event) {
event.preventDefault();
const slideScrollPosition = this.slider.scrollLeft + this.slider.clientWidth * (this.sliderControlLinksArray.indexOf(event.currentTarget) + 1 - this.currentPage);
this.slider.scrollTo({
left: slideScrollPosition
});
}
}
Hope it helps!
This is an accepted solution.
Hi @Ben1000,
Please follow the steps below:
- Step 1: Please go to multicolumn.liquid file and change code here:
before
after
Code:
<div class="slider-counter caption">
<div class="slideshow__control-wrapper">
{%- for block in section.blocks -%}
<button class="slider-counter__link slider-counter__link--dots link" aria-label="{{ 'sections.slideshow.load_slide' | t }} {{ forloop.index }} {{ 'general.slider.of' | t }} {{ forloop.length }}" aria-controls="Slider-{{ section.id }}">
<span class="dot"></span>
</button>
{%- endfor -%}
</div>
</div>
- Step 2: Go to global.js file, find 'class SliderComponent' and change code here:
Code:
class SliderComponent extends HTMLElement {
constructor() {
super();
this.slider = this.querySelector('[id^="Slider-"]');
this.sliderItems = this.querySelectorAll('[id^="Slide-"]');
this.enableSliderLooping = false;
this.currentPageElement = this.querySelector('.slider-counter--current');
this.pageTotalElement = this.querySelector('.slider-counter--total');
this.prevButton = this.querySelector('button[name="previous"]');
this.nextButton = this.querySelector('button[name="next"]');
if (!this.slider || !this.nextButton) return;
this.initPages();
const resizeObserver = new ResizeObserver(entries => this.initPages());
resizeObserver.observe(this.slider);
this.slider.addEventListener('scroll', this.update.bind(this));
this.prevButton.addEventListener('click', this.onButtonClick.bind(this));
this.nextButton.addEventListener('click', this.onButtonClick.bind(this));
this.sliderControlWrapper = this.querySelector('.slider-buttons');
if (!this.sliderControlWrapper) return;
this.sliderFirstItemNode = this.slider.querySelector('.slideshow__slide');
this.sliderControlLinksArray = Array.from(this.sliderControlWrapper.querySelectorAll('.slider-counter__link'));
this.sliderControlLinksArray.forEach(link => link.addEventListener('click', this.linkToSlide.bind(this)));
}
initPages() {
this.sliderItemsToShow = Array.from(this.sliderItems).filter(element => element.clientWidth > 0);
if (this.sliderItemsToShow.length < 2) return;
this.sliderItemOffset = this.sliderItemsToShow[1].offsetLeft - this.sliderItemsToShow[0].offsetLeft;
this.slidesPerPage = Math.floor(this.slider.clientWidth / this.sliderItemOffset);
this.totalPages = this.sliderItemsToShow.length - this.slidesPerPage + 1;
this.update();
}
resetPages() {
this.sliderItems = this.querySelectorAll('[id^="Slide-"]');
this.initPages();
}
update() {
const previousPage = this.currentPage;
this.currentPage = Math.round(this.slider.scrollLeft / this.sliderItemOffset) + 1;
if (this.currentPageElement && this.pageTotalElement) {
this.currentPageElement.textContent = this.currentPage;
this.pageTotalElement.textContent = this.totalPages;
}
if (this.currentPage != previousPage) {
this.dispatchEvent(new CustomEvent('slideChanged', { detail: {
currentPage: this.currentPage,
currentElement: this.sliderItemsToShow[this.currentPage - 1]
}}));
}
if (this.enableSliderLooping) return;
if (this.isSlideVisible(this.sliderItemsToShow[0])) {
this.prevButton.setAttribute('disabled', 'disabled');
} else {
this.prevButton.removeAttribute('disabled');
}
if (this.isSlideVisible(this.sliderItemsToShow[this.sliderItemsToShow.length - 1])) {
this.nextButton.setAttribute('disabled', 'disabled');
} else {
this.nextButton.removeAttribute('disabled');
}
this.sliderControlButtons = this.querySelectorAll('.slider-counter__link');
this.prevButton.removeAttribute('disabled');
if (!this.sliderControlButtons.length) return;
this.sliderControlButtons.forEach(link => {
link.classList.remove('slider-counter__link--active');
link.removeAttribute('aria-current');
});
this.sliderControlButtons[this.currentPage - 1].classList.add('slider-counter__link--active');
this.sliderControlButtons[this.currentPage - 1].setAttribute('aria-current', true);
}
isSlideVisible(element, offset = 0) {
const lastVisibleSlide = this.slider.clientWidth + this.slider.scrollLeft - offset;
return (element.offsetLeft + element.clientWidth) <= lastVisibleSlide && element.offsetLeft >= this.slider.scrollLeft;
}
onButtonClick(event) {
event.preventDefault();
const step = event.currentTarget.dataset.step || 1;
this.slideScrollPosition = event.currentTarget.name === 'next' ? this.slider.scrollLeft + (step * this.sliderItemOffset) : this.slider.scrollLeft - (step * this.sliderItemOffset);
this.slider.scrollTo({
left: this.slideScrollPosition
});
}
linkToSlide(event) {
event.preventDefault();
const slideScrollPosition = this.slider.scrollLeft + this.slider.clientWidth * (this.sliderControlLinksArray.indexOf(event.currentTarget) + 1 - this.currentPage);
this.slider.scrollTo({
left: slideScrollPosition
});
}
}
Hope it helps!
Hi @LitCommerce,
Thank you for your help! Almost everything is working well:
Thanks!
global.js
class SliderComponent extends HTMLElement {
constructor() {
super();
this.slider = this.querySelector('[id^="Slider-"]');
this.sliderItems = this.querySelectorAll('[id^="Slide-"]');
this.enableSliderLooping = false;
this.currentPageElement = this.querySelector('.slider-counter--current');
this.pageTotalElement = this.querySelector('.slider-counter--total');
this.prevButton = this.querySelector('button[name="previous"]');
this.nextButton = this.querySelector('button[name="next"]');
if (!this.slider || !this.nextButton) return;
this.initPages();
const resizeObserver = new ResizeObserver(entries => this.initPages());
resizeObserver.observe(this.slider);
this.slider.addEventListener('scroll', this.update.bind(this));
this.prevButton.addEventListener('click', this.onButtonClick.bind(this));
this.nextButton.addEventListener('click', this.onButtonClick.bind(this));
}
initPages() {
this.sliderItemsToShow = Array.from(this.sliderItems).filter(element => element.clientWidth > 0);
this.sliderLastItem = this.sliderItemsToShow[this.sliderItemsToShow.length - 1];
if (this.sliderItemsToShow.length === 0) return;
this.slidesPerPage = Math.floor(this.slider.clientWidth / this.sliderItemsToShow[0].clientWidth);
this.totalPages = this.sliderItemsToShow.length - this.slidesPerPage + 1;
this.update();
}
resetPages() {
this.sliderItems = this.querySelectorAll('[id^="Slide-"]');
this.initPages();
}
update() {
const previousPage = this.currentPage;
this.currentPage = Math.round(this.slider.scrollLeft / this.sliderLastItem.clientWidth) + 1;
if (this.currentPageElement && this.pageTotalElement) {
this.currentPageElement.textContent = this.currentPage;
this.pageTotalElement.textContent = this.totalPages;
}
if (this.currentPage != previousPage) {
this.dispatchEvent(new CustomEvent('slideChanged', { detail: {
currentPage: this.currentPage,
currentElement: this.sliderItemsToShow[this.currentPage - 1]
}}));
}
if (this.enableSliderLooping) return;
if (this.isSlideVisible(this.sliderItemsToShow[0])) {
this.prevButton.setAttribute('disabled', 'disabled');
} else {
this.prevButton.removeAttribute('disabled');
}
if (this.isSlideVisible(this.sliderLastItem)) {
this.nextButton.setAttribute('disabled', 'disabled');
} else {
this.nextButton.removeAttribute('disabled');
}
}
isSlideVisible(element, offset = 0) {
const lastVisibleSlide = this.slider.clientWidth + this.slider.scrollLeft - offset;
return (element.offsetLeft + element.clientWidth) <= lastVisibleSlide && element.offsetLeft >= this.slider.scrollLeft;
}
onButtonClick(event) {
event.preventDefault();
const step = event.currentTarget.dataset.step || 1;
this.slideScrollPosition = event.currentTarget.name === 'next' ? this.slider.scrollLeft + (step * this.sliderLastItem.clientWidth) : this.slider.scrollLeft - (step * this.sliderLastItem.clientWidth);
this.slider.scrollTo({
left: this.slideScrollPosition
});
}
}
customElements.define('slider-component', SliderComponent);
class SlideshowComponent extends SliderComponent {
constructor() {
super();
this.sliderControlWrapper = this.querySelector('.slider-buttons');
this.enableSliderLooping = true;
if (!this.sliderControlWrapper) return;
this.sliderFirstItemNode = this.slider.querySelector('.slideshow__slide');
if (this.sliderItemsToShow.length > 0) this.currentPage = 1;
this.sliderControlLinksArray = Array.from(this.sliderControlWrapper.querySelectorAll('.slider-counter__link'));
this.sliderControlLinksArray.forEach(link => link.addEventListener('click', this.linkToSlide.bind(this)));
this.slider.addEventListener('scroll', this.setSlideVisibility.bind(this));
this.setSlideVisibility();
if (this.slider.getAttribute('data-autoplay') === 'true') this.setAutoPlay();
}
setAutoPlay() {
this.sliderAutoplayButton = this.querySelector('.slideshow__autoplay');
this.autoplaySpeed = this.slider.dataset.speed * 1000;
this.sliderAutoplayButton.addEventListener('click', this.autoPlayToggle.bind(this));
this.addEventListener('mouseover', this.focusInHandling.bind(this));
this.addEventListener('mouseleave', this.focusOutHandling.bind(this));
this.addEventListener('focusin', this.focusInHandling.bind(this));
this.addEventListener('focusout', this.focusOutHandling.bind(this));
this.play();
this.autoplayButtonIsSetToPlay = true;
}
onButtonClick(event) {
super.onButtonClick(event);
const isFirstSlide = this.currentPage === 1;
const isLastSlide = this.currentPage === this.sliderItemsToShow.length;
if (!isFirstSlide && !isLastSlide) return;
if (isFirstSlide && event.currentTarget.name === 'previous') {
this.slideScrollPosition = this.slider.scrollLeft + this.sliderFirstItemNode.clientWidth * this.sliderItemsToShow.length;
} else if (isLastSlide && event.currentTarget.name === 'next') {
this.slideScrollPosition = 0;
}
this.slider.scrollTo({
left: this.slideScrollPosition
});
}
update() {
super.update();
this.sliderControlButtons = this.querySelectorAll('.slider-counter__link');
this.prevButton.removeAttribute('disabled');
if (!this.sliderControlButtons.length) return;
this.sliderControlButtons.forEach(link => {
link.classList.remove('slider-counter__link--active');
link.removeAttribute('aria-current');
});
this.sliderControlButtons[this.currentPage - 1].classList.add('slider-counter__link--active');
this.sliderControlButtons[this.currentPage - 1].setAttribute('aria-current', true);
}
autoPlayToggle() {
this.togglePlayButtonState(this.autoplayButtonIsSetToPlay);
this.autoplayButtonIsSetToPlay ? this.pause() : this.play();
this.autoplayButtonIsSetToPlay = !this.autoplayButtonIsSetToPlay;
}
focusOutHandling(event) {
const focusedOnAutoplayButton = event.target === this.sliderAutoplayButton || this.sliderAutoplayButton.contains(event.target);
if (!this.autoplayButtonIsSetToPlay || focusedOnAutoplayButton) return;
this.play();
}
focusInHandling(event) {
const focusedOnAutoplayButton = event.target === this.sliderAutoplayButton || this.sliderAutoplayButton.contains(event.target);
if (focusedOnAutoplayButton && this.autoplayButtonIsSetToPlay) {
this.play();
} else if (this.autoplayButtonIsSetToPlay) {
this.pause();
}
}
play() {
this.slider.setAttribute('aria-live', 'off');
clearInterval(this.autoplay);
this.autoplay = setInterval(this.autoRotateSlides.bind(this), this.autoplaySpeed);
}
pause() {
this.slider.setAttribute('aria-live', 'polite');
clearInterval(this.autoplay);
}
togglePlayButtonState(pauseAutoplay) {
if (pauseAutoplay) {
this.sliderAutoplayButton.classList.add('slideshow__autoplay--paused');
this.sliderAutoplayButton.setAttribute('aria-label', window.accessibilityStrings.playSlideshow);
} else {
this.sliderAutoplayButton.classList.remove('slideshow__autoplay--paused');
this.sliderAutoplayButton.setAttribute('aria-label', window.accessibilityStrings.pauseSlideshow);
}
}
autoRotateSlides() {
const slideScrollPosition = this.currentPage === this.sliderItems.length ? 0 : this.slider.scrollLeft + this.slider.querySelector('.slideshow__slide').clientWidth;
this.slider.scrollTo({
left: slideScrollPosition
});
}
setSlideVisibility() {
this.sliderItemsToShow.forEach((item, index) => {
const button = item.querySelector('a');
if (index === this.currentPage - 1) {
if (button) button.removeAttribute('tabindex');
item.setAttribute('aria-hidden', 'false');
item.removeAttribute('tabindex');
} else {
if (button) button.setAttribute('tabindex', '-1');
item.setAttribute('aria-hidden', 'true');
item.setAttribute('tabindex', '-1');
}
});
}
linkToSlide(event) {
event.preventDefault();
const slideScrollPosition = this.slider.scrollLeft + this.sliderFirstItemNode.clientWidth * (this.sliderControlLinksArray.indexOf(event.currentTarget) + 1 - this.currentPage);
this.slider.scrollTo({
left: slideScrollPosition
});
}
}
customElements.define('slideshow-component', SlideshowComponent);
class VariantSelects extends HTMLElement {
constructor() {
super();
this.addEventListener('change', this.onVariantChange);
}
onVariantChange() {
this.updateOptions();
this.updateMasterId();
this.toggleAddButton(true, '', false);
this.updatePickupAvailability();
this.removeErrorMessage();
if (!this.currentVariant) {
this.toggleAddButton(true, '', true);
this.setUnavailable();
} else {
this.updateMedia();
this.updateURL();
this.updateVariantInput();
this.renderProductInfo();
this.updateShareUrl();
}
}
updateOptions() {
this.options = Array.from(this.querySelectorAll('select'), (select) => select.value);
}
updateMasterId() {
this.currentVariant = this.getVariantData().find((variant) => {
return !variant.options.map((option, index) => {
return this.options[index] === option;
}).includes(false);
});
}
updateMedia() {
if (!this.currentVariant) return;
if (!this.currentVariant.featured_media) return;
const mediaGallery = document.getElementById(`MediaGallery-${this.dataset.section}`);
mediaGallery.setActiveMedia(`${this.dataset.section}-${this.currentVariant.featured_media.id}`, true);
const modalContent = document.querySelector(`#ProductModal-${this.dataset.section} .product-media-modal__content`);
const newMediaModal = modalContent.querySelector( `[data-media-id="${this.currentVariant.featured_media.id}"]`);
modalContent.prepend(newMediaModal);
}
updateURL() {
if (!this.currentVariant || this.dataset.updateUrl === 'false') return;
window.history.replaceState({ }, '', `${this.dataset.url}?variant=${this.currentVariant.id}`);
}
updateShareUrl() {
const shareButton = document.getElementById(`Share-${this.dataset.section}`);
if (!shareButton) return;
shareButton.updateUrl(`${window.shopUrl}${this.dataset.url}?variant=${this.currentVariant.id}`);
}
updateVariantInput() {
const productForms = document.querySelectorAll(`#product-form-${this.dataset.section}, #product-form-installment`);
productForms.forEach((productForm) => {
const input = productForm.querySelector('input[name="id"]');
input.value = this.currentVariant.id;
input.dispatchEvent(new Event('change', { bubbles: true }));
});
}
updatePickupAvailability() {
const pickUpAvailability = document.querySelector('pickup-availability');
if (!pickUpAvailability) return;
if (this.currentVariant && this.currentVariant.available) {
pickUpAvailability.fetchAvailability(this.currentVariant.id);
} else {
pickUpAvailability.removeAttribute('available');
pickUpAvailability.innerHTML = '';
}
}
removeErrorMessage() {
const section = this.closest('section');
if (!section) return;
const productForm = section.querySelector('product-form');
if (productForm) productForm.handleErrorMessage();
}
renderProductInfo() {
fetch(`${this.dataset.url}?variant=${this.currentVariant.id}§ion_id=${this.dataset.section}`)
.then((response) => response.text())
.then((responseText) => {
const id = `price-${this.dataset.section}`;
const html = new DOMParser().parseFromString(responseText, 'text/html')
const destination = document.getElementById(id);
const source = html.getElementById(id);
if (source && destination) destination.innerHTML = source.innerHTML;
const price = document.getElementById(`price-${this.dataset.section}`);
if (price) price.classList.remove('visibility-hidden');
this.toggleAddButton(!this.currentVariant.available, window.variantStrings.soldOut);
});
}
toggleAddButton(disable = true, text, modifyClass = true) {
const productForm = document.getElementById(`product-form-${this.dataset.section}`);
if (!productForm) return;
const addButton = productForm.querySelector('[name="add"]');
const addButtonText = productForm.querySelector('[name="add"] > span');
if (!addButton) return;
if (disable) {
addButton.setAttribute('disabled', 'disabled');
if (text) addButtonText.textContent = text;
} else {
addButton.removeAttribute('disabled');
addButtonText.textContent = window.variantStrings.addToCart;
}
if (!modifyClass) return;
}
setUnavailable() {
const button = document.getElementById(`product-form-${this.dataset.section}`);
const addButton = button.querySelector('[name="add"]');
const addButtonText = button.querySelector('[name="add"] > span');
const price = document.getElementById(`price-${this.dataset.section}`);
if (!addButton) return;
addButtonText.textContent = window.variantStrings.unavailable;
if (price) price.classList.add('visibility-hidden');
}
getVariantData() {
this.variantData = this.variantData || JSON.parse(this.querySelector('[type="application/json"]').textContent);
return this.variantData;
}
}
Hi @Ben1000,
Can you send me link preview site changed code, I will help you to test it
Hi @LitCommerce,
Here is the link to my website.
After implementing the code, I see that this function is shown up twice in global.json:
linkToSlide(event) {
event.preventDefault();
const slideScrollPosition = this.slider.scrollLeft + this.slider.clientWidth * (this.sliderControlLinksArray.indexOf(event.currentTarget) + 1 - this.currentPage);
this.slider.scrollTo({
left: slideScrollPosition
});
}
In:
class SlideshowComponent extends SliderComponent
class SliderComponent extends HTMLElement
Should it be like that?
Here is a reminder for the last message I sent:
Thanks!
Hi @Ben1000,
I checked and everything works fine:
Have you retested it on another device?
Thanks @LitCommerce, what needs to be changed to make the arrows disappear in the slider?
Hi @Ben1000,
Go to Assets > base.css and paste this at the bottom of the file:
.multicolumn .slider-buttons .slider-button {
display: none !important;
}
Hope it helps!
Hello @LitCommerce !
Thank you for this helpfull code it works really well on multicolumn section.
But when I tried it on Main-product.liquid section ( past the same code ) it shows a lot of dots while I have only 4 product images.
Is there any solution you could help with ?
Hi! Nothing happened when I input the codes I have the dawn theme too..
Hi @LitCommerce,
For some reason, these code changes do not affect my slider buttons. I would appreciate your help with this issue.
Thank you
User | RANK |
---|---|
229 | |
165 | |
66 | |
55 | |
49 |
On our Shopify Expert Marketplace, you can find many trusted third party developers and fr...
By Arno Nov 27, 2023You've downloaded the Search & Discovery app from the Shopify App store, and as you're ...
By Skye Nov 8, 2023The year-end shopping season is just around the corner. Is a flash sale on your radar? Are...
By Jasonh Nov 6, 2023