This commit is contained in:
lesley 2019-09-26 12:00:16 -05:00
Родитель b437d8ec8f
Коммит a9edda808c
11 изменённых файлов: 623 добавлений и 34 удалений

Просмотреть файл

@ -72,6 +72,22 @@ function getAboutPage(req, res) {
});
}
function getBentoStrings(req, res) {
const localizedBentoStrings = {
bentoButtonTitle: req.fluentFormat("bento-button-title"),
bentoHeadline: req.fluentFormat("fx-makes-tech"),
bentoBottomLink: req.fluentFormat("made-by-Mozilla"),
mobileCloseBentoButtonTitle: req.fluentFormat("mobile-close-bento-button-title"),
fxDesktop: req.fluentFormat("fx-desktop"),
fxLockwise: req.fluentFormat("fx-lockwise"),
fxMobile: req.fluentFormat("fx-mobile"),
fxMonitor: req.fluentFormat("fx-monitor"),
pocket: req.fluentFormat("pocket"),
fxSend: req.fluentFormat("fx-send"),
};
return res.json(localizedBentoStrings);
}
function notFound(req, res) {
res.status(404);
res.render("subpage", {
@ -84,6 +100,7 @@ module.exports = {
home,
getAboutPage,
getAllBreaches,
getBentoStrings,
getSecurityTips,
notFound,
};

10
locales/en/bento.ftl Normal file
Просмотреть файл

@ -0,0 +1,10 @@
bento-button-title = Firefox apps and services
fx-makes-tech = Firefox is tech that fights for your online privacy.
made-by-Mozilla = Made by Mozilla
fx-desktop = Firefox Browser for Desktop
fx-mobile = Firefox Browser for Mobile
fx-lockwise = Firefox Lockwise
pocket = Pocket
fx-monitor = Firefox Monitor
fx-send = Firefox Send
mobile-close-bento-button-title = Close menu

Просмотреть файл

@ -221,7 +221,7 @@ a.btn-dark {
justify-content: center;
text-align: center;
background-color: transparent;
border-radius: 0.25rem;
border-radius: 8px;
min-height: 2rem;
font-size: var(--buttonFontSize);
font-weight: 600;
@ -370,6 +370,14 @@ ul li {
flex-direction: row;
}
.flx-end {
justify-content: flex-end;
}
.flx-auto {
flex: 1 1 auto;
}
.space-between {
justify-content: space-between;
}
@ -557,10 +565,6 @@ div.sprite {
--headline: 2.4rem;
}
.clear-header {
padding-top: 16rem;
}
.hide-mobile {
display: none !important;
visibility: hidden;
@ -624,6 +628,10 @@ div.sprite {
--subhead: 1.1rem;
}
.row-full-width {
padding: 24px;
}
.headline {
margin-bottom: 1rem;
}

329
public/css/fx-bento.css Normal file
Просмотреть файл

@ -0,0 +1,329 @@
firefox-apps {
--bentoButtonHeight: 36px;
--zIndex: 10000;
--appIconHeight: 16px;
--textColor: rgb(32, 18, 58);
display: flex;
justify-content: center;
margin: 0 32px;
position: relative;
text-align: center;
z-index: var(--zIndex);
font-size: 12px;
color: var(--textColor);
}
.fx-bento-content {
display: none;
flex-direction: column;
position: absolute;
top: calc(var(--bentoButtonHeight) + 10px);
background: rgb(249, 249, 250);
border-radius: 8px;
min-width: 260px;
z-index: calc(var(--zIndex) + 1);
padding: 24px 0;
box-shadow: 0 7px 12px -3px rgba(28, 28, 29, 0.502);
}
.fx-bento-content.active {
display: flex;
transform: translateY(5px);
animation: fxBentoAppear ease 0.3s;
-webkit-animation: fxBentoAppear ease 0.3s;
-moz-animation: fxBentoAppear ease 0.3s;
}
.fx-bento-content.active::after { /* tool tip top notch */
display: block;
content: "";
height: 12px;
width: 12px;
position: absolute;
top: -6px;
left: 0;
right: 0;
margin: auto;
transform: rotate(45deg);
border-top-left-radius: 1px;
background-color: rgb(249, 249, 250);
}
.fx-bento-logo {
background: url("/img/fx-master-brand.svg");
background-repeat: no-repeat;
background-size: contain;
height: 40px;
width: 40px;
margin: auto auto 8px auto;
}
.fx-bento-headline {
font-size: 16px;
font-family: "Metropolis", sans-serif;
line-height: 20px;
font-weight: 600;
max-width: 280px;
margin-right: auto;
margin-left: auto;
padding: 0 24px;
}
.fx-bento-bottom-link {
color: rgb(0, 96, 223);
text-decoration: underline;
margin: 16px 24px 0 24px;
}
a.fx-bento-app-link {
padding: 10px 16px 10px calc(var(--appIconHeight) + 32px);
background-color: rgba(255, 255, 255, 0);
box-shadow: 0 -1px 0 0 rgba(255, 255, 255, 0);
position: relative;
white-space: nowrap;
color: var(--textColor);
text-align: left;
}
.fx-bento-app-link::before {
display: block;
position: absolute;
left: 24px;
top: 0;
bottom: 0;
margin: auto;
height: var(--appIconHeight);
width: var(--appIconHeight);
background: url("/img/fx-bento-sprites.png");
background-repeat: no-repeat;
background-size: cover !important;
content: "";
}
.fx-lockwise::before {
background-position-x: -54px;
width: calc(var(--appIconHeight) + 1px);
}
.fx-bento-app-link.pocket::before {
background-position-x: -18px;
}
.fx-bento-app-link.fx-send::before {
background-position-x: -71px;
}
.fx-bento-app-link.fx-monitor::before {
background-position-x: -88px;
}
.fx-bento-app-link.fx-mobile::before {
background-position-x: -104px;
}
.fx-bento-app-link:hover,
.fx-bento-app-link:focus {
background-color: rgba(12, 12, 13, 0.1);
opacity: 1 !important;
}
.fx-bento-button {
background: url("/img/fx-bento-sprites.png");
background-repeat: no-repeat;
background-position: center center;
background-position-x: -322px;
background-size: cover;
width: 37px;
height: 36px;
max-height: 37px;
border: none;
border-radius: 1px;
padding: 0;
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0);
pointer-events: all;
}
.fx-bento-button:hover,
.fx-bento-button:focus {
box-shadow: 0 0 0 5px rgba(255, 255, 255, 0.2);
background-color: rgba(255, 255, 255, 0.2);
border: 0;
outline: 0;
}
.fx-bento-button:hover::-moz-focus-inner, /* hide dotted line */
.fx-bento-button:focus::-moz-focus-inner {
border: 0;
outline: 0;
}
.fx-bento-close.active { /* transparent div that spans the entire background height/width of the browser window when the bento is open and captures clicks to close the bento */
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: -1;
}
button.fx-bento-mobile-close {
display: none; /* button is only displayed on smaller screen sizes */
position: absolute;
right: var(--appIconHeight);
top: var(--appIconHeight);
height: calc(var(--appIconHeight) * 1.25);
min-height: calc(var(--appIconHeight) * 1.25);
width: calc(var(--appIconHeight) * 1.25);
max-width: calc(var(--appIconHeight) * 1.25);
min-width: calc(var(--appIconHeight) * 1.25);
background-color: rgb(55, 54, 111);
background-image: url("/img/fx-bento-sprites.png");
background-size: cover;
background-repeat: no-repeat;
background-position-x: -229px;
padding: 0 !important;
border: none !important;
border-radius: 50%;
}
.fx-bento-current-site {
pointer-events: none;
opacity: 0.7;
}
.fx-bento-app-link:first-of-type {
margin-top: var(--appIconHeight);
}
.fx-bento-fade-out {
opacity: 0;
transform: translateY(0) !important;
pointer-events: none !important;
}
a.fx-bento-app-link,
a.fx-bento-app-link:hover,
a.fx-bento-app-link:focus,
.fx-bento-button,
.fx-bento-button:hover,
.fx-bento-button:focus,
.fx-bento-fade-out {
transition: 0.2s ease-in-out;
}
@media screen and (max-width: 900px) {
firefox-apps {
margin: 0 24px;
}
.fx-bento-button {
transform: scale(0.9);
}
}
@media screen and (max-width: 500px) {
firefox-apps {
--appIconHeight: 24px;
--mobileClickableCloseArea: 12vh;
position: inherit;
margin: 0 16px;
}
button.fx-bento-mobile-close { /* Unhide purple "X" button */
display: block;
}
.fx-bento-mobile-close::after { /* create pseudo element to increase the clickable area of .fx-bento-mobile-close button */
content: "";
display: block;
position: fixed;
right: 0;
top: 0;
left: 0;
height: var(--mobileClickableCloseArea);
}
.fx-bento-content {
top: 0;
left: 0;
right: 0;
height: 100vh;
border-radius: 0;
transform: translateY(0) !important;
}
.fx-bento-logo {
height: 48px;
width: 48px;
min-height: 48px;
margin-top: var(--mobileClickableCloseArea);
}
.fx-bento-headline {
font-size: 20px;
line-height: 24px;
padding: 0 12px;
}
.fx-bento-bottom-link {
margin-bottom: auto;
margin-top: 24px;
}
.fx-bento-bottom-link,
.fx-bento-app-link {
font-size: 16px;
}
.fx-bento-content.active {
transform: translateY(0) !important;
animation: none !important;
-webkit-animation: none !important;
}
.fx-bento-content.active::after {
display: none;
}
a.fx-bento-app-link {
line-height: 23px;
padding-left: calc(var(--appIconHeight) + 40px);
}
.fx-bento-app-link.pocket::before {
background-position-x: -26px;
}
.fx-bento-app-link.fx-send::before {
background-position-x: -107px;
width: calc(var(--appIconHeight) + 1px);
}
.fx-bento-app-link.fx-monitor::before {
background-position-x: -132px;
}
.fx-bento-app-link.fx-mobile::before {
background-position-x: -157px;
margin-left: 1px;
}
.fx-bento-app-link.fx-lockwise::before {
background-position-x: -79px;
width: calc(var(--appIconHeight) + 2px);
}
}
@keyframes fxBentoAppear {
0% {
opacity: 0;
transform: translateY(0);
}
100% {
opacity: 1;
transform: translateY(5px);
}
}

Просмотреть файл

@ -37,8 +37,8 @@ header.show-shadow {
}
.fx-monitor-logo-wrapper,
.desktop-menu {
flex: 1 1 100%;
.bento-sign-up {
flex: 1 1 30%;
}
.fx-monitor-logo-wrapper:hover {
@ -55,7 +55,8 @@ header.show-shadow {
.fx-monitor-logotype {
background: url("/img/svg/fx-monitor-logotype.svg");
width: 100%;
margin-left: var(--margin);
max-width: 212px;
margin-left: 8px;
background-position: left;
background-size: contain;
background-repeat: no-repeat;
@ -74,22 +75,24 @@ nav {
padding-left: calc(1rem + 1vw);
justify-content: center;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
font-weight: 700;
font-size: 16px;
}
.active-link {
font-weight: 700;
position: relative;
display: block;
}
.active-link-underline::after {
height: 0.15rem;
height: 2px;
background: var(--monitorGradient);
opacity: 0;
display: block;
content: "";
position: absolute;
bottom: -10px;
bottom: -8px;
left: 0;
right: 0;
}
@ -104,23 +107,24 @@ nav {
}
.sign-in {
margin-left: 1rem;
white-space: nowrap;
border: 2px solid rgba(255, 255, 255, 1);
font-size: 16px;
}
.sign-in:hover {
background-color: var(--blue4);
border: 1px solid var(--blue4);
border: 2px solid var(--blue4);
}
.sign-in:active {
background-color: var(--blue5);
border: 1px solid var(--blue5);
border: 2px solid var(--blue5);
}
.sign-in:focus,
.sign-in:active {
border-width: 1px;
border-width: 2px;
outline: none;
box-shadow: none;
}
@ -175,8 +179,9 @@ nav {
.avatar-wrapper,
.avatar {
height: 36px;
width: 36px;
height: 42px;
width: 42px;
border: 2px solid rgba(255, 255, 255, 1);
}
/* signed-in user menu */
@ -275,14 +280,14 @@ nav {
font-size: 16px;
}
.learn-more {
/* .learn-more {
margin-left: 0.5rem;
color: rgba(255, 255, 255, 1);
text-decoration: underline;
white-space: nowrap;
}
} */
.join-fx-wrap {
/* .join-fx-wrap {
align-items: center;
}
@ -295,15 +300,33 @@ nav {
min-width: 24px;
height: 24px;
margin: auto 16px auto auto;
} */
@media screen and (max-width: 1200px) {
.bento-sign-up {
flex: 0 1 auto;
}
.desktop-menu {
justify-content: flex-end;
}
.nav-link {
padding-right: 0;
}
}
@media screen and (max-width: 800px) {
.bento-sign-up {
margin-left: 0;
}
.desktop-menu {
flex: 1 1 auto;
}
.fx-monitor-logotype {
margin-left: 0.25rem;
margin-left: 4px;
height: 25px;
}
@ -430,16 +453,12 @@ nav {
font-size: 1rem;
}
.sprite.fx-monitor-logo {
transform: scale(0.9);
}
.fx-monitor-logotype {
height: 18px;
.fx-monitor-logo-wrapper {
flex: 1 1 100%;
}
.sign-in {
padding: 0.65rem 1.35rem;
padding: 8px 16px;
}
.mobile-menu {
@ -455,3 +474,19 @@ nav {
padding: 1rem 1.75rem;
}
}
@media screen and (max-width: 500px) {
.sprite.fx-monitor-logo {
transform: scale(0.9);
}
.fx-monitor-logotype {
height: 18px;
}
}
@media screen and (max-width: 450px) {
.fx-monitor-logotype {
display: none;
}
}

Двоичные данные
public/img/fx-bento-sprites.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 14 KiB

183
public/js/fx-bento.js Normal file
Просмотреть файл

@ -0,0 +1,183 @@
"use strict";
/* global ga */
/* eslint-disable selector-type-no-unknown */
function getFxAppLinkInfo(localizedBentoStrings, referringSiteURL) {
return [
[localizedBentoStrings.fxSend, "https://send.firefox.com/", "fx-send"],
[localizedBentoStrings.fxMonitor, "https://monitor.firefox.com/", "fx-monitor"],
[localizedBentoStrings.pocket, "https://app.adjust.com/hr2n0yz?engagement_type=fallback_click&fallback=https%3A%2F%2Fgetpocket.com%2Ffirefox_learnmore%3Fsrc%3Dff_bento&fallback_lp=https%3A%2F%2Fapps.apple.com%2Fapp%2Fpocket-save-read-grow%2Fid309601447", "pocket"],
[localizedBentoStrings.fxDesktop, `https://www.mozilla.org/firefox/new/?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, "fx-desktop"],
[localizedBentoStrings.fxMobile, "https://www.firefox.com/", "fx-mobile"],
[localizedBentoStrings.fxLockwise, "https://lockwise.monitor.com", "fx-lockwise"],
];
}
function createAndAppendEl(wrapper, tagName, className = null) {
const newEl = document.createElement(tagName);
if (className) {
newEl.setAttribute("class", className);
}
wrapper.appendChild(newEl);
return newEl;
}
async function getlocalizedBentoStrings() {
let localizedBentoStrings;
try {
const res = await fetch(
"http://localhost:6060/getBentoStrings",
{
mode: "cors",
}
);
localizedBentoStrings = await res.json();
} catch(e) {
// Error fetching the localized strings. Defaulting to English.
localizedBentoStrings = {
bentoButtonTitle: "Firefox apps and services",
bentoHeadline: "Firefox is tech that fights for your online privacy.",
bentoBottomLink: "Made by Mozilla",
mobileCloseBentoButtonTitle: "Close menu",
fxDesktop: "Firefox Browser for Desktop",
fxMobile: "Firefox Browser for Mobile",
fxSend: "Firefox Send",
fxMonitor: "Firefox Monitor",
fxLockwise: "Firefox Lockwise",
pocket: "Pocket",
};
}
return localizedBentoStrings;
}
class FirefoxApps extends HTMLElement {
constructor() {
super();
}
async connectedCallback() {
this._currentSite = document.body.dataset.bentoAppId;
this._localizedBentoStrings = await getlocalizedBentoStrings();
this._active = false; // Becomes true when the bento is opened.
this._frag = document.createDocumentFragment(); // Wrapping fragment for bento button and bento content.
this._bentoButton = createAndAppendEl(this._frag, "button", "fx-bento-button toggle-bento"); // Button toggles dropdown.
this._bentoButton.title = this._localizedBentoStrings.bentoButtonTitle;
this._bentoButton.addEventListener("click", this);
this._bentoContent = createAndAppendEl(this._frag, "div", "fx-bento-content");
this._mobileCloseBentoButton = createAndAppendEl(this._bentoContent, "button", "fx-bento-mobile-close toggle-bento");
this._mobileCloseBentoButton.setAttribute("title", this._localizedBentoStrings.mobileCloseBentoButtonTitle);
this._mobileCloseBentoButton.addEventListener("click", this);
this._firefoxLogo = createAndAppendEl(this._bentoContent, "div", "fx-bento-logo");
this._messageTop = createAndAppendEl(this._bentoContent, "span", "fx-bento-headline");
this._messageTop.textContent = this._localizedBentoStrings.bentoHeadline;
this._appList = this.makeAppList();
this._messageBottomLink = createAndAppendEl(this._bentoContent, "a", "fx-bento-bottom-link");
this._messageBottomLink.textContent = this._localizedBentoStrings.bentoBottomLink;
this._messageBottomLink.href = "https://www.mozilla.com/";
this._frag.querySelectorAll("a").forEach(anchorEl => {
anchorEl.rel = "noopener noreferrer";
anchorEl.target = "_blank";
anchorEl.addEventListener("click", this);
});
this._frag.appendChild(this._bentoContent);
this._clickableBG = createAndAppendEl(this._frag, "div", "fx-bento-close");
this._clickableBG.addEventListener("click", this);
this.appendChild(this._frag);
}
metricsSendEvent(eventAction, eventLabel) {
return ga("send", "event", "bento", eventAction, eventLabel);
}
toggleClass(whichClass) {
[this._bentoContent, this._clickableBG].forEach(el => {
el.classList.toggle(whichClass);
});
}
handleEvent(event) {
event.preventDefault();
this._active = !this._active;
const clickTarget = event.target;
if (clickTarget.classList.contains("fx-bento-app-link")) {
const appToOpenId = clickTarget.dataset.bentoAppLinkId;
const url = new URL(clickTarget.href); // add any additional UTM params - or whatever.
url.searchParams.append("utm_source", this._currentSite);
url.searchParams.append("utm_medium", "bento");
url.searchParams.append("utm_campaign", "bento-skyline");
window.open(url, "_blank", "noopener");
return this.metricsSendEvent("bento-app-link-click", appToOpenId);
}
if (!this._active) {
this.metricsSendEvent("bento-closed", this._currentSite);
this.toggleClass("fx-bento-fade-out"); // Set "fx-bento-fade-out" class to transition opacity smoothly since we can't transition smoothly to `display: none`.
setTimeout(() => {
this.toggleClass("fx-bento-fade-out");
this.toggleClass("active");
}, 1000);
return this.handleBentoFocusTrap();
}
this.metricsSendEvent("bento-opened", this._currentSite);
this.toggleClass("active");
return this.handleBentoFocusTrap();
}
handleBentoFocusTrap() {
const nonBentoPageElements = document.querySelectorAll(
"a:not(.fx-bento-app-link):not(.fx-bento-bottom-link), button:not(.toggle-bento ), input, select, option, textarea, radio, [tabindex]"
);
if (this._active) {
nonBentoPageElements.forEach(el => {
if (el.tabIndex > -1) {
el.dataset.oldTabIndex = el.tabIndex;
}
el.tabIndex = -1;
});
return;
}
nonBentoPageElements.forEach(el => {
if (el.dataset.oldTabIndex) {
el.tabIndex = el.dataset.oldTabIndex;
delete el.dataset.oldTabIndex;
return;
}
el.tabIndex = 0;
});
}
makeAppList() {
const appLinks = getFxAppLinkInfo(this._localizedBentoStrings);
appLinks.forEach(app => {
const newLink = document.createElement("a");
newLink.setAttribute("class", `fx-bento-app-link ${app[2]}`);
newLink["textContent"] = app[0];
["href", "data-bento-app-link-id"].forEach((attributeName, index) => {
newLink.setAttribute(attributeName, app[index + 1]);
});
if (newLink.dataset.bentoAppLinkId === this._currentSite) {
newLink.classList.add("fx-bento-current-site");
this._bentoContent.insertBefore(newLink, this._bentoContent.querySelector(".fx-bento-app-link"));
return;
}
this._bentoContent.appendChild(newLink);
});
}
}
customElements.define("firefox-apps", FirefoxApps);

Просмотреть файл

@ -3,7 +3,7 @@
const express = require("express");
const csrf = require("csurf");
const {home, getAboutPage, getAllBreaches, getSecurityTips, notFound} = require("../controllers/home");
const {home, getAboutPage, getAllBreaches, getBentoStrings, getSecurityTips, notFound} = require("../controllers/home");
const router = express.Router();
@ -13,6 +13,7 @@ router.get("/", csrfProtection, home);
router.get("/about", getAboutPage);
router.get("/breaches", getAllBreaches);
router.get("/security-tips", getSecurityTips);
router.get("/getBentoStrings", getBentoStrings);
router.use(notFound);
module.exports = router;

Просмотреть файл

@ -21,7 +21,7 @@
<link rel="icon" href="/img/favicons/favicon-128.png" sizes="128x128" />
<link rel="icon" href="/img/favicons/favicon-256.png" sizes="256x256" />
</head>
<body {{> analytics/default_dataset }}>
<body {{> analytics/default_dataset }} data-bento-app-id="fx-monitor">
<div id="close-menu" class="close-menu"></div>
{{> header/header }}
{{{ body }}}

Просмотреть файл

@ -1,19 +1,22 @@
<header id="header">
{{> branding-strip }}
{{!-- {{> branding-strip }} --}}
<div id="navigation-wrapper">
<section class="row-full-width fxm-branding">
<a class="flx-cntr fx-monitor-logo-wrapper" href="/" aria-label="{{ getString 'home' }}" {{> analytics/internal-link eventLabel="Fx-Monitor-Logo" }}>
<div class="sprite fx-monitor-logo"><!--Firefox Monitor logo--></div>
<div class="fx-monitor-logotype"></div>
</a>
<nav class="desktop-menu flx-cntr">
<nav class="desktop-menu flx-auto flx-cntr jst-cntr">
{{> header/nav-links }}
</nav>
<div class="flx flx-end flx-cntr bento-sign-up">
<firefox-apps></firefox-apps>
{{#if req.session.user}}
{{> header/fxa-menu }}
{{else}}
<button id="sign-in-btn" class="open-oauth sign-in btn-light" {{> analytics/fxa id="fx-monitor-sign-in-button" }} data-event-category="Sign In Button">{{ getString "sign-in" }}</button>
{{/if}}
</nav>
</div>
</section>
<!--mobile navigation-->
<section class="mobile-nav show-mobile">

Просмотреть файл

@ -1,3 +1,5 @@
{{#ifCompare UTM_SOURCE "===" "localhost"}}
<link rel="stylesheet" href="/css/all-breaches.css">
<link rel="stylesheet" href="/css/app.css">
@ -11,6 +13,7 @@
<link rel="stylesheet" href="/css/footer.css">
<link rel="stylesheet" href="/css/footer-about.css">
<link rel="stylesheet" href="/css/forms.css">
<link rel="stylesheet" href="/css/fx-bento.css">
<link rel="stylesheet" href="/css/header.css">
<link rel="stylesheet" href="/css/latest-breach.css">
<link rel="stylesheet" href="/css/metropolis.css">
@ -20,10 +23,10 @@
<link rel="stylesheet" href="/css/sign-up-banner.css">
<link rel="stylesheet" href="/css/subpage.css">
<script type="text/javascript" src="/js/all-breaches/all-breaches.js"></script>
<script type="text/javascript" src="/js/analytics_dnt-helper.js" defer></script>
<script type="text/javascript" src="/js/analytics_tracking_protection.js" defer></script>
<script type="text/javascript" src="/js/fx-bento.js"></script>
<script type="text/javascript" src="/js/fxa-analytics.js" defer></script>
<script type="text/javascript" src="/js/fxa-menu.js" defer></script>
<script type="text/javascript" src="/js/dashboard.js" defer></script>