Family Tree Maker App (built 100% by AI)

David Morgan
8 min readOct 22, 2024

--

This React-based app allows users to create, edit, and visualize family trees interactively.

You can access it here:

https://9tg6f6.csb.app/

Here’s a breakdown of its features and how to use them:

  1. Main Interface:
  • The app displays a canvas where you can add and manipulate boxes representing family members.
  • At the top, there’s a control panel with various buttons and options.
  • A draggable title is present, which you can customize.

2. Adding Family Members:

  • Click the “Add Box” button in the control panel.
  • A new box will appear on the canvas with a default label (e.g., “Box 1”).

3. Editing Family Member Information:

  • Double-click on any box to edit its content.
  • A text area will appear where you can enter multiple lines of text.
  • Press Enter (without Shift) or click outside the box to save changes.

4. Moving Family Members:

  • Click and drag any box to reposition it on the canvas.

5. Deleting Family Members:

  • Hover over a box to reveal a red “X” button in the top-right corner.
  • Click the “X” to delete the family member and any associated links.

6. Creating Relationships (Links):

  • Use the dropdown menus in the control panel to select two family members.
  • Choose “Link from” and “Link to” boxes.
  • Click “Create Link” to draw a line between the selected boxes.

7. Editing the Title:

  • Click on the title text to edit it.
  • Enter the new title and press Enter or click outside to save.

8. Moving the Title:

  • Click and drag the title box to reposition it on the canvas.

9. Exporting the Family Tree:

  • Click the “Export to SVG” button to save your family tree as an SVG file.
  • This file can be opened in web browsers or vector graphics software.

10. Saving the Current State:

  • Click the “Save State” button to download a JSON file containing your family tree data.
  • This file includes box positions, labels, links, and title information.

11. Loading a Saved State:

  • Click the “Load State” button to open a file dialog.
  • Select a previously saved JSON file to restore your family tree.

Responsive Design:

  • The app adjusts to different screen sizes, allowing you to create family trees on various devices.

Tips for Effective Use:

  • Use concise labels for family members to keep boxes readable.
  • Arrange boxes in a logical order (e.g., generations from top to bottom).
  • Use links to show direct relationships between family members.
  • Save your work regularly using the “Save State” feature.
  • Export to SVG for high-quality prints or further editing in graphics software [svgviewer.dev is the best way to view the svg file and you can amend it or save as PNG].

This family tree app provides an intuitive interface for creating, editing, and visualizing family relationships. It’s flexible enough to accommodate various family structures and allows for easy rearrangement and modification as needed.

The React Code (built entirely by AI — Perplexity.ai)

The original code if you want to see how it works and build your own:

import React, { useState, useRef, useEffect } from ‘react’;

interface Box {
id: number;
x: number;
y: number;
label: string;
}

interface Link {
from: number;
to: number;
}

interface AppState {
boxes: Box[];
links: Link[];
titlePosition: { x: number; y: number };
title: string;
}

function isValidAppState(state: any): state is AppState {
return (
Array.isArray(state.boxes) &&
Array.isArray(state.links) &&
typeof state.titlePosition === ‘object’ &&
typeof state.titlePosition.x === ‘number’ &&
typeof state.titlePosition.y === ‘number’ &&
typeof state.title === ‘string’
);
}

const BOX_WIDTH = 200;
const BOX_HEIGHT = 100;
const TITLE_HEIGHT = 60;
const TITLE_WIDTH = 400;
const CONTROL_HEIGHT = 50;

const App: React.FC = () => {
const [boxes, setBoxes] = useState<Box[]>([{ id: 1, x: 20, y: CONTROL_HEIGHT + 20, label: ‘Box 1’ }]);
const [links, setLinks] = useState<Link[]>([]);
const [dragging, setDragging] = useState<number | null>(null);
const [draggingTitle, setDraggingTitle] = useState(false);
const [offsetX, setOffsetX] = useState(0);
const [offsetY, setOffsetY] = useState(0);
const [editingLabel, setEditingLabel] = useState<number | null>(null);
const [editingText, setEditingText] = useState(‘’);
const [linkFrom, setLinkFrom] = useState<number | ‘’>(‘’);
const [linkTo, setLinkTo] = useState<number | ‘’>(‘’);
const [isHovering, setIsHovering] = useState(false);
const [title, setTitle] = useState(‘Family Tree for XXXXXXXX’);
const [editingTitle, setEditingTitle] = useState(false);
const [titlePosition, setTitlePosition] = useState({ x: 20, y: CONTROL_HEIGHT + 20 });
const [loadKey, setLoadKey] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);

const handleMouseDown = (id: number | ‘title’, e: React.MouseEvent<HTMLDivElement>) => {
if (e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLButtonElement) return;
if (id === ‘title’) {
setDraggingTitle(true);
} else {
setDragging(id);
}
setOffsetX(e.clientX — e.currentTarget.offsetLeft);
setOffsetY(e.clientY — e.currentTarget.offsetTop);
};

const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (dragging !== null) {
const x = e.clientX — offsetX;
const y = e.clientY — offsetY;
setBoxes(boxes.map(box => box.id === dragging ? { …box, x, y } : box));
} else if (draggingTitle) {
const x = e.clientX — offsetX;
const y = e.clientY — offsetY;
setTitlePosition({ x, y });
}
};

const handleMouseUp = () => {
setDragging(null);
setDraggingTitle(false);
};

const addBox = () => {
const newId = boxes.length + 1;
setBoxes([…boxes, {
id: newId,
x: Math.random() * (window.innerWidth — BOX_WIDTH — 40) + 20,
y: Math.random() * (window.innerHeight — BOX_HEIGHT — CONTROL_HEIGHT — 40) + CONTROL_HEIGHT + 20,
label: `Box ${newId}`
}]);
};

const deleteBox = (id: number) => {
setBoxes(boxes.filter(box => box.id !== id));
setLinks(links.filter(link => link.from !== id && link.to !== id));
};

const addLink = () => {
if (linkFrom !== ‘’ && linkTo !== ‘’ && linkFrom !== linkTo) {
if (!links.some(link => link.from === linkFrom && link.to === linkTo)) {
setLinks([…links, { from: linkFrom, to: linkTo }]);
setLinkFrom(‘’);
setLinkTo(‘’);
}
}
};

const startEditing = (id: number, currentLabel: string) => {
setEditingLabel(id);
setEditingText(currentLabel);
};

const finishEditing = () => {
if (editingLabel !== null) {
setBoxes(boxes.map(box =>
box.id === editingLabel ? { …box, label: editingText.trim() } : box
));
setEditingLabel(null);
setEditingText(‘’);
}
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === ‘Enter’ && !e.shiftKey) {
e.preventDefault();
finishEditing();
}
};

const calculateLinkCoordinates = (fromBox: Box, toBox: Box) => {
const fromCenterX = fromBox.x + BOX_WIDTH / 2;
const fromCenterY = fromBox.y + BOX_HEIGHT / 2;
const toCenterX = toBox.x + BOX_WIDTH / 2;
const toCenterY = toBox.y + BOX_HEIGHT / 2;

const angle = Math.atan2(toCenterY — fromCenterY, toCenterX — fromCenterX);
const fromX = fromCenterX + Math.cos(angle) * BOX_WIDTH / 2;
const fromY = fromCenterY + Math.sin(angle) * BOX_HEIGHT / 2;
const toX = toCenterX — Math.cos(angle) * BOX_WIDTH / 2;
const toY = toCenterY — Math.sin(angle) * BOX_HEIGHT / 2;

return { fromX, fromY, toX, toY };
};
const exportToSVG = () => {
if (containerRef.current) {
const svgWidth = containerRef.current.scrollWidth;
const svgHeight = containerRef.current.scrollHeight;

let svgContent = `
<svg xmlns=”http://www.w3.org/2000/svg" width=”${svgWidth}” height=”${svgHeight}”>
<style>
.box { fill: #3b82f6; stroke: #1d4ed8; stroke-width: 2; }
.box-text { fill: white; font-family: Arial, sans-serif; font-size: 14px; }
.link { stroke: black; stroke-width: 2; }
.title { font-family: Arial, sans-serif; font-size: 24px; font-weight: bold; }
</style>
<text x=”${titlePosition.x + TITLE_WIDTH/2}” y=”${titlePosition.y + TITLE_HEIGHT/2}” text-anchor=”middle” class=”title” dominant-baseline=”middle”>${title}</text>
`;

boxes.forEach(box => {
svgContent += `
<rect x=”${box.x}” y=”${box.y}” width=”${BOX_WIDTH}” height=”${BOX_HEIGHT}” class=”box” />
<text x=”${box.x + BOX_WIDTH/2}” y=”${box.y + BOX_HEIGHT/2}” text-anchor=”middle” dominant-baseline=”middle” class=”box-text”>
${box.label}
</text>
`;
});

links.forEach(link => {
const fromBox = boxes.find(box => box.id === link.from);
const toBox = boxes.find(box => box.id === link.to);
if (fromBox && toBox) {
const { fromX, fromY, toX, toY } = calculateLinkCoordinates(fromBox, toBox);
svgContent += `
<line x1=”${fromX}” y1=”${fromY}” x2=”${toX}” y2=”${toY}” class=”link” />
`;
}
});

svgContent += ‘</svg>’;

const svgBlob = new Blob([svgContent], { type: “image/svg+xml;charset=utf-8” });
const svgUrl = URL.createObjectURL(svgBlob);
const downloadLink = document.createElement(“a”);
downloadLink.href = svgUrl;
downloadLink.download = “family_tree.svg”;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}
};

const saveState = () => {
const state: AppState = {
boxes,
links,
titlePosition,
title
};
const stateString = JSON.stringify(state);
console.log(“Saving state:”, state); // Log the state being saved
const blob = new Blob([stateString], {type: “application/json”});
const url = URL.createObjectURL(blob);
const link = document.createElement(‘a’);
link.download = ‘family_tree_state.json’;
link.href = url;
link.click();
};

const loadState = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result as string;
try {
const state = JSON.parse(content);
console.log(“Loaded state:”, state); // Log the loaded state
if (isValidAppState(state)) {
setBoxes(state.boxes);
setLinks(state.links);
setTitlePosition(state.titlePosition);
setTitle(state.title);
setLoadKey(prevKey => prevKey + 1); // Force re-render
} else {
throw new Error(“Invalid state format”);
}
} catch (error) {
console.error(“Error parsing file:”, error);
alert(“Invalid file format”);
}
};
reader.readAsText(file);
}
};

useEffect(() => {
const container = containerRef.current;
if (container) {
container.addEventListener(‘mousemove’, handleMouseMove as any);
container.addEventListener(‘mouseup’, handleMouseUp);
return () => {
container.removeEventListener(‘mousemove’, handleMouseMove as any);
container.removeEventListener(‘mouseup’, handleMouseUp);
};
}
}, [dragging, draggingTitle, offsetX, offsetY]);

useEffect(() => {
// Any additional setup or calculations needed after loading state
console.log(“State updated:”, { boxes, links, titlePosition, title });
}, [boxes, links, titlePosition, title]);

return (
<div key={loadKey} className=”h-screen w-screen relative p-4" ref={containerRef}>
<div
className=”absolute bg-white p-2 rounded cursor-move”
style={{
left: titlePosition.x,
top: titlePosition.y,
width: `${TITLE_WIDTH}px`,
height: `${TITLE_HEIGHT}px`,
}}
onMouseDown={(e) => handleMouseDown(‘title’, e)}
>
{editingTitle ? (
<input
type=”text”
value={title}
onChange={(e) => setTitle(e.target.value)}
onBlur={() => setEditingTitle(false)}
onKeyPress={(e) => e.key === ‘Enter’ && setEditingTitle(false)}
className=”text-2xl font-bold w-full text-center”
autoFocus
/>
) : (
<h1
className=”text-2xl font-bold cursor-pointer text-center”
onClick={() => setEditingTitle(true)}
>
{title}
</h1>
)}
</div>

{boxes.map(box => (
<div
key={box.id}
className=”absolute bg-blue-500 text-white p-2 rounded overflow-hidden”
style={{
left: box.x,
top: box.y,
width: `${BOX_WIDTH}px`,
height: `${BOX_HEIGHT}px`,
display: ‘flex’,
alignItems: ‘center’,
justifyContent: ‘center’,
cursor: isHovering ? ‘cell’ : ‘move’
}}
onMouseDown={(e) => handleMouseDown(box.id, e)}
onDoubleClick={() => startEditing(box.id, box.label)}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
{editingLabel === box.id ? (
<textarea
value={editingText}
onChange={(e) => setEditingText(e.target.value)}
onBlur={finishEditing}
onKeyDown={handleKeyDown}
autoFocus
className=”w-full h-full bg-white text-black resize-none p-1"
/>
) : (
<div className=”w-full h-full overflow-auto whitespace-pre-wrap”>{box.label}</div>
)}
{isHovering && (
<button
className=”absolute top-0 right-0 bg-red-500 text-white p-1 rounded-bl”
onClick={() => deleteBox(box.id)}
>
X
</button>
)}
</div>
))}
{links.map(link => {
const fromBox = boxes.find(box => box.id === link.from);
const toBox = boxes.find(box => box.id === link.to);
if (fromBox && toBox) {
const { fromX, fromY, toX, toY } = calculateLinkCoordinates(fromBox, toBox);
return (
<svg key={`${link.from}-${link.to}`} className=”absolute top-0 left-0 w-full h-full pointer-events-none”>
<line
x1={fromX}
y1={fromY}
x2={toX}
y2={toY}
stroke=”black”
strokeWidth=”2"
/>
</svg>
);
}
return null;
})}

<div className=”absolute top-0 left-0 right-0 bg-gray-100 p-2" style={{ height: `${CONTROL_HEIGHT}px` }}>
<div className=”flex justify-between items-center”>
<div>
<button className=”bg-green-500 text-white p-2 rounded mr-2" onClick={addBox}>
Add Box
</button>
<button className=”bg-purple-500 text-white p-2 rounded mr-2" onClick={exportToSVG}>
Export to SVG
</button>
<button className=”bg-blue-500 text-white p-2 rounded mr-2" onClick={saveState}>
Save State
</button>
<input
type=”file”
ref={fileInputRef}
style={{ display: ‘none’ }}
onChange={loadState}
accept=”.json”
/>
<button
className=”bg-yellow-500 text-white p-2 rounded mr-2"
onClick={() => fileInputRef.current?.click()}
>
Load State
</button>
</div>
<div className=”flex items-center”>
<select
className=”bg-white p-2 rounded mr-2"
value={linkFrom}
onChange={(e) => setLinkFrom(Number(e.target.value))}
>
<option value=””>Link from</option>
{boxes.map(box => (
<option key={box.id} value={box.id}>
{box.label.split(‘\n’)[0]}
</option>
))}
</select>
<select
className=”bg-white p-2 rounded mr-2"
value={linkTo}
onChange={(e) => setLinkTo(Number(e.target.value))}
>
<option value=””>Link to</option>
{boxes.map(box => (
<option key={box.id} value={box.id}>
{box.label.split(‘\n’)[0]}
</option>
))}
</select>
<button
className=”bg-blue-500 text-white p-2 rounded”
onClick={addLink}
disabled={linkFrom === ‘’ || linkTo === ‘’ || linkFrom === linkTo}
>
Create Link
</button>
</div>
</div>
</div>
</div>
);
};

export default App;

--

--

David Morgan
David Morgan

Written by David Morgan

Was developing apps for social good e.g. Zung Test, Accident Book. BA Hons and student of criminology. Writing about true crime. Next cancer patient.

No responses yet