mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-07-27 14:11:47 +00:00
Add Immediate Family section to person details in side panel
This commit is contained in:
@@ -162,7 +162,7 @@ div.zoom {
|
||||
|
||||
.details .item-header .header {
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
margin: 0 0 8px 0;
|
||||
min-width: 40%;
|
||||
}
|
||||
|
||||
@@ -335,6 +335,14 @@ div.zoom {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.parents-block {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.children-block {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (max-width: 450px) {
|
||||
/* Hide the title from the top bar when the viewport is too small to display
|
||||
it alongside the search box. */
|
||||
|
||||
@@ -9,8 +9,10 @@ This directory contains React components for displaying detailed information abo
|
||||
- [details.tsx](details.tsx): The main component that orchestrates the display of all details for an individual, including name, images, events, facts, notes, and sources.
|
||||
- [event-extras.tsx](event-extras.tsx): Displays additional content for events (images, notes, sources, files) in a tabbed interface.
|
||||
- [events.tsx](events.tsx): Handles the logic and display for all events related to an individual, sorting them by date and grouping them by life stages.
|
||||
- [immediate-family.tsx](immediate-family.tsx): A dedicated side panel module for grouping and displaying parents, spouses, and children as rapid-navigation links.
|
||||
- [linkify-new-tab.tsx](linkify-new-tab.tsx): A helper component that wraps content and makes URLs clickable, opening them in a new tab.
|
||||
- [multiline-text.tsx](multiline-text.tsx): Helper component to display multi-line text with linkified URLs.
|
||||
- [person-link.tsx](person-link.tsx): Component that renders a clickable link to navigate to a person's profile view.
|
||||
- [sources.tsx](sources.tsx): Displays a list of sources cited for an entry.
|
||||
- [translated-tag.tsx](translated-tag.tsx): Component to translate GEDCOM tags into human-readable labels.
|
||||
- [wrapped-image.tsx](wrapped-image.tsx): Component to display images with loading placeholders, error fallback, and a click-to-enlarge modal.
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import {Config, Ids} from '../config/config';
|
||||
import {AdditionalFiles, FileEntry} from './additional-files';
|
||||
import {ALL_SUPPORTED_EVENT_TYPES, Events} from './events';
|
||||
import {ImmediateFamily} from './immediate-family';
|
||||
import {MultilineText} from './multiline-text';
|
||||
import {Sources} from './sources';
|
||||
import {TranslatedTag} from './translated-tag';
|
||||
@@ -353,6 +354,7 @@ export function Details(props: Props) {
|
||||
['OBJE'],
|
||||
imageDetails,
|
||||
)}
|
||||
<ImmediateFamily gedcom={props.gedcom} indi={props.indi} />
|
||||
<Events gedcom={props.gedcom} entries={entries} indi={props.indi} />
|
||||
{props.config.id === Ids.SHOW ? getSectionForId(props.indi) : null}
|
||||
{getSectionForEachMatchingEntry(
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import flatMap from 'array.prototype.flatmap';
|
||||
import {GedcomEntry} from 'parse-gedcom';
|
||||
import queryString from 'query-string';
|
||||
import {FormattedMessage, IntlShape, useIntl} from 'react-intl';
|
||||
import {Link, useLocation} from 'react-router';
|
||||
import {IntlShape, useIntl} from 'react-intl';
|
||||
import {Header, Item} from 'semantic-ui-react';
|
||||
import {DateOrRange, getDate} from 'topola';
|
||||
import {calcAge} from '../../util/age_util';
|
||||
@@ -13,39 +11,17 @@ import {
|
||||
getData,
|
||||
getFileName,
|
||||
getImageFileEntry,
|
||||
getName,
|
||||
getNonImageFileEntry,
|
||||
mapToSource,
|
||||
pointerToId,
|
||||
resolveDate,
|
||||
resolveType,
|
||||
Source,
|
||||
} from '../../util/gedcom_util';
|
||||
import {FileEntry} from './additional-files';
|
||||
import {EventExtras, Image} from './event-extras';
|
||||
import {PersonLink} from './person-link';
|
||||
import {TranslatedTag} from './translated-tag';
|
||||
|
||||
function PersonLink(props: {person: GedcomEntry}) {
|
||||
const location = useLocation();
|
||||
|
||||
const name = getName(props.person);
|
||||
|
||||
const search = queryString.parse(location.search);
|
||||
search['indi'] = pointerToId(props.person.pointer);
|
||||
|
||||
return (
|
||||
<Item.Meta>
|
||||
<Link to={{pathname: '/view', search: queryString.stringify(search)}}>
|
||||
{name ? (
|
||||
name
|
||||
) : (
|
||||
<FormattedMessage id="name.unknown_name" defaultMessage="N.N." />
|
||||
)}
|
||||
</Link>
|
||||
</Item.Meta>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
gedcom: GedcomData;
|
||||
indi: string;
|
||||
|
||||
218
src/sidepanel/details/immediate-family.tsx
Normal file
218
src/sidepanel/details/immediate-family.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import {GedcomEntry} from 'parse-gedcom';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {Header, Item} from 'semantic-ui-react';
|
||||
import {getDate} from 'topola';
|
||||
import {compareDates} from '../../util/date_util';
|
||||
import {GedcomData, pointerToId, resolveDate} from '../../util/gedcom_util';
|
||||
import {PersonLink} from './person-link';
|
||||
|
||||
interface Props {
|
||||
gedcom: GedcomData;
|
||||
indi: string;
|
||||
}
|
||||
|
||||
function mapSpousalFamily(
|
||||
familyRecord: GedcomEntry,
|
||||
gedcom: GedcomData,
|
||||
indi: string,
|
||||
) {
|
||||
const spouseSubEntry = familyRecord.tree?.find(
|
||||
(sub) =>
|
||||
['HUSB', 'WIFE'].includes(sub.tag) && pointerToId(sub.data) !== indi,
|
||||
);
|
||||
const spouseId = spouseSubEntry
|
||||
? pointerToId(spouseSubEntry.data)
|
||||
: undefined;
|
||||
const spouseRecord = spouseId ? gedcom.indis[spouseId] : undefined;
|
||||
|
||||
const chilSubEntries =
|
||||
familyRecord.tree?.filter((sub) => sub.tag === 'CHIL') || [];
|
||||
|
||||
const validChildren = chilSubEntries.filter(
|
||||
(childSub) => !!gedcom.indis[pointerToId(childSub.data)],
|
||||
);
|
||||
|
||||
// Pre-parse dates to avoid redundant O(N log N) parsing inside sort comparator
|
||||
const mappedChildren = validChildren.map((childSub) => {
|
||||
const childEntry = gedcom.indis[pointerToId(childSub.data)];
|
||||
const birt = childEntry?.tree.find((sub) => sub.tag === 'BIRT');
|
||||
const dateSub = birt ? resolveDate(birt) : undefined;
|
||||
const parsedDate = dateSub ? getDate(dateSub.data) : undefined;
|
||||
return {
|
||||
childEntry,
|
||||
parsedDate,
|
||||
};
|
||||
});
|
||||
|
||||
const sortedChildren = mappedChildren
|
||||
.sort((a, b) => compareDates(a.parsedDate, b.parsedDate))
|
||||
.map((item) => item.childEntry);
|
||||
|
||||
return {
|
||||
familyId: pointerToId(familyRecord.pointer),
|
||||
spouseTag: spouseSubEntry?.tag,
|
||||
spouseRecord,
|
||||
children: sortedChildren,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the immediate family section of a person, displaying their parents,
|
||||
* spouses, and children.
|
||||
*/
|
||||
export function ImmediateFamily(props: Props) {
|
||||
const personEntry = props.gedcom.indis[props.indi];
|
||||
if (!personEntry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entries = personEntry.tree;
|
||||
|
||||
// --- Parents Block Renderer ---
|
||||
const famcEntry = entries.find((sub) => sub.tag === 'FAMC');
|
||||
const parentalFamily = famcEntry
|
||||
? props.gedcom.fams[pointerToId(famcEntry.data)]
|
||||
: undefined;
|
||||
|
||||
const isParentalFamilyValid = parentalFamily?.tag === 'FAM';
|
||||
|
||||
const husbEntry = isParentalFamilyValid
|
||||
? parentalFamily?.tree.find((sub) => sub.tag === 'HUSB')
|
||||
: undefined;
|
||||
const wifeEntry = isParentalFamilyValid
|
||||
? parentalFamily?.tree.find((sub) => sub.tag === 'WIFE')
|
||||
: undefined;
|
||||
|
||||
const fatherId = husbEntry ? pointerToId(husbEntry.data) : undefined;
|
||||
const fatherRecord = fatherId ? props.gedcom.indis[fatherId] : undefined;
|
||||
|
||||
const motherId = wifeEntry ? pointerToId(wifeEntry.data) : undefined;
|
||||
const motherRecord = motherId ? props.gedcom.indis[motherId] : undefined;
|
||||
|
||||
const hasParents = fatherRecord || motherRecord;
|
||||
|
||||
// --- Spouses and Children Block Renderer ---
|
||||
const famsEntries = entries.filter((sub) => sub.tag === 'FAMS');
|
||||
const spousalFamilies = famsEntries.map(
|
||||
(entry) => props.gedcom.fams[pointerToId(entry.data)],
|
||||
);
|
||||
|
||||
const mappedFamilies = spousalFamilies.map((familyRecord) =>
|
||||
mapSpousalFamily(familyRecord, props.gedcom, props.indi),
|
||||
);
|
||||
|
||||
const validMappedFamilies = mappedFamilies.filter(
|
||||
(group) => group.spouseRecord || group.children.length > 0,
|
||||
);
|
||||
|
||||
const totalValidFams = validMappedFamilies.length;
|
||||
const hasSpousesOrChildren = totalValidFams > 0;
|
||||
|
||||
if (!hasParents && !hasSpousesOrChildren) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Item>
|
||||
<Item.Content>
|
||||
<div className="item-header">
|
||||
<Header as="span" size="small">
|
||||
<FormattedMessage
|
||||
id="family.immediate_family"
|
||||
defaultMessage="Immediate Family"
|
||||
/>
|
||||
</Header>
|
||||
</div>
|
||||
{hasParents && (
|
||||
<div className="parents-block">
|
||||
{fatherRecord && (
|
||||
<div>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="family.father"
|
||||
defaultMessage="Father"
|
||||
/>
|
||||
</strong>
|
||||
: <PersonLink person={fatherRecord} />
|
||||
</div>
|
||||
)}
|
||||
{motherRecord && (
|
||||
<div>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="family.mother"
|
||||
defaultMessage="Mother"
|
||||
/>
|
||||
</strong>
|
||||
: <PersonLink person={motherRecord} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{validMappedFamilies.map((group) => {
|
||||
const showUnknownSpouse = !group.spouseRecord && totalValidFams > 1;
|
||||
return (
|
||||
<div key={group.familyId} className="spousal-group">
|
||||
{group.spouseRecord ? (
|
||||
<div>
|
||||
<strong>
|
||||
{group.spouseTag === 'HUSB' ? (
|
||||
<FormattedMessage
|
||||
id="family.husband"
|
||||
defaultMessage="Husband"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="family.wife"
|
||||
defaultMessage="Wife"
|
||||
/>
|
||||
)}
|
||||
</strong>
|
||||
: <PersonLink person={group.spouseRecord} />
|
||||
</div>
|
||||
) : showUnknownSpouse ? (
|
||||
<div>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="family.unknown_spouse"
|
||||
defaultMessage="Unknown Spouse"
|
||||
/>
|
||||
</strong>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{group.children.length > 0 && (
|
||||
<div className="children-block">
|
||||
<div>
|
||||
<strong>
|
||||
{group.children.length === 1 ? (
|
||||
<FormattedMessage
|
||||
id="family.child"
|
||||
defaultMessage="Child"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="family.children"
|
||||
defaultMessage="Children"
|
||||
/>
|
||||
)}
|
||||
</strong>
|
||||
:
|
||||
</div>
|
||||
<div>
|
||||
{group.children.map((child) => (
|
||||
<div key={child.pointer}>
|
||||
<PersonLink person={child} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Item.Content>
|
||||
</Item>
|
||||
);
|
||||
}
|
||||
31
src/sidepanel/details/person-link.tsx
Normal file
31
src/sidepanel/details/person-link.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import {GedcomEntry} from 'parse-gedcom';
|
||||
import queryString from 'query-string';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {Link, useLocation} from 'react-router';
|
||||
import {getName, pointerToId} from '../../util/gedcom_util';
|
||||
|
||||
interface Props {
|
||||
person: GedcomEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a clickable link to an individual's profile view, preserving existing query parameters.
|
||||
*/
|
||||
export function PersonLink(props: Props) {
|
||||
const location = useLocation();
|
||||
|
||||
const name = getName(props.person);
|
||||
|
||||
const search = queryString.parse(location.search);
|
||||
search['indi'] = pointerToId(props.person.pointer);
|
||||
|
||||
return (
|
||||
<Link to={{pathname: '/view', search: queryString.stringify(search)}}>
|
||||
{name ? (
|
||||
name
|
||||
) : (
|
||||
<FormattedMessage id="name.unknown_name" defaultMessage="N.N." />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -134,5 +134,14 @@
|
||||
"extras.images": "Изображение",
|
||||
"extras.notes": "Бележки",
|
||||
"extras.sources": "Източници",
|
||||
"extras.files": "Допълнителни файлове"
|
||||
"extras.files": "Допълнителни файлове",
|
||||
"family.immediate_family": "Най-близко семейство",
|
||||
"family.parents": "Родители",
|
||||
"family.father": "Баща",
|
||||
"family.mother": "Майка",
|
||||
"family.husband": "Съпруг",
|
||||
"family.wife": "Съпруга",
|
||||
"family.unknown_spouse": "Неизвестен съпруг/а",
|
||||
"family.children": "Деца",
|
||||
"family.child": "Дете"
|
||||
}
|
||||
|
||||
@@ -134,5 +134,14 @@
|
||||
"extras.images": "Obrázky",
|
||||
"extras.notes": "Poznámky",
|
||||
"extras.sources": "Zdroje",
|
||||
"extras.files": "Další soubory"
|
||||
"extras.files": "Další soubory",
|
||||
"family.immediate_family": "Nejbližší rodina",
|
||||
"family.parents": "Rodiče",
|
||||
"family.father": "Otec",
|
||||
"family.mother": "Matka",
|
||||
"family.husband": "Manžel",
|
||||
"family.wife": "Manželka",
|
||||
"family.unknown_spouse": "Neznámý manžel/ka",
|
||||
"family.children": "Děti",
|
||||
"family.child": "Dítě"
|
||||
}
|
||||
|
||||
@@ -134,5 +134,14 @@
|
||||
"extras.images": "Bilder",
|
||||
"extras.notes": "Notizen",
|
||||
"extras.sources": "Quellen",
|
||||
"extras.files": "Weitere Dateien"
|
||||
"extras.files": "Weitere Dateien",
|
||||
"family.immediate_family": "Engere Familie",
|
||||
"family.parents": "Eltern",
|
||||
"family.father": "Vater",
|
||||
"family.mother": "Mutter",
|
||||
"family.husband": "Ehemann",
|
||||
"family.wife": "Ehefrau",
|
||||
"family.unknown_spouse": "Unbekannter Ehepartner",
|
||||
"family.children": "Kinder",
|
||||
"family.child": "Kind"
|
||||
}
|
||||
|
||||
@@ -134,5 +134,14 @@
|
||||
"extras.images": "Images",
|
||||
"extras.notes": "Notes",
|
||||
"extras.sources": "Sources",
|
||||
"extras.files": "Fichiers supplémentaires"
|
||||
"extras.files": "Fichiers supplémentaires",
|
||||
"family.immediate_family": "Famille proche",
|
||||
"family.parents": "Parents",
|
||||
"family.father": "Père",
|
||||
"family.mother": "Mère",
|
||||
"family.husband": "Mari",
|
||||
"family.wife": "Femme",
|
||||
"family.unknown_spouse": "Conjoint inconnu",
|
||||
"family.children": "Enfants",
|
||||
"family.child": "Enfant"
|
||||
}
|
||||
|
||||
@@ -134,5 +134,14 @@
|
||||
"extras.images": "Immagini",
|
||||
"extras.notes": "Appunti",
|
||||
"extras.sources": "Fonti",
|
||||
"extras.files": "File aggiuntivi"
|
||||
"extras.files": "File aggiuntivi",
|
||||
"family.immediate_family": "Famiglia stretta",
|
||||
"family.parents": "Genitori",
|
||||
"family.father": "Padre",
|
||||
"family.mother": "Madre",
|
||||
"family.husband": "Marito",
|
||||
"family.wife": "Moglie",
|
||||
"family.unknown_spouse": "Coniuge sconosciuto",
|
||||
"family.children": "Figli",
|
||||
"family.child": "Figlio"
|
||||
}
|
||||
|
||||
@@ -134,5 +134,14 @@
|
||||
"extras.images": "Zdjęcia",
|
||||
"extras.notes": "Notatki",
|
||||
"extras.sources": "Źródła",
|
||||
"extras.files": "Dodatkowe pliki"
|
||||
"extras.files": "Dodatkowe pliki",
|
||||
"family.immediate_family": "Najbliższa rodzina",
|
||||
"family.parents": "Rodzice",
|
||||
"family.father": "Ojciec",
|
||||
"family.mother": "Matka",
|
||||
"family.husband": "Mąż",
|
||||
"family.wife": "Żona",
|
||||
"family.unknown_spouse": "Nieznany współmałżonek",
|
||||
"family.children": "Dzieci",
|
||||
"family.child": "Dziecko"
|
||||
}
|
||||
|
||||
@@ -134,5 +134,14 @@
|
||||
"extras.images": "Картинки",
|
||||
"extras.notes": "Примечание",
|
||||
"extras.sources": "Источники",
|
||||
"extras.files": "Дополнительные файлы"
|
||||
"extras.files": "Дополнительные файлы",
|
||||
"family.immediate_family": "Ближайшие родственники",
|
||||
"family.parents": "Родители",
|
||||
"family.father": "Отец",
|
||||
"family.mother": "Мать",
|
||||
"family.husband": "Муж",
|
||||
"family.wife": "Жена",
|
||||
"family.unknown_spouse": "Неизвестный супруг(а)",
|
||||
"family.children": "Дети",
|
||||
"family.child": "Ребёнок"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user