mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-03-19 05:53:44 +00:00
Display images, notes, sources for events as collapsible tabs (#158)
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
|||||||
GedcomData,
|
GedcomData,
|
||||||
getData,
|
getData,
|
||||||
getFileName,
|
getFileName,
|
||||||
isImageFile,
|
getImageFileEntry
|
||||||
} from '../util/gedcom_util';
|
} from '../util/gedcom_util';
|
||||||
import {Events} from './events';
|
import {Events} from './events';
|
||||||
import {GedcomEntry} from 'parse-gedcom';
|
import {GedcomEntry} from 'parse-gedcom';
|
||||||
@@ -56,12 +56,7 @@ function dataDetails(entry: GedcomEntry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fileDetails(objectEntry: GedcomEntry) {
|
function fileDetails(objectEntry: GedcomEntry) {
|
||||||
const imageFileEntry = objectEntry.tree.find(
|
const imageFileEntry = getImageFileEntry(objectEntry);
|
||||||
(entry) =>
|
|
||||||
entry.tag === 'FILE' &&
|
|
||||||
entry.data.startsWith('http') &&
|
|
||||||
isImageFile(entry.data),
|
|
||||||
);
|
|
||||||
|
|
||||||
return imageFileEntry ? (
|
return imageFileEntry ? (
|
||||||
<div className="person-image">
|
<div className="person-image">
|
||||||
|
|||||||
191
src/details/event-extras.tsx
Normal file
191
src/details/event-extras.tsx
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import {FormattedMessage, IntlShape, useIntl} from 'react-intl';
|
||||||
|
import {
|
||||||
|
Icon,
|
||||||
|
Item,
|
||||||
|
List,
|
||||||
|
Menu,
|
||||||
|
MenuItemProps,
|
||||||
|
Popup,
|
||||||
|
Tab,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
|
import {useState} from 'react';
|
||||||
|
import {WrappedImage} from './wrapped-image';
|
||||||
|
import * as React from 'react';
|
||||||
|
import {MultilineText} from './multiline-text';
|
||||||
|
import {DateOrRange} from 'topola';
|
||||||
|
import {formatDateOrRange} from '../util/date_util';
|
||||||
|
import Linkify from 'react-linkify';
|
||||||
|
|
||||||
|
export interface Image {
|
||||||
|
url: string;
|
||||||
|
filename: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Source {
|
||||||
|
title?: string;
|
||||||
|
author?: string;
|
||||||
|
page?: string;
|
||||||
|
date?: DateOrRange;
|
||||||
|
publicationInfo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
images?: Image[];
|
||||||
|
notes?: string[][];
|
||||||
|
sources?: Source[];
|
||||||
|
indi: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventImages(images: Image[] | undefined) {
|
||||||
|
return (
|
||||||
|
!!images &&
|
||||||
|
images.map((image, index) => (
|
||||||
|
<List key={index}>
|
||||||
|
<List.Item>
|
||||||
|
<WrappedImage
|
||||||
|
url={image.url}
|
||||||
|
filename={image.filename}
|
||||||
|
title={image.title}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
</List>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventNotes(notes: string[][] | undefined) {
|
||||||
|
return (
|
||||||
|
!!notes?.length &&
|
||||||
|
notes.map((note, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<MultilineText
|
||||||
|
lines={note.map((line, index) => (
|
||||||
|
<i key={index}>{line}</i>
|
||||||
|
))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [activeIndex, setActiveIndex] = useState(-1);
|
||||||
|
const [indi, setIndi] = useState('');
|
||||||
|
|
||||||
|
if (!indi || indi !== props.indi) {
|
||||||
|
setActiveIndex(-1);
|
||||||
|
setIndi(props.indi);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTabOnClick(
|
||||||
|
event: React.MouseEvent<HTMLAnchorElement>,
|
||||||
|
menuItemProps: MenuItemProps,
|
||||||
|
) {
|
||||||
|
menuItemProps.index !== undefined && activeIndex !== menuItemProps.index
|
||||||
|
? setActiveIndex(menuItemProps.index)
|
||||||
|
: setActiveIndex(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageTab = props.images?.length && {
|
||||||
|
menuItem: (
|
||||||
|
<Menu.Item fitted key="images" onClick={handleTabOnClick}>
|
||||||
|
<Popup
|
||||||
|
content={
|
||||||
|
<FormattedMessage id="extras.images" defaultMessage="Images" />
|
||||||
|
}
|
||||||
|
size="mini"
|
||||||
|
position="bottom center"
|
||||||
|
trigger={<Icon circular name="camera" />}
|
||||||
|
/>
|
||||||
|
</Menu.Item>
|
||||||
|
),
|
||||||
|
render: () => <Tab.Pane>{eventImages(props.images)}</Tab.Pane>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const noteTab = props.notes?.length && {
|
||||||
|
menuItem: (
|
||||||
|
<Menu.Item fitted key="notes" onClick={handleTabOnClick}>
|
||||||
|
<Popup
|
||||||
|
content={
|
||||||
|
<FormattedMessage id="extras.notes" defaultMessage="Notes" />
|
||||||
|
}
|
||||||
|
size="mini"
|
||||||
|
position="bottom center"
|
||||||
|
trigger={<Icon circular name="sticky note outline" />}
|
||||||
|
/>
|
||||||
|
</Menu.Item>
|
||||||
|
),
|
||||||
|
render: () => <Tab.Pane>{eventNotes(props.notes)}</Tab.Pane>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceTab = props.sources?.length && {
|
||||||
|
menuItem: (
|
||||||
|
<Menu.Item fitted key="sources" onClick={handleTabOnClick}>
|
||||||
|
<Popup
|
||||||
|
content={
|
||||||
|
<FormattedMessage id="extras.sources" defaultMessage="Sources" />
|
||||||
|
}
|
||||||
|
size="mini"
|
||||||
|
position="bottom center"
|
||||||
|
trigger={<Icon circular name="quote right" />}
|
||||||
|
/>
|
||||||
|
</Menu.Item>
|
||||||
|
),
|
||||||
|
render: () => <Tab.Pane>{eventSources(props.sources, intl)}</Tab.Pane>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const panes = [imageTab, noteTab, sourceTab].flatMap((tab) =>
|
||||||
|
tab ? [tab] : [],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (panes.length) {
|
||||||
|
return (
|
||||||
|
<Item.Description>
|
||||||
|
<Tab
|
||||||
|
className="event-extras"
|
||||||
|
activeIndex={activeIndex}
|
||||||
|
renderActiveOnly={true}
|
||||||
|
menu={{
|
||||||
|
tabular: true,
|
||||||
|
attached: true,
|
||||||
|
compact: true,
|
||||||
|
borderless: true,
|
||||||
|
}}
|
||||||
|
panes={panes}
|
||||||
|
/>
|
||||||
|
</Item.Description>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -3,14 +3,21 @@ import flatMap from 'array.prototype.flatmap';
|
|||||||
import {calcAge} from '../util/age_util';
|
import {calcAge} from '../util/age_util';
|
||||||
import {compareDates, formatDateOrRange} from '../util/date_util';
|
import {compareDates, formatDateOrRange} from '../util/date_util';
|
||||||
import {DateOrRange, getDate} from 'topola';
|
import {DateOrRange, getDate} from 'topola';
|
||||||
import {dereference, GedcomData, getData, getName} from '../util/gedcom_util';
|
import {
|
||||||
|
dereference,
|
||||||
|
GedcomData,
|
||||||
|
getData,
|
||||||
|
getImageFileEntry,
|
||||||
|
getFileName,
|
||||||
|
getName,
|
||||||
|
} from '../util/gedcom_util';
|
||||||
import {GedcomEntry} from 'parse-gedcom';
|
import {GedcomEntry} from 'parse-gedcom';
|
||||||
import {FormattedMessage, IntlShape, useIntl} from 'react-intl';
|
import {FormattedMessage, IntlShape, useIntl} from 'react-intl';
|
||||||
import {Link, useLocation} from 'react-router-dom';
|
import {Link, useLocation} from 'react-router-dom';
|
||||||
import {MultilineText} from './multiline-text';
|
|
||||||
import {pointerToId} from '../util/gedcom_util';
|
import {pointerToId} from '../util/gedcom_util';
|
||||||
import {TranslatedTag} from './translated-tag';
|
import {TranslatedTag} from './translated-tag';
|
||||||
import {Header, Item} from 'semantic-ui-react';
|
import {Header, Item} from 'semantic-ui-react';
|
||||||
|
import {EventExtras, Image, Source} from './event-extras';
|
||||||
|
|
||||||
function PersonLink(props: {person: GedcomEntry}) {
|
function PersonLink(props: {person: GedcomEntry}) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -45,7 +52,10 @@ interface EventData {
|
|||||||
age?: string;
|
age?: string;
|
||||||
personLink?: GedcomEntry;
|
personLink?: GedcomEntry;
|
||||||
place?: string[];
|
place?: string[];
|
||||||
notes: string[][];
|
images?: Image[];
|
||||||
|
notes?: string[][];
|
||||||
|
sources?: Source[];
|
||||||
|
indi: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EVENT_TAGS = [
|
const EVENT_TAGS = [
|
||||||
@@ -115,6 +125,71 @@ function eventPlace(entry: GedcomEntry) {
|
|||||||
return place?.data ? getData(place) : undefined;
|
return place?.data ? getData(place) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function eventImages(entry: GedcomEntry, gedcom: GedcomData): Image[] {
|
||||||
|
return entry.tree
|
||||||
|
.filter((subEntry) => 'OBJE' === subEntry.tag)
|
||||||
|
.map((objectEntry) =>
|
||||||
|
dereference(objectEntry, gedcom, (gedcom) => gedcom.other),
|
||||||
|
)
|
||||||
|
.map((objectEntry) => getImageFileEntry(objectEntry))
|
||||||
|
.flatMap((imageFileEntry) =>
|
||||||
|
imageFileEntry
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
url: imageFileEntry?.data || '',
|
||||||
|
filename: getFileName(imageFileEntry) || '',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventSources(entry: GedcomEntry, gedcom: GedcomData): Source[] {
|
||||||
|
return entry.tree
|
||||||
|
.filter((subEntry) => 'SOUR' === subEntry.tag)
|
||||||
|
.map((sourceEntryReference) => {
|
||||||
|
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[][] {
|
||||||
return entry.tree
|
return entry.tree
|
||||||
.filter((subentry) => ['NOTE', 'TYPE'].includes(subentry.tag))
|
.filter((subentry) => ['NOTE', 'TYPE'].includes(subentry.tag))
|
||||||
@@ -147,7 +222,10 @@ function toIndiEvent(
|
|||||||
type: entry.tag,
|
type: entry.tag,
|
||||||
age: getAge(entry, indi, gedcom, intl),
|
age: getAge(entry, indi, gedcom, intl),
|
||||||
place: eventPlace(entry),
|
place: eventPlace(entry),
|
||||||
|
images: eventImages(entry, gedcom),
|
||||||
notes: eventNotes(entry, gedcom),
|
notes: eventNotes(entry, gedcom),
|
||||||
|
sources: eventSources(entry, gedcom),
|
||||||
|
indi: indi,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -171,7 +249,10 @@ function toFamilyEvents(
|
|||||||
type: familyMarriageEvent.tag,
|
type: familyMarriageEvent.tag,
|
||||||
personLink: getSpouse(indi, family, gedcom),
|
personLink: getSpouse(indi, family, gedcom),
|
||||||
place: eventPlace(familyMarriageEvent),
|
place: eventPlace(familyMarriageEvent),
|
||||||
|
images: eventImages(familyMarriageEvent, gedcom),
|
||||||
notes: eventNotes(familyMarriageEvent, gedcom),
|
notes: eventNotes(familyMarriageEvent, gedcom),
|
||||||
|
sources: eventSources(familyMarriageEvent, gedcom),
|
||||||
|
indi: indi,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -188,19 +269,12 @@ function Event(props: {event: EventData}) {
|
|||||||
{!!props.event.place && (
|
{!!props.event.place && (
|
||||||
<Item.Description>{props.event.place}</Item.Description>
|
<Item.Description>{props.event.place}</Item.Description>
|
||||||
)}
|
)}
|
||||||
{!!props.event.notes.length && (
|
<EventExtras
|
||||||
<Item.Description>
|
images={props.event.images}
|
||||||
{props.event.notes.map((note, index) => (
|
notes={props.event.notes}
|
||||||
<div key={index}>
|
sources={props.event.sources}
|
||||||
<MultilineText
|
indi={props.event.indi}
|
||||||
lines={note.map((line, index) => (
|
/>
|
||||||
<i key={index}>{line}</i>
|
|
||||||
))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Item.Description>
|
|
||||||
)}
|
|
||||||
</Item.Content>
|
</Item.Content>
|
||||||
</Item>
|
</Item>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -206,3 +206,29 @@ div.zoom {
|
|||||||
.image-placeholder {
|
.image-placeholder {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.event-extras .ui.attached.menu {
|
||||||
|
border: none;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-extras .ui.attached.segment.tab {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-extras .ui.tabular.menu .item {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-extras .ui.menu .active.item .icon{
|
||||||
|
background-color: #1b1c1d;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-extras .ui.attached.segment.active.tab{
|
||||||
|
word-break: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
max-width: 289px;
|
||||||
|
padding: 14px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,5 +91,8 @@
|
|||||||
"config.colors.NO_COLOR": "brak",
|
"config.colors.NO_COLOR": "brak",
|
||||||
"config.colors.COLOR_BY_GENERATION": "według pokolenia",
|
"config.colors.COLOR_BY_GENERATION": "według pokolenia",
|
||||||
"config.colors.COLOR_BY_SEX": "według płci",
|
"config.colors.COLOR_BY_SEX": "według płci",
|
||||||
"name.unknown_name": "N.N."
|
"name.unknown_name": "N.N.",
|
||||||
|
"extras.images": "Zdjęcia",
|
||||||
|
"extras.notes": "Notatki",
|
||||||
|
"extras.sources": "Źródła"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ function formatDate(date: TopolaDate, intl: IntlShape) {
|
|||||||
formatOptions,
|
formatOptions,
|
||||||
).format(dateObject);
|
).format(dateObject);
|
||||||
|
|
||||||
return [translatedQualifier, translatedDate].join(' ');
|
return [translatedQualifier, translatedDate]
|
||||||
|
.filter((dateElement) => dateElement)
|
||||||
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateRage(dateRange: DateRange, intl: IntlShape) {
|
function formatDateRage(dateRange: DateRange, intl: IntlShape) {
|
||||||
|
|||||||
@@ -291,3 +291,14 @@ export function getFileName(fileEntry: GedcomEntry): string | undefined {
|
|||||||
|
|
||||||
return fileTitle && fileExtension && fileTitle + '.' + fileExtension;
|
return fileTitle && fileExtension && fileTitle + '.' + fileExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getImageFileEntry(
|
||||||
|
objectEntry: GedcomEntry,
|
||||||
|
): GedcomEntry | undefined {
|
||||||
|
return objectEntry.tree.find(
|
||||||
|
(entry) =>
|
||||||
|
entry.tag === 'FILE' &&
|
||||||
|
entry.data.startsWith('http') &&
|
||||||
|
isImageFile(entry.data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user