mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-02-18 02:55:48 +00:00
Display images, notes, sources for events as collapsible tabs (#158)
This commit is contained in:
parent
95f304eae3
commit
de45eacbdb
@ -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">
|
||||
|
||||
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 {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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user