diff --git a/src/details/details.tsx b/src/details/details.tsx index 47db67c..be66953 100644 --- a/src/details/details.tsx +++ b/src/details/details.tsx @@ -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 ? (
diff --git a/src/details/event-extras.tsx b/src/details/event-extras.tsx new file mode 100644 index 0000000..31c345c --- /dev/null +++ b/src/details/event-extras.tsx @@ -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) => ( + + + + + + )) + ); +} + +function eventNotes(notes: string[][] | undefined) { + return ( + !!notes?.length && + notes.map((note, index) => ( +
+ ( + {line} + ))} + /> +
+ )) + ); +} + +function eventSources(sources: Source[] | undefined, intl: IntlShape) { + return ( + !!sources?.length && ( + + {sources.map((source, index) => ( + + + + + + {[source.author, source.title, source.publicationInfo] + .filter((sourceElement) => sourceElement) + .join(', ')} + + + + {source.page} + {source.date + ? ' [' + formatDateOrRange(source.date, intl) + ']' + : null} + + + + ))} + + ) + ); +} + +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, + menuItemProps: MenuItemProps, + ) { + menuItemProps.index !== undefined && activeIndex !== menuItemProps.index + ? setActiveIndex(menuItemProps.index) + : setActiveIndex(-1); + } + + const imageTab = props.images?.length && { + menuItem: ( + + + } + size="mini" + position="bottom center" + trigger={} + /> + + ), + render: () => {eventImages(props.images)}, + }; + + const noteTab = props.notes?.length && { + menuItem: ( + + + } + size="mini" + position="bottom center" + trigger={} + /> + + ), + render: () => {eventNotes(props.notes)}, + }; + + const sourceTab = props.sources?.length && { + menuItem: ( + + + } + size="mini" + position="bottom center" + trigger={} + /> + + ), + render: () => {eventSources(props.sources, intl)}, + }; + + const panes = [imageTab, noteTab, sourceTab].flatMap((tab) => + tab ? [tab] : [], + ); + + if (panes.length) { + return ( + + + + ); + } + return null; +} diff --git a/src/details/events.tsx b/src/details/events.tsx index 32b2fd3..c84689c 100644 --- a/src/details/events.tsx +++ b/src/details/events.tsx @@ -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 && ( {props.event.place} )} - {!!props.event.notes.length && ( - - {props.event.notes.map((note, index) => ( -
- ( - {line} - ))} - /> -
- ))} -
- )} + ); diff --git a/src/index.css b/src/index.css index 103391b..21805ee 100644 --- a/src/index.css +++ b/src/index.css @@ -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; +} + diff --git a/src/translations/pl.json b/src/translations/pl.json index e46783e..dd9fb30 100644 --- a/src/translations/pl.json +++ b/src/translations/pl.json @@ -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" } diff --git a/src/util/date_util.ts b/src/util/date_util.ts index b1df6cd..df66761 100644 --- a/src/util/date_util.ts +++ b/src/util/date_util.ts @@ -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) { diff --git a/src/util/gedcom_util.ts b/src/util/gedcom_util.ts index 731ff6d..bda9e3b 100644 --- a/src/util/gedcom_util.ts +++ b/src/util/gedcom_util.ts @@ -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), + ); +}