Display images, notes, sources for events as collapsible tabs (#158)

This commit is contained in:
czifumasa 2023-07-21 09:53:30 +02:00 committed by GitHub
parent 95f304eae3
commit de45eacbdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 327 additions and 25 deletions

View File

@ -4,7 +4,7 @@ import {
GedcomData,
getData,
getFileName,
isImageFile,
getImageFileEntry
} from '../util/gedcom_util';
import {Events} from './events';
import {GedcomEntry} from 'parse-gedcom';
@ -56,12 +56,7 @@ function dataDetails(entry: GedcomEntry) {
}
function fileDetails(objectEntry: GedcomEntry) {
const imageFileEntry = objectEntry.tree.find(
(entry) =>
entry.tag === 'FILE' &&
entry.data.startsWith('http') &&
isImageFile(entry.data),
);
const imageFileEntry = getImageFileEntry(objectEntry);
return imageFileEntry ? (
<div className="person-image">

View 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;
}

View File

@ -3,14 +3,21 @@ import flatMap from 'array.prototype.flatmap';
import {calcAge} from '../util/age_util';
import {compareDates, formatDateOrRange} from '../util/date_util';
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 {FormattedMessage, IntlShape, useIntl} from 'react-intl';
import {Link, useLocation} from 'react-router-dom';
import {MultilineText} from './multiline-text';
import {pointerToId} from '../util/gedcom_util';
import {TranslatedTag} from './translated-tag';
import {Header, Item} from 'semantic-ui-react';
import {EventExtras, Image, Source} from './event-extras';
function PersonLink(props: {person: GedcomEntry}) {
const location = useLocation();
@ -45,7 +52,10 @@ interface EventData {
age?: string;
personLink?: GedcomEntry;
place?: string[];
notes: string[][];
images?: Image[];
notes?: string[][];
sources?: Source[];
indi: string;
}
const EVENT_TAGS = [
@ -115,6 +125,71 @@ function eventPlace(entry: GedcomEntry) {
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[][] {
return entry.tree
.filter((subentry) => ['NOTE', 'TYPE'].includes(subentry.tag))
@ -147,7 +222,10 @@ function toIndiEvent(
type: entry.tag,
age: getAge(entry, indi, gedcom, intl),
place: eventPlace(entry),
images: eventImages(entry, gedcom),
notes: eventNotes(entry, gedcom),
sources: eventSources(entry, gedcom),
indi: indi,
},
];
}
@ -171,7 +249,10 @@ function toFamilyEvents(
type: familyMarriageEvent.tag,
personLink: getSpouse(indi, family, gedcom),
place: eventPlace(familyMarriageEvent),
images: eventImages(familyMarriageEvent, gedcom),
notes: eventNotes(familyMarriageEvent, gedcom),
sources: eventSources(familyMarriageEvent, gedcom),
indi: indi,
};
});
}
@ -188,19 +269,12 @@ function Event(props: {event: EventData}) {
{!!props.event.place && (
<Item.Description>{props.event.place}</Item.Description>
)}
{!!props.event.notes.length && (
<Item.Description>
{props.event.notes.map((note, index) => (
<div key={index}>
<MultilineText
lines={note.map((line, index) => (
<i key={index}>{line}</i>
))}
/>
</div>
))}
</Item.Description>
)}
<EventExtras
images={props.event.images}
notes={props.event.notes}
sources={props.event.sources}
indi={props.event.indi}
/>
</Item.Content>
</Item>
);

View File

@ -206,3 +206,29 @@ div.zoom {
.image-placeholder {
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;
}

View File

@ -91,5 +91,8 @@
"config.colors.NO_COLOR": "brak",
"config.colors.COLOR_BY_GENERATION": "według pokolenia",
"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"
}

View File

@ -27,7 +27,9 @@ function formatDate(date: TopolaDate, intl: IntlShape) {
formatOptions,
).format(dateObject);
return [translatedQualifier, translatedDate].join(' ');
return [translatedQualifier, translatedDate]
.filter((dateElement) => dateElement)
.join(' ');
}
function formatDateRage(dateRange: DateRange, intl: IntlShape) {

View File

@ -291,3 +291,14 @@ export function getFileName(fileEntry: GedcomEntry): string | undefined {
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),
);
}