diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5fa95f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +pnpm-lock.yaml +*.tsbuildinfo +server/ diff --git a/README.md b/README.md index 0d3ea6a..ebe173e 100644 --- a/README.md +++ b/README.md @@ -1 +1,173 @@ -# Web5-Hack \ No newline at end of file +# PulsePal-MVP 🌟 + +## Welcome to the PulsePal MVP repository + +

+ site +

+ +> ## Table of contents +- [Overview](#overview) +- [Core Features Implemented](#core-features-implemented) +- [Technologies](#technologies) +- [Repo Setup](#repo-setup) +- [Requirements](#requirements) +- [Setup the Project](#setup-the-project) +- [Setup the Frontend](#setup-the-frontend) + - [Install Dependencies](#install-dependencies) + - [Steps to host the live site on Vercel](#steps-to-host-the-live-site-on-vercel) +- [Live Link](#live-link) +- [Contributors](#contributors) +- [Contributing to the project](#contributing-to-the-project) +# +> ## Overview +

+Welcome to PulsePal, your health guardian in WEB5. Safely store medical records with decentralized identifiers and web nodes, ensuring data security. Connect with similar conditions, share home remedies with a rating system, and access specialized doctors based on your location. PulsePal – where advanced technology meets user-friendly design for your secure, connected health journey. +

+ + + +# +> ## Core Features Implemented + +1. Secure Medical Record Storage: +- Users can securely store their medical records using decentralized identifiers (DIDs) in the app. +- Data stored through decentralized web nodes (DWNs) ensures security and user ownership. + +2. Connect with Similar Medical Conditions: +- An algorithm will match users based on their medical records, enabling them to chat and connect. +- Users can share experiences, insights, and support each other within the platform. + +3. Home Remedies Section: +- Users can contribute and share simple, effective home remedies. +- Remedies will be rated by users to gauge their usefulness. + +4. Recommend specialized doctors in your nearby areas, based on your current location + + + +

+ +# +> ## Technologies +| Stack | Usage | +| :------------------ | :------------------ | +| **`Web5`** | Backend | +| **`React JS + tauri + tailwind`** | Frontend | + +# +> ## Repo Setup + +

+To setup the repo, first fork the Web5-hack Repo, then clone the forked repository to create a copy on the local machine. +

+ + $ git clone https://github.com/coder12git/Web5-Hack.git + +

+Change directory to the cloned repo and set the original Web5-Hack repository as the "upstream" and your forked repository as the "origin" using gitbash. and make sure to switch to dev branch +

+ + $ git remote add upstream https://github.com/coder12git/Web5-Hack.git + +# + +> ## Requirements +# +- [Cors extension](https://chromewebstore.google.com/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf) +- Node JS + +# +> ## Setup the Frontend +- First run the frontend on your local server to ensure it's fully functional before building for production. +# +1. Clone the repository + +``` +git clone https://github.com/coder12git/Web5-Hack.git +``` + +2. Navigate to the project folder + +``` +cd Web5-Hack/tauri-app +``` + +3. Install dependencies + +``` +npm install +``` + +4. To start the server run + +``` +npm run dev +``` + + +# + +## Useful links + +## View attribution files here + + + +## Explainer video (User POV) + + + +https://github.com/coder12git/Web5-Hack/assets/108334168/f465aff5-d1b9-4116-bb91-87e1906baec6 + + + +## Demo Video (Clients POV) + + + + +- [Pitch Deck](https://github.com/coder12git/Web5-Hack/files/13853501/PULSEPal.pdf) +- [Frontend Deployment](https://web5-hack-413v.vercel.app/#/) + + + +> ## Contributors + +This Project was created by these awesome dedicated members + +

+ Team +

+ + +> ## What's next for PulsePal + +- Personalized Health Insights: "Our plan includes integrating AI-driven analytics to offer personalized health insights, assessing individual health risks and predicting future health trends based on users' medical records." + +- Natural Language Processing (NLP) Chatbot: "We aim to implement an NLP-powered chatbot that assists users in comprehending medical terminology, interpreting test results, and providing preliminary guidance in emergencies based on entered symptoms." + +- AI-Driven Diagnostics: "Incorporating an AI system for preliminary diagnostics, we intend to analyze symptoms and user-entered data to provide suggestive measures or identify potential health issues, prompting users to seek professional medical advice." + +- Augmented Reality (AR) for First Aid Guidance: "Our future plan involves integrating AR technology to guide users through basic first aid procedures by overlaying instructional visuals onto real-time surroundings." + +- Health Gamification and Incentivization: "To encourage healthy habits, we plan to introduce gamification within PulsePal, utilizing AI to personalize challenges, rewards, and reminders aligned with individual health goals." + +- Predictive Appointment Scheduling: "Utilizing AI algorithms, we aim to predict potential health concerns or follow-up appointments based on stored medical records, proactively suggesting relevant appointments or check-ups." + + +# +> ## Contributing to the project + +If you find something worth contributing, please fork the repo, make a pull request and add valid and well-reasoned explanations about your changes or comments. + +Before adding a pull request, please note: + +- This is an open source project. +- Your contributions should be inviting and clear. +- Any additions should be relevant. +- New features should be easy to contribute to. + +All **`suggestions`** are welcome! +# +> ##### README Created by `Enebeli Emmanuel` for PulsePal diff --git a/tauri-app/index.html b/tauri-app/index.html index 03abf55..36ecb79 100644 --- a/tauri-app/index.html +++ b/tauri-app/index.html @@ -4,7 +4,21 @@ - Tauri + React + TS + PulsePal + + + + + diff --git a/tauri-app/package-lock.json b/tauri-app/package-lock.json index c747970..5bfe707 100644 --- a/tauri-app/package-lock.json +++ b/tauri-app/package-lock.json @@ -19,6 +19,8 @@ "axios": "^1.6.3", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "date-fns": "^3.1.0", + "hono": "^3.12.0", "lodash": "^4.17.21", "lucide-react": "^0.298.0", "react": "^18.2.0", @@ -26,6 +28,7 @@ "react-dropzone": "^14.2.3", "react-easy-crop": "^5.0.4", "react-hook-form": "^7.49.2", + "react-hot-toast": "^2.4.1", "react-pdf": "^7.6.0", "react-router-dom": "^6.21.1", "react-spinners": "^0.13.8", @@ -43,6 +46,7 @@ "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-window": "^1.8.8", + "@types/readable-stream": "^4.0.10", "@vitejs/plugin-react": "^4.0.3", "autoprefixer": "^10.4.16", "postcss": "^8.4.32", @@ -2094,6 +2098,22 @@ "@types/react": "*" } }, + "node_modules/@types/readable-stream": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.10.tgz", + "integrity": "sha512-AbUKBjcC8SHmImNi4yK2bbjogQlkFSg7shZCcicxPQapniOlajG8GCc39lvXzCWX4lLRRs7DM3VAeSlqmEVZUA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "node_modules/@types/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -2990,8 +3010,16 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/date-fns": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.1.0.tgz", + "integrity": "sha512-ZO7yefXV/wCWzd3I9haCHmfzlfA3i1a2HHO7ZXjtJrRjXt8FULKJ2Vl8wji3XYF4dQ0ZJ/tokXDZeYlFvgms9Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } }, "node_modules/debug": { "version": "4.3.4", @@ -3457,6 +3485,14 @@ "node": ">=4" } }, + "node_modules/goober": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", + "integrity": "sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/graceful-goodbye": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/graceful-goodbye/-/graceful-goodbye-1.3.0.tgz", @@ -3509,6 +3545,14 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/hono/-/hono-3.12.0.tgz", + "integrity": "sha512-UPEtZuLY7Wo7g0mqKWSOjLFdT8t7wJ60IYEcxKl3AQNU4u+R2QqU2fJMPmSu24C+/ag20Z8mOTQOErZzK4DMvA==", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -4861,6 +4905,21 @@ "react": "^16.8.0 || ^17 || ^18" } }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/tauri-app/package.json b/tauri-app/package.json index 7b4e845..f90ccef 100644 --- a/tauri-app/package.json +++ b/tauri-app/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "vite build", "preview": "vite preview", "tauri": "tauri" }, @@ -21,6 +21,8 @@ "axios": "^1.6.3", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "date-fns": "^3.1.0", + "hono": "^3.12.0", "lodash": "^4.17.21", "lucide-react": "^0.298.0", "react": "^18.2.0", @@ -28,9 +30,11 @@ "react-dropzone": "^14.2.3", "react-easy-crop": "^5.0.4", "react-hook-form": "^7.49.2", + "react-hot-toast": "^2.4.1", "react-pdf": "^7.6.0", "react-router-dom": "^6.21.1", "react-spinners": "^0.13.8", + "react-toastify": "^9.1.3", "react-virtualized-auto-sizer": "^1.0.20", "react-window": "^1.8.10", "tailwind-merge": "^2.1.0", @@ -45,6 +49,7 @@ "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-window": "^1.8.8", + "@types/readable-stream": "^4.0.10", "@vitejs/plugin-react": "^4.0.3", "autoprefixer": "^10.4.16", "postcss": "^8.4.32", diff --git a/tauri-app/public/pg.jpg b/tauri-app/public/pg.jpg new file mode 100644 index 0000000..86a828a Binary files /dev/null and b/tauri-app/public/pg.jpg differ diff --git a/tauri-app/public/pic.jpg b/tauri-app/public/pic.jpg new file mode 100644 index 0000000..0195eff Binary files /dev/null and b/tauri-app/public/pic.jpg differ diff --git a/tauri-app/public/pic2.jpg b/tauri-app/public/pic2.jpg new file mode 100644 index 0000000..986a06f Binary files /dev/null and b/tauri-app/public/pic2.jpg differ diff --git a/tauri-app/public/pm.jpg b/tauri-app/public/pm.jpg new file mode 100644 index 0000000..8cb5de7 Binary files /dev/null and b/tauri-app/public/pm.jpg differ diff --git a/tauri-app/src/App.tsx b/tauri-app/src/App.tsx index 69726d4..0f543ce 100644 --- a/tauri-app/src/App.tsx +++ b/tauri-app/src/App.tsx @@ -1,33 +1,42 @@ -import { RouterProvider, createHashRouter } from "react-router-dom"; +import { RouterProvider, createHashRouter, Navigate } from "react-router-dom"; import useWeb5Store from "./stores/useWeb5Store"; import { useEffect } from "react"; -import Home from "./pages/home"; import Connect from "./pages/connect"; import Remedy from "./pages/remedy"; -import MedicPage from "./pages/medic"; import Doctors from "./pages/nearbyDoctor"; +import HomePage from "./pages/Home"; +import SharedLayout from "./pages/SharedLayout/"; +import Records from "./pages/Records"; +import Chat from "./pages/Chat/"; +import { Toaster } from "react-hot-toast"; +import ProfileGuard from "./components/Auth/Profile/Guard"; const router = createHashRouter([ { path: "/", - element: , + element: , + children: [ + { path: "/", element: }, + { + path: "/records", + element: , + }, + { path: "/connect", element: }, + { + path: "/remedies", + element: , + }, + { path: "/contact", element: }, + { + path: "/chat", + element: ( + + + + ), + } + ], }, - { - path: "/connect", - element: , - }, - { - path: "/remedy", - element: , - }, - { - path: "/medic", - element: , - }, - { - path: "/nearbyDoctors", - element: , - } ]); function App() { @@ -39,8 +48,14 @@ function App() { if (!web5) connect(); }, []); - return <>{web5 ? :
error
}; - + return ( + <> + <> + {web5 ? :
Connecting...
} + + + + ); } export default App; diff --git a/tauri-app/src/components/AddCardComponent/index.css b/tauri-app/src/components/AddCardComponent/index.css new file mode 100644 index 0000000..a644737 --- /dev/null +++ b/tauri-app/src/components/AddCardComponent/index.css @@ -0,0 +1,239 @@ +.main-add-card-container { + background: var(--color-white); + width: 500px; + max-height: 600px; + position: relative; + z-index: 7; + border-radius: 15px; + padding: var(--padding-desktop); + margin-bottom: 30px; +} + +.main-add-card-container h1 { + font-family: "Roboto"; + font-size: 35px; + font-weight: 400; + margin-bottom: 20px; +} + +.input-form { + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: column; + padding: 10px; + overflow-y: scroll; + overflow-x: hidden; + max-height: 300px; +} + +.input-form::-webkit-scrollbar { + width: 6px; +} + +.input-form::-webkit-scrollbar-track { + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1); + border-radius: 5px; +} + +.input-form::-webkit-scrollbar-thumb { + width: 2px; + background: var(--color-blue); + border-radius: 5px; + margin: 0 3px; +} + +.input-form input[type="text"] { + width: 60%; + border: none; + background: var(--color-slate); + padding: 10px 2px; + border-radius: 8px; + font-family: "Roboto"; + text-indent: 10px; + outline: none; + margin-bottom: 10px; + margin-right: 40%; +} + +.input-form select { + width: 60%; + border: none; + background: var(--color-slate); + padding: 10px 2px; + border-radius: 8px; + font-family: "Roboto"; + text-indent: 10px; + outline: none; + margin-bottom: 10px; + margin-right: 40%; +} + + +.input-form textarea { + width: 100%; + border: none; + background: var(--color-slate); + padding: 10px 2px; + border-radius: 8px; + font-family: "Roboto"; + text-indent: 10px; + outline: none; + min-height: 80px; + margin-bottom: 10px; +} + +.input-form button { + width: 30%; + border: none; + color: var(--color-white); + background: var(--color-green); + box-shadow: var(--box-shadow-green); + margin-top: 20px; + padding: 5px 2px; + margin-right: 70%; + font-size: 24px; + border-radius: 8px; + font-family: "Pacifico"; + text-indent: 10px; + outline: none; +} + +.input-form p { + color: var(--color-red); + font-family: "Roboto"; + margin-top: 10px; +} + +.form-container { + border: 2px solid var(--color-blue); + border-style: dashed; + border-radius: 15px; + width: 90%; + min-height: 60px; + margin: 15px; + display: flex; + justify-content: center; + align-items: center; + transition: 0.3s; +} + +.form-container-active { + border: 2px solid var(--color-green); + border-radius: 15px; + width: 90%; + min-height: 60px; + margin: 15px; + display: flex; + justify-content: center; + align-items: center; + transition: 0.3s; +} + +.form-container input { + width: 40%; + position: relative; + margin: auto auto; +} + +.form-container-active input { + width: 40%; + position: relative; + margin: auto auto; +} + +.add-mu { + background: var(--color-red); + color: var(--color-white); + box-shadow: var(--box-shadow-red); + width: 35px; + height: 35px; + border-radius: 50%; + font-size: 20px; + display: flex; + justify-content: center; + align-items: center; + position: absolute; + left: 15px; + transform: translateY(20px); +} + +.sadd-mu { + background: var(--color-red); + color: var(--color-white); + box-shadow: var(--box-shadow-red); + width: 35px; + height: 35px; + border-radius: 50%; + font-size: 20px; + display: flex; + justify-content: center; + align-items: center; + position: absolute; + left: 15px; + transform: translateY(50px); + opacity: 0.5; +} + +.uploader-content { + width: 100%; +} + +@media (max-width: 750px) { + .main-add-card-container { + width: 300px; + padding: var(--padding-mobile); + margin-top: 50px; + z-index: 10; + position: relative; + } + + .input-form { + overflow-y: scroll; + height: 400px; + padding: 5px; + } + + .main-add-card-container h1 { + font-size: 25px; + } + + .add-mu, + .sadd-mu { + top: 20px; + left: 265px; + } + + .sadd-mu { + transform: translateY(13px); + } + + .form-container, + .form-container-active { + height: 60px; + } + + .input-form input[type="text"] { + width: 100%; + margin-right: 0; + } + + .input-form select { + width: 100%; + margin-right: 0; + } + + + .input-form input[type="file"] { + padding: 10px 0; + } + + .input-form button { + width: 100%; + margin-right: 0; + } + + .input-form textarea { + width: 100%; + } +} diff --git a/tauri-app/src/components/AddCardComponent/index.tsx b/tauri-app/src/components/AddCardComponent/index.tsx new file mode 100644 index 0000000..f939403 --- /dev/null +++ b/tauri-app/src/components/AddCardComponent/index.tsx @@ -0,0 +1,160 @@ +import "./index.css"; +import React, { FunctionComponent, useState } from "react"; +import { z } from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import toast from "react-hot-toast"; +import DocumentUtils from "@/utils/document"; +import { Agent } from "../Auth/types"; +import { ProfileState, useProfile } from "@/stores/profile"; + +const possibleConditions = [ + "Cancer", + "Diabetes", + "Heart Disease", + "High Blood Pressure", + "High Cholesterol", + "Mental Illness", +] + +interface FileProp { + file: File | null | undefined; +} + +const FileUploader: FunctionComponent<{ required?: boolean, onChange: (file: File | null) => void }> = ({ required, onChange }) => { + const [file, setFile] = useState({ file: null }); + + return ( +
+ { + const file = e?.target?.files?.[0] + onChange(file ? file : null) + setFile({ file: e?.target?.files?.[0] }); + }} + type="file" + accept=".jpg,.png,.jpeg" + /> +
+ ); +}; + +// const FileUploader = () => { +// const [file, setFile] = useState({ file: null }); +// +// return ( +//
+// { +// setFile({ file: e?.target?.files?.[0] }); +// }} +// type="file" +// accept=".jpg,.png,.jpeg,.docx,.pdf" +// /> +//
+// ); +// }; + +const formSchema = z.object({ + title: z.string().optional(), + description: z.string().min(1), + condition: z.string().min(1), + file: z.instanceof(File), + otherFiles: z.array(z.instanceof(File)) +}) + +const index: FunctionComponent<{ agent: Agent, onClose: () => void }> = ({ agent, onClose }) => { + const { profile, addCondition } = useProfile(store => ({ profile: store.state.profile!, addCondition: store.addCondition })) + const [numFileUploaders, setNumFileUploaders] = useState(0) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + otherFiles: [] + } + }) + // console.log(form.formState.errors) + + const onSubmit = async (data: z.infer) => { + const payload = { + ...data, + profileId: profile.id + } + const createdRecord = await DocumentUtils.createDocumentRecord(agent, payload) + if (!createdRecord) { + toast.error('Sorry an error occurred!') + return + } + + const hasAddedCondition = await addCondition(agent, data.condition) + if (hasAddedCondition) + console.log("Added condition to profile") + + toast.success('Successfully created record!') + + onClose() + } + + return ( +
+

Create New Record

+
+
+ {numFileUploaders > 2 &&

Max file uploads reached

} +
2 ? "sadd-mu" : "add-mu"} + onClick={() => + numFileUploaders > 2 + ? null + : setNumFileUploaders(numFileUploaders + 1) + } + > + +
+ {Array.from({ length: numFileUploaders }) + .map((_, index) => ( + { + const otherFiles = form.getValues("otherFiles") + otherFiles[index] = file as File + form.setValue("otherFiles", otherFiles) + }} /> + ))} + + +
+ ); +}; + +//@ts-ignore +const index: FC = ({saveFunc, formFunc, form}) => { + const [steps, setSteps] = useState([ + , + ]); + + return ( +
+

Create New Remedy

+ { + e.preventDefault() + saveFunc() + }} + > + {/* @ts-ignore */} + +
+ formFunc({ ...form, name: e.target.value })} + /> +