diff --git a/.gitignore b/.gitignore index 0572417..ff049cf 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +# Local notes +*.local.md + # Runtime data pids *.pid diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b943dbc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} \ No newline at end of file diff --git a/README.md b/README.md index 2816407..042658d 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,22 @@ This repository contains the codebase for the KinTree project. This project stems from the CSE 3213/3223 Software Engineering Senior Project I/II course sequence at Mississippi State University. -KinTree is an application that allows users to connect with family members and build virtual, visualized, and dynamic family trees collaboratively. This project began in the Fall of 2024 and is ongoing. KinTree utilizes React, Node.js, and MySQL. +KinTree is an application that allows users to connect with family members and build virtual, visualized, and dynamic family trees collaboratively. KinTree utilizes React, Node.js, and MySQL. This project originated in [Fall 2024](https://github.com/OwenAdams2023/SeniorProject_KinTree#). The project is ongoing through this forked repository in Fall 2025. -The current KinTree project team as of Spring 2025 includes Owen Adams, Kennedi James, Destiny Milsap, and Jade Thompson. The primary stakeholder for this project is Dr. Charan Gudla. +The current KinTree project team as of Fall 2025 includes Andrea Ambrose, Matthew Loyed, Xiang Chen, and Charles Lenson. The primary stakeholder for this project is Dr. Charan Gudla. # Install ### Prerequisites -Node.js (install the correct version for your own OS [here](https://nodejs.org/en)) -MySQL Install (https://dev.mysql.com/downloads/mysql/) +Node.js (install the correct version for your own OS [here](https://nodejs.org/en)) and get your Supabase URL, CLIENT KEY, and SERVICE KEY from your project dashboard [here](https://supabase.com/) -### Setup +### Database Setup + +In the /server/ folder, add an .env file with variables `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY`. +In the /client/ folder, add an .env file with variables `REACT_APP_SUPABASE_URL` and `REACT_APP_SUPABASE_ANON_KEY`. + +### Web Application Setup To set up the KinTree codebase on your own machine, start by cloning the repository to your local file system. @@ -21,13 +25,13 @@ From the command line, navigate to the /SeniorProject_KinTree/client directory, `npm install` -Once all dependencies are installed, you can run the frontend of the project (from the /SeniorProject_KinTree/client directory) by running the following command: +Once all dependencies are installed, you can run the frontend of the project from the /SeniorProject_KinTree/client directory by running the following command: `npm start` -This command runs the application in development mode. You can open http://localhost:3000 in your browser to view the application. Any changes made to the source code will cause the application page to refresh and show reflected changes (once the changed file is saved). +This command runs the application in development mode. You can open http://localhost:3000 in your browser to view the application and saved changes. -To run the backend of code (server/API), you must first install the backend node dependencies. Open another command line window and run the following command in the backend directory (/SeniorProject_KinTree/server): +To run the backend, you must first install the backend node dependencies. Open another command line window and run the following command in the backend directory (/SeniorProject_KinTree/server): `npm install` @@ -35,16 +39,8 @@ Then, from the same directory, run the following command to run the server/API: `node server.js` -### Database Setup - -Run the command `npm install knex mysql2` - -Create a .env file with MySQL information. Add username, password, and database name. - -Run the command `node mysql-connection.js` to verify the connection. - -Run the command `knex:migrate status` to ensure proper migration files are loaded. - -Run the command `knex migrate:latest` to create existing database tables. +### Emails setup +Follow the steps on [sending emails with Resend](https://github.com/resend/resend-examples/tree/main/express-resend-examples) to verify your domain and obtain your RESEND_API_KEY to store in `.env`. +Set EMAIL_FROM in `.env`. \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 782fd83..2d46024 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,12 +9,13 @@ "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^4.1.3", + "@supabase/supabase-js": "^2.75.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.7.7", "d3": "^7.9.0", - "family-chart": "^0.2.1", + "family-chart": "^0.9.0", "knex": "^3.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -3700,6 +3701,123 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.75.0.tgz", + "integrity": "sha512-J8TkeqCOMCV4KwGKVoxmEBuDdHRwoInML2vJilthOo7awVCro2SM+tOcpljORwuBQ1vHUtV62Leit+5wlxrNtw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.75.0.tgz", + "integrity": "sha512-18yk07Moj/xtQ28zkqswxDavXC3vbOwt1hDuYM3/7xPnwwpKnsmPyZ7bQ5th4uqiJzQ135t74La9tuaxBR6e7w==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/@supabase/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@supabase/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.75.0.tgz", + "integrity": "sha512-YfBz4W/z7eYCFyuvHhfjOTTzRrQIvsMG2bVwJAKEVVUqGdzqfvyidXssLBG0Fqlql1zJFgtsPpK1n4meHrI7tg==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.75.0.tgz", + "integrity": "sha512-B4Xxsf2NHd5cEnM6MGswOSPSsZKljkYXpvzKKmNxoUmNQOfB7D8HOa6NwHcUBSlxcjV+vIrYKcYXtavGJqeGrw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/realtime-js/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.75.0.tgz", + "integrity": "sha512-wpJMYdfFDckDiHQaTpK+Ib14N/O2o0AAWWhguKvmmMurB6Unx17GGmYp5rrrqCTf8S1qq4IfIxTXxS4hzrUySg==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.75.0.tgz", + "integrity": "sha512-8UN/vATSgS2JFuJlMVr51L3eUDz+j1m7Ww63wlvHLKULzCDaVWYzvacCjBTLW/lX/vedI2LBI4Vg+01G9ufsJQ==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.75.0", + "@supabase/functions-js": "2.75.0", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "2.75.0", + "@supabase/realtime-js": "2.75.0", + "@supabase/storage-js": "2.75.0" + } + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -4765,6 +4883,12 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/prettier": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", @@ -4896,9 +5020,9 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", - "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -9434,352 +9558,14 @@ "license": "MIT" }, "node_modules/family-chart": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/family-chart/-/family-chart-0.2.1.tgz", - "integrity": "sha512-lB8dOg8Mll/PVfSR7wTG+mP/nk6fxbY+3paEGsr85xRKGJMIsC3Va9/D2J5QOVSk67nikur/MiTFnGeLS7kT2Q==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/family-chart/-/family-chart-0.9.0.tgz", + "integrity": "sha512-+JdLr1Oo+YFnQWUXgdnk4nCMTbe1MXKdpbx3KEBXPeq2oX+2v5ccmrcK39CZ761/zQfgSHFZ2cT/+gbaeeACcA==", "license": "ISC", "dependencies": { - "d3": "6" + "d3": "^7.9.0" } }, - "node_modules/family-chart/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/family-chart/node_modules/d3": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-6.7.0.tgz", - "integrity": "sha512-hNHRhe+yCDLUG6Q2LwvR/WdNFPOJQ5VWqsJcwIYVeI401+d2/rrCjxSXkiAdIlpx7/73eApFB4Olsmh3YN7a6g==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "2", - "d3-axis": "2", - "d3-brush": "2", - "d3-chord": "2", - "d3-color": "2", - "d3-contour": "2", - "d3-delaunay": "5", - "d3-dispatch": "2", - "d3-drag": "2", - "d3-dsv": "2", - "d3-ease": "2", - "d3-fetch": "2", - "d3-force": "2", - "d3-format": "2", - "d3-geo": "2", - "d3-hierarchy": "2", - "d3-interpolate": "2", - "d3-path": "2", - "d3-polygon": "2", - "d3-quadtree": "2", - "d3-random": "2", - "d3-scale": "3", - "d3-scale-chromatic": "2", - "d3-selection": "2", - "d3-shape": "2", - "d3-time": "2", - "d3-time-format": "3", - "d3-timer": "2", - "d3-transition": "2", - "d3-zoom": "2" - } - }, - "node_modules/family-chart/node_modules/d3-array": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", - "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", - "license": "BSD-3-Clause", - "dependencies": { - "internmap": "^1.0.0" - } - }, - "node_modules/family-chart/node_modules/d3-axis": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-2.1.0.tgz", - "integrity": "sha512-z/G2TQMyuf0X3qP+Mh+2PimoJD41VOCjViJzT0BHeL/+JQAofkiWZbWxlwFGb1N8EN+Cl/CW+MUKbVzr1689Cw==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-brush": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-2.1.0.tgz", - "integrity": "sha512-cHLLAFatBATyIKqZOkk/mDHUbzne2B3ZwxkzMHvFTCZCmLaXDpZRihQSn8UNXTkGD/3lb/W2sQz0etAftmHMJQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-dispatch": "1 - 2", - "d3-drag": "2", - "d3-interpolate": "1 - 2", - "d3-selection": "2", - "d3-transition": "2" - } - }, - "node_modules/family-chart/node_modules/d3-chord": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-2.0.0.tgz", - "integrity": "sha512-D5PZb7EDsRNdGU4SsjQyKhja8Zgu+SHZfUSO5Ls8Wsn+jsAKUUGkcshLxMg9HDFxG3KqavGWaWkJ8EpU8ojuig==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-path": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", - "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-contour": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-2.0.0.tgz", - "integrity": "sha512-9unAtvIaNk06UwqBmvsdHX7CZ+NPDZnn8TtNH1myW93pWJkhsV25JcgnYAu0Ck5Veb1DHiCv++Ic5uvJ+h50JA==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "2" - } - }, - "node_modules/family-chart/node_modules/d3-delaunay": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.3.0.tgz", - "integrity": "sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==", - "license": "ISC", - "dependencies": { - "delaunator": "4" - } - }, - "node_modules/family-chart/node_modules/d3-dispatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz", - "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-drag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-2.0.0.tgz", - "integrity": "sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-dispatch": "1 - 2", - "d3-selection": "2" - } - }, - "node_modules/family-chart/node_modules/d3-dsv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-2.0.0.tgz", - "integrity": "sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==", - "license": "BSD-3-Clause", - "dependencies": { - "commander": "2", - "iconv-lite": "0.4", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json", - "csv2tsv": "bin/dsv2dsv", - "dsv2dsv": "bin/dsv2dsv", - "dsv2json": "bin/dsv2json", - "json2csv": "bin/json2dsv", - "json2dsv": "bin/json2dsv", - "json2tsv": "bin/json2dsv", - "tsv2csv": "bin/dsv2dsv", - "tsv2json": "bin/dsv2json" - } - }, - "node_modules/family-chart/node_modules/d3-ease": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-2.0.0.tgz", - "integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-fetch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-2.0.0.tgz", - "integrity": "sha512-TkYv/hjXgCryBeNKiclrwqZH7Nb+GaOwo3Neg24ZVWA3MKB+Rd+BY84Nh6tmNEMcjUik1CSUWjXYndmeO6F7sw==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-dsv": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-force": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz", - "integrity": "sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-dispatch": "1 - 2", - "d3-quadtree": "1 - 2", - "d3-timer": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-format": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz", - "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-geo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.2.tgz", - "integrity": "sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^2.5.0" - } - }, - "node_modules/family-chart/node_modules/d3-hierarchy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz", - "integrity": "sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-interpolate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", - "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-color": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz", - "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-polygon": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-2.0.0.tgz", - "integrity": "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-quadtree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-2.0.0.tgz", - "integrity": "sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-random": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-2.2.2.tgz", - "integrity": "sha512-0D9P8TRj6qDAtHhRQn6EfdOtHMfsUWanl3yb/84C4DqpZ+VsgfI5iTVRNRbELCfNvRfpMr8OrqqUTQ6ANGCijw==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-scale": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz", - "integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^2.3.0", - "d3-format": "1 - 2", - "d3-interpolate": "1.2.0 - 2", - "d3-time": "^2.1.1", - "d3-time-format": "2 - 3" - } - }, - "node_modules/family-chart/node_modules/d3-scale-chromatic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz", - "integrity": "sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-color": "1 - 2", - "d3-interpolate": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-selection": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz", - "integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-shape": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz", - "integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-path": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-time": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", - "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "2" - } - }, - "node_modules/family-chart/node_modules/d3-time-format": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", - "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-time": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-timer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz", - "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-transition": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-2.0.0.tgz", - "integrity": "sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-color": "1 - 2", - "d3-dispatch": "1 - 2", - "d3-ease": "1 - 2", - "d3-interpolate": "1 - 2", - "d3-timer": "1 - 2" - }, - "peerDependencies": { - "d3-selection": "2" - } - }, - "node_modules/family-chart/node_modules/d3-zoom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz", - "integrity": "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-dispatch": "1 - 2", - "d3-drag": "2", - "d3-interpolate": "1 - 2", - "d3-selection": "2", - "d3-transition": "2" - } - }, - "node_modules/family-chart/node_modules/delaunator": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz", - "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==", - "license": "ISC" - }, - "node_modules/family-chart/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/family-chart/node_modules/internmap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", - "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", - "license": "ISC" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/client/package.json b/client/package.json index 70c255b..f0b2061 100644 --- a/client/package.json +++ b/client/package.json @@ -4,12 +4,13 @@ "private": true, "dependencies": { "@hookform/resolvers": "^4.1.3", + "@supabase/supabase-js": "^2.75.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.7.7", "d3": "^7.9.0", - "family-chart": "^0.2.1", + "family-chart": "^0.9.0", "knex": "^3.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/client/src/App.css b/client/src/App.css index aea3993..2dfcff0 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -14,15 +14,16 @@ } .App-header { - background-color: #E7F2D8; + background-color: var(--bg-color); min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); - color: white; + color: var(--text-color); gap: 40px; + transition: background-color 0.3s ease, color 0.3s ease; } .App-link { @@ -42,11 +43,12 @@ display: flex; align-items: center; justify-content: center; - background-color: white; - color: black; + background-color: var(--card-bg); + color: var(--text-color); border-radius: 50px; padding: 20px 40px; margin: 0px 20px; + transition: background-color 0.3s ease, color 0.3s ease; } .font-face-alata { diff --git a/client/src/CurrentUserProvider.js b/client/src/CurrentUserProvider.js index 45a788e..c49310e 100644 --- a/client/src/CurrentUserProvider.js +++ b/client/src/CurrentUserProvider.js @@ -1,20 +1,23 @@ import { React, useState, createContext, useContext, useEffect } from "react" -import { set } from "react-hook-form"; +import { supabase } from "./utils/supabaseClient"; +import { buildSyncPayload } from "./utils/metadataHelpers"; +import { SERVER_URL } from "./config/urls"; export const currentContext = createContext(); export const CurrentUserProvider = ({ children }) => { const [currentUserID, setCurrentUserIDState] = useState(''); - const [currentAccountID, setCurrentAccountIDState] = useState(''); // TODO login will set this + const [currentAccountID, setCurrentAccountIDState] = useState(''); const [currentUserName, setCurrentUserNameState] = useState(''); const [loading, setLoading] = useState(true); + const [supabaseUser, setSupabaseUser] = useState(null); - const setCurrentAccountID = (accountID) => { // logging in will trigger this + const setCurrentAccountID = (accountID) => { localStorage.setItem("currentAccountID", accountID); setCurrentAccountIDState(accountID); } - const setCurrentUserID = (userID) => { // this then needs to be called to get the family member ID + const setCurrentUserID = (userID) => { localStorage.setItem("currentUserID", userID); setCurrentUserIDState(userID); } @@ -32,18 +35,16 @@ export const CurrentUserProvider = ({ children }) => { } const fetchCurrentUserID = async () => { - // temp - // setCurrentUserIDState('23'); - fetch(`http://localhost:5000/api/family-members/active/${currentAccountID}`) + if (!currentAccountID) return; // Prevent fetching if we don't have the auth UUID yet + fetch(`${SERVER_URL}/api/family-members/active/${currentAccountID}`) .then(async(response) => { if (response.ok) { const data = await response.json(); - // console.log('Current user ID:', data); setCurrentUserID(data.id); - setCurrentUserName(data.firstName + " " + data.lastName); + setCurrentUserName(data.firstname + " " + data.lastname); } else { - console.error('Error:', response); + console.error('Error fetching tree member:', response); } }) .catch(error => { @@ -51,30 +52,99 @@ export const CurrentUserProvider = ({ children }) => { }); } - - // init + // Automatically fetch the tree member ID whenever the Supabase Account ID is set useEffect(() => { - const initializeState = () => { - const storedAccountID = localStorage.getItem("currentAccountID"); - const storedUserID = localStorage.getItem("currentUserID"); - const storedUserName = localStorage.getItem("currentUserName"); + if (currentAccountID) { + fetchCurrentUserID(); + } + }, [currentAccountID]); - if (storedAccountID) { - setCurrentAccountIDState(storedAccountID); - } - if (storedUserID) { - setCurrentUserIDState(storedUserID); - } - if (storedUserName) { - setCurrentUserNameState(storedUserName); + + // Initialize Supabase auth state + useEffect(() => { + const initializeAuth = async () => { + try { + // Get initial session + const { data: { session } } = await supabase.auth.getSession(); + + if (session?.user) { + setSupabaseUser(session.user); + setCurrentAccountIDState(session.user.id); + setCurrentUserNameState(session.user.email); // Use email as default username + } + + setLoading(false); + } catch (error) { + console.error('Error initializing auth:', error); + setLoading(false); } - setLoading(false); }; - initializeState(); + + initializeAuth(); + + // Listen for auth changes + const { data: { subscription } } = supabase.auth.onAuthStateChange( + async (event, session) => { + if (session?.user) { + setSupabaseUser(session.user); + let response = await fetch(`${SERVER_URL}/api/auth/user/${session.user.id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + response = await response.json(); + setCurrentAccountIDState(response.id); + setCurrentUserNameState(session.user.email); + + if (event === 'SIGNED_IN') { + try { + const m = session.user.user_metadata || {}; + const hasExtendedMetadata = m.address || m.city || m.state || m.country || m.zipcode || + m.firstName || m.first_name || m.lastName || m.last_name || + m.phoneNumber || m.phone_number; + + if (hasExtendedMetadata) { + const syncPayload = buildSyncPayload(session.user.id, session.user.email, m); + const syncResponse = await fetch(`${SERVER_URL}/api/auth/sync`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(syncPayload) + }); + + if (!syncResponse.ok) { + console.warn('Auth sync failed on login'); + } + } + } catch (e) { + console.warn('Auth sync error on login:', e?.message || e); + } + } + } else { + setSupabaseUser(null); + setCurrentAccountIDState(''); + setCurrentUserIDState(''); + setCurrentUserNameState(''); + } + setLoading(false); + } + ); + + return () => subscription.unsubscribe(); }, []); return ( - + {children} ) @@ -82,7 +152,6 @@ export const CurrentUserProvider = ({ children }) => { export const useCurrentUser = () => { const context = useContext(currentContext); - // console.log('CurrentUserContext:', context); if (!context) { throw new Error("useCurrentUser must be used within a CurrentUserProvider"); } diff --git a/client/src/ThemeProvider.js b/client/src/ThemeProvider.js new file mode 100644 index 0000000..c21bec6 --- /dev/null +++ b/client/src/ThemeProvider.js @@ -0,0 +1,55 @@ +import { React, useState, createContext, useContext, useEffect } from "react"; + +export const themeContext = createContext(); + +export const ThemeProvider = ({ children }) => { + const [darkMode, setDarkModeState] = useState(() => { + // Initialize from localStorage or default to false + const saved = localStorage.getItem("darkMode"); + return saved ? JSON.parse(saved) : false; + }); + + // Apply dark mode class on mount and whenever darkMode changes + useEffect(() => { + // Apply dark mode class to document body + if (darkMode) { + document.documentElement.classList.add("dark-mode"); + document.body.classList.add("dark-mode"); + } else { + document.documentElement.classList.remove("dark-mode"); + document.body.classList.remove("dark-mode"); + } + }, [darkMode]); + + // Save to localStorage whenever darkMode changes + useEffect(() => { + localStorage.setItem("darkMode", JSON.stringify(darkMode)); + }, [darkMode]); + + const toggleDarkMode = () => { + setDarkModeState(prev => !prev); + }; + + const setDarkMode = (value) => { + setDarkModeState(value); + }; + + return ( + + {children} + + ); +}; + +export const useTheme = () => { + const context = useContext(themeContext); + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +}; + diff --git a/client/src/components/AddFamilyMember/AddFamilyMember.js b/client/src/components/AddFamilyMember/AddFamilyMember.js index a81ee3d..91e05a6 100644 --- a/client/src/components/AddFamilyMember/AddFamilyMember.js +++ b/client/src/components/AddFamilyMember/AddFamilyMember.js @@ -2,12 +2,16 @@ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { Link } from 'react-router-dom'; import Popup from 'reactjs-popup'; import 'reactjs-popup/dist/index.css'; -import { useForm } from 'react-hook-form'; +import { set, useForm } from 'react-hook-form'; import * as styles from './styles'; import './popup.css'; import { ReactComponent as CloseIcon } from '../../assets/exit.svg'; import { ReactComponent as ImportIcon } from '../../assets/import.svg'; import { useCurrentUser } from '../../CurrentUserProvider'; +import { familyTreeService } from '../../services/familyTreeService'; +import { SERVER_URL } from '../../config/urls'; +import { relationshipService } from '../../services/relationshipService'; +import { addRelationship } from '../../utils/relationUtil'; // TODO: make form clear when dismissed by clicking outside of modal // make sync contact button functional @@ -16,6 +20,10 @@ function AddFamilyMemberPopup({ trigger, userid }) { const [manual, setManual] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [searchResults, setSearchResults] = useState([]); + const [errorMessage, setErrorMessage] = useState(""); + const [result, setResult] = useState(null); + const [treeInfo, setTreeInfo] = useState(null); // Full tree index for smart checks + const [userTreeId, setUserTreeId] = useState(null); // The tree-node ID for "You" const { currentUserID, currentAccountID } = useCurrentUser(); var family = useRef([]); @@ -27,83 +35,162 @@ function AddFamilyMemberPopup({ trigger, userid }) { reset, watch, handleSubmit, - } = useForm({defaultValues: {selectedMember: '', selectedMemberRelationship: '', matPat: ''}}); + setValue, + } = useForm({ defaultValues: { selectedMember: '', selectedMemberRelationship: '', matPat: '' } }); - // stored list of family members that require maternal/paternal distinction (maybe shift this to retrieval from backend, so that it can be updated without changing code) - let matPat = useMemo(() => ["parent", "cousin", "aunt", "uncle", "grandparent", "niece", "nephew"], []); + // second form -- manually add family member + const { + register: register2, + watch: watch2, + handleSubmit: handleSubmit2, + setValue: setValue2, + } = useForm({ defaultValues: { firstName: '', lastName: '', relationship: '', matPat2: '', location: '', birthday: '', birthplace: '', deathdate: '', gender: '' } }); + + // stored list of family members that require maternal/paternal distinction + let matPat = useMemo(() => ["parent", "mother", "father", "cousin", "aunt", "uncle", "grandparent", "grandmother", "grandfather", "niece", "nephew"], []); // manage mat/pat selector using currently selected relationship type const selectedMemberRelationship = watch("selectedMemberRelationship"); + + // Reusable logic for both forms + const getSideChoiceType = (rel) => { + const fixed = { "mother": "maternal", "father": "paternal" }; + const required = ["grandparent", "grandmother", "grandfather", "aunt", "uncle", "cousin", "niece", "nephew", "parent"]; + if (fixed[rel]) return { status: 'fixed', value: fixed[rel] }; + if (required.includes(rel)) return { status: 'required' }; + return { status: 'none' }; + }; + + useEffect(() => { + const choice = getSideChoiceType(selectedMemberRelationship); + if (choice.status === 'fixed') { + setValue("matPat", choice.value); + } + }, [selectedMemberRelationship, setValue]); + var enableMatPat = useRef(false); useEffect(() => { - if(matPat.includes(selectedMemberRelationship)) { - enableMatPat.current = true; + const choice = getSideChoiceType(selectedMemberRelationship); + enableMatPat.current = (choice.status === 'required'); + }, [selectedMemberRelationship]); + + const selectedManualRelationship = watch2("relationship"); + const selectedManualMatPat = watch2("matPat2"); + + useEffect(() => { + // Auto-set gender and side based on relationship selection + const genderMap = { + "mother": "F", "father": "M", + "sister": "F", "brother": "M", + "aunt": "F", "uncle": "M", + "niece": "F", "nephew": "M", + "grandmother": "F", "grandfather": "M", + "daughter": "F", "son": "M" + }; + if (genderMap[selectedManualRelationship]) { + setValue2("gender", genderMap[selectedManualRelationship]); } - else { - enableMatPat.current = false; + + // Auto-set side for unambiguous relationships + if (selectedManualRelationship === "mother") { + setValue2("matPat2", "maternal"); + } else if (selectedManualRelationship === "father") { + setValue2("matPat2", "paternal"); } - }, [selectedMemberRelationship, matPat]); + }, [selectedManualRelationship, setValue2]); + + // Determine if the "Side" selector should be shown + const isSideChoiceNeeded = (rel) => { + const multiSideRel = ["grandparent", "grandmother", "grandfather", "aunt", "uncle", "cousin"]; + // Mother and Father have fixed sides, and Siblings/Nieces/Nephews are relative to your branch + if (rel === "mother" || rel === "father" || rel === "sibling" || rel === "niece" || rel === "nephew") return false; + return multiSideRel.includes(rel); + }; + const [treeMembers, setTreeMembers] = useState([]); - // second form -- manually add family member - const { - register: register2, - handleSubmit: handleSubmit2, - } = useForm({defaultValues: {firstName: '', lastName: '', relationship: '', matPat2: '', location: '', birthday: '', birthplace: '', deathdate: ''}}); + // relationships that require or benefit from a "Connect To" link for proper placement + const extendedRelations = relationshipService.EXTENDED_RELATIONS; + + // Fetch all current tree members for the dropdown + useEffect(() => { + const loadTreeMembers = async () => { + try { + const [members, fullTree, userTreeMember] = await Promise.all([ + familyTreeService.getFamilyMembersByUserId(currentAccountID), + familyTreeService.getFamilyTreeByUserId(currentAccountID), + familyTreeService.getFamilyMemberByUserId(currentAccountID) + ]); + setTreeMembers(members); + setUserTreeId(userTreeMember?.id); + const index = Object.fromEntries(fullTree.map(p => [p.id, p])); + setTreeInfo(index); + } catch (err) { + console.error("Failed to load tree data for smart detection", err); + } + }; + if (currentAccountID) loadTreeMembers(); + }, [currentAccountID]); + + // Smart detection now handled by relationshipService + + const smartTarget = useMemo(() => { + return relationshipService.findSmartTarget(treeInfo, userTreeId, selectedMemberRelationship || "", watch("matPat") || ""); + }, [selectedMemberRelationship, watch("matPat"), treeInfo, userTreeId]); + + useEffect(() => { + if (smartTarget) { + setValue("connectTo", smartTarget); + } + }, [smartTarget, setValue]); + + const smartTargetManual = useMemo(() => { + return relationshipService.findSmartTarget(treeInfo, userTreeId, selectedManualRelationship || "", watch2("matPat2") || ""); + }, [selectedManualRelationship, watch2("matPat2"), treeInfo, userTreeId]); + + useEffect(() => { + if (smartTargetManual) { + setValue2("connectTo2", smartTargetManual); + } + }, [smartTargetManual, setValue2]); // populate search results for existing user search useEffect(() => { + setErrorMessage(""); // no search term, clear results if (searchTerm === "") { setSearchResults([]); return; } - // get non-friends const fetchResults = async () => { - let requestOptionsMembers = { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - } - let requestOptionsUsers = { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, + // get family members of current user + try { + family.current = await familyTreeService.getFamilyMembersByUserId(currentAccountID); + console.log('Family members for user', currentAccountID, ':', family.current); + } catch (error) { + console.error('Error fetching family members for user', currentAccountID, error.message); + setErrorMessage(error.message) + return; } - fetch(`http://localhost:5000/api/family-members/user/${currentAccountID}`, requestOptionsMembers) // gets all family members - .then(async(response) => { - if (response.ok) { - const responseData = await response.json(); - console.log(responseData); - family.current = responseData; - } - else { - // print message in return body - console.error('Error:', response); - } - }) - // fetch all users (this is totally scalable) - .then(fetch(`http://localhost:5000/api/auth/users`, requestOptionsUsers) - .then(async(response) => { - if (response.ok) { - const responseData = await response.json(); - console.log(responseData); - users.current = responseData.filter(user => - user.username.toLowerCase().includes(searchTerm.toLowerCase()) && - !family.current.some(member => member.memberUserId === user.id) - ); - setSearchResults(users.current); - } - else { - // print message in return body - console.error('Error:', response); - } - }) - ) + // populate users list + try { + const responseData = await familyTreeService.getRegisteredUsers(); + console.log('Registered users:', responseData); + users.current = responseData.filter(user => + user.username.toLowerCase().includes(searchTerm.toLowerCase()) && + !family.current.some(member => member.id === user.id)); + setSearchResults(users.current); + } catch (error) { + console.error('Error fetching users:', error.message); + setErrorMessage(error.message); + setSearchResults([]); + return; + } }; - // go fetch! fetchResults(); }, [searchTerm, currentAccountID]); @@ -114,147 +201,292 @@ function AddFamilyMemberPopup({ trigger, userid }) { setManual(false); setSearchTerm(""); setSearchResults([]); + setErrorMessage(""); reset(); }; // form submission (existing user) - const onSubmitExisting = (data) => { + const onSubmitExisting = async (data, close) => { console.log("submit attempted"); - let memberId = data.selectedMember; - let requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - "firstName": users.current.find(user => user.id === Number(memberId)).username.split(" ")[0], - "lastName": users.current.find(user => user.id === Number(memberId)).username.split(" ")[1], - "birthDate": null, - "deathDate" : null, - "location": null, - "phoneNumber": null, - "userId": userid, - "memberUserId": users.current.find(user => user.id === Number(memberId)).id - }) - }; + console.log("Form data:", data); // Log the form data to see what we're getting + setErrorMessage(""); + try { + const selectedUser = users.current.find(user => user.id === Number(data.selectedMember)); + + if (selectedUser.id === currentAccountID) { + setErrorMessage("You cannot add yourself as a family member."); + console.error("Attempted to add self as family member"); + return; + } - let nextRequestOptions = {}; // will populate later + const existingMember = family.current.find(member => member.memberuserid === selectedUser.id); + + if (!selectedUser) { + setErrorMessage("Selected user not found"); + console.error("Selected user not found in users list"); + return; + } - let treeUserId = currentUserID; - let treeMemberId; + if (!selectedUser) { + setErrorMessage("Selected user not found"); + console.error("Selected user not found in users list"); + return; + } - // add user to family members table - fetch(`http://localhost:5000/api/family-members/`, requestOptions) // add new family member - .then(async(response) => { - if (response.ok) { - const responseData = await response.json(); - console.log(responseData); - console.log(responseData.message); - treeMemberId = responseData.member; - nextRequestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - person1_id: treeUserId, - person2_id: treeMemberId, - relationshipType: data.selectedMemberRelationship, - relationshipStatus: "active", - side: data.matPat || null, - userId: userid, - memberUserId: null - }) - }; - return fetch(`http://localhost:5000/api/relationships/`, nextRequestOptions); + // Relationship Hint: Mother/Father selection should override the profile's gender + // to ensure the tree visualization is correct. + const relHint = relationshipService.EXTENDED_RELATIONS[data.selectedMemberRelationship]; + const finalGender = relHint?.gender || selectedUser.gender; + + const selectedUserData = { + firstname: selectedUser.firstname, + lastname: selectedUser.lastname, + birthdate: selectedUser.birthdate || null, + deathdate: selectedUser.deathdate || null, + location: selectedUser.location || null, + phonenumber: selectedUser.phonenumber || null, + userid: currentAccountID, // The user adding the family member + memberuserid: selectedUser.id, // Existing user's ID + gender: finalGender + }; + + // get account treemember id + let treeUser = await familyTreeService.getFamilyMemberByUserId(currentAccountID); + const treeUserId = treeUser.id; + + let treeMemberId; + if (existingMember) { + console.log('Member already exists, using existing ID:', existingMember.id); + treeMemberId = existingMember.id; + } else { + const treeMemberRes = await familyTreeService.createFamilyMember(selectedUserData); + treeMemberId = treeMemberRes.member.id; } - else { - // print message in return body - const errorData = await response.json(); - console.error('Error:', errorData.message); - nextRequestOptions = { + + // Resolve relationships to create across the database + // IMPORTANT: Use treeUserId (the member ID), not currentAccountID (the user ID) + const relsToCreate = relationshipService.getRequiredDBRelationships( + treeUserId, + treeMemberId, + data.selectedMemberRelationship, + data.connectTo, + data.matPat || null + ); + + console.log('Relationships to create (Existing):', relsToCreate); + + for (const rel of relsToCreate) { + const relRes = await fetch(`http://localhost:5000/api/relationships/`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - person1_id: treeUserId, - person2_id: treeMemberId, - relationshipType: data.selectedMemberRelationship, + ...rel, relationshipStatus: "active", - side: data.matPat || null, - userId: userid, + userId: currentAccountID }) - }; - } - }) - .then(async(response) => { - if (response.ok) { - const data = await response.json(); - console.log(data.message); - return window.location.href = `/account/${treeMemberId}`; + }); + if (!relRes.ok) { + const relData = await relRes.json(); + console.error('SERVER RELATIONSHIP ERROR:', relData); + const errorMsg = relData.details || relData.error || 'Failed to add relationship'; + throw new Error(errorMsg); + } } - else { - // print message in return body - console.error('Error:', response); + + // --- Update treeinfo so the new member appears on the tree visualization --- + try { + const treeData = await familyTreeService.getFamilyTreeByUserId(currentAccountID); + const treeIndex = Object.fromEntries(treeData.map(person => [person.id, person])); + + // Use the treeMemberId as the node ID for the new member + const newNodeId = `${treeMemberId}`; + const accountNodeId = `${treeUserId}`; + + // Add new node to tree index + treeIndex[newNodeId] = { + "id": newNodeId, + "data": { + "first name": selectedUser.firstname, + "last name": selectedUser.lastname, + "gender": finalGender || "" + }, + "rels": {} + }; + + // Add the relationship links in the tree data + const res = addRelationship(treeIndex, accountNodeId, newNodeId, data.selectedMemberRelationship, data.matPat || null, data.connectTo); + + // Persist updated tree + const updatedTreeData = Object.values(treeIndex); + await familyTreeService.updateTreeInfo(currentAccountID, updatedTreeData); + console.log('Tree info updated with new member'); + + // Show success summary + const dbLinkCount = relsToCreate.length; + const mainRel = data.selectedMemberRelationship; + let factualSummary = `Added as your ${mainRel.charAt(0).toUpperCase() + mainRel.slice(1)}`; + if (dbLinkCount > 1 && data.connectTo) { + const partnerName = treeInfo[data.connectTo]?.data["first name"] || "Partner"; + factualSummary += ` and linked to ${partnerName}`; + } + + setResult({ + ...res, + name: `${selectedUser.firstname} ${selectedUser.lastname}`, + summary: factualSummary, + dbStatus: `Saved ${dbLinkCount} relationship record(s) to Database`, + treeStatus: "Tree visualization data updated successfully" + }); + } catch (treeError) { + console.error('Warning: Member added but tree visualization update failed:', treeError); + setResult({ + success: true, + name: `${selectedUser.firstname} ${selectedUser.lastname}`, + summary: "Member added to database, but visualization failed to update.", + dbStatus: "Success", + treeStatus: "Failed: " + treeError.message + }); } - }) - .catch(error => { + + reset(); + // close(); // Don't close immediately, let user see summary + // return window.location.href = `/tree`; + + } catch (error) { console.error('Error:', error); - }); + setErrorMessage(error.message); + } }; // form submission (manual entry) - const onSubmitManual = (data) => { + const onSubmitManual = async (data, close) => { console.log("submit attempted"); - // add new member to family members table - let requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - "firstName": data.firstName, - "lastName": data.lastName, - "birthDate": data.birthday || null, - "deathDate" : data.deathDate || null, - "location": data.location || null, - "phoneNumber": "", - "userId": userid - }) - }; - - let treeUserId = currentUserID; // TODO: will retrieve this from a service or something - let treeMemberId; + // console.log("Form data:", data); + setErrorMessage(""); + + try { + // Final gender enforcement for manual entry + const relHint2 = relationshipService.EXTENDED_RELATIONS[data.relationship]; + const finalGender2 = relHint2?.gender || data.gender; + + // add new member to family members table + let memberData = { + firstname: data.firstName, + lastname: data.lastName, + birthdate: data.birthDate, + deathdate: data.deathDate, + location: data.location || null, + phonenumber: data.phoneNumber || null, + userid: currentAccountID, // The user adding the family member + memberuserid: null, // manually added members do not have associated user accounts + gender: finalGender2 + } - fetch(`http://localhost:5000/api/family-members/`, requestOptions) // add new family member - .then(async(response) => { - if (response.ok) { - const responseData = await response.json(); - console.log(responseData.message); - treeMemberId = responseData.member; - } - else { - // print message in return body - console.error('Error:', response); - } - return fetch(`http://localhost:5000/api/relationships/`, { + // get account treemember id + let treeUser = await familyTreeService.getFamilyMemberByUserId(currentAccountID); + const treeUserId = treeUser.id; + console.log(treeUserId, 'user treemember id'); + console.log('current Account ID', currentAccountID); + + // add new member to treemembers table + const treeMember = await familyTreeService.createFamilyMember(memberData); + const treeMemberId = treeMember.member.id; + console.log('added user treeMemberId', treeMemberId); + + // Resolve relationships to create across the database + // IMPORTANT: Use treeUserId (the member ID), not currentAccountID (the user ID) + const relsToCreate = relationshipService.getRequiredDBRelationships( + treeUserId, + treeMemberId, + data.relationship, + data.connectTo2, + data.matPat2 || null + ); + + console.log('Relationships to create (Manual):', relsToCreate); + + for (const rel of relsToCreate) { + const relRes = await fetch(`http://localhost:5000/api/relationships/`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - person1_id: treeUserId, - person2_id: treeMemberId, - relationshipType: data.relationship, + ...rel, relationshipStatus: "active", - side: data.matPat2 || null, - userId: userid, + userId: currentAccountID }) }); - }) - .then(async(response) => { - if (response.ok) { - const responseData = await response.json(); - console.log(responseData.message); - // redirect to account page - return window.location.href = `/account/${treeMemberId}`; + if (!relRes.ok) { + const relData = await relRes.json(); + console.error('SERVER RELATIONSHIP ERROR (Manual):', relData); + const errorMsg = relData.details || relData.error || 'Failed to add relationship'; + throw new Error(errorMsg); } - else { - // print message in return body - console.error('Error:', response); + } + + // --- Update treeinfo so the new member appears on the tree visualization --- + try { + const treeData = await familyTreeService.getFamilyTreeByUserId(currentAccountID); + const treeIndex = Object.fromEntries(treeData.map(person => [person.id, person])); + + // Use the treeMemberId as the node ID for the new member + const newNodeId = `${treeMemberId}`; + const accountNodeId = `${treeUserId}`; + + // Add new node to tree index + treeIndex[newNodeId] = { + "id": newNodeId, + "data": { + "first name": data.firstName, + "last name": data.lastName, + "gender": finalGender2 || "" + }, + "rels": {} + }; + + // Add the relationship links in the tree data + // new integration uses account user as the subject, new member = relative as the actor (newNode is account's relative) + const res = addRelationship(treeIndex, accountNodeId, newNodeId, data.relationship, data.matPat2 || null, data.connectTo2); + + // Persist updated tree + const updatedTreeData = Object.values(treeIndex); + await familyTreeService.updateTreeInfo(currentAccountID, updatedTreeData); + console.log('Tree info updated with new manual member'); + + // Show success summary + const dbLinkCount = relsToCreate.length; + const mainRel = data.relationship; + let factualSummary = `Added as your ${mainRel.charAt(0).toUpperCase() + mainRel.slice(1)}`; + if (dbLinkCount > 1 && data.connectTo2) { + const partnerName = treeInfo[data.connectTo2]?.data["first name"] || "Partner"; + factualSummary += ` and linked to ${partnerName}`; } - }); + + setResult({ + ...res, + name: `${data.firstName} ${data.lastName}`, + summary: factualSummary, + dbStatus: `Saved ${dbLinkCount} relationship record(s)`, + treeStatus: "Tree structure updated" + }); + } catch (treeError) { + console.error('Warning: Member added but tree visualization update failed:', treeError); + setResult({ + success: true, + name: `${data.firstName} ${data.lastName}`, + summary: "Member added, but tree update failed.", + dbStatus: "Success", + treeStatus: "Failed" + }); + } + + reset(); + // close(); + // return window.location.href = `/tree`; + + } catch (error) { + console.error('Error:', error); + setErrorMessage(error.message); + } }; return ( @@ -267,25 +499,87 @@ function AddFamilyMemberPopup({ trigger, userid }) {
{/* close button */}
-
-
onSubmitExisting(data, close))}> + {result ? ( +
+
+

Member Added!

+
+

Name: {result.name}

+

Action: {result.summary}

+ + {result.warnings?.length > 0 && ( +
+

Note:

+
    + {result.warnings.map((w, i) =>
  • {w}
  • )} +
+
+ )} + +
+

Database: {result.dbStatus}

+

Tree Visualization: {result.treeStatus}

+
+
+ + +
+
+ ) : ( + onSubmitExisting(data, close))}>

Add Family Member

- + + {/* Error message display */} + {errorMessage && ( +
+ {errorMessage} +
+ )} + {/* search for existing user */}
setSearchTerm(e.target.value)} + type="text" + placeholder="Search for a family member..." + style={{ width: '100%', padding: '10px', borderRadius: '5px', border: '1px solid #ccc', fontFamily: 'Alata', margin: '10px' }} + value={searchTerm} + onChange={e => setSearchTerm(e.target.value)} />
@@ -318,25 +612,55 @@ function AddFamilyMemberPopup({ trigger, userid }) {
-
- Maternal - Paternal + {isSideChoiceNeeded(selectedMemberRelationship) && ( +
+ +
+ )} + + {/* Extended relationship "Connect To" dropdown - Hide if auto-detected for ANY relationship */} +
+ +

+ * Link to an existing member for better accuracy. +

{/* add button */} @@ -345,44 +669,107 @@ function AddFamilyMemberPopup({ trigger, userid }) {
- +
+ )} - {/* ----------------------- */} + {/* ----------------------- */} {/* manual content */} - +
{/* close button */}
-
{/* fill out info about family member */} -
onSubmitManual(data, close))} style={styles.FormStyle}> + {result ? ( +
+
+

Member Added!

+
+

Name: {result.name}

+

Action: {result.summary}

+ + {result.warnings?.length > 0 && ( +
+

Note:

+
    + {result.warnings.map((w, i) =>
  • {w}
  • )} +
+
+ )} + +
+

Database: {result.dbStatus}

+

Tree Visualization: {result.treeStatus}

+
+
+ + +
+
+ ) : ( + onSubmitManual(data, close))} style={styles.FormStyle}>

Manually Add Family Member

- + {/* sync contacts button */}
- {/* TODO create handler, backend (?) */} - + {/* TODO create handler, backend (?) */} +
- + + {/* Error message display */} + {errorMessage && ( +
+ {errorMessage} +
+ )} +
    - {/* required fields */} + {/* required fields */}
  • +
  • + +
  • {/* note: might need to consider adding options to connect new family member to previous ones, to more accurately place them on tree */}
  • @@ -405,11 +801,14 @@ function AddFamilyMemberPopup({ trigger, userid }) { + + - + + @@ -417,13 +816,39 @@ function AddFamilyMemberPopup({ trigger, userid }) {
  • -
    - Maternal - Paternal + {isSideChoiceNeeded(selectedManualRelationship) && ( +
    + +
    + )} + + {/* Extended relationship "Connect To" dropdown for manual entry - Hide if auto-detected */} +
    + +

    + * Link to an existing member for better accuracy. +

    {/* optional fields */} -

  • @@ -445,7 +870,7 @@ function AddFamilyMemberPopup({ trigger, userid }) {
@@ -454,6 +879,7 @@ function AddFamilyMemberPopup({ trigger, userid }) {
+ )} )} diff --git a/client/src/components/AddFamilyMember/popup.css b/client/src/components/AddFamilyMember/popup.css index c230a6a..9a912c3 100644 --- a/client/src/components/AddFamilyMember/popup.css +++ b/client/src/components/AddFamilyMember/popup.css @@ -2,4 +2,5 @@ width: auto; border-radius: 30px; min-width: 0px; + align-items: center; } \ No newline at end of file diff --git a/client/src/components/AddFamilyMember/styles.js b/client/src/components/AddFamilyMember/styles.js index 80059fe..c61ccac 100644 --- a/client/src/components/AddFamilyMember/styles.js +++ b/client/src/components/AddFamilyMember/styles.js @@ -13,7 +13,7 @@ export const ListStyle = { fontFamily: 'Alata', display: 'flex', flexDirection: 'column', - alignItems: 'flex-end', + textAlign: 'left', marginRight: '15%' }; diff --git a/client/src/components/AddToTree/AddToTree.js b/client/src/components/AddToTree/AddToTree.js index 5816c2a..e6c27a8 100644 --- a/client/src/components/AddToTree/AddToTree.js +++ b/client/src/components/AddToTree/AddToTree.js @@ -1,192 +1,84 @@ import { React, useState, useEffect, useRef } from 'react'; import Popup from 'reactjs-popup'; import 'reactjs-popup/dist/index.css'; -import { useForm } from 'react-hook-form'; +import { set, useForm } from 'react-hook-form'; import * as styles from './styles'; import './popup.css'; import { ReactComponent as CloseIcon } from '../../assets/exit.svg'; import { Link } from 'react-router-dom'; import { useCurrentUser } from '../../CurrentUserProvider'; // import the context +import { addRelationship }from '../../utils/relationUtil.js'; +import { familyTreeService } from '../../services/familyTreeService'; // john jane parent jane is john's mom -function AddTreeMember (userId, accountUserId, relativeUserId, relativeRelationship, accountUserName, treeData, results, currentAccountID) { +async function AddTreeMember (userId, accountUserId, relativeUserId, relativeRelationship, accountUserName, treeData, results, currentAccountID, gender) { const treeIndex = Object.fromEntries(treeData.map(person => [person.id, person])); - // maybe - check if account user is already in tree, return error if they are (user will need to delete them and re-add) - - console.log("relative " + relativeUserId); - console.log("account " + accountUserId); - console.log(results.current.find(result => Number(result.id) === Number(accountUserId))) - - // intialize user in tree + console.log(treeIndex); + // Link relative to user; + console.log("relative id" + relativeUserId); + console.log("account id" + accountUserId); + console.log("Initializing treeIndex:", treeIndex); + + if (treeIndex[`${accountUserId}`]) { + throw new Error(`${accountUserName} is already in your family tree.`); // Re-throw the error to be caught by onSubmit + } + // Initialize user in tree treeIndex[`${accountUserId}`] = { "id": `${accountUserId}`, - "rels": { - "children": [], - "spouses": [], - }, "data": { "first name": `${accountUserName.split(" ")[0]}`, "last name": `${accountUserName.split(" ")[1]}`, - "gender": `${results.current.find(result => Number(result.id) === Number(accountUserId))["gender"]}` - } - } - - - if(relativeRelationship === "parent"){ - // add account user as child to relative - treeIndex[`${relativeUserId}`]["rels"]["children"].push(`${accountUserId}`); - // add relative as parent to account user - if(treeIndex[`${relativeUserId}`]["data"]["gender"] === "M") { - treeIndex[`${accountUserId}`]["rels"]["father"] = `${relativeUserId}`; - } - else if(treeIndex[`${relativeUserId}`]["data"]["gender"] === "F") { - treeIndex[`${accountUserId}`]["rels"]["mother"] = `${relativeUserId}`; - } - else{ - console.log("no gender found"); - } - // add relative's spouse(s) (if any) as parent to account user - treeIndex[`${relativeUserId}`]["rels"]["spouses"].forEach(spouse => { - if(treeIndex[`${spouse}`]["data"]["gender"] === "M") { - treeIndex[`${accountUserId}`]["rels"]["father"] = `${spouse}`; - } - else if(treeIndex[`${spouse}`]["data"]["gender"] === "F") { - treeIndex[`${accountUserId}`]["rels"]["mother"] = `${spouse}`; - } - else { - console.log("no gender found"); - } - }); - // add account user as child to relative's spouse(s) (if any) - treeIndex[`${relativeUserId}`]["rels"]["spouses"].forEach(spouse => { - treeIndex[`${spouse}`]["rels"]["children"].push(`${accountUserId}`); - }); - } - - else if(relativeRelationship === "child") { - // add account user as parent to relative - if(treeIndex[`${accountUserId}`]["data"]["gender"] === "M"){ - treeIndex[`${relativeUserId}`]["rels"]["father"] = `${accountUserId}`; - } - else if(treeIndex[`${accountUserId}`]["data"]["gender"] === "F"){ - treeIndex[`${relativeUserId}`]["rels"]["mother"] = `${accountUserId}`; - } - else{ - console.log("no gender found"); - } - // add relative as child to account user - treeIndex[`${accountUserId}`]["rels"]["children"].push(`${relativeUserId}`); - // add account user as spouse to relative's existing parent(s) (if any) + make account user parent of relative's siblings (if any) - if(treeIndex[`${relativeUserId}`]["rels"]["father"] !== undefined && treeIndex[`${accountUserId}`]["data"]["gender"] === "F") { // make account user the wife - treeIndex[`${relativeUserId}`["rels"]["father"]]["rels"]["spouses"].push(`${accountUserId}`); - treeIndex[`${relativeUserId}`["rels"]["father"]]["rels"]["children"].forEach(child => { // make account user mother of relative's siblings - if(child !== `${relativeUserId}`) { - treeIndex[`${child}`]["rels"]["mother"] = `${accountUserId}`; - } - }); - } - else if(treeIndex[`${relativeUserId}`]["rels"]["mother"] !== undefined && treeIndex[`${accountUserId}`]["data"]["gender"] === "M"){ // make account user the husband - treeIndex[`${relativeUserId}`["rels"]["mother"]]["rels"]["spouses"].push(`${accountUserId}`); - treeIndex[`${relativeUserId}`["rels"]["father"]]["rels"]["children"].forEach(child => { // make account user father of relative's siblings - if(child !== `${relativeUserId}`) { - treeIndex[`${child}`]["rels"]["father"] = `${accountUserId}`; - } - }); - } - else{ - console.log("no gender found or no existing parent"); - } - } - - else if(relativeRelationship === "sibling") { - // add account user as child to relative's parent(s) - if(treeIndex[`${relativeUserId}`]["rels"]["father"] !== undefined) { - treeIndex[`${relativeUserId}`["rels"]["father"]]["rels"]["children"].push(`${accountUserId}`); - treeIndex[`${accountUserId}`]["rels"]["father"] = `${treeIndex[`${relativeUserId}`]["rels"]["father"]}`; - } - if(treeIndex[`${relativeUserId}`]["rels"]["mother"] !== undefined) { - treeIndex[`${relativeUserId}`["rels"]["mother"]]["rels"]["children"].push(`${accountUserId}`); - treeIndex[`${accountUserId}`]["rels"]["mother"] = `${treeIndex[`${relativeUserId}`]["rels"]["mother"]}`; - } - } - - else if(relativeRelationship === "spouse") { - // add account user as spouse to relative - treeIndex[`${relativeUserId}`]["rels"]["spouses"].push(`${accountUserId}`); - // add relative as spouse to account user - treeIndex[`${accountUserId}`]["rels"]["spouses"].push(`${relativeUserId}`); - // add account user as parent to relative's children (if any) - treeIndex[`${relativeUserId}`]["rels"]["children"].forEach(child => { - if(treeIndex[`${accountUserId}`]["data"]["gender"] === "M") { - treeIndex[`${child}`]["rels"]["father"] = `${accountUserId}`; - } - else if(treeIndex[`${accountUserId}`]["data"]["gender"] === "F"){ - treeIndex[`${child}`]["rels"]["mother"] = `${accountUserId}`; - } - treeIndex[`${accountUserId}`]["rels"]["children"].push(`${child}`); - }); - } + "gender": `${gender}` + }, + "rels": {} + }; - else { - console.log("Error: invalid relationship type"); + try { + // Add the primary relationship + // TODO: Figure out if this is necessary given that family-chart does this automatically + await addRelationship(treeIndex, accountUserId, relativeUserId, relativeRelationship); + + // Update the tree info in the database in order of addition + let updatedTreeData = [...treeData]; + const newAccountUser = treeIndex[`${accountUserId}`]; + updatedTreeData.push(newAccountUser); + + console.log("Updated tree data after adding relationship:", updatedTreeData); + await familyTreeService.updateTreeInfo(currentAccountID, updatedTreeData); + } catch (error) { + console.error("Error updating tree info:", error.message); + throw new Error("Error updating your tree."); // Re-throw the error to be caught by onSubmit } - - let updatedTreeData = Object.values(treeIndex); - console.log(treeData); - console.log(updatedTreeData); - - let requestOptions = { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(updatedTreeData) - }; - fetch(`http://localhost:5000/api/tree-info/${currentAccountID}`, requestOptions) - .then(async(response) => { - if (response.ok) { - console.log("Tree object updated successfully"); - } - else{ - console.error('Error:', response); - } - }); } -function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAccountRelationshipType, userId }) { + // populated from Account.js +function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAccountRelationshipType, userId, gender }) { const [searchTerm, setSearchTerm] = useState(""); const [searchResults, setSearchResults] = useState([]); + const [errorMessage, setErrorMessage] = useState(""); var results = useRef([]); var filteredResults = useRef([]); const { currentAccountID } = useCurrentUser(); const [treeData, setTreeData] = useState([]); + // retrieve a list of all family members that are within the tree object useEffect(() => { const fetchResults = async () => { - fetch(`http://localhost:5000/api/family-members/user/${currentAccountID}`) - .then(async(response) => { - if (response.ok) { - results.current = await response.json(); - console.log(results.current); - } - else { - console.log('Error:', response); - } - }) - .then(() => { - // grab tree object - fetch(`http://localhost:5000/api/tree-info/${currentAccountID}`) - .then(async(response) => { - if (response.ok) { - let responseData = await response.json(); - console.log(responseData.object); - setTreeData(responseData.object); - } - else { - console.error('Error:', response); - } - }); - }) + try { + // get family members + results.current = await familyTreeService.getFamilyMembersByUserId(currentAccountID) + console.log("Results.current members:", results.current); + + // get tree data + const data = await familyTreeService.getFamilyTreeByUserId(currentAccountID); + setTreeData(data); + console.log("Fetched tree data:", data); + } catch (error) { + setErrorMessage("Error getting tree data: " + error.message); + return; + } } fetchResults(); }, [currentAccountID]); @@ -198,12 +90,19 @@ function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAc } // populate filteredResults - filteredResults.current = results.current.filter((result) => - treeData.map((person) => Number(person.id)).includes(Number(result.id)) - ); - - console.log("Filtered Results:", filteredResults.current); - }, [treeData, results.current]); + if (Array.isArray(treeData)) { + try { + filteredResults.current = results.current.filter(obj1 => + treeData.some(obj2 => Number(obj2.id) === (Number(obj1.memberuserid) || Number(obj1.id))) + ); + console.log("Filtered Results:", filteredResults.current); + } catch (error) { + console.error("Error parsing treeData:", error); + return; + } + } + + }, [treeData]); // form const { @@ -216,53 +115,41 @@ function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAc let selectedMember = watch("selectedMember"); const onSubmit = async (data, close) => { - console.log(data); - reset(); - close(); - // wait for the API request to complete - const response = await fetch(`http://localhost:5000/api/tree-info/${currentAccountID}`, { - method: 'GET', - headers: { 'Content-Type': 'application/json' } - }); - - if (response.ok) { - // parse the response - const treeResponse = await response.json(); - setTreeData(treeResponse.object); - console.log(treeData); - - // call after the API request is complete - return AddTreeMember(userId, accountUserId, data.selectedMember, data.memberRelationshipType, accountUserName, treeData, results, currentAccountID); - } - else { - const errorData = await response.json(); - console.error('Error:', errorData.message); + console.log("Submission data:", data); + setErrorMessage(""); // Clear any previous error messages + + try { + await AddTreeMember(userId, accountUserId, data.selectedMember, data.memberRelationshipType, accountUserName, treeData, results, currentAccountID, gender); + reset(); + close(); + return window.location.href = `/tree`; + } catch (error) { + console.error("Error adding member to tree:", error.message); + setErrorMessage(error.message || "Failed to add member to tree." ); + return; } }; // populate search results useEffect(() => { - if (searchTerm === "") { - setSearchResults([]); - reset({ selectedMember: "" }); - return; - } - const fetchResults = () => { const filtered = filteredResults.current.filter(member => - member.firstName.toLowerCase().includes(searchTerm.toLowerCase()) || - member.lastName.toLowerCase().includes(searchTerm.toLowerCase()) + member.firstname.toLowerCase().includes(searchTerm.toLowerCase()) || + member.lastname.toLowerCase().includes(searchTerm.toLowerCase()) ); setSearchResults(filtered); // update searchResults with the filtered array // console.log("Filtered Search Results:", filtered); }; fetchResults(); - }, [searchTerm, reset, selectedMember, accountUserId]); + }, [searchTerm, selectedMember, accountUserId]); - useEffect(() => { - reset({ selectedMember: "" }); - }, [reset, searchTerm]); + useEffect(() => { + reset({ selectedMember: "" }); + setSearchResults([]); + setErrorMessage(""); + setSearchTerm(""); + }, [reset]); return ( @@ -270,7 +157,7 @@ function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAc
{/* close button */}
-
@@ -300,16 +187,16 @@ function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAc
{searchResults.length > 0 ? ( searchResults.map(result => ( -
+
@@ -325,7 +212,7 @@ function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAc
  • + {errorMessage && ( +
    + {errorMessage} +
    + )}
    diff --git a/client/src/components/CreateEvent/CreateEvent.js b/client/src/components/CreateEvent/CreateEvent.js deleted file mode 100644 index 614615e..0000000 --- a/client/src/components/CreateEvent/CreateEvent.js +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react'; -import Popup from 'reactjs-popup'; -import 'reactjs-popup/dist/index.css'; -import { useForm } from 'react-hook-form'; -import * as styles from './styles'; -import './popup.css'; -import { ReactComponent as CloseIcon } from '../../assets/exit.svg'; -import { ReactComponent as UploadIcon } from '../../assets/upload.svg'; - -// TODO: make form clear when dismissed by clicking outside of modal - -function CreateEventPopup({ trigger }) { - const { register, handleSubmit, reset } = useForm(); - - // TODO: connect to backend - const onSubmit = (data, close) => { - console.log(data); - reset(); - close(); - }; - - return ( - - {close => ( -
    - {/* close button */} -
    - -
    - {/* fill out info about event */} -
    onSubmit(data, close))} style={styles.FormStyle}> -
    -

    Create New Event

    -
    - -
      - {/* required fields */} -
      -
    • - -
    • -
    • - -
    • -
      - - - {/* optional fields */} -

      -
      -
    • -