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

@@ -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));