mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-03-11 18:13:43 +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:
@@ -11,7 +11,7 @@ import {
|
||||
DEFALUT_CONFIG,
|
||||
} from './config';
|
||||
import {DataSourceEnum, SourceSelection} from './datasource/data_source';
|
||||
import {Details} from './details';
|
||||
import {Details} from './details/details';
|
||||
import {EmbeddedDataSource, EmbeddedSourceSpec} from './datasource/embedded';
|
||||
import {FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl';
|
||||
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.WWW": "Strona WWW",
|
||||
"gedcom._UPD": "Ostatnia aktualizacja",
|
||||
"gedcom.MARR": "Małżeństwo",
|
||||
"gedcom.DIV": "Rozwód",
|
||||
"date.abt": "około",
|
||||
"date.cal": "wyliczone",
|
||||
"date.est": "oszacowane",
|
||||
|
||||
@@ -97,3 +97,34 @@ export function formatDateOrRange(
|
||||
export function translateDate(gedcomDate: string, intl: IntlShape): string {
|
||||
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 {TopolaError} from './error';
|
||||
import {
|
||||
gedcomEntriesToJson,
|
||||
JsonFam,
|
||||
JsonGedcomData,
|
||||
JsonIndi,
|
||||
gedcomEntriesToJson,
|
||||
JsonImage,
|
||||
JsonEvent,
|
||||
JsonIndi,
|
||||
} from 'topola';
|
||||
import {compareDates} from './date_util';
|
||||
|
||||
export interface GedcomData {
|
||||
/** The HEAD entry. */
|
||||
@@ -76,33 +76,6 @@ function strcmp(a: string, b: string) {
|
||||
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. */
|
||||
function birthDatesComparator(gedcom: JsonGedcomData) {
|
||||
const indiMap = idToIndiMap(gedcom);
|
||||
@@ -179,6 +152,41 @@ function sortSpouses(gedcom: JsonGedcomData): JsonGedcomData {
|
||||
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. */
|
||||
export function normalizeGedcom(gedcom: JsonGedcomData): JsonGedcomData {
|
||||
return sortSpouses(sortChildren(gedcom));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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({
|
||||
breakpoints: {
|
||||
small: 320,
|
||||
|
||||
Reference in New Issue
Block a user