diff --git a/package.json b/package.json index 263f453..c4ca7b7 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "nextBuild": "next build" }, "dependencies": { + "@ionic/react": "^8.7.5", "@tauri-apps/api": "2.8.0", "@tauri-apps/plugin-autostart": "2.5.0", "@tauri-apps/plugin-dialog": "2.4.0", @@ -36,6 +37,7 @@ "@tauri-apps/plugin-updater": "2.9.0", "chart.js": "^4.4.6", "chartjs-plugin-datalabels": "^2.2.0", + "ionicons": "^8.0.13", "moment": "^2.30.1", "next": "^15.5.4", "react": "^19.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3002a5d..d2bede1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@ionic/react': + specifier: ^8.7.5 + version: 8.7.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tauri-apps/api': specifier: 2.8.0 version: 2.8.0 @@ -41,6 +44,9 @@ importers: chartjs-plugin-datalabels: specifier: ^2.2.0 version: 2.2.0(chart.js@4.5.0) + ionicons: + specifier: ^8.0.13 + version: 8.0.13 moment: specifier: ^2.30.1 version: 2.30.1 @@ -209,6 +215,15 @@ packages: cpu: [x64] os: [win32] + '@ionic/core@8.7.5': + resolution: {integrity: sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==} + + '@ionic/react@8.7.5': + resolution: {integrity: sha512-ID1in1YhmjlpLUF1aMv9zSEVc+ZiXs1fNWKJLK4U02LRQoNxmKagwYLxItAuls0KqduCErcqfC5pOcBJDtMl4Q==} + peerDependencies: + react: '>=16.8.6' + react-dom: '>=16.8.6' + '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} @@ -263,6 +278,56 @@ packages: cpu: [x64] os: [win32] + '@rollup/rollup-darwin-arm64@4.34.9': + resolution: {integrity: sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.34.9': + resolution: {integrity: sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-linux-arm64-gnu@4.34.9': + resolution: {integrity: sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.34.9': + resolution: {integrity: sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.34.9': + resolution: {integrity: sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.34.9': + resolution: {integrity: sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.34.9': + resolution: {integrity: sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.34.9': + resolution: {integrity: sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==} + cpu: [x64] + os: [win32] + + '@stencil/core@4.36.2': + resolution: {integrity: sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==} + engines: {node: '>=16.0.0', npm: '>=7.10.0'} + hasBin: true + + '@stencil/core@4.38.0': + resolution: {integrity: sha512-oC3QFKO0X1yXVvETgc8OLY525MNKhn9vISBrbtKnGoPlokJ6rI8Vk1RK22TevnNrHLI4SExNLbcDnqilKR35JQ==} + engines: {node: '>=16.0.0', npm: '>=7.10.0'} + hasBin: true + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -417,6 +482,9 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + ionicons@8.0.13: + resolution: {integrity: sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==} + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} @@ -638,6 +706,20 @@ snapshots: '@img/sharp-win32-x64@0.34.3': optional: true + '@ionic/core@8.7.5': + dependencies: + '@stencil/core': 4.36.2 + ionicons: 8.0.13 + tslib: 2.8.1 + + '@ionic/react@8.7.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@ionic/core': 8.7.5 + ionicons: 8.0.13 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + tslib: 2.8.1 + '@kurkle/color@0.3.4': {} '@next/env@15.5.4': {} @@ -666,6 +748,52 @@ snapshots: '@next/swc-win32-x64-msvc@15.5.4': optional: true + '@rollup/rollup-darwin-arm64@4.34.9': + optional: true + + '@rollup/rollup-darwin-x64@4.34.9': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.34.9': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.34.9': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.34.9': + optional: true + + '@rollup/rollup-linux-x64-musl@4.34.9': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.34.9': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.34.9': + optional: true + + '@stencil/core@4.36.2': + optionalDependencies: + '@rollup/rollup-darwin-arm64': 4.34.9 + '@rollup/rollup-darwin-x64': 4.34.9 + '@rollup/rollup-linux-arm64-gnu': 4.34.9 + '@rollup/rollup-linux-arm64-musl': 4.34.9 + '@rollup/rollup-linux-x64-gnu': 4.34.9 + '@rollup/rollup-linux-x64-musl': 4.34.9 + '@rollup/rollup-win32-arm64-msvc': 4.34.9 + '@rollup/rollup-win32-x64-msvc': 4.34.9 + + '@stencil/core@4.38.0': + optionalDependencies: + '@rollup/rollup-darwin-arm64': 4.34.9 + '@rollup/rollup-darwin-x64': 4.34.9 + '@rollup/rollup-linux-arm64-gnu': 4.34.9 + '@rollup/rollup-linux-arm64-musl': 4.34.9 + '@rollup/rollup-linux-x64-gnu': 4.34.9 + '@rollup/rollup-linux-x64-musl': 4.34.9 + '@rollup/rollup-win32-arm64-msvc': 4.34.9 + '@rollup/rollup-win32-x64-msvc': 4.34.9 + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -806,6 +934,10 @@ snapshots: detect-libc@2.0.4: optional: true + ionicons@8.0.13: + dependencies: + '@stencil/core': 4.38.0 + is-arrayish@0.3.2: optional: true diff --git a/public/images/ionic/business-outline.svg b/public/images/ionic/business-outline.svg new file mode 100644 index 0000000..6ac61f7 --- /dev/null +++ b/public/images/ionic/business-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/ionic/person-circle-outline.svg b/public/images/ionic/person-circle-outline.svg new file mode 100644 index 0000000..c5c086e --- /dev/null +++ b/public/images/ionic/person-circle-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/onboarding/_components/SelectableItem.module.css b/src/app/onboarding/_components/SelectableItem.module.css new file mode 100644 index 0000000..ad70518 --- /dev/null +++ b/src/app/onboarding/_components/SelectableItem.module.css @@ -0,0 +1,36 @@ +.longItemSelected { + display: flex; + max-width: 350px; + height: 150px; + + border: solid 2px var(--dm-blue); + border-radius: 10px; + + margin: 15px; + padding: 10px 20px; +} + +.longItemSelected:hover { + cursor: pointer; +} + +.longItemNotSelected { + display: flex; + max-width: 350px; + height: 150px; + + border: solid 2px var(--dm-gray); + border-radius: 10px; + + margin: 15px; + padding: 10px 20px; +} + +.longItemNotSelected:hover { + cursor: pointer; +} + +.text { + display: flex; + align-items: flex-start; +} diff --git a/src/app/onboarding/_components/SelectableItem.tsx b/src/app/onboarding/_components/SelectableItem.tsx new file mode 100644 index 0000000..64638f7 --- /dev/null +++ b/src/app/onboarding/_components/SelectableItem.tsx @@ -0,0 +1,27 @@ +import styles from "./SelectableItem.module.css"; + +type SelectableItemProp = { + selected: boolean; + title: string; + description: string; +}; + +export default function SelectableItem({ + selected, + title, + description, +}: SelectableItemProp) { + return ( +
  • + +

    + {title}. {description} +

    +
    +
  • + ); +} diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx index aa0d5c4..b021205 100644 --- a/src/app/onboarding/page.tsx +++ b/src/app/onboarding/page.tsx @@ -8,10 +8,11 @@ import styles from "./page.module.css"; import { useRouter } from "next/navigation"; import Dialog from "@/components/Dialog"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { DataPackReceipt } from "@/types/settings"; import Layout from "@/components/Layout"; +import { getConfig } from "@/utils/settings"; export default function Onboarding() { const router = useRouter(); @@ -21,6 +22,18 @@ export default function Onboarding() { content: "", hideSelectButton: false, }); + + useEffect(() => { + async function initPage() { + const providerConfig = await getConfig("provider"); + + if (!providerConfig.onboarding_completed) { + router.push("/onboarding/provider"); + } + } + initPage(); + }, []); + return (
    diff --git a/src/app/onboarding/provider/basics/page.module.css b/src/app/onboarding/provider/basics/page.module.css new file mode 100644 index 0000000..eede78d --- /dev/null +++ b/src/app/onboarding/provider/basics/page.module.css @@ -0,0 +1,44 @@ +.basics label { + display: flex; + flex-direction: column; + margin: 35px 0; + width: 300px; +} + +.basics label input { + margin: 5px 0; +} + +.iconAndInfo { + width: 98dvw; + display: flex; + justify-content: space-around; +} + +.icon { + width: 200px; + margin: 20px; + + filter: invert(100%); +} + +.orgHeaderPreview { + margin: 10px 0; + border-radius: 10px; +} + +.buttons { + position: fixed; + bottom: 5%; + right: 5%; +} + +.buttonsContainer { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.buttons p { + font-size: 0.7rem; +} diff --git a/src/app/onboarding/provider/basics/page.tsx b/src/app/onboarding/provider/basics/page.tsx new file mode 100644 index 0000000..1b71df2 --- /dev/null +++ b/src/app/onboarding/provider/basics/page.tsx @@ -0,0 +1,72 @@ +"use client"; +import Layout from "@/components/Layout"; +import styles from "./page.module.css"; +import { useState } from "react"; +import Link from "next/link"; + +export default function Page() { + const [orgName, setOrgName] = useState(""); + const [orgImage, setOrgImage] = useState(""); + + return ( + +
    +
    +

    Your organization

    +

    We'll use this information to apply branding to the software.

    + + +
    + Business building icon +
    +
    +
    + + + + {orgImage === "" && ( +

    + Continuing without an organization header will instead use the + Ojos Project header. +

    + )} +
    +
    +
    + ); +} diff --git a/src/app/onboarding/provider/complete/page.tsx b/src/app/onboarding/provider/complete/page.tsx new file mode 100644 index 0000000..dd97be0 --- /dev/null +++ b/src/app/onboarding/provider/complete/page.tsx @@ -0,0 +1,13 @@ +import Layout from "@/components/Layout"; +import Link from "next/link"; + +export default function Page() { + return ( + +

    You're completed the setup.

    + + + +
    + ); +} diff --git a/src/app/onboarding/provider/contacts/page.module.css b/src/app/onboarding/provider/contacts/page.module.css new file mode 100644 index 0000000..598450f --- /dev/null +++ b/src/app/onboarding/provider/contacts/page.module.css @@ -0,0 +1,39 @@ +.infoAndIcon { + display: flex; + justify-content: space-around; +} + +.icon { + width: 200px; + margin: 20px; + + filter: invert(100%); +} + +.info label { + display: flex; + flex-direction: column; + + margin: 10px 0; + max-width: 75%; +} + +.info label input { + margin: 4px 0; +} + +.info { + max-width: 40dvw; +} + +.buttons { + position: fixed; + bottom: 5%; + right: 5%; +} + +.buttons div { + display: flex; + flex-direction: column; + align-items: flex-end; +} diff --git a/src/app/onboarding/provider/contacts/page.tsx b/src/app/onboarding/provider/contacts/page.tsx new file mode 100644 index 0000000..520ac2a --- /dev/null +++ b/src/app/onboarding/provider/contacts/page.tsx @@ -0,0 +1,46 @@ +import Layout from "@/components/Layout"; +import styles from "./page.module.css"; +import Link from "next/link"; + +export default function Page() { + return ( + +
    +
    +

    Staff contact information

    +

    + Patients or caregivers could benefit from having staff's work + contact information. If your organization has this information, add + them here. +

    + + + + +
    + Person icon +
    +
    +
    + + + + +
    +
    +
    + ); +} diff --git a/src/app/onboarding/provider/features/page.module.css b/src/app/onboarding/provider/features/page.module.css new file mode 100644 index 0000000..f733c47 --- /dev/null +++ b/src/app/onboarding/provider/features/page.module.css @@ -0,0 +1,5 @@ +.buttons { + position: fixed; + bottom: 5%; + right: 5%; +} diff --git a/src/app/onboarding/provider/features/page.tsx b/src/app/onboarding/provider/features/page.tsx new file mode 100644 index 0000000..1b78fa7 --- /dev/null +++ b/src/app/onboarding/provider/features/page.tsx @@ -0,0 +1,26 @@ +import Layout from "@/components/Layout"; +import SelectableItem from "../../_components/SelectableItem"; +import styles from "./page.module.css"; +import Link from "next/link"; + +export default function Page() { + return ( + + + +
    + + + +
    +
    + ); +} diff --git a/src/app/onboarding/provider/page.module.css b/src/app/onboarding/provider/page.module.css new file mode 100644 index 0000000..939d38c --- /dev/null +++ b/src/app/onboarding/provider/page.module.css @@ -0,0 +1,40 @@ +.buttons { + display: flex; + justify-content: center; + width: 95dvw; +} + +.buttons a { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + color: var(--lm-black); + text-decoration: none; + + background-color: var(--dm-white); + height: 400px; + width: 350px; + text-align: center; + margin: 20px; + border: 3px solid var(--lm-blue); + border-radius: 10px; +} + +.buttons p, +.buttons strong { + color: var(--lm-black); + margin: 0; +} + +.icon { + width: 150px; + margin: 20px; +} + +@media (prefers-color-scheme: dark) { + .buttons a { + color: var(--dm-white); + } +} diff --git a/src/app/onboarding/provider/page.tsx b/src/app/onboarding/provider/page.tsx new file mode 100644 index 0000000..4e79eda --- /dev/null +++ b/src/app/onboarding/provider/page.tsx @@ -0,0 +1,38 @@ +"use client"; +import Layout from "@/components/Layout"; +import styles from "./page.module.css"; +import Link from "next/link"; +import { completeOnboarding } from "@/utils/settings"; + +export default function Page() { + return ( + +
    + + Business building icon + A healthcare provider +

    Add branding to the software.

    + + { + await completeOnboarding("provider"); + }} + href="/onboarding/" + > + Person icon + A patient or caregiver +

    Add the patient's details.

    + +
    +
    + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 830c40e..1c03f6f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -47,9 +47,12 @@ export default function Hub() { try { await setupOnboarding(); const c = await getConfig(); + const providerConfig = await getConfig("provider"); setOnboardingCompleted(c.onboarding_completed); - if (!c.onboarding_completed) { + if (!providerConfig.onboarding_completed) { + router.push("/onboarding/provider"); + } else if (!c.onboarding_completed) { router.push("/onboarding"); } diff --git a/src/utils/folders.ts b/src/utils/folders.ts index bfb7358..d477c42 100644 --- a/src/utils/folders.ts +++ b/src/utils/folders.ts @@ -15,3 +15,8 @@ export async function userConfigDir() { const base = await appConfigDir(); return await join(base, "user"); } + +export async function providerConfigDir() { + const base = await appConfigDir(); + return await join(base, "provider"); +} diff --git a/src/utils/settings.ts b/src/utils/settings.ts index 3617a37..3766aa1 100644 --- a/src/utils/settings.ts +++ b/src/utils/settings.ts @@ -1,5 +1,5 @@ import { Config } from "@/types/settings"; -import { userConfigDir } from "./folders"; +import { userConfigDir, providerConfigDir } from "./folders"; import { exists, mkdir, @@ -8,8 +8,12 @@ import { } from "@tauri-apps/plugin-fs"; import { join } from "@tauri-apps/api/path"; -export async function getConfig(): Promise { - const configDir = await userConfigDir(); +export async function getConfig( + of = "user" as "user" | "provider", +): Promise { + const uConfigDir = await userConfigDir(); + const pConfigDir = await providerConfigDir(); + const configDir = of === "user" ? uConfigDir : pConfigDir; const configPath = await join(configDir, "config.json"); const configExists = await exists(configPath); @@ -32,8 +36,9 @@ export async function getConfig(): Promise { } } -export async function completeOnboarding() { - const configDir = await userConfigDir(); +export async function completeOnboarding(of = "user" as "user" | "provider") { + const configDir = + of === "user" ? await userConfigDir() : await providerConfigDir(); const configPath = await join(configDir, "config.json"); const config = await getConfig();