First version of side panel with person details.

Currently hidden. Can be shown by adding &sidePanel=true to URL.
Will be shown by default when it's ready.
This commit is contained in:
Przemek Wiech 2019-03-06 22:58:39 +01:00
parent 730642fb4e
commit 018bbe9ff0
7 changed files with 235 additions and 32 deletions

View File

@ -10,6 +10,7 @@
"history": "^4.7.2",
"jspdf": "^1.5.3",
"md5": "^2.2.1",
"parse-gedcom": "^1.0.4",
"query-string": "^5.1.1",
"react": "latest",
"react-dom": "latest",
@ -17,7 +18,7 @@
"react-router-dom": "^4.3.1",
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^0.84.0",
"topola": "^2.2"
"topola": "^2.2.2"
},
"devDependencies": {
"@types/d3": "^5.5.0",

View File

@ -1,12 +1,14 @@
import * as queryString from 'query-string';
import * as React from 'react';
import {Chart} from './chart';
import {Details} from './details';
import {getSelection, loadFromUrl, loadGedcom} from './load_data';
import {IndiInfo, JsonGedcomData} from 'topola';
import {IndiInfo} from 'topola';
import {Intro} from './intro';
import {Loader, Message} from 'semantic-ui-react';
import {Route, RouteComponentProps, Switch, Redirect} from 'react-router-dom';
import {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';
import {TopBar} from './top_bar';
import {TopolaData} from './gedcom_util';
/** Shows an error message. */
export function ErrorMessage(props: {message: string}) {
@ -20,7 +22,7 @@ export function ErrorMessage(props: {message: string}) {
interface State {
/** Loaded data. */
data?: JsonGedcomData;
data?: TopolaData;
/** Selected individual. */
selection?: IndiInfo;
/** Hash of the GEDCOM contents. */
@ -31,6 +33,8 @@ interface State {
loading: boolean;
/** URL of the data that is loaded or is being loaded. */
url?: string;
/** Whether the side panel is shoen. */
showSidePanel?: boolean;
}
export class App extends React.Component<RouteComponentProps, {}> {
@ -65,7 +69,8 @@ export class App extends React.Component<RouteComponentProps, {}> {
const parsedGen = Number(getParam('gen'));
const generation = !isNaN(parsedGen) ? parsedGen : undefined;
const hash = getParam('file');
const handleCors = getParam('handleCors') !== 'false';
const handleCors = getParam('handleCors') !== 'false'; // True by default.
const showSidePanel = getParam('sidePanel') === 'true'; // False by default.
if (!url && !hash) {
this.props.history.replace({pathname: '/'});
@ -80,10 +85,11 @@ export class App extends React.Component<RouteComponentProps, {}> {
Object.assign({}, this.state, {
data,
hash,
selection: getSelection(data, indi, generation),
selection: getSelection(data.chartData, indi, generation),
error: undefined,
loading: false,
url,
showSidePanel,
}),
);
},
@ -110,7 +116,11 @@ export class App extends React.Component<RouteComponentProps, {}> {
);
} else if (this.state.data && this.state.selection) {
// Update selection if it has changed in the URL.
const selection = getSelection(this.state.data, indi, generation);
const selection = getSelection(
this.state.data.chartData,
indi,
generation,
);
if (
this.state.selection.id !== selection.id ||
this.state.selection.generation !== selection.generation
@ -140,12 +150,22 @@ export class App extends React.Component<RouteComponentProps, {}> {
private renderMainArea = () => {
if (this.state.data && this.state.selection) {
return (
<Chart
data={this.state.data}
onSelection={this.onSelection}
selection={this.state.selection}
ref={(ref) => (this.chartRef = ref)}
/>
<div id="content">
<Chart
data={this.state.data.chartData}
onSelection={this.onSelection}
selection={this.state.selection}
ref={(ref) => (this.chartRef = ref)}
/>
{this.state.showSidePanel ? (
<div id="sidePanel">
<Details
gedcom={this.state.data.gedcom}
indi={this.state.selection.id}
/>
</div>
) : null}
</div>
);
}
if (this.state.error) {

118
src/details.tsx Normal file
View File

@ -0,0 +1,118 @@
import * as React from 'react';
import {GedcomData} from './gedcom_util';
import {GedcomEntry} from 'parse-gedcom';
interface Props {
gedcom: GedcomData;
indi: string;
}
function eventDetails(entry: GedcomEntry, header: string) {
const lines = [];
const date = entry.tree.find((subentry) => subentry.tag === 'DATE');
if (date && date.data) {
lines.push(date.data);
}
const place = entry.tree.find((subentry) => subentry.tag === 'PLAC');
if (place && place.data) {
lines.push(place.data);
}
entry.tree
.filter((subentry) => subentry.tag === 'NOTE')
.forEach((note) => {
lines.push(<i>{note.data}</i>);
});
if (!lines.length) {
return null;
}
return (
<>
<div className="ui sub header">{header}</div>
<span>
{lines.map((line) => (
<>
{line}
<br />
</>
))}
</span>
</>
);
}
function dataDetails(entry: GedcomEntry, header: string) {
const lines = [];
if (entry.data) {
lines.push(entry.data);
}
entry.tree
.filter((subentry) => subentry.tag === 'NOTE')
.forEach((note) => {
lines.push(<i>{note.data}</i>);
});
if (!lines.length) {
return null;
}
return (
<>
<div className="ui sub header">{header}</div>
<span>
{lines.map((line) => (
<>
{line}
<br />
</>
))}
</span>
</>
);
}
function nameDetails(entry: GedcomEntry, header: string) {
return (
<h2 className="ui header">
{entry.data
.split('/')
.filter((name) => !!name)
.map((name) => (
<>
{name}
<br />
</>
))}
</h2>
);
}
function getDetails(
entries: GedcomEntry[],
tags: string[],
detailsFunction: (entry: GedcomEntry, header: string) => JSX.Element | null,
): JSX.Element[] {
return tags
.flatMap((tag) =>
entries
.filter((entry) => entry.tag === tag)
.map((entry) => detailsFunction(entry, tag)),
)
.filter((element) => element !== null)
.map((element) => <div className="ui segment">{element}</div>);
}
const NAME_TAGS = ['NAME'];
const EVENT_TAGS = ['BIRT', 'BAPM', 'CHR', 'DEAT', 'BURI'];
const DATA_TAGS = ['TITL', 'OCCU', 'WWW', 'EMAIL'];
export class Details extends React.Component<Props, {}> {
render() {
const entries = this.props.gedcom.indis[this.props.indi].tree;
return (
<div className="ui segments" id="details">
{getDetails(entries, NAME_TAGS, nameDetails)}
{getDetails(entries, EVENT_TAGS, eventDetails)}
{getDetails(entries, DATA_TAGS, dataDetails)}
</div>
);
}
}

View File

@ -1,4 +1,38 @@
import {gedcomToJson, JsonFam, JsonGedcomData, JsonIndi} from 'topola';
import {JsonFam, JsonGedcomData, JsonIndi, gedcomEntriesToJson} from 'topola';
import {GedcomEntry, parse as parseGedcom} from 'parse-gedcom';
export interface GedcomData {
head: GedcomEntry;
indis: {[key: string]: GedcomEntry};
fams: {[key: string]: GedcomEntry};
}
export interface TopolaData {
chartData: JsonGedcomData;
gedcom: GedcomData;
}
/**
* Returns the identifier extracted from a pointer string.
* E.g. '@I123@' -> 'I123'
*/
function pointerToId(pointer: string): string {
return pointer.substring(1, pointer.length - 1);
}
function prepareGedcom(entries: GedcomEntry[]): GedcomData {
const head = entries.find((entry) => entry.tag === 'HEAD')!;
const indis: {[key: string]: GedcomEntry} = {};
const fams: {[key: string]: GedcomEntry} = {};
entries.forEach((entry) => {
if (entry.tag === 'INDI') {
indis[pointerToId(entry.pointer)] = entry;
} else if (entry.tag === 'FAM') {
fams[pointerToId(entry.pointer)] = entry;
}
});
return {head, indis, fams};
}
function strcmp(a: string, b: string) {
if (a < b) {
@ -94,8 +128,9 @@ function filterImages(gedcom: JsonGedcomData): JsonGedcomData {
* - sort children by birth date
* - remove images that are not HTTP links.
*/
export function convertGedcom(gedcom: string): JsonGedcomData {
const json = gedcomToJson(gedcom);
export function convertGedcom(gedcom: string): TopolaData {
const entries = parseGedcom(gedcom);
const json = gedcomEntriesToJson(entries);
if (
!json ||
!json.indis ||
@ -105,5 +140,9 @@ export function convertGedcom(gedcom: string): JsonGedcomData {
) {
throw new Error('Failed to read GEDCOM file');
}
return filterImages(sortChildren(json));
return {
chartData: filterImages(sortChildren(json)),
gedcom: prepareGedcom(entries),
};
}

View File

@ -12,9 +12,20 @@ body, html {
flex-direction: column;
}
#content {
flex: 1 1 auto;
display: flex;
}
#svgContainer {
flex: 1 1 auto;
overflow: scroll;
overflow: auto;
}
#sidePanel {
flex: 0 0 320px;
overflow: auto;
border-left: solid #ccc 1px;
}
.hidden {

View File

@ -1,4 +1,4 @@
import {convertGedcom} from './gedcom_util';
import {convertGedcom, TopolaData} from './gedcom_util';
import {IndiInfo, JsonGedcomData} from 'topola';
/**
@ -16,11 +16,22 @@ export function getSelection(
};
}
function prepareData(gedcom: string, cacheId: string): TopolaData {
const data = convertGedcom(gedcom);
const serializedData = JSON.stringify(data);
try {
sessionStorage.setItem(cacheId, serializedData);
} catch (e) {
console.warn('Failed to store data in session storage: ' + e);
}
return data;
}
/** Fetches data from the given URL. Uses cors-anywhere if handleCors is true. */
export function loadFromUrl(
url: string,
handleCors: boolean,
): Promise<JsonGedcomData> {
): Promise<TopolaData> {
const cachedData = sessionStorage.getItem(url);
if (cachedData) {
return Promise.resolve(JSON.parse(cachedData));
@ -38,10 +49,7 @@ export function loadFromUrl(
return response.text();
})
.then((gedcom) => {
const data = convertGedcom(gedcom);
const serializedData = JSON.stringify(data);
sessionStorage.setItem(url, serializedData);
return data;
return prepareData(gedcom, url);
});
}
@ -54,17 +62,11 @@ function loadGedcomSync(hash: string, gedcom?: string) {
if (!gedcom) {
throw new Error('Error loading data. Please upload your file again.');
}
const data = convertGedcom(gedcom);
const serializedData = JSON.stringify(data);
sessionStorage.setItem(hash, serializedData);
return data;
return prepareData(gedcom, hash);
}
/** Loads data from the given GEDCOM file contents. */
export function loadGedcom(
hash: string,
gedcom?: string,
): Promise<JsonGedcomData> {
export function loadGedcom(hash: string, gedcom?: string): Promise<TopolaData> {
try {
return Promise.resolve(loadGedcomSync(hash, gedcom));
} catch (e) {

12
src/parse-gedcom.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
// Data type definitions for the parse-gedcom library.
declare module 'parse-gedcom' {
interface GedcomEntry {
level: number;
pointer: string;
tag: string;
data: string;
tree: GedcomEntry[];
}
export function parse(input: string): GedcomEntry[];
}