mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-05-26 15:16:14 +00:00
Improvements for events in info panel (#72)
* Improved visuals of events panel, handle family events * Create separate package for details panel * Improved handling of multiple event notes
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ cypress/plugins/index.js
|
|||||||
cypress/support/commands.js
|
cypress/support/commands.js
|
||||||
cypress/support/index.js
|
cypress/support/index.js
|
||||||
cypress/videos
|
cypress/videos
|
||||||
|
.idea
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
DEFALUT_CONFIG,
|
DEFALUT_CONFIG,
|
||||||
} from './config';
|
} from './config';
|
||||||
import {DataSourceEnum, SourceSelection} from './datasource/data_source';
|
import {DataSourceEnum, SourceSelection} from './datasource/data_source';
|
||||||
import {Details} from './details';
|
import {Details} from './details/details';
|
||||||
import {EmbeddedDataSource, EmbeddedSourceSpec} from './datasource/embedded';
|
import {EmbeddedDataSource, EmbeddedSourceSpec} from './datasource/embedded';
|
||||||
import {FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl';
|
import {FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl';
|
||||||
import {getI18nMessage} from './util/error_i18n';
|
import {getI18nMessage} from './util/error_i18n';
|
||||||
|
|||||||
235
src/details.tsx
235
src/details.tsx
@@ -1,235 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import flatMap from 'array.prototype.flatmap';
|
|
||||||
import Linkify from 'react-linkify';
|
|
||||||
import {
|
|
||||||
FormattedMessage,
|
|
||||||
injectIntl,
|
|
||||||
IntlShape,
|
|
||||||
WrappedComponentProps,
|
|
||||||
} from 'react-intl';
|
|
||||||
import {GedcomData, pointerToId} from './util/gedcom_util';
|
|
||||||
import {GedcomEntry} from 'parse-gedcom';
|
|
||||||
import {translateDate} from './util/date_util';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
gedcom: GedcomData;
|
|
||||||
indi: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EVENT_TAGS = ['BIRT', 'BAPM', 'CHR', 'EVEN', 'CENS', 'DEAT', 'BURI'];
|
|
||||||
const EXCLUDED_TAGS = ['NAME', 'SEX', 'FAMC', 'FAMS', 'NOTE', 'SOUR'];
|
|
||||||
const TAG_DESCRIPTIONS = new Map([
|
|
||||||
['ADOP', 'Adoption'],
|
|
||||||
['BAPM', 'Baptism'],
|
|
||||||
['BIRT', 'Birth'],
|
|
||||||
['BURI', 'Burial'],
|
|
||||||
['CENS', 'Census'],
|
|
||||||
['CHR', 'Christening'],
|
|
||||||
['CREM', 'Cremation'],
|
|
||||||
['DEAT', 'Death'],
|
|
||||||
['EDUC', 'Education'],
|
|
||||||
['EMAIL', 'E-mail'],
|
|
||||||
['EMIG', 'Emigration'],
|
|
||||||
['EVEN', 'Event'],
|
|
||||||
['FACT', 'Fact'],
|
|
||||||
['IMMI', 'Immigration'],
|
|
||||||
['MARR', 'Marriage'],
|
|
||||||
['MILT', 'Military services'],
|
|
||||||
['NATU', 'Naturalization'],
|
|
||||||
['OCCU', 'Occupation'],
|
|
||||||
['TITL', 'Title'],
|
|
||||||
['WWW', 'WWW'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
function translateTag(tag: string) {
|
|
||||||
const normalizedTag = tag.replace(/_/g, '');
|
|
||||||
return (
|
|
||||||
<FormattedMessage
|
|
||||||
id={`gedcom.${normalizedTag}`}
|
|
||||||
defaultMessage={TAG_DESCRIPTIONS.get(normalizedTag) || normalizedTag}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function joinLines(lines: (JSX.Element | string)[]) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{lines.map((line, index) => (
|
|
||||||
<div key={index}>
|
|
||||||
<Linkify properties={{target: '_blank'}}>{line}</Linkify>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the data for the given GEDCOM entry as an array of lines. Supports
|
|
||||||
* continuations with CONT and CONC.
|
|
||||||
*/
|
|
||||||
function getData(entry: GedcomEntry) {
|
|
||||||
const result = [entry.data];
|
|
||||||
entry.tree.forEach((subentry) => {
|
|
||||||
if (subentry.tag === 'CONC' && subentry.data) {
|
|
||||||
const last = result.length - 1;
|
|
||||||
result[last] += subentry.data;
|
|
||||||
} else if (subentry.tag === 'CONT' && subentry.data) {
|
|
||||||
result.push(subentry.data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function eventDetails(entry: GedcomEntry, intl: IntlShape) {
|
|
||||||
const lines = [];
|
|
||||||
if (entry.data && entry.data.length > 1) {
|
|
||||||
lines.push(<i>{entry.data}</i>);
|
|
||||||
}
|
|
||||||
const date = entry.tree.find((subentry) => subentry.tag === 'DATE');
|
|
||||||
if (date && date.data) {
|
|
||||||
lines.push(translateDate(date.data, intl));
|
|
||||||
}
|
|
||||||
const place = entry.tree.find((subentry) => subentry.tag === 'PLAC');
|
|
||||||
if (place && place.data) {
|
|
||||||
lines.push(...getData(place));
|
|
||||||
}
|
|
||||||
entry.tree
|
|
||||||
.filter((subentry) => subentry.tag === 'NOTE')
|
|
||||||
.forEach((note) =>
|
|
||||||
getData(note).forEach((line) => lines.push(<i>{line}</i>)),
|
|
||||||
);
|
|
||||||
if (!lines.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="ui sub header">{translateTag(entry.tag)}</div>
|
|
||||||
<span>{joinLines(lines)}</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dataDetails(entry: GedcomEntry) {
|
|
||||||
const lines = [];
|
|
||||||
if (entry.data) {
|
|
||||||
lines.push(...getData(entry));
|
|
||||||
}
|
|
||||||
entry.tree
|
|
||||||
.filter((subentry) => subentry.tag === 'NOTE')
|
|
||||||
.forEach((note) =>
|
|
||||||
getData(note).forEach((line) => lines.push(<i>{line}</i>)),
|
|
||||||
);
|
|
||||||
if (!lines.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="ui sub header">{translateTag(entry.tag)}</div>
|
|
||||||
<span>{joinLines(lines)}</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function noteDetails(entry: GedcomEntry) {
|
|
||||||
return joinLines(
|
|
||||||
getData(entry).map((line, index) => <i key={index}>{line}</i>),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function nameDetails(entry: GedcomEntry) {
|
|
||||||
return (
|
|
||||||
<h2 className="ui header">
|
|
||||||
{entry.data
|
|
||||||
.split('/')
|
|
||||||
.filter((name) => !!name)
|
|
||||||
.map((name, index) => (
|
|
||||||
<div key={index}>
|
|
||||||
{name}
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</h2>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDetails(
|
|
||||||
entries: GedcomEntry[],
|
|
||||||
tags: string[],
|
|
||||||
detailsFunction: (entry: GedcomEntry) => JSX.Element | null,
|
|
||||||
): JSX.Element[] {
|
|
||||||
return flatMap(tags, (tag) =>
|
|
||||||
entries
|
|
||||||
.filter((entry) => entry.tag === tag)
|
|
||||||
.map((entry) => detailsFunction(entry)),
|
|
||||||
)
|
|
||||||
.filter((element) => element !== null)
|
|
||||||
.map((element, index) => (
|
|
||||||
<div className="ui segment" key={index}>
|
|
||||||
{element}
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if there is displayable information in this entry.
|
|
||||||
* Returns false if there is no data in this entry or this is only a reference
|
|
||||||
* to another entry.
|
|
||||||
*/
|
|
||||||
function hasData(entry: GedcomEntry) {
|
|
||||||
return entry.tree.length > 0 || (entry.data && !entry.data.startsWith('@'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOtherDetails(entries: GedcomEntry[]) {
|
|
||||||
return entries
|
|
||||||
.filter(
|
|
||||||
(entry) =>
|
|
||||||
!EXCLUDED_TAGS.includes(entry.tag) && !EVENT_TAGS.includes(entry.tag),
|
|
||||||
)
|
|
||||||
.filter(hasData)
|
|
||||||
.map((entry) => dataDetails(entry))
|
|
||||||
.filter((element) => element !== null)
|
|
||||||
.map((element, index) => (
|
|
||||||
<div className="ui segment" key={index}>
|
|
||||||
{element}
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the entry is a reference to a top-level entry, the referenced entry is
|
|
||||||
* returned. Otherwise, returns the given entry unmodified.
|
|
||||||
*/
|
|
||||||
function dereference(entry: GedcomEntry, gedcom: GedcomData) {
|
|
||||||
if (entry.data) {
|
|
||||||
const dereferenced = gedcom.other[pointerToId(entry.data)];
|
|
||||||
if (dereferenced) {
|
|
||||||
return dereferenced;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DetailsComponent extends React.Component<
|
|
||||||
Props & WrappedComponentProps,
|
|
||||||
{}
|
|
||||||
> {
|
|
||||||
render() {
|
|
||||||
const entries = this.props.gedcom.indis[this.props.indi].tree;
|
|
||||||
const entriesWithData = entries
|
|
||||||
.map((entry) => dereference(entry, this.props.gedcom))
|
|
||||||
.filter(hasData);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ui segments details">
|
|
||||||
{getDetails(entries, ['NAME'], nameDetails)}
|
|
||||||
{getDetails(entries, EVENT_TAGS, (entry) =>
|
|
||||||
eventDetails(entry, this.props.intl),
|
|
||||||
)}
|
|
||||||
{getOtherDetails(entriesWithData)}
|
|
||||||
{getDetails(entriesWithData, ['NOTE'], noteDetails)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const Details = injectIntl(DetailsComponent);
|
|
||||||
148
src/details/details.tsx
Normal file
148
src/details/details.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import flatMap from 'array.prototype.flatmap';
|
||||||
|
import {injectIntl, WrappedComponentProps} from 'react-intl';
|
||||||
|
import {dereference, GedcomData, getData} from '../util/gedcom_util';
|
||||||
|
import {GedcomEntry} from 'parse-gedcom';
|
||||||
|
import {TranslatedTag} from './translated-tag';
|
||||||
|
import {Events} from './events';
|
||||||
|
import {MultilineText} from './multiline-text';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
gedcom: GedcomData;
|
||||||
|
indi: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXCLUDED_TAGS = [
|
||||||
|
'BIRT',
|
||||||
|
'BAPM',
|
||||||
|
'CHR',
|
||||||
|
'EVEN',
|
||||||
|
'CENS',
|
||||||
|
'DEAT',
|
||||||
|
'BURI',
|
||||||
|
'NAME',
|
||||||
|
'SEX',
|
||||||
|
'FAMC',
|
||||||
|
'FAMS',
|
||||||
|
'NOTE',
|
||||||
|
'SOUR',
|
||||||
|
];
|
||||||
|
|
||||||
|
function dataDetails(entry: GedcomEntry) {
|
||||||
|
const lines = [];
|
||||||
|
if (entry.data) {
|
||||||
|
lines.push(...getData(entry));
|
||||||
|
}
|
||||||
|
entry.tree
|
||||||
|
.filter((subentry) => subentry.tag === 'NOTE')
|
||||||
|
.forEach((note) =>
|
||||||
|
getData(note).forEach((line) => lines.push(<i>{line}</i>)),
|
||||||
|
);
|
||||||
|
if (!lines.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="ui sub header">
|
||||||
|
<TranslatedTag tag={entry.tag} />
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
<MultilineText lines={lines} />
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function noteDetails(entry: GedcomEntry) {
|
||||||
|
return (
|
||||||
|
<MultilineText
|
||||||
|
lines={getData(entry).map((line, index) => (
|
||||||
|
<i key={index}>{line}</i>
|
||||||
|
))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nameDetails(entry: GedcomEntry) {
|
||||||
|
return (
|
||||||
|
<h2 className="ui header">
|
||||||
|
{entry.data
|
||||||
|
.split('/')
|
||||||
|
.filter((name) => !!name)
|
||||||
|
.map((name, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
{name}
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDetails(
|
||||||
|
entries: GedcomEntry[],
|
||||||
|
tags: string[],
|
||||||
|
detailsFunction: (entry: GedcomEntry) => JSX.Element | null,
|
||||||
|
): JSX.Element[] {
|
||||||
|
return flatMap(tags, (tag) =>
|
||||||
|
entries
|
||||||
|
.filter((entry) => entry.tag === tag)
|
||||||
|
.map((entry) => detailsFunction(entry)),
|
||||||
|
)
|
||||||
|
.filter((element) => element !== null)
|
||||||
|
.map((element, index) => (
|
||||||
|
<div className="ui segment" key={index}>
|
||||||
|
{element}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if there is displayable information in this entry.
|
||||||
|
* Returns false if there is no data in this entry or this is only a reference
|
||||||
|
* to another entry.
|
||||||
|
*/
|
||||||
|
function hasData(entry: GedcomEntry) {
|
||||||
|
return entry.tree.length > 0 || (entry.data && !entry.data.startsWith('@'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOtherDetails(entries: GedcomEntry[]) {
|
||||||
|
return entries
|
||||||
|
.filter((entry) => !EXCLUDED_TAGS.includes(entry.tag))
|
||||||
|
.filter(hasData)
|
||||||
|
.map((entry) => dataDetails(entry))
|
||||||
|
.filter((element) => element !== null)
|
||||||
|
.map((element, index) => (
|
||||||
|
<div className="ui segment" key={index}>
|
||||||
|
{element}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
class DetailsComponent extends React.Component<
|
||||||
|
Props & WrappedComponentProps,
|
||||||
|
{}
|
||||||
|
> {
|
||||||
|
render() {
|
||||||
|
const entries = this.props.gedcom.indis[this.props.indi].tree;
|
||||||
|
const entriesWithData = entries
|
||||||
|
.map((entry) =>
|
||||||
|
dereference(entry, this.props.gedcom, (gedcom) => gedcom.other),
|
||||||
|
)
|
||||||
|
.filter(hasData);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ui segments details">
|
||||||
|
{getDetails(entries, ['NAME'], nameDetails)}
|
||||||
|
<Events
|
||||||
|
gedcom={this.props.gedcom}
|
||||||
|
entries={entries}
|
||||||
|
indi={this.props.indi}
|
||||||
|
/>
|
||||||
|
{getOtherDetails(entriesWithData)}
|
||||||
|
{getDetails(entriesWithData, ['NOTE'], noteDetails)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const Details = injectIntl(DetailsComponent);
|
||||||
235
src/details/events.tsx
Normal file
235
src/details/events.tsx
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {injectIntl, IntlShape, WrappedComponentProps} from 'react-intl';
|
||||||
|
import {dereference, GedcomData, getData} from '../util/gedcom_util';
|
||||||
|
import {GedcomEntry} from 'parse-gedcom';
|
||||||
|
import {compareDates, translateDate} from '../util/date_util';
|
||||||
|
import {DateOrRange, getDate} from 'topola';
|
||||||
|
import {TranslatedTag} from './translated-tag';
|
||||||
|
import {MultilineText} from './multiline-text';
|
||||||
|
import flatMap from 'array.prototype.flatmap';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
gedcom: GedcomData;
|
||||||
|
indi: string;
|
||||||
|
entries: GedcomEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
type: string;
|
||||||
|
date: DateOrRange | undefined;
|
||||||
|
header: JSX.Element;
|
||||||
|
subHeader: JSX.Element | null;
|
||||||
|
place: JSX.Element | null;
|
||||||
|
notes: JSX.Element | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EVENT_TAGS = [
|
||||||
|
'BIRT',
|
||||||
|
'BAPM',
|
||||||
|
'CHR',
|
||||||
|
'FAMS',
|
||||||
|
'EVEN',
|
||||||
|
'CENS',
|
||||||
|
'DEAT',
|
||||||
|
'BURI',
|
||||||
|
];
|
||||||
|
|
||||||
|
const FAMILY_EVENT_TAGS = ['MARR', 'DIV'];
|
||||||
|
|
||||||
|
function eventHeader(tag: string, date: GedcomEntry | null, intl: IntlShape) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span style={{textTransform: 'uppercase'}} className="ui small header">
|
||||||
|
<TranslatedTag tag={tag} />
|
||||||
|
</span>
|
||||||
|
{date && date.data ? (
|
||||||
|
<span className="ui sub header right floated">
|
||||||
|
{translateDate(date.data, intl)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventFamilyDetails(
|
||||||
|
entry: GedcomEntry,
|
||||||
|
indi: string,
|
||||||
|
familyEntry: GedcomEntry,
|
||||||
|
gedcom: GedcomData,
|
||||||
|
) {
|
||||||
|
const spouseReference = familyEntry.tree
|
||||||
|
.filter((familySubEntry) => ['WIFE', 'HUSB'].includes(familySubEntry.tag))
|
||||||
|
.find((familySubEntry) => !familySubEntry.data.includes(indi));
|
||||||
|
|
||||||
|
if (spouseReference) {
|
||||||
|
const spouseName = dereference(
|
||||||
|
spouseReference,
|
||||||
|
gedcom,
|
||||||
|
(gedcom) => gedcom.indis,
|
||||||
|
)
|
||||||
|
.tree.filter((subEntry) => subEntry.tag === 'NAME')
|
||||||
|
.find(
|
||||||
|
(subEntry) =>
|
||||||
|
subEntry.tree.filter(
|
||||||
|
(nameEntry) =>
|
||||||
|
nameEntry.tag === 'TYPE' && nameEntry.data === 'married',
|
||||||
|
).length === 0,
|
||||||
|
);
|
||||||
|
if (spouseName) {
|
||||||
|
return <div className="meta">{spouseName.data.replaceAll('/', '')}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventPlace(entry: GedcomEntry) {
|
||||||
|
const place = entry.tree.find((subEntry) => subEntry.tag === 'PLAC');
|
||||||
|
if (place && place.data) {
|
||||||
|
return <div className="description">{getData(place)}</div>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventNotes(entry: GedcomEntry, gedcom: GedcomData) {
|
||||||
|
const notes = entry.tree
|
||||||
|
.filter((subentry) => ['NOTE', 'TYPE'].includes(subentry.tag))
|
||||||
|
.map((note) => dereference(note, gedcom, (gedcom) => gedcom.other))
|
||||||
|
.map((note) => noteDetails(note));
|
||||||
|
|
||||||
|
if (notes && notes.length) {
|
||||||
|
return (
|
||||||
|
<div className="description">
|
||||||
|
{notes.map((note, index) => (
|
||||||
|
<div key={index}>{note}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function noteDetails(entry: GedcomEntry) {
|
||||||
|
return (
|
||||||
|
<MultilineText
|
||||||
|
lines={getData(entry).map((line, index) => (
|
||||||
|
<i key={index}>{line}</i>
|
||||||
|
))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventDetails(event: Event) {
|
||||||
|
return (
|
||||||
|
<div className="content">
|
||||||
|
{event.header}
|
||||||
|
{event.subHeader}
|
||||||
|
{event.place}
|
||||||
|
{event.notes}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventDetails(
|
||||||
|
entries: GedcomEntry[],
|
||||||
|
gedcom: GedcomData,
|
||||||
|
indi: string,
|
||||||
|
intl: IntlShape,
|
||||||
|
): JSX.Element | null {
|
||||||
|
const events = flatMap(EVENT_TAGS, (tag) =>
|
||||||
|
entries
|
||||||
|
.filter((entry) => entry.tag === tag)
|
||||||
|
.map((eventEntry) => toEvent(eventEntry, gedcom, indi, intl))
|
||||||
|
.flatMap((events) => events)
|
||||||
|
.sort((event1, event2) => compareDates(event1.date, event2.date))
|
||||||
|
.map((event) => eventDetails(event)),
|
||||||
|
);
|
||||||
|
if (events && events.length) {
|
||||||
|
return (
|
||||||
|
<div className="ui segment divided items">
|
||||||
|
{events.map((eventElement, index) => (
|
||||||
|
<div className="ui attached item" key={index}>
|
||||||
|
{eventElement}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toEvent(
|
||||||
|
entry: GedcomEntry,
|
||||||
|
gedcom: GedcomData,
|
||||||
|
indi: string,
|
||||||
|
intl: IntlShape,
|
||||||
|
): Event[] {
|
||||||
|
if (entry.tag === 'FAMS') {
|
||||||
|
return toFamilyEvents(entry, gedcom, indi, intl);
|
||||||
|
}
|
||||||
|
return toIndiEvent(entry, gedcom, indi, intl);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toIndiEvent(
|
||||||
|
entry: GedcomEntry,
|
||||||
|
gedcom: GedcomData,
|
||||||
|
indi: string,
|
||||||
|
intl: IntlShape,
|
||||||
|
): Event[] {
|
||||||
|
const date = resolveDate(entry) || null;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
date: date ? getDate(date.data) : undefined,
|
||||||
|
type: entry.tag,
|
||||||
|
header: eventHeader(entry.tag, date, intl),
|
||||||
|
subHeader: null,
|
||||||
|
place: eventPlace(entry),
|
||||||
|
notes: eventNotes(entry, gedcom),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDate(entry: GedcomEntry) {
|
||||||
|
return entry.tree.find((subEntry) => subEntry.tag === 'DATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFamilyEvents(
|
||||||
|
entry: GedcomEntry,
|
||||||
|
gedcom: GedcomData,
|
||||||
|
indi: string,
|
||||||
|
intl: IntlShape,
|
||||||
|
): Event[] {
|
||||||
|
const family = dereference(entry, gedcom, (gedcom) => gedcom.fams);
|
||||||
|
return flatMap(FAMILY_EVENT_TAGS, (tag) =>
|
||||||
|
family.tree.filter((entry) => entry.tag === tag),
|
||||||
|
).map((familyMarriageEvent) => {
|
||||||
|
const date = resolveDate(familyMarriageEvent) || null;
|
||||||
|
return {
|
||||||
|
date: date ? getDate(date.data) : undefined,
|
||||||
|
type: familyMarriageEvent.tag,
|
||||||
|
header: eventHeader(familyMarriageEvent.tag, date, intl),
|
||||||
|
subHeader: eventFamilyDetails(familyMarriageEvent, indi, family, gedcom),
|
||||||
|
place: eventPlace(familyMarriageEvent),
|
||||||
|
notes: eventNotes(familyMarriageEvent, gedcom),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventsComponent extends React.Component<
|
||||||
|
Props & WrappedComponentProps,
|
||||||
|
{}
|
||||||
|
> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{getEventDetails(
|
||||||
|
this.props.entries,
|
||||||
|
this.props.gedcom,
|
||||||
|
this.props.indi,
|
||||||
|
this.props.intl,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Events = injectIntl(EventsComponent);
|
||||||
31
src/details/multiline-text.tsx
Normal file
31
src/details/multiline-text.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {injectIntl, WrappedComponentProps} from 'react-intl';
|
||||||
|
import Linkify from 'react-linkify';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
lines: (JSX.Element | string)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinLines(lines: (JSX.Element | string)[]) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{lines.map((line, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<Linkify properties={{target: '_blank'}}>{line}</Linkify>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultilineTextComponent extends React.Component<
|
||||||
|
Props & WrappedComponentProps,
|
||||||
|
{}
|
||||||
|
> {
|
||||||
|
render() {
|
||||||
|
return joinLines(this.props.lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultilineText = injectIntl(MultilineTextComponent);
|
||||||
51
src/details/translated-tag.tsx
Normal file
51
src/details/translated-tag.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TAG_DESCRIPTIONS = new Map([
|
||||||
|
['ADOP', 'Adoption'],
|
||||||
|
['BAPM', 'Baptism'],
|
||||||
|
['BIRT', 'Birth'],
|
||||||
|
['BURI', 'Burial'],
|
||||||
|
['CENS', 'Census'],
|
||||||
|
['CHR', 'Christening'],
|
||||||
|
['CREM', 'Cremation'],
|
||||||
|
['DEAT', 'Death'],
|
||||||
|
['EDUC', 'Education'],
|
||||||
|
['EMAIL', 'E-mail'],
|
||||||
|
['EMIG', 'Emigration'],
|
||||||
|
['EVEN', 'Event'],
|
||||||
|
['FACT', 'Fact'],
|
||||||
|
['IMMI', 'Immigration'],
|
||||||
|
['MARR', 'Marriage'],
|
||||||
|
['DIV', 'Divorce'],
|
||||||
|
['MILT', 'Military services'],
|
||||||
|
['NATU', 'Naturalization'],
|
||||||
|
['OCCU', 'Occupation'],
|
||||||
|
['TITL', 'Title'],
|
||||||
|
['WWW', 'WWW'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
function translateTag(tag: string) {
|
||||||
|
const normalizedTag = tag.replace(/_/g, '');
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id={`gedcom.${normalizedTag}`}
|
||||||
|
defaultMessage={TAG_DESCRIPTIONS.get(normalizedTag) || normalizedTag}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TranslatedTagComponent extends React.Component<
|
||||||
|
Props & WrappedComponentProps,
|
||||||
|
{}
|
||||||
|
> {
|
||||||
|
render() {
|
||||||
|
return translateTag(this.props.tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TranslatedTag = injectIntl(TranslatedTagComponent);
|
||||||
@@ -54,6 +54,8 @@
|
|||||||
"gedcom.TITL": "Tytuł",
|
"gedcom.TITL": "Tytuł",
|
||||||
"gedcom.WWW": "Strona WWW",
|
"gedcom.WWW": "Strona WWW",
|
||||||
"gedcom._UPD": "Ostatnia aktualizacja",
|
"gedcom._UPD": "Ostatnia aktualizacja",
|
||||||
|
"gedcom.MARR": "Małżeństwo",
|
||||||
|
"gedcom.DIV": "Rozwód",
|
||||||
"date.abt": "około",
|
"date.abt": "około",
|
||||||
"date.cal": "wyliczone",
|
"date.cal": "wyliczone",
|
||||||
"date.est": "oszacowane",
|
"date.est": "oszacowane",
|
||||||
|
|||||||
@@ -97,3 +97,34 @@ export function formatDateOrRange(
|
|||||||
export function translateDate(gedcomDate: string, intl: IntlShape): string {
|
export function translateDate(gedcomDate: string, intl: IntlShape): string {
|
||||||
return formatDateOrRange(getDate(gedcomDate), intl);
|
return formatDateOrRange(getDate(gedcomDate), intl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Compares a dates given in GEDCOM format. */
|
||||||
|
export function compareDates(
|
||||||
|
firstDateOrRange: DateOrRange | undefined,
|
||||||
|
secondDateOrRange: DateOrRange | undefined,
|
||||||
|
): number {
|
||||||
|
const date1 =
|
||||||
|
firstDateOrRange &&
|
||||||
|
(firstDateOrRange.date ||
|
||||||
|
(firstDateOrRange.dateRange && firstDateOrRange.dateRange.from));
|
||||||
|
const date2 =
|
||||||
|
secondDateOrRange &&
|
||||||
|
(secondDateOrRange.date ||
|
||||||
|
(secondDateOrRange.dateRange && secondDateOrRange.dateRange.from));
|
||||||
|
if (!date1 || !date1.year || !date2 || !date2.year) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (date1.year !== date2.year) {
|
||||||
|
return date1.year - date2.year;
|
||||||
|
}
|
||||||
|
if (!date1.month || !date2.month) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (date1.month !== date2.month) {
|
||||||
|
return date1.month - date2.month;
|
||||||
|
}
|
||||||
|
if (date1.day && date2.day && date1.day !== date2.day) {
|
||||||
|
return date1.month - date2.month;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {GedcomEntry, parse as parseGedcom} from 'parse-gedcom';
|
import {GedcomEntry, parse as parseGedcom} from 'parse-gedcom';
|
||||||
import {TopolaError} from './error';
|
import {TopolaError} from './error';
|
||||||
import {
|
import {
|
||||||
|
gedcomEntriesToJson,
|
||||||
JsonFam,
|
JsonFam,
|
||||||
JsonGedcomData,
|
JsonGedcomData,
|
||||||
JsonIndi,
|
|
||||||
gedcomEntriesToJson,
|
|
||||||
JsonImage,
|
JsonImage,
|
||||||
JsonEvent,
|
JsonIndi,
|
||||||
} from 'topola';
|
} from 'topola';
|
||||||
|
import {compareDates} from './date_util';
|
||||||
|
|
||||||
export interface GedcomData {
|
export interface GedcomData {
|
||||||
/** The HEAD entry. */
|
/** The HEAD entry. */
|
||||||
@@ -76,33 +76,6 @@ function strcmp(a: string, b: string) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Compares dates of the given events. */
|
|
||||||
function compareDates(
|
|
||||||
event1: JsonEvent | undefined,
|
|
||||||
event2: JsonEvent | undefined,
|
|
||||||
): number {
|
|
||||||
const date1 =
|
|
||||||
event1 && (event1.date || (event1.dateRange && event1.dateRange.from));
|
|
||||||
const date2 =
|
|
||||||
event2 && (event2.date || (event2.dateRange && event2.dateRange.from));
|
|
||||||
if (!date1 || !date1.year || !date2 || !date2.year) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (date1.year !== date2.year) {
|
|
||||||
return date1.year - date2.year;
|
|
||||||
}
|
|
||||||
if (!date1.month || !date2.month) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (date1.month !== date2.month) {
|
|
||||||
return date1.month - date2.month;
|
|
||||||
}
|
|
||||||
if (date1.day && date2.day && date1.day !== date2.day) {
|
|
||||||
return date1.month - date2.month;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Birth date comparator for individuals. */
|
/** Birth date comparator for individuals. */
|
||||||
function birthDatesComparator(gedcom: JsonGedcomData) {
|
function birthDatesComparator(gedcom: JsonGedcomData) {
|
||||||
const indiMap = idToIndiMap(gedcom);
|
const indiMap = idToIndiMap(gedcom);
|
||||||
@@ -179,6 +152,41 @@ function sortSpouses(gedcom: JsonGedcomData): JsonGedcomData {
|
|||||||
return Object.assign({}, gedcom, {indis: newIndis});
|
return Object.assign({}, gedcom, {indis: newIndis});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the entry is a reference to a top-level entry, the referenced entry is
|
||||||
|
* returned. Otherwise, returns the given entry unmodified.
|
||||||
|
*/
|
||||||
|
export function dereference(
|
||||||
|
entry: GedcomEntry,
|
||||||
|
gedcom: GedcomData,
|
||||||
|
getterFunction: (gedcom: GedcomData) => {[key: string]: GedcomEntry},
|
||||||
|
) {
|
||||||
|
if (entry.data) {
|
||||||
|
const dereferenced = getterFunction(gedcom)[pointerToId(entry.data)];
|
||||||
|
if (dereferenced) {
|
||||||
|
return dereferenced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data for the given GEDCOM entry as an array of lines. Supports
|
||||||
|
* continuations with CONT and CONC.
|
||||||
|
*/
|
||||||
|
export function getData(entry: GedcomEntry) {
|
||||||
|
const result = [entry.data];
|
||||||
|
entry.tree.forEach((subentry) => {
|
||||||
|
if (subentry.tag === 'CONC' && subentry.data) {
|
||||||
|
const last = result.length - 1;
|
||||||
|
result[last] += subentry.data;
|
||||||
|
} else if (subentry.tag === 'CONT' && subentry.data) {
|
||||||
|
result.push(subentry.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/** Sorts children and spouses. */
|
/** Sorts children and spouses. */
|
||||||
export function normalizeGedcom(gedcom: JsonGedcomData): JsonGedcomData {
|
export function normalizeGedcom(gedcom: JsonGedcomData): JsonGedcomData {
|
||||||
return sortSpouses(sortChildren(gedcom));
|
return sortSpouses(sortChildren(gedcom));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {createMedia} from '@artsy/fresnel';
|
import {createMedia} from '@artsy/fresnel';
|
||||||
|
|
||||||
/** Defines the breakpoints at which to show different UI variants. */
|
/** Defines the breakpoints at which to show different UI variants.*/
|
||||||
const AppMedia = createMedia({
|
const AppMedia = createMedia({
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
small: 320,
|
small: 320,
|
||||||
|
|||||||
Reference in New Issue
Block a user