mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-02-18 02:55:48 +00:00
Display sources and links to non-image files in details panel (#210)
This commit is contained in:
parent
91a8bd0115
commit
3438c29ab8
29
src/details/additional-files.tsx
Normal file
29
src/details/additional-files.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import {List} from 'semantic-ui-react';
|
||||||
|
|
||||||
|
export interface FileEntry {
|
||||||
|
url: string;
|
||||||
|
filename?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
files?: FileEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdditionalFiles({files}: Props) {
|
||||||
|
if (!files?.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List>
|
||||||
|
{files.map((file, index) => (
|
||||||
|
<List.Item key={index}>
|
||||||
|
<List.Icon verticalAlign="middle" name="circle" size="tiny" />
|
||||||
|
<List.Content>
|
||||||
|
<a target="_blank" href={file.url} rel="noopener noreferrer">
|
||||||
|
{file.filename || file.url.split('/').pop() || file.url}
|
||||||
|
</a>
|
||||||
|
</List.Content>
|
||||||
|
</List.Item>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -8,9 +8,13 @@ import {
|
|||||||
getData,
|
getData,
|
||||||
getFileName,
|
getFileName,
|
||||||
getImageFileEntry,
|
getImageFileEntry,
|
||||||
|
getNonImageFileEntry,
|
||||||
|
mapToSource,
|
||||||
} from '../util/gedcom_util';
|
} from '../util/gedcom_util';
|
||||||
|
import {AdditionalFiles} from './additional-files';
|
||||||
import {Events} from './events';
|
import {Events} from './events';
|
||||||
import {MultilineText} from './multiline-text';
|
import {MultilineText} from './multiline-text';
|
||||||
|
import {Sources} from './sources';
|
||||||
import {TranslatedTag} from './translated-tag';
|
import {TranslatedTag} from './translated-tag';
|
||||||
import {WrappedImage} from './wrapped-image';
|
import {WrappedImage} from './wrapped-image';
|
||||||
|
|
||||||
@ -55,23 +59,95 @@ function dataDetails(entry: GedcomEntry) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileDetails(objectEntry: GedcomEntry) {
|
function imageDetails(objectEntryReference: GedcomEntry, gedcom: GedcomData) {
|
||||||
const imageFileEntry = getImageFileEntry(objectEntry);
|
const imageEntry = dereference(
|
||||||
|
objectEntryReference,
|
||||||
|
gedcom,
|
||||||
|
(gedcom) => gedcom.other,
|
||||||
|
);
|
||||||
|
|
||||||
return imageFileEntry ? (
|
const imageFileEntry = getImageFileEntry(imageEntry);
|
||||||
|
|
||||||
|
if (!imageFileEntry || !hasData(imageEntry)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="person-image">
|
<div className="person-image">
|
||||||
<WrappedImage
|
<WrappedImage
|
||||||
url={imageFileEntry.data}
|
url={imageFileEntry.data}
|
||||||
filename={getFileName(imageFileEntry) || ''}
|
filename={getFileName(imageFileEntry) || ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceDetails(
|
||||||
|
sourceReferenceEntries: GedcomEntry[],
|
||||||
|
gedcom: GedcomData,
|
||||||
|
) {
|
||||||
|
const sources = sourceReferenceEntries.map((sourceEntryReference) =>
|
||||||
|
mapToSource(sourceEntryReference, gedcom),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sources.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="item-header">
|
||||||
|
<Header as="span" size="small">
|
||||||
|
<TranslatedTag tag="SOUR" />
|
||||||
|
</Header>
|
||||||
|
</div>
|
||||||
|
<Sources sources={sources} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileDetails(objectEntries: GedcomEntry[], gedcom: GedcomData) {
|
||||||
|
const files = objectEntries
|
||||||
|
.map((objectEntry) =>
|
||||||
|
dereference(objectEntry, gedcom, (gedcom) => gedcom.other),
|
||||||
|
)
|
||||||
|
.map((objectEntry) => getNonImageFileEntry(objectEntry))
|
||||||
|
.filter((objectEntry): objectEntry is GedcomEntry => !!objectEntry)
|
||||||
|
.map((fileEntry) => ({
|
||||||
|
url: fileEntry.data,
|
||||||
|
filename: getFileName(fileEntry),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!files.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="item-header">
|
||||||
|
<Header as="span" size="small">
|
||||||
|
<TranslatedTag tag="OBJE" />
|
||||||
|
</Header>
|
||||||
|
</div>
|
||||||
|
<AdditionalFiles files={files} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function noteDetails(noteEntryReference: GedcomEntry, gedcom: GedcomData) {
|
||||||
|
const noteEntry = dereference(
|
||||||
|
noteEntryReference,
|
||||||
|
gedcom,
|
||||||
|
(gedcom) => gedcom.other,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!noteEntry || !hasData(noteEntry)) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function noteDetails(entry: GedcomEntry) {
|
|
||||||
return (
|
return (
|
||||||
<MultilineText
|
<MultilineText
|
||||||
lines={getData(entry).map((line, index) => (
|
lines={getData(noteEntry).map((line, index) => (
|
||||||
<i key={index}>{line}</i>
|
<i key={index}>{line}</i>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
@ -103,15 +179,19 @@ function nameDetails(entry: GedcomEntry) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDetails(
|
function getSectionForEachMatchingEntry(
|
||||||
entries: GedcomEntry[],
|
entries: GedcomEntry[],
|
||||||
|
gedcom: GedcomData,
|
||||||
tags: string[],
|
tags: string[],
|
||||||
detailsFunction: (entry: GedcomEntry) => React.ReactNode | null,
|
detailsFunction: (
|
||||||
|
entry: GedcomEntry,
|
||||||
|
gedcom: GedcomData,
|
||||||
|
) => React.ReactNode | null,
|
||||||
): React.ReactNode[] {
|
): React.ReactNode[] {
|
||||||
return flatMap(tags, (tag) =>
|
return flatMap(tags, (tag) =>
|
||||||
entries
|
entries
|
||||||
.filter((entry) => entry.tag === tag)
|
.filter((entry) => entry.tag === tag)
|
||||||
.map((entry) => detailsFunction(entry)),
|
.map((entry) => detailsFunction(entry, gedcom)),
|
||||||
)
|
)
|
||||||
.filter((element) => element !== null)
|
.filter((element) => element !== null)
|
||||||
.map((element, index) => (
|
.map((element, index) => (
|
||||||
@ -121,6 +201,34 @@ function getDetails(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function combineAllMatchingEntriesIntoSingleSection(
|
||||||
|
entries: GedcomEntry[],
|
||||||
|
gedcom: GedcomData,
|
||||||
|
tags: string[],
|
||||||
|
detailsFunction: (
|
||||||
|
entries: GedcomEntry[],
|
||||||
|
gedcom: GedcomData,
|
||||||
|
) => React.ReactNode | null,
|
||||||
|
): React.ReactNode {
|
||||||
|
const entriesWithMatchingTag = flatMap(tags, (tag) =>
|
||||||
|
entries.filter((entry) => entry.tag === tag),
|
||||||
|
).filter((element) => element !== null);
|
||||||
|
|
||||||
|
const sectionWithDetails = entriesWithMatchingTag.length
|
||||||
|
? detailsFunction(entriesWithMatchingTag, gedcom)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!sectionWithDetails) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Item>
|
||||||
|
<Item.Content>{sectionWithDetails}</Item.Content>
|
||||||
|
</Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if there is displayable information in this entry.
|
* 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
|
* Returns false if there is no data in this entry or this is only a reference
|
||||||
@ -130,9 +238,10 @@ function hasData(entry: GedcomEntry) {
|
|||||||
return entry.tree.length > 0 || (entry.data && !entry.data.startsWith('@'));
|
return entry.tree.length > 0 || (entry.data && !entry.data.startsWith('@'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOtherDetails(entries: GedcomEntry[]) {
|
function getOtherSections(entries: GedcomEntry[], gedcom: GedcomData) {
|
||||||
return entries
|
return entries
|
||||||
.filter((entry) => !EXCLUDED_TAGS.includes(entry.tag))
|
.filter((entry) => !EXCLUDED_TAGS.includes(entry.tag))
|
||||||
|
.map((entry) => dereference(entry, gedcom, (gedcom) => gedcom.other))
|
||||||
.filter(hasData)
|
.filter(hasData)
|
||||||
.map((entry) => dataDetails(entry))
|
.map((entry) => dataDetails(entry))
|
||||||
.filter((element) => element !== null)
|
.filter((element) => element !== null)
|
||||||
@ -150,18 +259,42 @@ interface Props {
|
|||||||
|
|
||||||
export function Details(props: Props) {
|
export function Details(props: Props) {
|
||||||
const entries = props.gedcom.indis[props.indi].tree;
|
const entries = props.gedcom.indis[props.indi].tree;
|
||||||
const entriesWithData = entries
|
|
||||||
.map((entry) => dereference(entry, props.gedcom, (gedcom) => gedcom.other))
|
|
||||||
.filter(hasData);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="details">
|
<div className="details">
|
||||||
<Item.Group divided>
|
<Item.Group divided>
|
||||||
{getDetails(entries, ['NAME'], nameDetails)}
|
{getSectionForEachMatchingEntry(
|
||||||
{getDetails(entriesWithData, ['OBJE'], fileDetails)}
|
entries,
|
||||||
|
props.gedcom,
|
||||||
|
['NAME'],
|
||||||
|
nameDetails,
|
||||||
|
)}
|
||||||
|
{getSectionForEachMatchingEntry(
|
||||||
|
entries,
|
||||||
|
props.gedcom,
|
||||||
|
['OBJE'],
|
||||||
|
imageDetails,
|
||||||
|
)}
|
||||||
<Events gedcom={props.gedcom} entries={entries} indi={props.indi} />
|
<Events gedcom={props.gedcom} entries={entries} indi={props.indi} />
|
||||||
{getOtherDetails(entriesWithData)}
|
{getOtherSections(entries, props.gedcom)}
|
||||||
{getDetails(entriesWithData, ['NOTE'], noteDetails)}
|
{getSectionForEachMatchingEntry(
|
||||||
|
entries,
|
||||||
|
props.gedcom,
|
||||||
|
['NOTE'],
|
||||||
|
noteDetails,
|
||||||
|
)}
|
||||||
|
{combineAllMatchingEntriesIntoSingleSection(
|
||||||
|
entries,
|
||||||
|
props.gedcom,
|
||||||
|
['OBJE'],
|
||||||
|
fileDetails,
|
||||||
|
)}
|
||||||
|
{combineAllMatchingEntriesIntoSingleSection(
|
||||||
|
entries,
|
||||||
|
props.gedcom,
|
||||||
|
['SOUR'],
|
||||||
|
sourceDetails,
|
||||||
|
)}
|
||||||
</Item.Group>
|
</Item.Group>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import {useState} from 'react';
|
import {useState} from 'react';
|
||||||
import {FormattedMessage, IntlShape, useIntl} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
import Linkify from 'react-linkify';
|
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Item,
|
Item,
|
||||||
@ -11,9 +9,10 @@ import {
|
|||||||
Popup,
|
Popup,
|
||||||
Tab,
|
Tab,
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import {DateOrRange} from 'topola';
|
import {Source} from '../util/gedcom_util';
|
||||||
import {formatDateOrRange} from '../util/date_util';
|
import {AdditionalFiles, FileEntry} from './additional-files';
|
||||||
import {MultilineText} from './multiline-text';
|
import {MultilineText} from './multiline-text';
|
||||||
|
import {Sources} from './sources';
|
||||||
import {WrappedImage} from './wrapped-image';
|
import {WrappedImage} from './wrapped-image';
|
||||||
|
|
||||||
export interface Image {
|
export interface Image {
|
||||||
@ -22,19 +21,12 @@ export interface Image {
|
|||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Source {
|
|
||||||
title?: string;
|
|
||||||
author?: string;
|
|
||||||
page?: string;
|
|
||||||
date?: DateOrRange;
|
|
||||||
publicationInfo?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
images?: Image[];
|
images?: Image[];
|
||||||
notes?: string[][];
|
notes?: string[][];
|
||||||
sources?: Source[];
|
sources?: Source[];
|
||||||
indi: string;
|
indi: string;
|
||||||
|
files?: FileEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventImages(images: Image[] | undefined) {
|
function eventImages(images: Image[] | undefined) {
|
||||||
@ -69,37 +61,7 @@ function eventNotes(notes: string[][] | undefined) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventSources(sources: Source[] | undefined, intl: IntlShape) {
|
|
||||||
return (
|
|
||||||
!!sources?.length && (
|
|
||||||
<List>
|
|
||||||
{sources.map((source, index) => (
|
|
||||||
<List.Item key={index}>
|
|
||||||
<List.Icon verticalAlign="middle" name="circle" size="tiny" />
|
|
||||||
<List.Content>
|
|
||||||
<List.Header>
|
|
||||||
<Linkify properties={{target: '_blank'}}>
|
|
||||||
{[source.author, source.title, source.publicationInfo]
|
|
||||||
.filter((sourceElement) => sourceElement)
|
|
||||||
.join(', ')}
|
|
||||||
</Linkify>
|
|
||||||
</List.Header>
|
|
||||||
<List.Description>
|
|
||||||
<Linkify properties={{target: '_blank'}}>{source.page}</Linkify>
|
|
||||||
{source.date
|
|
||||||
? ' [' + formatDateOrRange(source.date, intl) + ']'
|
|
||||||
: null}
|
|
||||||
</List.Description>
|
|
||||||
</List.Content>
|
|
||||||
</List.Item>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EventExtras(props: Props) {
|
export function EventExtras(props: Props) {
|
||||||
const intl = useIntl();
|
|
||||||
const [activeIndex, setActiveIndex] = useState(-1);
|
const [activeIndex, setActiveIndex] = useState(-1);
|
||||||
const [indi, setIndi] = useState('');
|
const [indi, setIndi] = useState('');
|
||||||
|
|
||||||
@ -109,7 +71,7 @@ export function EventExtras(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleTabOnClick(
|
function handleTabOnClick(
|
||||||
event: React.MouseEvent<HTMLAnchorElement>,
|
_event: React.MouseEvent<HTMLAnchorElement>,
|
||||||
menuItemProps: MenuItemProps,
|
menuItemProps: MenuItemProps,
|
||||||
) {
|
) {
|
||||||
menuItemProps.index !== undefined && activeIndex !== menuItemProps.index
|
menuItemProps.index !== undefined && activeIndex !== menuItemProps.index
|
||||||
@ -162,10 +124,37 @@ export function EventExtras(props: Props) {
|
|||||||
/>
|
/>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
),
|
),
|
||||||
render: () => <Tab.Pane>{eventSources(props.sources, intl)}</Tab.Pane>,
|
render: () => (
|
||||||
|
<Tab.Pane>
|
||||||
|
<Sources sources={props.sources} />
|
||||||
|
</Tab.Pane>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const panes = [imageTab, noteTab, sourceTab].flatMap((tab) =>
|
const filesTab = props.files?.length && {
|
||||||
|
menuItem: (
|
||||||
|
<Menu.Item fitted key="files" onClick={handleTabOnClick}>
|
||||||
|
<Popup
|
||||||
|
content={
|
||||||
|
<FormattedMessage
|
||||||
|
id="extras.files"
|
||||||
|
defaultMessage="Additonal files"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
size="mini"
|
||||||
|
position="bottom center"
|
||||||
|
trigger={<Icon circular name="file alternate outline" />}
|
||||||
|
/>
|
||||||
|
</Menu.Item>
|
||||||
|
),
|
||||||
|
render: () => (
|
||||||
|
<Tab.Pane>
|
||||||
|
<AdditionalFiles files={props.files} />
|
||||||
|
</Tab.Pane>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const panes = [imageTab, noteTab, sourceTab, filesTab].flatMap((tab) =>
|
||||||
tab ? [tab] : [],
|
tab ? [tab] : [],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -14,9 +14,14 @@ import {
|
|||||||
getFileName,
|
getFileName,
|
||||||
getImageFileEntry,
|
getImageFileEntry,
|
||||||
getName,
|
getName,
|
||||||
|
getNonImageFileEntry,
|
||||||
|
mapToSource,
|
||||||
pointerToId,
|
pointerToId,
|
||||||
|
resolveDate,
|
||||||
|
Source,
|
||||||
} from '../util/gedcom_util';
|
} from '../util/gedcom_util';
|
||||||
import {EventExtras, Image, Source} from './event-extras';
|
import {FileEntry} from './additional-files';
|
||||||
|
import {EventExtras, Image} from './event-extras';
|
||||||
import {TranslatedTag} from './translated-tag';
|
import {TranslatedTag} from './translated-tag';
|
||||||
|
|
||||||
function PersonLink(props: {person: GedcomEntry}) {
|
function PersonLink(props: {person: GedcomEntry}) {
|
||||||
@ -53,6 +58,7 @@ interface EventData {
|
|||||||
personLink?: GedcomEntry;
|
personLink?: GedcomEntry;
|
||||||
place?: string[];
|
place?: string[];
|
||||||
images?: Image[];
|
images?: Image[];
|
||||||
|
files?: FileEntry[];
|
||||||
notes?: string[][];
|
notes?: string[][];
|
||||||
sources?: Source[];
|
sources?: Source[];
|
||||||
indi: string;
|
indi: string;
|
||||||
@ -74,7 +80,7 @@ const FAMILY_EVENT_TAGS = ['MARR', 'DIV'];
|
|||||||
function EventHeader(props: {event: EventData}) {
|
function EventHeader(props: {event: EventData}) {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
<div className="event-header">
|
<div className="item-header">
|
||||||
<Header as="span" size="small">
|
<Header as="span" size="small">
|
||||||
<TranslatedTag tag={props.event.type} />
|
<TranslatedTag tag={props.event.type} />
|
||||||
</Header>
|
</Header>
|
||||||
@ -144,48 +150,29 @@ function eventImages(entry: GedcomEntry, gedcom: GedcomData): Image[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function eventFiles(entry: GedcomEntry, gedcom: GedcomData): Image[] {
|
||||||
|
return entry.tree
|
||||||
|
.filter((subEntry) => 'OBJE' === subEntry.tag)
|
||||||
|
.map((objectEntry) =>
|
||||||
|
dereference(objectEntry, gedcom, (gedcom) => gedcom.other),
|
||||||
|
)
|
||||||
|
.map((objectEntry) => getNonImageFileEntry(objectEntry))
|
||||||
|
.flatMap((fileEntry) =>
|
||||||
|
fileEntry
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
url: fileEntry?.data || '',
|
||||||
|
filename: getFileName(fileEntry) || '',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function eventSources(entry: GedcomEntry, gedcom: GedcomData): Source[] {
|
function eventSources(entry: GedcomEntry, gedcom: GedcomData): Source[] {
|
||||||
return entry.tree
|
return entry.tree
|
||||||
.filter((subEntry) => 'SOUR' === subEntry.tag)
|
.filter((subEntry) => 'SOUR' === subEntry.tag)
|
||||||
.map((sourceEntryReference) => {
|
.map((sourceEntryReference) => mapToSource(sourceEntryReference, gedcom));
|
||||||
const sourceEntry = dereference(
|
|
||||||
sourceEntryReference,
|
|
||||||
gedcom,
|
|
||||||
(gedcom) => gedcom.other,
|
|
||||||
);
|
|
||||||
|
|
||||||
const title = sourceEntry.tree.find(
|
|
||||||
(subEntry) => 'TITL' === subEntry.tag,
|
|
||||||
);
|
|
||||||
|
|
||||||
const abbr = sourceEntry.tree.find((subEntry) => 'ABBR' === subEntry.tag);
|
|
||||||
|
|
||||||
const author = sourceEntry.tree.find(
|
|
||||||
(subEntry) => 'AUTH' === subEntry.tag,
|
|
||||||
);
|
|
||||||
|
|
||||||
const publicationInfo = sourceEntry.tree.find(
|
|
||||||
(subEntry) => 'PUBL' === subEntry.tag,
|
|
||||||
);
|
|
||||||
|
|
||||||
const page = sourceEntryReference.tree.find(
|
|
||||||
(subEntry) => 'PAGE' === subEntry.tag,
|
|
||||||
);
|
|
||||||
|
|
||||||
const sourceData = sourceEntryReference.tree.find(
|
|
||||||
(subEntry) => 'DATA' === subEntry.tag,
|
|
||||||
);
|
|
||||||
|
|
||||||
const date = sourceData ? resolveDate(sourceData) : undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: title?.data || abbr?.data,
|
|
||||||
author: author?.data,
|
|
||||||
page: page?.data,
|
|
||||||
date: date ? getDate(date.data) : undefined,
|
|
||||||
publicationInfo: publicationInfo?.data,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventNotes(entry: GedcomEntry, gedcom: GedcomData): string[][] {
|
function eventNotes(entry: GedcomEntry, gedcom: GedcomData): string[][] {
|
||||||
@ -221,6 +208,7 @@ function toIndiEvent(
|
|||||||
age: getAge(entry, indi, gedcom, intl),
|
age: getAge(entry, indi, gedcom, intl),
|
||||||
place: eventPlace(entry),
|
place: eventPlace(entry),
|
||||||
images: eventImages(entry, gedcom),
|
images: eventImages(entry, gedcom),
|
||||||
|
files: eventFiles(entry, gedcom),
|
||||||
notes: eventNotes(entry, gedcom),
|
notes: eventNotes(entry, gedcom),
|
||||||
sources: eventSources(entry, gedcom),
|
sources: eventSources(entry, gedcom),
|
||||||
indi: indi,
|
indi: indi,
|
||||||
@ -228,10 +216,6 @@ function toIndiEvent(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDate(entry: GedcomEntry) {
|
|
||||||
return entry.tree.find((subEntry) => subEntry.tag === 'DATE');
|
|
||||||
}
|
|
||||||
|
|
||||||
function toFamilyEvents(
|
function toFamilyEvents(
|
||||||
entry: GedcomEntry,
|
entry: GedcomEntry,
|
||||||
gedcom: GedcomData,
|
gedcom: GedcomData,
|
||||||
@ -248,6 +232,7 @@ function toFamilyEvents(
|
|||||||
personLink: getSpouse(indi, family, gedcom),
|
personLink: getSpouse(indi, family, gedcom),
|
||||||
place: eventPlace(familyMarriageEvent),
|
place: eventPlace(familyMarriageEvent),
|
||||||
images: eventImages(familyMarriageEvent, gedcom),
|
images: eventImages(familyMarriageEvent, gedcom),
|
||||||
|
files: eventFiles(familyMarriageEvent, gedcom),
|
||||||
notes: eventNotes(familyMarriageEvent, gedcom),
|
notes: eventNotes(familyMarriageEvent, gedcom),
|
||||||
sources: eventSources(familyMarriageEvent, gedcom),
|
sources: eventSources(familyMarriageEvent, gedcom),
|
||||||
indi: indi,
|
indi: indi,
|
||||||
@ -272,6 +257,7 @@ function Event(props: {event: EventData}) {
|
|||||||
notes={props.event.notes}
|
notes={props.event.notes}
|
||||||
sources={props.event.sources}
|
sources={props.event.sources}
|
||||||
indi={props.event.indi}
|
indi={props.event.indi}
|
||||||
|
files={props.event.files}
|
||||||
/>
|
/>
|
||||||
</Item.Content>
|
</Item.Content>
|
||||||
</Item>
|
</Item>
|
||||||
|
|||||||
38
src/details/sources.tsx
Normal file
38
src/details/sources.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import Linkify from 'react-linkify';
|
||||||
|
import {List} from 'semantic-ui-react';
|
||||||
|
import {formatDateOrRange} from '../util/date_util';
|
||||||
|
import {Source} from '../util/gedcom_util';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
sources?: Source[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sources({sources}: Props) {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
if (!sources?.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List>
|
||||||
|
{sources.map((source, index) => (
|
||||||
|
<List.Item key={index}>
|
||||||
|
<List.Icon verticalAlign="middle" name="circle" size="tiny" />
|
||||||
|
<List.Content>
|
||||||
|
<List.Header>
|
||||||
|
<Linkify properties={{target: '_blank'}}>
|
||||||
|
{[source.author, source.title, source.publicationInfo]
|
||||||
|
.filter((sourceElement) => !!sourceElement)
|
||||||
|
.join(', ')}
|
||||||
|
</Linkify>
|
||||||
|
</List.Header>
|
||||||
|
<List.Description>
|
||||||
|
<Linkify properties={{target: '_blank'}}>{source.page}</Linkify>
|
||||||
|
{source.date && ` [${formatDateOrRange(source.date, intl)}]`}
|
||||||
|
</List.Description>
|
||||||
|
</List.Content>
|
||||||
|
</List.Item>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -22,6 +22,8 @@ const TAG_DESCRIPTIONS = new Map([
|
|||||||
['OCCU', 'Occupation'],
|
['OCCU', 'Occupation'],
|
||||||
['TITL', 'Title'],
|
['TITL', 'Title'],
|
||||||
['WWW', 'WWW'],
|
['WWW', 'WWW'],
|
||||||
|
['OBJE', 'Additional files'],
|
||||||
|
['SOUR', 'Sources'],
|
||||||
['birth', 'Birth name'],
|
['birth', 'Birth name'],
|
||||||
['married', 'Married name'],
|
['married', 'Married name'],
|
||||||
['maiden', 'Maiden name'],
|
['maiden', 'Maiden name'],
|
||||||
|
|||||||
@ -153,13 +153,13 @@ div.zoom {
|
|||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details .event-header {
|
.details .item-header {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
display: flex;
|
display: flex;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details .event-header .header {
|
.details .item-header .header {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 40%;
|
min-width: 40%;
|
||||||
|
|||||||
@ -62,6 +62,8 @@
|
|||||||
"gedcom.RIN": "ID",
|
"gedcom.RIN": "ID",
|
||||||
"gedcom.TITL": "Обръщение",
|
"gedcom.TITL": "Обръщение",
|
||||||
"gedcom.WWW": "Линк",
|
"gedcom.WWW": "Линк",
|
||||||
|
"gedcom.OBJE": "Допълнителни файлове",
|
||||||
|
"gedcom.SOUR": "Източници",
|
||||||
"gedcom._UPD": "Последно обновление",
|
"gedcom._UPD": "Последно обновление",
|
||||||
"gedcom.birth": "Рождено име",
|
"gedcom.birth": "Рождено име",
|
||||||
"gedcom.married": "Име след брак",
|
"gedcom.married": "Име след брак",
|
||||||
@ -106,5 +108,6 @@
|
|||||||
"name.unknown_name": "Неизвестно име",
|
"name.unknown_name": "Неизвестно име",
|
||||||
"extras.images": "Изображение",
|
"extras.images": "Изображение",
|
||||||
"extras.notes": "Бележки",
|
"extras.notes": "Бележки",
|
||||||
"extras.sources": "Източници"
|
"extras.sources": "Източници",
|
||||||
|
"extras.files": "Допълнителни файлове"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,6 +63,8 @@
|
|||||||
"gedcom.RIN": "ID",
|
"gedcom.RIN": "ID",
|
||||||
"gedcom.TITL": "Titul",
|
"gedcom.TITL": "Titul",
|
||||||
"gedcom.WWW": "Stránka WWW",
|
"gedcom.WWW": "Stránka WWW",
|
||||||
|
"gedcom.OBJE": "Další soubory",
|
||||||
|
"gedcom.SOUR": "Zdroje",
|
||||||
"gedcom.RELI": "Vyznání",
|
"gedcom.RELI": "Vyznání",
|
||||||
"gedcom._UPD": "Poslední aktualizace",
|
"gedcom._UPD": "Poslední aktualizace",
|
||||||
"gedcom.birth": "Rodné jméno",
|
"gedcom.birth": "Rodné jméno",
|
||||||
@ -107,5 +109,6 @@
|
|||||||
"name.unknown_name": "N.N.",
|
"name.unknown_name": "N.N.",
|
||||||
"extras.images": "Obrázky",
|
"extras.images": "Obrázky",
|
||||||
"extras.notes": "Poznámky",
|
"extras.notes": "Poznámky",
|
||||||
"extras.sources": "Zdroje"
|
"extras.sources": "Zdroje",
|
||||||
|
"extras.files": "Další soubory"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,8 @@
|
|||||||
"gedcom.RIN": "ID",
|
"gedcom.RIN": "ID",
|
||||||
"gedcom.TITL": "Titel",
|
"gedcom.TITL": "Titel",
|
||||||
"gedcom.WWW": "Website",
|
"gedcom.WWW": "Website",
|
||||||
|
"gedcom.OBJE": "Zusätzliche Dateien",
|
||||||
|
"gedcom.SOUR": "Quellen",
|
||||||
"gedcom._UPD": "Zuletzt aktualisiert",
|
"gedcom._UPD": "Zuletzt aktualisiert",
|
||||||
"gedcom.birth": "Geburtsname",
|
"gedcom.birth": "Geburtsname",
|
||||||
"gedcom.married": "Ehenamen",
|
"gedcom.married": "Ehenamen",
|
||||||
|
|||||||
@ -60,6 +60,8 @@
|
|||||||
"gedcom.RIN": "ID",
|
"gedcom.RIN": "ID",
|
||||||
"gedcom.TITL": "Titre",
|
"gedcom.TITL": "Titre",
|
||||||
"gedcom.WWW": "Site Web",
|
"gedcom.WWW": "Site Web",
|
||||||
|
"gedcom.OBJE": "Fichiers supplémentaires",
|
||||||
|
"gedcom.SOUR": "Sources",
|
||||||
"gedcom._UPD": "Dernière mise à jour",
|
"gedcom._UPD": "Dernière mise à jour",
|
||||||
"gedcom.MARR": "Mariage",
|
"gedcom.MARR": "Mariage",
|
||||||
"gedcom.DIV": "Divorce",
|
"gedcom.DIV": "Divorce",
|
||||||
|
|||||||
@ -52,6 +52,8 @@
|
|||||||
"gedcom.RIN": "ID",
|
"gedcom.RIN": "ID",
|
||||||
"gedcom.TITL": "Titolo",
|
"gedcom.TITL": "Titolo",
|
||||||
"gedcom.WWW": "Sito web",
|
"gedcom.WWW": "Sito web",
|
||||||
|
"gedcom.OBJE": "File aggiuntivi",
|
||||||
|
"gedcom.SOUR": "Fonti",
|
||||||
"gedcom._UPD": "Ultimo aggiornamento",
|
"gedcom._UPD": "Ultimo aggiornamento",
|
||||||
"gedcom.birth": "Nome alla nascita",
|
"gedcom.birth": "Nome alla nascita",
|
||||||
"gedcom.married": "Nome da coniugato/a",
|
"gedcom.married": "Nome da coniugato/a",
|
||||||
|
|||||||
@ -55,6 +55,8 @@
|
|||||||
"gedcom.RIN": "ID",
|
"gedcom.RIN": "ID",
|
||||||
"gedcom.TITL": "Tytuł",
|
"gedcom.TITL": "Tytuł",
|
||||||
"gedcom.WWW": "Strona WWW",
|
"gedcom.WWW": "Strona WWW",
|
||||||
|
"gedcom.OBJE": "Dodatkowe pliki",
|
||||||
|
"gedcom.SOUR": "Źródła",
|
||||||
"gedcom._UPD": "Ostatnia aktualizacja",
|
"gedcom._UPD": "Ostatnia aktualizacja",
|
||||||
"gedcom.MARR": "Małżeństwo",
|
"gedcom.MARR": "Małżeństwo",
|
||||||
"gedcom.DIV": "Rozwód",
|
"gedcom.DIV": "Rozwód",
|
||||||
@ -94,5 +96,6 @@
|
|||||||
"name.unknown_name": "N.N.",
|
"name.unknown_name": "N.N.",
|
||||||
"extras.images": "Zdjęcia",
|
"extras.images": "Zdjęcia",
|
||||||
"extras.notes": "Notatki",
|
"extras.notes": "Notatki",
|
||||||
"extras.sources": "Źródła"
|
"extras.sources": "Źródła",
|
||||||
|
"extras.files": "Dodatkowe pliki"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,6 +60,8 @@
|
|||||||
"gedcom.RIN": "ID",
|
"gedcom.RIN": "ID",
|
||||||
"gedcom.TITL": "Титул",
|
"gedcom.TITL": "Титул",
|
||||||
"gedcom.WWW": "Веб-сайт WWW",
|
"gedcom.WWW": "Веб-сайт WWW",
|
||||||
|
"gedcom.OBJE": "Дополнительные файлы",
|
||||||
|
"gedcom.SOUR": "Источники",
|
||||||
"gedcom._UPD": "Последнее обновление",
|
"gedcom._UPD": "Последнее обновление",
|
||||||
"gedcom.birth": "Имя при рождении",
|
"gedcom.birth": "Имя при рождении",
|
||||||
"gedcom.married": "Имя в браке",
|
"gedcom.married": "Имя в браке",
|
||||||
@ -97,5 +99,6 @@
|
|||||||
"name.unknown_name": "Н.И.",
|
"name.unknown_name": "Н.И.",
|
||||||
"extras.images": "Картинки",
|
"extras.images": "Картинки",
|
||||||
"extras.notes": "Примечание",
|
"extras.notes": "Примечание",
|
||||||
"extras.sources": "Источники"
|
"extras.sources": "Источники",
|
||||||
|
"extras.files": "Дополнительные файлы"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import {GedcomEntry, parse as parseGedcom} from 'parse-gedcom';
|
import {GedcomEntry, parse as parseGedcom} from 'parse-gedcom';
|
||||||
import {
|
import {
|
||||||
|
DateOrRange,
|
||||||
gedcomEntriesToJson,
|
gedcomEntriesToJson,
|
||||||
|
getDate,
|
||||||
JsonFam,
|
JsonFam,
|
||||||
JsonGedcomData,
|
JsonGedcomData,
|
||||||
JsonImage,
|
JsonImage,
|
||||||
@ -25,6 +27,14 @@ export interface TopolaData {
|
|||||||
gedcom: GedcomData;
|
gedcom: GedcomData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Source {
|
||||||
|
title?: string;
|
||||||
|
author?: string;
|
||||||
|
page?: string;
|
||||||
|
date?: DateOrRange;
|
||||||
|
publicationInfo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the identifier extracted from a pointer string.
|
* Returns the identifier extracted from a pointer string.
|
||||||
* E.g. '@I123@' -> 'I123'
|
* E.g. '@I123@' -> 'I123'
|
||||||
@ -296,13 +306,67 @@ export function getFileName(fileEntry: GedcomEntry): string | undefined {
|
|||||||
return fileTitle && fileExtension && fileTitle + '.' + fileExtension;
|
return fileTitle && fileExtension && fileTitle + '.' + fileExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getImageFileEntry(
|
function findFileEntry(
|
||||||
objectEntry: GedcomEntry,
|
objectEntry: GedcomEntry,
|
||||||
|
predicate: (entry: GedcomEntry) => boolean,
|
||||||
): GedcomEntry | undefined {
|
): GedcomEntry | undefined {
|
||||||
return objectEntry.tree.find(
|
return objectEntry.tree.find(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
entry.tag === 'FILE' &&
|
entry.tag === 'FILE' && entry.data.startsWith('http') && predicate(entry),
|
||||||
entry.data.startsWith('http') &&
|
|
||||||
isImageFile(entry.data),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNonImageFileEntry(
|
||||||
|
objectEntry: GedcomEntry,
|
||||||
|
): GedcomEntry | undefined {
|
||||||
|
return findFileEntry(objectEntry, (entry) => !isImageFile(entry.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getImageFileEntry(
|
||||||
|
objectEntry: GedcomEntry,
|
||||||
|
): GedcomEntry | undefined {
|
||||||
|
return findFileEntry(objectEntry, (entry) => isImageFile(entry.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDate(entry: GedcomEntry) {
|
||||||
|
return entry.tree.find((subEntry) => subEntry.tag === 'DATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToSource(
|
||||||
|
sourceEntryReference: GedcomEntry,
|
||||||
|
gedcom: GedcomData,
|
||||||
|
) {
|
||||||
|
const sourceEntry = dereference(
|
||||||
|
sourceEntryReference,
|
||||||
|
gedcom,
|
||||||
|
(gedcom) => gedcom.other,
|
||||||
|
);
|
||||||
|
|
||||||
|
const title = sourceEntry.tree.find((subEntry) => 'TITL' === subEntry.tag);
|
||||||
|
|
||||||
|
const abbr = sourceEntry.tree.find((subEntry) => 'ABBR' === subEntry.tag);
|
||||||
|
|
||||||
|
const author = sourceEntry.tree.find((subEntry) => 'AUTH' === subEntry.tag);
|
||||||
|
|
||||||
|
const publicationInfo = sourceEntry.tree.find(
|
||||||
|
(subEntry) => 'PUBL' === subEntry.tag,
|
||||||
|
);
|
||||||
|
|
||||||
|
const page = sourceEntryReference.tree.find(
|
||||||
|
(subEntry) => 'PAGE' === subEntry.tag,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sourceData = sourceEntryReference.tree.find(
|
||||||
|
(subEntry) => 'DATA' === subEntry.tag,
|
||||||
|
);
|
||||||
|
|
||||||
|
const date = sourceData ? resolveDate(sourceData) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: title?.data || abbr?.data,
|
||||||
|
author: author?.data,
|
||||||
|
page: page?.data,
|
||||||
|
date: date ? getDate(date.data) : undefined,
|
||||||
|
publicationInfo: publicationInfo?.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user