Allow users to dismiss announcements (#8396) [deploy]
* Provide default crayon styles when creating banners * Import crayons styles into /internal pages. * Allow admins to use crayons banner styles when creating broadcasts. * Preview banner styles with crayons. * Add a "x" close button to announcements, fix styling to accommodate it * Use constant and helper for broadcast banner styles * Add VALID_BANNER_STYLES frozen constant to Broadcast class. * Add banner_style helper for determining banner style class. * Clean up preview CSS, import broadcast styles into preview * Add close button click handler, clean up initializeBroadcast * Add close functionality to announcement banner Also hide announcement if the user has explicitly seen and "X"-ed out of it. * Add system specs around rendering + dismissing broadcasts * Add some (truly beautiful) JS documentation * Add visible class to broadcast when previewed from internal
This commit is contained in:
parent
2b30f769f3
commit
e532ccbe42
7 changed files with 202 additions and 34 deletions
|
|
@ -3,6 +3,7 @@
|
|||
* Parses the broadcast object on the document into JSON.
|
||||
*
|
||||
* @function broadcastData
|
||||
* @returns {Object} Returns an object of the parsed broadcast data.
|
||||
*/
|
||||
function broadcastData() {
|
||||
const { broadcast = null } = document.body.dataset;
|
||||
|
|
@ -11,8 +12,77 @@ function broadcastData() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Inserts the broadcast's HTML into `active-broadcast` element
|
||||
* as the first child within the document's body, and only inserts the HTML once.
|
||||
* Parses the broadcast object on the document into JSON.
|
||||
*
|
||||
* @function camelizedBroadcastKey
|
||||
* @param {string} title The title of the broadcast.
|
||||
* @returns {string} Returns the camelized title appended with "Seen".
|
||||
*/
|
||||
function camelizedBroadcastKey(title) {
|
||||
const camelizedTitle = title.replace(/\W+(.)/g, (match, string) => {
|
||||
return string.toUpperCase();
|
||||
});
|
||||
|
||||
return `${camelizedTitle}Seen`;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that finds the close button and adds a click handler to it.
|
||||
*
|
||||
* @function addCloseButtonClickHandle
|
||||
* @param {string} title The title of the broadcast.
|
||||
*/
|
||||
function addCloseButtonClickHandle(title) {
|
||||
var closeButton = document.getElementsByClassName(
|
||||
'close-announcement-button',
|
||||
)[0];
|
||||
closeButton.onclick = (e) => {
|
||||
document.getElementById('active-broadcast').style.display = 'none';
|
||||
localStorage.setItem(camelizedBroadcastKey(title), true);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to insert the broadcast's HTML into the `active-broadcast` element.
|
||||
* Determines what classes to add to the broadcast element,
|
||||
* and inserts a close button and adds a click handler to it.
|
||||
*
|
||||
* Adds a `.visible` class to the broadcastElement to make it render.
|
||||
*
|
||||
* @function initializeBroadcast
|
||||
* @param {string} broadcastElement The HTML element for the broadcast, with a class of `.active-broadcast`.
|
||||
* @param {Object} data An object representing the parsed broadcast data.
|
||||
*/
|
||||
function renderBroadcast(broadcastElement, data) {
|
||||
const { banner_class, html, title } = data;
|
||||
|
||||
if (banner_class) {
|
||||
const [defaultClass, additionalClass] = banner_class.split(' ');
|
||||
if (additionalClass) {
|
||||
broadcastElement.classList.add(defaultClass, additionalClass);
|
||||
} else {
|
||||
broadcastElement.classList.add(defaultClass);
|
||||
}
|
||||
}
|
||||
|
||||
const closeButton = `<button class="close-announcement-button">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.99974 5.58623L11.9497 0.63623L13.3637 2.05023L8.41374 7.00023L13.3637 11.9502L11.9497 13.3642L6.99974 8.41423L2.04974 13.3642L0.635742 11.9502L5.58574 7.00023L0.635742 2.05023L2.04974 0.63623L6.99974 5.58623Z" fill="white" />
|
||||
</svg>
|
||||
</button>`;
|
||||
|
||||
broadcastElement.insertAdjacentHTML(
|
||||
'afterbegin',
|
||||
`<div class='broadcast-data'>${html}</div>${closeButton}`,
|
||||
);
|
||||
addCloseButtonClickHandle(title);
|
||||
broadcastElement.classList.add('visible');
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to determine if a broadcast should render
|
||||
* Does not render broadcast it has already been inserted,
|
||||
* or if the key for the broadcast's title exists in localStorage.
|
||||
*
|
||||
* @function initializeBroadcast
|
||||
*/
|
||||
|
|
@ -21,16 +91,17 @@ function initializeBroadcast() {
|
|||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const { html, banner_class } = data;
|
||||
const el = document.getElementById('active-broadcast');
|
||||
|
||||
const { title } = data;
|
||||
if (JSON.parse(localStorage.getItem(camelizedBroadcastKey(title))) === true) {
|
||||
return; // Do not render broadcast if previously dismissed by user.
|
||||
}
|
||||
|
||||
const el = document.getElementById('active-broadcast');
|
||||
if (el.firstElementChild) {
|
||||
return; // Only append HTML once, on first render.
|
||||
}
|
||||
|
||||
const bannerDiv = `<div class='broadcast-data ${
|
||||
banner_class || ''
|
||||
}'>${html}</div>`;
|
||||
el.insertAdjacentHTML('afterbegin', bannerDiv);
|
||||
renderBroadcast(el, data);
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
@import '../variables';
|
||||
@import '../crayons';
|
||||
@import '../scaffolds';
|
||||
@import '../variables';
|
||||
|
||||
#siteConfigBodyContainer {
|
||||
padding: 20px;
|
||||
|
|
@ -50,6 +51,11 @@
|
|||
border: 20px solid black;
|
||||
}
|
||||
|
||||
.broadcast-wrapper {
|
||||
position: relative;
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
min-height: 100vh;
|
||||
background: $light-gray;
|
||||
|
|
|
|||
|
|
@ -165,13 +165,31 @@ body.ten-x-hacker-theme {
|
|||
}
|
||||
|
||||
.broadcast-wrapper {
|
||||
display: none;
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
min-height: 45px;
|
||||
|
||||
// TODO: [@thepracticaldev/delightful]: Remove these styles once
|
||||
// https://github.com/thepracticaldev/dev.to/pull/8234 is merged.
|
||||
z-index: 101; // Must be higher than the z-index of the sticky-nav.
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
|
||||
&.visible {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.broadcast-data {
|
||||
flex-grow: 1;
|
||||
padding: var(--su-2);
|
||||
}
|
||||
|
||||
.close-announcement-button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
body.static-navbar-config {
|
||||
|
|
|
|||
|
|
@ -22,8 +22,10 @@
|
|||
<% if @broadcast.processed_html %>
|
||||
<div>
|
||||
</p><strong>Preview</strong></p>
|
||||
<div class="<%= banner_class(@broadcast) %>">
|
||||
<%= sanitize @broadcast.processed_html, attributes: %w[href style src] %>
|
||||
<div class="broadcast-wrapper visible <%= banner_class(@broadcast) %>">
|
||||
<div class="broadcast-data">
|
||||
<%= sanitize @broadcast.processed_html, attributes: %w[href style src] %>
|
||||
</div>
|
||||
</div>
|
||||
</p><em>Please note: announcement broadcasts will render directly below the nav bar once activated.</em></p>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ FactoryBot.define do
|
|||
factory :announcement_broadcast do
|
||||
title { "A Very Important Announcement" }
|
||||
type_of { "Announcement" }
|
||||
processed_html { "<div style='background-color: salmon;'><p style='width: 100%;'>Hello, World!</p></div>" }
|
||||
processed_html { "<p>Hello, World!</p>" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -31,25 +31,6 @@ RSpec.describe "User visits a homepage", type: :system do
|
|||
expect(page).to have_text("DESIGN YOUR EXPERIENCE")
|
||||
end
|
||||
|
||||
context "when rendering broadcasts" do
|
||||
let!(:broadcast) { create(:announcement_broadcast) }
|
||||
|
||||
it "renders the broadcast if active", js: true do
|
||||
get "/async_info/base_data" # Explicitly ensure broadcast data is loaded before doing any checks
|
||||
visit "/"
|
||||
within ".broadcast-wrapper" do
|
||||
expect(page).to have_text("Hello, World!")
|
||||
end
|
||||
end
|
||||
|
||||
it "does not render a broadcast if inactive", js: true do
|
||||
broadcast.update!(active: false)
|
||||
get "/async_info/base_data" # Explicitly ensure broadcast data is loaded before doing any checks
|
||||
visit "/"
|
||||
expect(page).not_to have_css(".broadcast-wrapper")
|
||||
end
|
||||
end
|
||||
|
||||
describe "link tags" do
|
||||
it "contains the qualified community name in the search link" do
|
||||
visit "/"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
require "rails_helper"
|
||||
|
||||
def expect_broadcast_data(page)
|
||||
within ".broadcast-wrapper" do
|
||||
expect(page).to have_selector(".broadcast-data")
|
||||
expect(page).to have_text("Hello, World!")
|
||||
end
|
||||
end
|
||||
|
||||
def expect_no_broadcast_data(page)
|
||||
expect(page).not_to have_css(".broadcast-wrapper")
|
||||
expect(page).not_to have_selector(".broadcast-data")
|
||||
expect(page).not_to have_text("Hello, World!")
|
||||
end
|
||||
|
||||
RSpec.describe "User visits a homepage", type: :system do
|
||||
context "when user hasn't logged in" do
|
||||
context "with an active announcement" do
|
||||
before do
|
||||
create(:announcement_broadcast)
|
||||
get "/async_info/base_data" # Explicitly ensure broadcast data is loaded before doing any checks
|
||||
visit "/"
|
||||
end
|
||||
|
||||
it "renders the broadcast", js: true do
|
||||
expect_broadcast_data(page)
|
||||
end
|
||||
|
||||
it "dismisses the broadcast", js: true do
|
||||
wait_for_javascript
|
||||
|
||||
find(".close-announcement-button").click
|
||||
expect_no_broadcast_data(page)
|
||||
end
|
||||
end
|
||||
|
||||
context "without an active announcement" do
|
||||
before do
|
||||
create(:announcement_broadcast, active: false)
|
||||
get "/async_info/base_data" # Explicitly ensure broadcast data is loaded before doing any checks
|
||||
visit "/"
|
||||
end
|
||||
|
||||
it "does not render the broadcast", js: true do
|
||||
expect_no_broadcast_data(page)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when user has logged in" do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context "with an active announcement" do
|
||||
before do
|
||||
create(:announcement_broadcast)
|
||||
get "/async_info/base_data" # Explicitly ensure broadcast data is loaded before doing any checks
|
||||
visit "/"
|
||||
end
|
||||
|
||||
it "renders the broadcast", js: true do
|
||||
expect_broadcast_data(page)
|
||||
end
|
||||
|
||||
it "dismisses the broadcast", js: true do
|
||||
get "/async_info/base_data"
|
||||
visit "/"
|
||||
wait_for_javascript
|
||||
|
||||
find(".close-announcement-button").click
|
||||
expect_no_broadcast_data(page)
|
||||
end
|
||||
end
|
||||
|
||||
context "without an active announcement" do
|
||||
before do
|
||||
create(:announcement_broadcast, active: false)
|
||||
get "/async_info/base_data" # Explicitly ensure broadcast data is loaded before doing any checks
|
||||
visit "/"
|
||||
end
|
||||
|
||||
it "does not render the broadcast", js: true do
|
||||
expect_no_broadcast_data(page)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue