Remove design-system dependency

This commit is contained in:
Lachlan Kermode
2022-03-03 20:24:38 -05:00
parent 20025dac57
commit 723c4b7007
14 changed files with 609 additions and 43 deletions

View File

@@ -16,7 +16,6 @@
},
"dependencies": {
"@babel/core": "7.12.3",
"@forensic-architecture/design-system": "0.6.2",
"@pmmmwh/react-refresh-webpack-plugin": "0.4.2",
"@svgr/webpack": "5.4.0",
"@testing-library/jest-dom": "^5.11.6",

View File

@@ -484,13 +484,8 @@ export function makeNiceDate(datetime) {
month: "long",
day: "2-digit",
});
const [
{ value: month },
,
{ value: day },
,
{ value: year },
] = dateTimeFormat.formatToParts(datetime);
const [{ value: month }, , { value: day }, , { value: year }] =
dateTimeFormat.formatToParts(datetime);
return `${day} ${month}, ${year}`;
}
@@ -573,3 +568,5 @@ export function getFilterIdx(
else if (narrativesExist && categoriesExist) return numCategoryPanels + 1;
else return 0;
}
export const isEmptyString = (s) => s.length === 0;

View File

@@ -0,0 +1,232 @@
import React, { useState } from "react";
import CardText from "./atoms/Text";
import CardTime from "./atoms/Time";
import CardButton from "./atoms/Button";
import CardCaret from "./atoms/Caret";
import CardCustom from "./atoms/CustomField";
import CardMedia from "./atoms/Media";
import { makeNiceDate, isEmptyString } from "../../common/utilities";
import hash from "object-hash";
export const generateCardLayout = {
basic: ({ event }) => {
return [
[
{
kind: "date",
title: "Incident Date",
value: event.datetime || event.date || ``,
},
{
kind: "text",
title: "Location",
value: event.location || ``,
},
],
[{ kind: "line-break", times: 0.4 }],
[
{
kind: "text",
title: "Summary",
value: event.description || ``,
scaleFont: 1.1,
},
],
];
},
sourced: ({ event }) => {
return [
[
{
kind: "date",
title: "Incident Date",
value: event.datetime || event.date || ``,
},
{
kind: "text",
title: "Location",
value: event.location || ``,
},
],
[
{
kind: "text",
title: "Summary",
value: event.description || ``,
scaleFont: 1.1,
},
],
...event.sources.flatMap((source, idx) => [
[
{
kind: "text",
title: `Source ${idx}`,
value: source.description || ``,
scaleFont: 1.1,
},
],
source.paths.map((p) => ({
kind: "media",
title: "Media",
value: [{ src: p, title: null }],
})),
]),
];
},
};
export const Card = ({
content = [],
isLoading = true,
onSelect = () => {},
sources = [],
isSelected = false,
language = "en-US",
}) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
// NB: should be internationalized.
const renderTime = (field) => (
<CardTime
language={language}
timelabel={makeNiceDate(field.value)}
{...field}
/>
);
const renderCaret = () =>
sources.length === 0 && (
<CardCaret toggle={() => toggle()} isOpen={isOpen} />
);
const renderMedia = ({ media, idx }) => {
return <CardMedia key={idx} src={media.src} title={media.title} />;
};
function renderField(field) {
switch (field.kind) {
case "media":
return (
<div className="card-cell">
{field.value.map((media, idx) => {
return renderMedia({ media, idx });
})}
</div>
);
case "line":
return (
<div style={{ height: `1rem`, width: `100%` }}>
<hr />
</div>
);
case "line-break":
return (
<div style={{ height: `${field.times || 1}rem`, width: `100%` }} />
);
case "item":
// this is like a span
return null;
case "markdown":
return <CardCustom {...field} />;
case "tag":
return (
<div
className="card-cell m0"
style={{
textTransform: `uppercase`,
fontSize: `.8em`,
lineHeight: `.8em`,
}}
>
<div
style={{
display: "flex",
justifyContent: `flex-${field.align || `start`}`,
}}
>
{field.value}
</div>
</div>
);
case "button":
return (
<div className="card-cell">
{field.title && <h4>{field.title}</h4>}
{/* <div className="card-row"> */}
{field.value.map((t, idx) => (
<CardButton key={`card-button-${idx}`} {...t} />
))}
{/* </div> */}
</div>
);
case "text":
return !isEmptyString(field.value) && <CardText {...field} />;
case "date":
return renderTime(field);
case "links":
return (
<div className="card-cell">
{field.title && <h4>{field.title}</h4>}
<div className="card-row m0">
{field.value.map(({ text, href }, idx) => (
<a href={href} key={`card-links-url-${idx}`}>
{text}
</a>
))}
</div>
</div>
);
case "list":
// Only render if some of the list's strings are non-empty
const shouldFieldRender =
!!field.value.length &&
!!field.value.filter((s) => !isEmptyString(s)).length;
return shouldFieldRender ? (
// <div className="card-cell">
<div>
{field.title && <h4>{field.title}</h4>}
<div className="card-row m0">
{field.value.map((t, idx) => (
<CardText key={`card-list-text-${idx}`} value={t} {...t} />
))}
</div>
</div>
) : null;
default:
return null;
}
}
function renderRow(row) {
return (
<div className="card-row" key={hash(row)}>
{row.map((field) => (
<span key={hash(field)}>{renderField(field)}</span>
))}
</div>
);
}
// TODO: render afterCaret appropriately from props
sources = [];
return (
<li
key={hash(content)}
className={`event-card ${isSelected ? "selected" : ""}`}
onClick={onSelect}
>
{content.map((row) => renderRow(row))}
{isOpen && (
<div className="card-bottomhalf">
{sources.map(() => (
<div className="card-row"></div>
))}
</div>
)}
{sources.length > 0 ? renderCaret() : null}
</li>
);
};

View File

@@ -1,9 +1,6 @@
import React from "react";
import { connect } from "react-redux";
import {
generateCardLayout,
Card,
} from "@forensic-architecture/design-system/dist/react";
import { generateCardLayout, Card } from "./Card";
import * as selectors from "../../selectors";
import { getFilterIdxFromColorSet } from "../../common/utilities";
@@ -70,6 +67,7 @@ class CardStack extends React.Component {
return events.map((event, idx) => {
const thisRef = React.createRef();
this.refs[idx] = thisRef;
console.log(event);
const content = generateTemplate({
event,

View File

@@ -0,0 +1,86 @@
import React from "react";
import PropTypes from "prop-types";
/**
* Primary UI component for user interaction
*/
export const Button = ({
primary,
backgroundColor,
borderRadius,
size,
label,
normalCursor,
...props
}) => {
const mode = primary ? "button--primary" : "button--secondary";
return (
<button
type="button"
className={[
"button",
`button--${size}`,
mode,
normalCursor ? "no-hover" : "",
].join(" ")}
style={{ backgroundColor: backgroundColor, borderRadius: borderRadius }}
{...props}
>
{label}
</button>
);
};
Button.propTypes = {
/**
* Is this the principal call to action on the page?
*/
primary: PropTypes.bool,
/**
* What background color to use
*/
backgroundColor: PropTypes.string,
/**
* How much rounded are they?
*/
borderRadius: PropTypes.string,
/**
* How large should the button be?
*/
size: PropTypes.oneOf(["small", "medium", "large"]),
/**
* Button contents
*/
label: PropTypes.string.isRequired,
/**
* Optional click handler
*/
onClick: PropTypes.func,
};
Button.defaultProps = {
backgroundColor: "red",
borderRadius: "0%",
primary: false,
size: "medium",
onClick: undefined,
};
const CardButton = ({
text,
color = "#000",
onClick = () => {},
normalCursor,
}) => (
<Button
size={"small"}
backgroundColor={color}
borderRadius={"12px"}
primary={false}
label={text}
onClick={onClick}
normalCursor={normalCursor}
/>
);
export default CardButton;

View File

@@ -0,0 +1,15 @@
import React from "react";
const CardCaret = ({ isOpen, toggle }) => {
let classes = isOpen ? "arrow-down" : "arrow-down folded";
return (
<div className="card-toggle" onClick={toggle}>
<p>
<i className={classes} />
</p>
</div>
);
};
export default CardCaret;

View File

@@ -0,0 +1,12 @@
import React from "react";
import marked from "marked";
// TODO could this be a security vulnerability?
const CardCustomField = ({ title, value }) => (
<div className="card-cell">
{title ? <h4>{title}</h4> : null}
<div dangerouslySetInnerHTML={{ __html: marked(`${value}`) }} />
</div>
);
export default CardCustomField;

View File

@@ -0,0 +1,61 @@
import React, { useRef } from "react";
import { useCallback } from "react";
import { typeForPath } from "../../../common/utilities";
const TITLE_LENGTH = 50;
// TODO should videos
// - play inline
// - appear zoomed out/in
// - only show cover image and then lightbox when clicked
// - show video control plane?
// TODO landscape image doesn't fit in box properly
const Media = ({ src, title }) => {
const videoRef = useRef();
const onVideoStart = useCallback(() => {
return videoRef.current?.play();
}, []);
const onVideoStop = useCallback(() => {
return videoRef.current?.pause();
}, []);
const type = typeForPath(src);
const formattedTitle =
title && title.length > TITLE_LENGTH
? `${title.slice(0, TITLE_LENGTH + 1)}...`
: title;
switch (type) {
case "Video":
return (
<div className="card-cell media">
{title && <h4 title={title}>{formattedTitle}</h4>}
<video
onMouseEnter={onVideoStart}
onMouseLeave={onVideoStop}
ref={videoRef}
// controls
// controlsList="nodownload noremoteplayback"
disablePictureInPicture
>
<source src={src} />
</video>
</div>
);
case "Image":
return (
<div className="card-cell media">
{title && <h4 title={title}>{formattedTitle}</h4>}
<div className="img-wrapper">
<img
src={src}
alt="an inline photograph for the event card component"
/>
</div>
</div>
);
default:
return null;
}
};
export default Media;

View File

@@ -0,0 +1,46 @@
import React, { useState } from "react";
const CardText = ({ title, value, hoverValue = null }) => {
const [showHover, setShowHover] = useState(false);
return (
<div className="card-cell">
{title ? <h4>{title}</h4> : null}
<div
style={{
width: `fit-content`,
}}
>
<div
onMouseOver={() => hoverValue && setShowHover(true)}
onMouseOut={() => hoverValue && setShowHover(false)}
>
{showHover ? (
<span
style={{
pointerEvents: `none`,
opacity: 0.8,
}}
>
<em>{hoverValue}</em>
</span>
) : (
<div
style={{
pointerEvents: `none`,
display: `inline-block`,
height: `1.1rem`,
borderBottom: hoverValue && `1px rgb(235, 68, 62) dashed`,
}}
>
{value}
</div>
)}
</div>
{/* {!showHover && value} */}
</div>
</div>
);
};
export default CardText;

View File

@@ -0,0 +1,29 @@
import React from "react";
import copy from "../../../common/data/copy.json";
import { isNotNullNorUndefined } from "../../../common/utilities";
const CardTime = ({ title = "Timestamp", timelabel, language, precision }) => {
const unknownLang = copy[language].cardstack.unknown_time;
if (isNotNullNorUndefined(timelabel)) {
return (
<div className="card-cell">
{/* <i className="material-icons left">today</i> */}
<h4>{title}</h4>
{timelabel}
{precision && precision !== "" ? ` - ${precision}` : null}
</div>
);
} else {
return (
<div className="card-cell">
{/* <i className="material-icons left">today</i> */}
<h4>{title}</h4>
{unknownLang}
</div>
);
}
};
export default CardTime;

View File

@@ -34,14 +34,14 @@ class TimelineAxis extends React.Component {
if (this.props.scaleX) {
this.x0 = d3
.axisBottom(this.props.scaleX)
.ticks(10)
.ticks(5)
.tickPadding(0)
.tickSize(contentHeight - TEXT_HEIGHT - marginTop)
.tickFormat(d3.timeFormat(fstFmt));
this.x1 = d3
.axisBottom(this.props.scaleX)
.ticks(10)
.ticks(5)
.tickPadding(marginTop)
.tickSize(0)
.tickFormat(d3.timeFormat(sndFmt));

37
src/scss/button.scss Normal file
View File

@@ -0,0 +1,37 @@
.button {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: normal;
border: 0;
border-radius: 0em;
cursor: pointer;
display: inline-block;
line-height: 1;
outline: none;
text-align: left;
}
.button--primary {
color: $offwhite;
background-color: $default;
}
.button--secondary {
color: #333;
background-color: transparent;
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 2px inset;
}
.button--small {
font-size: 12px;
padding: 10px 16px;
margin: 0.6em 0.3em 0 0;
}
.button--medium {
font-size: 14px;
padding: 11px 20px;
}
.button--large {
font-size: 16px;
padding: 12px 24px;
}
.no-hover {
cursor: auto !important;
}

View File

@@ -1,31 +1,35 @@
.event-card {
box-sizing: border-box;
margin: 1px 0 0 0;
margin: 2px 0;
padding: 15px;
border: 1px solid $black;
transition: 0.2 ease;
background: $midwhite;
opacity: 0.92;
color: $darkgrey;
box-shadow: 0 19px 19px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
list-style-type: none;
font-size: $large;
line-height: $xxlarge;
height: auto;
opacity: 0.9;
// height: auto;
// opacity: 0.9;
transition: background-color 0.4s;
text-align: left;
overflow-y: scroll;
height: 100%;
max-width: 400px;
&:hover {
background: $lightwhite;
transition: background-color 0.4s;
cursor: pointer;
// cursor: pointer;
}
h4 {
margin-bottom: 0;
margin-right: 5px;
text-transform: uppercase;
font-size: $xsmall;
font-size: $small;
color: $darkwhite;
font-weight: 100;
font-weight: 800;
&:first-child {
margin-top: 0;
@@ -36,27 +40,23 @@
margin: 0;
}
.material-icons {
font-size: $normal;
color: $darkwhite;
margin-right: 5px;
.card-row,
.card-col,
.card-cell {
margin: 5px 3px 5px 0px;
&.m0 {
margin: 0;
}
}
.card-row,
.card-col {
display: flex;
flex-direction: row;
margin: 5px 0 10px 0;
padding-bottom: 10px;
.card-cell {
flex: 1;
}
h4 {
min-width: 80px;
max-width: 80px;
}
}
.card-col {
@@ -72,23 +72,19 @@
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 8px 15px;
border-left: 5px solid $darkgrey;
padding: 5px 10px;
border-left: 3px solid $darkgrey;
background: linear-gradient(to right, $darkgrey 50%, transparent 50%);
background-size: 200% 100%;
background-position: right bottom;
margin-left: -16px;
margin-right: -16px;
&:hover {
background-color: $darkgrey;
color: white;
cursor: pointer;
.material-icons {
color: white;
}
background-position: left bottom;
transition: all 2s ease;
transition: all 1s ease-in;
}
}
@@ -98,6 +94,13 @@
font-size: 24px;
margin-right: 15px;
}
.source-type {
display: inline-block;
margin-right: 5px;
text-transform: uppercase;
font-weight: bold;
}
}
.card-cell {
@@ -177,6 +180,57 @@
}
}
.media {
display: flex;
max-height: 350px;
// justify-content: center;
flex-direction: column;
cursor: pointer;
.img-wrapper {
width: 100%;
display: flex;
img {
// width: auto;
// height: 100%;
max-width: 100%;
height: auto;
object-fit: cover;
// width: 100%;
// height: 250px;
}
}
video {
width: 100%;
padding-bottom: 10px;
user-select: none;
&:focus {
outline: 0 !important;
}
}
video::-webkit-media-controls-panel {
// remove Chrome's gradient
background-image: none !important;
filter: brightness(0.9);
display: flex;
align-self: flex-end;
// flex-basis: 35px;
background-color: rgba($red, 0.6);
}
/* Could Use thise as well for Individual Controls */
video::-webkit-media-controls-play-button {
align-self: center;
}
video::-webkit-media-controls-timeline {
display: none;
}
}
.category {
margin-bottom: 5px;

View File

@@ -1,11 +1,11 @@
// @import 'burger';
// @import 'card';
@import "card";
.card-stack {
position: absolute;
top: $card-right;
right: $card-right;
max-height: calc(100% - 180px);
max-height: calc(100% - 260px);
height: auto;
width: $card-width;
overflow-y: scroll;