Skip to content

Commit

Permalink
Merge pull request #1028 from liberu-genealogy/sweep/Implement-Intera…
Browse files Browse the repository at this point in the history
…ctive-Family-Tree-Builder-with-Drag-and-Drop-Functionality

Implement Interactive Family Tree Builder with Drag-and-Drop Functionality
  • Loading branch information
curtisdelicata authored Dec 25, 2024
2 parents 2e1cc1c + 5b2a1ed commit 7a17d02
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 0 deletions.
93 changes: 93 additions & 0 deletions app/Http/Livewire/FamilyTreeBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@


<?php

namespace App\Http\Livewire;

use App\Models\Person;
use App\Models\Family;
use Livewire\Component;

class FamilyTreeBuilder extends Component
{
public $treeData = [];
public $selectedPerson = null;

protected $listeners = [
'personMoved' => 'updatePersonPosition',
'personAdded' => 'addPerson',
'personRemoved' => 'removePerson'
];

public function mount()
{
$this->loadTreeData();
}

public function loadTreeData()
{
$people = Person::with(['childInFamily', 'familiesAsHusband', 'familiesAsWife'])
->get()
->map(function ($person) {
return [
'id' => $person->id,
'name' => $person->fullname(),
'position' => [
'x' => $person->tree_position_x ?? 0,
'y' => $person->tree_position_y ?? 0,
],
'relationships' => [
'parent_family' => $person->child_in_family_id,
'spouse_families' => array_merge(
$person->familiesAsHusband->pluck('id')->toArray(),
$person->familiesAsWife->pluck('id')->toArray()
)
]
];
});

$this->treeData = $people->toArray();
}

public function updatePersonPosition($personId, $x, $y)
{
$person = Person::find($personId);
if ($person) {
$person->update([
'tree_position_x' => $x,
'tree_position_y' => $y
]);
$this->emit('positionUpdated', $personId);
}
}

public function addPerson($data)
{
$person = Person::create([
'name' => $data['name'],
'givn' => $data['givn'] ?? '',
'surn' => $data['surn'] ?? '',
'sex' => $data['sex'] ?? 'U',
'tree_position_x' => $data['position']['x'],
'tree_position_y' => $data['position']['y']
]);

$this->loadTreeData();
$this->emit('personCreated', $person->id);
}

public function removePerson($personId)
{
$person = Person::find($personId);
if ($person) {
$person->delete();
$this->loadTreeData();
$this->emit('personDeleted', $personId);
}
}

public function render()
{
return view('livewire.family-tree-builder');
}
}
2 changes: 2 additions & 0 deletions app/Models/Person.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class Person extends Model
'resn',
'rfn',
'afn',
'tree_position_x',
'tree_position_y',
];
// public function searchableAs()
// {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddTreePositionToPeopleTable extends Migration
{
public function up()
{
Schema::table('people', function (Blueprint $table) {
$table->float('tree_position_x')->nullable();
$table->float('tree_position_y')->nullable();
});
}

public function down()
{
Schema::table('people', function (Blueprint $table) {
$table->dropColumn(['tree_position_x', 'tree_position_y']);
});
}
}
99 changes: 99 additions & 0 deletions resources/views/livewire/family-tree-builder.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@


<div class="family-tree-builder">
<div class="toolbar">
<button wire:click="$emit('addNewPerson')" class="btn btn-primary">
Add Person
</button>
</div>

<div id="tree-container" class="tree-container">
@foreach($treeData as $person)
<div class="person-node"
data-id="{{ $person['id'] }}"
style="left: {{ $person['position']['x'] }}px; top: {{ $person['position']['y'] }}px;">
<div class="person-content">
<h4>{{ $person['name'] }}</h4>
<div class="person-actions">
<button wire:click="$emit('editPerson', {{ $person['id'] }})"
class="btn btn-sm btn-secondary">
Edit
</button>
<button wire:click="removePerson({{ $person['id'] }})"
class="btn btn-sm btn-danger">
Remove
</button>
</div>
</div>
</div>
@endforeach
</div>
</div>

@push('styles')
<style>
.family-tree-builder {
position: relative;
width: 100%;
height: 800px;
overflow: auto;
}
.tree-container {
position: relative;
width: 3000px;
height: 2000px;
}
.person-node {
position: absolute;
width: 200px;
padding: 10px;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
cursor: move;
}
</style>
@endpush

@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/interactjs/dist/interact.min.js"></script>
<script>
document.addEventListener('livewire:load', function () {
interact('.person-node').draggable({
inertia: true,
modifiers: [
interact.modifiers.restrictRect({
restriction: 'parent',
endOnly: true
})
],
autoScroll: true,
listeners: {
move: dragMoveListener,
end: dragEndListener
}
});
function dragMoveListener(event) {
const target = event.target;
const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
target.style.transform = `translate(${x}px, ${y}px)`;
target.setAttribute('data-x', x);
target.setAttribute('data-y', y);
}
function dragEndListener(event) {
const target = event.target;
const personId = target.getAttribute('data-id');
const x = parseFloat(target.getAttribute('data-x')) || 0;
const y = parseFloat(target.getAttribute('data-y')) || 0;
@this.call('updatePersonPosition', personId, x, y);
}
});
</script>
@endpush

0 comments on commit 7a17d02

Please sign in to comment.