Skip to content

Commit 2a2a3d0

Browse files
authored
Merge pull request #196 from l0drex/beta
v2.1
2 parents ac4882b + f2e4f60 commit 2a2a3d0

38 files changed

+1567
-914
lines changed

README.md

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,39 @@
11
[![Build React app](https://github.com/l0drex/family-tree/actions/workflows/build.yml/badge.svg)](https://github.com/l0drex/family-tree/actions/workflows/build.yml)
22

3-
# About
3+
# 🌳 About
44

5-
This project allows displaying family tree data stored in csv tables as graphs on a website.
6-
It uses Cola.js, d3.js and gedcomx-js.
7-
The documentation can be found in the wiki.
5+
This is a web application to display family tree data locally in your browser.
6+
Just select a GedcomX json file and off you go!
7+
There is also a demo button to load some randomly generated example data to explore all features.
8+
Try it out over here on [GitHub Pages](https://l0drex.github.io/family-tree/)!.
89

9-
Supported languages[^1]: 🇺🇲/🇬🇧[^2] 🇩🇪
10+
To create a simple GedcomX file, you can create a spreadsheet
11+
and convert that with my [GedcomX converter](https://github.com/l0drex/csv_to_gedcomx).
12+
Note that you have to follow a specific format described on that page.
1013

11-
<!-- TODO add wiki page on how to add language support and then link it here -->
14+
Supported languages[^1]: 🇺🇲/🇬🇧[^2] 🇩🇪
15+
(See [Adding new languages](https://github.com/l0drex/family-tree/wiki/Localization) if you want to help translating.)
1216

1317
[^1]: No differentiation yet between country specific differences
1418
[^2]: Default, therefore used while loading and fallback if local language is not supported
1519

16-
17-
# 🌳 Usage
18-
19-
Upload a valid gedcomx-file on the home page. On submit, the family view should open and display the graph:
20-
21-
![grafik](https://user-images.githubusercontent.com/46622675/177526424-7507cbc5-e640-4657-bf1c-2a2d2a459685.png)
22-
23-
2420
# 🚧 GedcomX Support
2521
The following features of GedcomX are not supported:
2622

27-
### Source Description
23+
## Source Description
2824
Due to `gedcomx-js`, the following data can not be stored and therefore not displayed:
2925
- publisher
3026
- author
3127
- created
3228
- modified
3329
- published
3430

35-
### Group
36-
Not supported at all by `gedcomx-js`
31+
### Source Reference
32+
- qualifiers
3733

38-
### Event
39-
Will come in a future release
34+
## Group
35+
Not supported at all by `gedcomx-js`
4036

41-
### Relationship
37+
## Relationship
4238
Will come in a future release
4339
- facts

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@testing-library/dom": "^9.3.1",
2929
"@visx/axis": "^3.2.0",
3030
"@visx/geo": "^3.0.0",
31+
"@visx/network": "^3.0.0",
3132
"@visx/react-spring": "^3.1.0",
3233
"@visx/shape": "^3.0.0",
3334
"@visx/stats": "^3.2.0",

src/App.tsx

Lines changed: 67 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {createBrowserRouter, Outlet, RouterProvider, useLocation} from "react-ro
33
import {strings} from "./main";
44
import {useContext, useEffect, useMemo, useRef, useState} from "react";
55
import {db} from "./backend/db";
6-
import {SourceDescription, Document, Agent, Person} from "./backend/gedcomx-extensions";
6+
import {SourceDescription, Document, Agent, Person, EventExtended} from "./backend/gedcomx-extensions";
77
import {Home} from "./components/Home";
88
import Persons from "./components/Persons";
99
import Statistics from "./components/Statistics";
@@ -15,6 +15,13 @@ import {PlaceOverview, PlaceView} from "./components/Places";
1515
import ErrorBoundary from "./components/ErrorBoundary";
1616
import {ReactLink, ReactNavLink, VanillaLink} from "./components/GeneralComponents";
1717
import {Imprint} from "./components/Imprint";
18+
import {EventOverview, EventView} from "./components/Events";
19+
import emojis from './backend/emojies.json';
20+
21+
let personCache = {
22+
id: undefined,
23+
person: undefined
24+
}
1825

1926
const router = createBrowserRouter([
2027
{
@@ -23,11 +30,17 @@ const router = createBrowserRouter([
2330
path: "*", errorElement: <ErrorBoundary/>, children: [
2431
{index: true, Component: Home},
2532
{
26-
path: "persons/:id?", Component: Persons, loader: ({params}) => {
33+
path: "person/:id?", Component: Persons, loader: ({params}) => {
34+
if (personCache.person !== undefined && personCache.id === params.id) {
35+
return personCache.person;
36+
}
37+
38+
personCache.id = params.id;
39+
2740
if (!params.id) {
2841
// find a person whose id does not start with "missing-id-" if possible
2942
// persons with missing ids are not connected to any other persons, as they cannot be referenced in relationships
30-
return db.persons.toArray().then(ps => ps.sort((a, b) => {
43+
personCache.person = db.persons.toArray().then(ps => ps.sort((a, b) => {
3144
// Check if either string starts with "missing-id-"
3245
const aStartsWithMissingId = a.id.startsWith("missing-id-");
3346
const bStartsWithMissingId = b.id.startsWith("missing-id-");
@@ -42,13 +55,16 @@ const router = createBrowserRouter([
4255
return a.id.localeCompare(b.id);
4356
}
4457
})[0]).then(p => p ? new Person(p) : Promise.reject(new Error(strings.errors.noData)));
58+
} else {
59+
personCache.person = db.personWithId(params.id);
4560
}
46-
return db.personWithId(params.id);
61+
62+
return personCache.person;
4763
}
4864
},
4965
{path: "stats", Component: Statistics},
5066
{
51-
path: "sources", children: [
67+
path: "sourceDescription", children: [
5268
{
5369
index: true,
5470
Component: SourceDescriptionOverview,
@@ -62,7 +78,7 @@ const router = createBrowserRouter([
6278
]
6379
},
6480
{
65-
path: "documents", children: [
81+
path: "document", children: [
6682
{
6783
index: true,
6884
Component: DocumentOverview,
@@ -72,7 +88,7 @@ const router = createBrowserRouter([
7288
]
7389
},
7490
{
75-
path: "agents", children: [
91+
path: "agent", children: [
7692
{
7793
index: true,
7894
Component: AgentOverview,
@@ -81,17 +97,27 @@ const router = createBrowserRouter([
8197
{path: ":id", Component: AgentView, loader: ({params}) => db.elementWithId(params.id, "agent")}
8298
]
8399
},
84-
{path: "imprint", Component: Imprint},
85100
{
86-
path: "places", children: [
101+
path: "event", children: [
102+
{
103+
index: true,
104+
Component: EventOverview,
105+
loader: () => db.events.toArray().then(e => e.length ? e.map(d => new EventExtended(d)) : Promise.reject(new Error(strings.errors.noData)))
106+
},
107+
{path: ":id", Component: EventView, loader: ({params}) => db.elementWithId(params.id, "event")}
108+
]
109+
},
110+
{
111+
path: "place", children: [
87112
{
88113
index: true,
89114
Component: PlaceOverview,
90115
loader: () => db.places.toArray().then(p => p.length ? p.map(d => new PlaceDescription(d)) : Promise.reject(new Error(strings.errors.noData)))
91116
},
92117
{path: ":id", Component: PlaceView, loader: ({params}) => db.elementWithId(params.id, "place")}
93118
]
94-
}
119+
},
120+
{path: "imprint", Component: Imprint}
95121
]
96122
}]
97123
}], {basename: "/family-tree"});
@@ -103,10 +129,7 @@ export default function App() {
103129
interface ILayoutContext {
104130
setRightTitle: (string) => void,
105131
setHeaderChildren: (ReactNode) => void,
106-
sidebarVisible: boolean,
107-
isDark: boolean,
108-
allowExternalContent: boolean,
109-
toggleExternalContent: (boolean) => void,
132+
sidebarVisible: boolean
110133
}
111134

112135
export const LayoutContext = React.createContext<ILayoutContext>(undefined);
@@ -116,44 +139,51 @@ function Layout() {
116139
const [headerChildren, setChildren] = useState([]);
117140
const [navBarExtended, toggleNavBar] = useState(false);
118141
const [sidebarExtended, toggleSidebar] = useState(matchMedia("(min-width: 768px)").matches);
119-
const [allowExternalContent, toggleExternalContent] = useState(false);
120142
const dialog = useRef<HTMLDialogElement>();
121143
const location = useLocation();
122-
const darkQuery = matchMedia("(prefers-color-scheme: dark)");
123-
const [isDark, toggleDark] = useState(darkQuery.matches);
124-
darkQuery.addEventListener("change", e => toggleDark(e.matches));
125144
const query = matchMedia("(max-width: 639px)");
126145
const [isSmallScreen, setSmallScreen] = useState(query.matches);
127146
query.addEventListener("change", e => setSmallScreen(e.matches));
128147

129148
const nav = <nav className="row-start-2 row-span-2 dark:text-white">
130149
<ul className={`flex flex-col gap-2 ${isSmallScreen ? "" : "ml-2"} text-lg`}>
131-
<li><ReactNavLink to="">{"🏠" + (navBarExtended ? ` ${strings.home.title}` : "")}</ReactNavLink></li>
132-
<li><ReactNavLink to="persons">{"🌳" + (navBarExtended ? ` ${strings.gedcomX.person.persons}` : "")}</ReactNavLink>
150+
<li><ReactNavLink to="">
151+
{emojis.home + (navBarExtended ? ` ${strings.home.title}` : "")}
152+
</ReactNavLink></li>
153+
<li><ReactNavLink to="person">
154+
{emojis.tree + (navBarExtended ? ` ${strings.gedcomX.person.persons}` : "")}
155+
</ReactNavLink>
133156
</li>
134-
<li><ReactNavLink to="stats">{"📊" + (navBarExtended ? ` ${strings.statistics.title}` : "")}</ReactNavLink></li>
135-
<li><ReactNavLink
136-
to="sources">{"📚" + (navBarExtended ? ` ${strings.gedcomX.sourceDescription.sourceDescriptions}` : "")}</ReactNavLink>
157+
<li><ReactNavLink to="stats">
158+
{emojis.stats + (navBarExtended ? ` ${strings.statistics.title}` : "")}
159+
</ReactNavLink></li>
160+
<li><ReactNavLink to="sourceDescription">
161+
{emojis.source.default + (navBarExtended ? ` ${strings.gedcomX.sourceDescription.sourceDescriptions}` : "")}
162+
</ReactNavLink>
137163
</li>
138-
<li><ReactNavLink
139-
to="documents">{"📄" + (navBarExtended ? ` ${strings.gedcomX.document.documents}` : "")}</ReactNavLink></li>
140-
<li><ReactNavLink to="agents">{"👤" + (navBarExtended ? ` ${strings.gedcomX.agent.agents}` : "")}</ReactNavLink>
164+
<li><ReactNavLink to="document">
165+
{emojis.document.default + (navBarExtended ? ` ${strings.gedcomX.document.documents}` : "")}
166+
</ReactNavLink></li>
167+
<li><ReactNavLink to="agent">
168+
{emojis.agent.agent + (navBarExtended ? ` ${strings.gedcomX.agent.agents}` : "")}
169+
</ReactNavLink>
141170
</li>
142-
<li><ReactNavLink
143-
to="places">{"🌎" + (navBarExtended ? ` ${strings.gedcomX.placeDescription.places}` : "")}</ReactNavLink></li>
171+
<li><ReactNavLink to="place">
172+
{emojis.place + (navBarExtended ? ` ${strings.gedcomX.placeDescription.places}` : "")}
173+
</ReactNavLink></li>
174+
<li><ReactNavLink to="event">
175+
{emojis.event.default + (navBarExtended ? ` ${strings.gedcomX.event.events}` : "")}
176+
</ReactNavLink></li>
144177
</ul>
145178
</nav>
146179

147180
const layoutContext = useMemo(() => {
148181
return {
149182
setRightTitle: setTitleRight,
150183
setHeaderChildren: setChildren,
151-
sidebarVisible: sidebarExtended,
152-
isDark: isDark,
153-
allowExternalContent: allowExternalContent,
154-
toggleExternalContent: toggleExternalContent
184+
sidebarVisible: sidebarExtended
155185
}
156-
}, [allowExternalContent, isDark, sidebarExtended])
186+
}, [sidebarExtended])
157187

158188
useEffect(() => {
159189
if (navBarExtended && !dialog.current?.open) dialog.current?.showModal();
@@ -164,18 +194,18 @@ function Layout() {
164194

165195
return <>
166196
<div className="row-start-1 ml-4 font-bold text-xl h-full my-1 dark:text-white">
167-
<button onClick={() => toggleNavBar(!navBarExtended)}>{navBarExtended ? "⬅️" : "➡️"}</button>
197+
<button onClick={() => toggleNavBar(!navBarExtended)}>{navBarExtended ? emojis.left : emojis.right}</button>
168198
</div>
169199
{isSmallScreen ? <dialog ref={dialog} className="rounded-2xl">{nav}</dialog> : nav}
170200

171-
<header className="row-start-1 text-xl flex flex-row items-center justify-center gap-4 dark:text-white w-full">
201+
<header className="row-start-1 flex flex-row items-center justify-center gap-4 dark:text-white w-full">
172202
{headerChildren}
173203
</header>
174204

175205
{titleRight && <div className="row-start-1 text-right lg:text-center font-bold text-xl my-1 mr-4 dark:text-white">
176206
{sidebarExtended && <span className={`mr-4 hidden md:inline`}>{titleRight}</span>}
177207
<span className={`lg:hidden`}>
178-
<button onClick={() => toggleSidebar(!sidebarExtended)}>{sidebarExtended ? "➡️" : "⬅️"}</button>
208+
<button onClick={() => toggleSidebar(!sidebarExtended)}>{sidebarExtended ? emojis.right : emojis.left}</button>
179209
</span>
180210
</div>}
181211

@@ -228,7 +258,7 @@ export function Sidebar(props) {
228258

229259
if (layoutContext.sidebarVisible) {
230260
return <aside
231-
className={`row-start-2 md:row-span-2 mx-4 sm:ml-0 col-start-1 sm:col-start-2 md:col-start-3 col-span-3 sm:col-span-2 md:col-span-1 max-h-64 md:max-h-full md:max-w-xs overflow-y-auto overflow-x-scroll flex gap-4 flex-col dark:text-white`}>
261+
className={`row-start-2 md:row-span-2 mx-4 sm:ml-0 col-start-1 sm:col-start-2 md:col-start-3 col-span-3 sm:col-span-2 md:col-span-1 max-h-64 md:max-h-full md:max-w-xs overflow-y-auto overflow-x-scroll flex gap-6 flex-col dark:text-white`}>
232262
{props.children}
233263
</aside>
234264
}

src/backend/StatisticsProvider.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,10 @@ export async function getNames(type: "First" | "Last") {
184184
persons
185185
.map(p => new Person(p))
186186
.map(p => {
187-
let names = p.fullName.split(" ");
188-
if (type === "Last") return names.pop()
189-
else return names.filter(n => n !== "Dr.")[0]
187+
if (type === "First") return p.firstName;
188+
else return p.surname;
190189
})
191-
.filter(n => n !== "?"))
190+
.filter(n => n !== undefined))
192191
.sort((a, b) => b.count - a.count));
193192
return data
194193
.splice(0, 30)

0 commit comments

Comments
 (0)