* localize category strings

* truncate long company titles

* be able to add multiple rows into exposed info section

* fix width of button

* pass verified email into breach card

* add backup logo

* set unique colour for each exposure

* use fallback logo and shift rest of categories to the left, increase width of company name space

* remove comments

* fix lint errors

* use correct l10n strings

* fix lint error

* fix unit test failure

* dont hardcode :
This commit is contained in:
Kaitlyn Andres 2023-07-21 17:37:28 -04:00 коммит произвёл GitHub
Родитель 4aa64be7cb
Коммит cae921e55c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 191 добавлений и 39 удалений

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

@ -76,8 +76,8 @@ open-in-new-tab-alt = Open link in a new tab
# Status Pill
status-pill-action-needed = Action Needed
status-pill-progress = In Progress
status-pill-action-needed = Action needed
status-pill-progress = In progress
status-pill-fixed = Fixed
# Exposure Card
@ -103,7 +103,7 @@ exposure-card-description-info-for-sale-part-two = Remove this profile to protec
# $data_breach_date is the date of the data breach.
exposure-card-description-data-breach-part-one = Your information was exposed in the <data_breach_link>{ $data_breach_company } data breach on { $data_breach_date }.</data_breach_link>
exposure-card-description-data-breach-part-two = Well walk you through the steps to fix it.
exposure-card-your-exposed-info = Your exposed info
exposure-card-your-exposed-info = Your exposed info:
exposure-card-exposure-type-data-broker = Info for sale
exposure-card-exposure-type-data-breach = Data breach
exposure-card-cta = Lets fix it

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

@ -6,7 +6,6 @@
import { Session } from "next-auth";
import styles from "./View.module.scss";
import TwitterImage from "../../../../../components/client/assets/twitter-icon.png";
import { Toolbar } from "../../../../../components/client/toolbar/Toolbar";
import { DashboardTopBanner } from "./DashboardTopBanner";
import { useL10n } from "../../../../../hooks/l10n";
@ -62,13 +61,14 @@ export const View = (props: Props) => {
className={styles.exposureListItem}
>
<ExposureCard
exposureImg={TwitterImage}
exposureData={breach}
exposureName={breach.Name}
fromEmail={verifiedEmail.email}
exposureDetailsLink={""} //TODO: Find out what link to add in a breach card
dateFound={breach.AddedDate}
statusPillType={"needAction"}
statusPillType="needAction"
locale={props.locale}
color={getRandomLightNebulaColor(breach.Name)}
/>
</li>
);
@ -159,32 +159,46 @@ export const View = (props: Props) => {
const exposureCardElems = filteredExposures.map(
(exposure: ScanResult | HibpLikeDbBreach, index) => {
let email;
// Get the email assosciated with breach
if (!isScanResult(exposure)) {
props.userBreaches.breachesData.verifiedEmails.forEach(
(verifiedEmail) => {
if (
verifiedEmail.breaches.some((breach) => breach.Id === exposure.Id)
) {
email = verifiedEmail.email;
}
}
);
}
return (
<>
{isScanResult(exposure) ? (
// Scanned result
<li key={index} className={styles.exposureListItem}>
<ExposureCard
exposureImg={TwitterImage}
exposureData={exposure}
exposureName={exposure.data_broker}
exposureDetailsLink={exposure.link}
dateFound={dateObject(exposure.created_at)}
statusPillType={"needAction"}
statusPillType="needAction"
locale={props.locale}
color={getRandomLightNebulaColor(exposure.data_broker)}
/>
</li>
) : (
// Breaches result
<li key={index} className={styles.exposureListItem}>
<ExposureCard
exposureImg={TwitterImage}
exposureData={exposure}
exposureName={exposure.Name}
fromEmail={email}
exposureDetailsLink={""}
dateFound={exposure.AddedDate}
statusPillType={"needAction"}
statusPillType="needAction"
locale={props.locale}
color={getRandomLightNebulaColor(exposure.Name)}
/>
</li>
)}
@ -228,3 +242,49 @@ export const View = (props: Props) => {
</div>
);
};
// Same logic as breachLogo.js
function getRandomLightNebulaColor(name: string) {
const colors = [
"#C689FF",
"#D9BFFF",
"#AB71FF",
"#E7DFFF",
"#AB71FF",
"#3FE1B0",
"#54FFBD",
"#88FFD1",
"#B3FFE3",
"#D1FFEE",
"#F770FF",
"#F68FFF",
"#F6B8FF",
"#00B3F4",
"#00DDFF",
"#80EBFF",
"#FF8450",
"#FFA266",
"#FFB587",
"#FFD5B2",
"#FF848B",
"#FF9AA2",
"#FFBDC5",
"#FF8AC5",
"#FFB4DB",
];
const charValues = name.split("").map((letter) => letter.codePointAt(0));
const charSum = charValues.reduce((sum: number | undefined, codePoint) => {
if (codePoint === undefined) return sum;
if (sum === undefined) return codePoint;
return sum + codePoint;
}, undefined);
if (charSum === undefined) {
return colors[0];
}
const colorIndex = charSum % colors.length;
return colors[colorIndex];
}

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

@ -45,6 +45,17 @@
justify-content: space-between;
height: 30px; // fixed height to standardize image/icon heights
.fallbackLogo {
display: flex;
align-items: center;
justify-content: center;
width: 35px;
height: 35px;
border-radius: 50px;
color: black;
font: $text-title-3xs;
}
dd {
display: flex;
align-items: center;
@ -53,17 +64,30 @@
display: none;
}
.exposureCompanyTitle {
font: $text-body-sm;
max-width: 200px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
@media screen and (min-width: $screen-lg) {
flex: 1 1 0%;
&.exposureImageWrapper {
flex: 0.2 0 $width-first-column-filter-bar;
}
&.hideOnMobile {
display: block;
}
}
.exposureImage {
max-width: 120px;
height: 100%;
.exposureImageWrapper {
.exposureImage {
height: 100%;
}
}
}
}
@ -101,6 +125,26 @@
display: flex;
flex-direction: column;
gap: $spacing-md;
@media screen and (min-width: $screen-lg) {
flex: 1 1 0;
.fixItBtn {
flex-shrink: 0;
}
}
.exposedInfoTitle {
@media screen and (min-width: $screen-lg) {
align-self: center;
flex: 0 0 90px; // fix width of categories title
}
@media screen and (min-width: $screen-xl) {
align-self: center;
flex: 0 0 150px;
}
}
}
@media screen and (min-width: $screen-lg) {
@ -121,14 +165,20 @@
margin-left: 0;
padding-left: 0;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: $spacing-sm;
grid-template-columns: 1fr 1fr;
grid-row-gap: $spacing-sm;
@media screen and (min-width: $screen-sm) {
grid-template-columns: 1fr 1fr 1fr;
}
@media screen and (min-width: $screen-lg) {
display: flex;
flex-direction: row;
align-items: center;
gap: $layout-xs;
flex-wrap: wrap;
justify-content: flex-start;
}
.detailsFoundItem {

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

@ -4,7 +4,7 @@
"use client";
import React, { ReactElement, useState } from "react";
import React, { CSSProperties, ReactElement, useState } from "react";
import styles from "./ExposureCard.module.scss";
import { StatusPill, StatusPillType } from "../server/StatusPill";
import Image, { StaticImageData } from "next/image";
@ -50,18 +50,21 @@ export function isScanResult(
}
export type ExposureCardProps = {
exposureImg: StaticImageData;
exposureImg?: StaticImageData;
exposureName: string;
exposureData: Exposure;
exposureDetailsLink: string;
dateFound: Date;
statusPillType: StatusPillType;
locale: string;
fromEmail?: string;
color: string;
};
type BreachExposureCategoryProps = {
exposureCategoryLabel: string;
icon: ReactElement;
email: string;
};
type ScannedExposureCategoryProps = {
@ -78,10 +81,12 @@ export const ExposureCard = (props: ExposureCardProps) => {
exposureDetailsLink,
statusPillType,
locale,
color,
} = props;
const l10n = useL10n();
const [exposureCardExpanded, setExposureCardExpanded] = useState(false);
const dateFormatter = new Intl.DateTimeFormat(locale, {
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#datestyle
dateStyle: "medium",
@ -89,6 +94,8 @@ export const ExposureCard = (props: ExposureCardProps) => {
const exposureCategoriesArray: React.ReactElement[] = [];
const exposureItem = props.exposureData;
const verifiedEmailofBreach = props.fromEmail;
const BreachExposureCategory = (props: BreachExposureCategoryProps) => {
const description = l10n.getString("exposure-card-num-found", {
exposure_num: 1, // We don't count categories for breaches.
@ -100,7 +107,11 @@ export const ExposureCard = (props: ExposureCardProps) => {
<span className={styles.exposureTypeIcon}>{props.icon}</span>
{props.exposureCategoryLabel}
</dt>
<dd>{description}</dd>
<dd>
{props.email === "email-addresses"
? verifiedEmailofBreach
: description}
</dd>
</div>
);
};
@ -181,6 +192,7 @@ export const ExposureCard = (props: ExposureCardProps) => {
exposureCategoriesArray.push(
<BreachExposureCategory
key={item}
email={item}
icon={<EmailIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-email")}
/>
@ -189,6 +201,7 @@ export const ExposureCard = (props: ExposureCardProps) => {
exposureCategoriesArray.push(
<BreachExposureCategory
key={item}
email={item}
icon={<PasswordIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-password")}
/>
@ -197,6 +210,7 @@ export const ExposureCard = (props: ExposureCardProps) => {
exposureCategoriesArray.push(
<BreachExposureCategory
key={item}
email={item}
icon={<PhoneIcon alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-phone-number")}
/>
@ -205,6 +219,7 @@ export const ExposureCard = (props: ExposureCardProps) => {
exposureCategoriesArray.push(
<BreachExposureCategory
key={item}
email={item}
icon={<QuestionMarkCircle alt="" width="13" height="13" />}
exposureCategoryLabel={l10n.getString("exposure-card-ip-address")}
/>
@ -215,8 +230,9 @@ export const ExposureCard = (props: ExposureCardProps) => {
exposureCategoriesArray.push(
<BreachExposureCategory
key={item}
icon={<QuestionMarkCircle alt="" width="13" height="13" />}
exposureCategoryLabel={formatOtherBreachCategoriesLabel(item)} // Other exposure categories labels not localized for now
email={item}
icon={<QuestionMarkCircle alt="" width="13" height="13" />} // default icon for categories without a unique one
exposureCategoryLabel={l10n.getString(item)} // categories are localized in data-classes.ftl
/>
);
}
@ -232,27 +248,48 @@ export const ExposureCard = (props: ExposureCardProps) => {
return <>{listItems}</>;
};
function fallbackLogo(exposureId: string) {
const firstLetter = exposureId?.[0]?.toUpperCase() || "";
return (
<span
className={styles.fallbackLogo}
style={{ background: color } as CSSProperties}
>
{firstLetter}
</span>
);
}
const exposureCard = (
<div>
<div className={styles.exposureCard}>
<div className={styles.exposureHeader}>
<dl className={styles.exposureHeaderList}>
<dt className={styles.visuallyHidden}>
{l10n.getString("exposure-card-company-logo")}
{l10n.getString("exposure-card-label-company-logo")}
</dt>
<dd
className={`${styles.exposureImageWrapper} ${styles.hideOnMobile}`}
className={`${styles.hideOnMobile} ${styles.exposureImageWrapper}`}
>
<Image
className={styles.exposureImage}
alt=""
src={exposureImg}
/>
{/* While logo is not yet set, the fallback image is the first character of the exposure name */}
{exposureImg ? (
<Image
className={styles.exposureImage}
alt=""
src={exposureImg}
/>
) : (
<>{fallbackLogo(props.exposureName)}</>
)}
</dd>
<dt className={styles.visuallyHidden}>
{l10n.getString("exposure-card-company")}
{l10n.getString("exposure-card-label-company")}
</dt>
<dd>{exposureName}</dd>
<dd>
<span className={styles.exposureCompanyTitle}>
{exposureName}
</span>
</dd>
<dt className={styles.visuallyHidden}>
{l10n.getString("exposure-card-exposure-type")}
</dt>
@ -343,7 +380,7 @@ export const ExposureCard = (props: ExposureCardProps) => {
height="13"
/>
</span>
</a>{" "}
</a>
{l10n.getString(
"exposure-card-description-data-breach-part-two"
)}
@ -352,7 +389,9 @@ export const ExposureCard = (props: ExposureCardProps) => {
)}
<div className={styles.exposedInfoContainer}>
<div className={styles.exposedInfoWrapper}>
<p>{l10n.getString("exposure-card-your-exposed-info")}:</p>
<p className={styles.exposedInfoTitle}>
{l10n.getString("exposure-card-your-exposed-info")}
</p>
<dl>
<ExposureCategoriesListElem />
</dl>
@ -370,9 +409,3 @@ export const ExposureCard = (props: ExposureCardProps) => {
return exposureCard;
};
function formatOtherBreachCategoriesLabel(sentence: string): string {
return sentence
.replaceAll("-", " ")
.replace(/^[a-z]/, (i) => i.toUpperCase());
}

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

@ -51,6 +51,10 @@
@media screen and (min-width: $screen-lg) {
flex: 1 1 0%;
&.exposureImageWrapper {
flex: 0.2 0 $width-first-column-filter-bar;
}
&.hideOnMobile {
display: flex;
}

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

@ -212,7 +212,7 @@ export const ExposuresFilter = ({ setFilterValues }: ExposuresFilterProps) => {
<>
<div className={styles.filterHeaderWrapper}>
<ul className={styles.filterHeaderList}>
<li>
<li className={styles.exposureImageWrapper}>
<button
className={styles.filterBtn}
ref={filterBtnRef}

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

@ -4,7 +4,11 @@
font: $text-body-xs;
font-weight: 600;
display: inline-block;
min-width: 120px; // keep the width fixed
min-width: 90px; // keep the width fixed
@media screen and (min-width: $screen-md) {
min-width: 120px; // keep the width fixed
}
text-align: center;
padding: $spacing-sm 0;
border-radius: $border-radius-sm;

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

@ -238,6 +238,7 @@ $text-body-xs: 400 clamp(10px, 0.975svw, 12px) / 1.5 var(--font-inter),
sans-serif;
$tab-bar-height: 100px;
$width-first-column-filter-bar: 50px;
@mixin visually-hidden {
// These styles are taken from