From 00a354c3c812e866b44788745449ab4ddd9c7dfd Mon Sep 17 00:00:00 2001 From: Bennett-Eghan <62352130+abena07@users.noreply.github.com> Date: Fri, 12 Jul 2024 18:02:05 +0000 Subject: [PATCH] Quest Experience: Add Modal for creating & updating prompts (#229) * feat: added a modal for onboarding flow on the web :sparkles: * feat: completed the ui for how the modal should look :saprkles: * feat: made use of usecontext for passing down data in the modals :sparkles: * feat: displaying added prompts on the quest page+ using the prompts to save and edit quests :sparkles: * feat: added buttons for the user to edit and delete individual prompts :sparkles: * operations: linting :recycle: * operations: linting :recycle: * wip: debugging why the added prompts are not saved after refresh and also why we the prompts are not specific to quests * add: prompt interface * feat: (wip) move add prompt ui to single modal * fix: prompt create with single view, pass prompt as prop * feat: make 'prompts' a parameter in quest `config` schema * (temp): re-add configs for env build * nit: remove comment * fix: coalesce responseType * fix: update type cast for window.nostr to work --------- Co-authored-by: Ore Ogundipe --- frontend/.eslintrc.js | 2 + frontend/next.config.js | 10 +- frontend/package.json | 2 +- frontend/src/@types/index.ts | 43 ++++ frontend/src/@types/nextauth.d.ts | 2 +- frontend/src/components/charts/line-chart.tsx | 5 +- .../blog-section/blog-section.stories.tsx | 6 +- .../landing/blog-section/blog-section.tsx | 6 +- .../src/components/features/landing/index.ts | 2 +- frontend/src/components/lab/experiment.tsx | 6 +- frontend/src/components/quest/addprompts.tsx | 163 +++++++++++++++ frontend/src/components/quest/timepicker.tsx | 111 ++++++++++ frontend/src/components/quest/utils.ts | 10 + frontend/src/config/data.ts | 91 +++++++++ .../useNeurosityState/useNeurosityState.ts | 2 +- frontend/src/pages/_app.tsx | 24 +-- frontend/src/pages/auth/login.tsx | 4 +- frontend/src/pages/blog/[slug].tsx | 6 +- frontend/src/pages/blog/index.tsx | 2 +- frontend/src/pages/playground.tsx | 36 ++-- frontend/src/pages/profile.tsx | 2 +- frontend/src/pages/quests.tsx | 189 ++++++++++++++---- frontend/src/pages/recordings.tsx | 2 - frontend/src/pages/research.tsx | 2 +- frontend/src/pages/zuzalu-qf.tsx | 13 +- frontend/src/services/auth.service.ts | 10 +- frontend/src/utils/appInsights.ts | 10 +- frontend/src/utils/auth.ts | 2 +- frontend/src/utils/index.ts | 2 +- 29 files changed, 646 insertions(+), 119 deletions(-) create mode 100644 frontend/src/components/quest/addprompts.tsx create mode 100644 frontend/src/components/quest/timepicker.tsx create mode 100644 frontend/src/components/quest/utils.ts create mode 100644 frontend/src/config/data.ts diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 9fb3b511..4e21709a 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -30,6 +30,8 @@ module.exports = defineConfig({ rules: { "react/react-in-jsx-scope": "off", "unused-imports/no-unused-imports": "error", + "@next/next/no-document-import-in-page": "off", + "import/order": [ 1, { diff --git a/frontend/next.config.js b/frontend/next.config.js index 5fda552c..216f5ead 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -2,8 +2,8 @@ module.exports = { reactStrictMode: true, env: { - "NEXT_PUBLIC_FUSION_NOSTR_PUBLIC_KEY": "5f3a52d8027cdde03a41857e98224dafd69495204d93071199aa86921aa02674", - "NEXT_PUBLIC_FUSION_RELAY_URL": "wss://relay.usefusion.ai", - "NEXT_PUBLIC_NEUROFUSION_BACKEND_URL": "https://neurofusionbackendprd.azurewebsites.net" - } -} + NEXT_PUBLIC_FUSION_NOSTR_PUBLIC_KEY: "5f3a52d8027cdde03a41857e98224dafd69495204d93071199aa86921aa02674", + NEXT_PUBLIC_FUSION_RELAY_URL: "wss://relay.usefusion.ai", + NEXT_PUBLIC_NEUROFUSION_BACKEND_URL: "https://neurofusionbackendprd.azurewebsites.net", + }, +}; diff --git a/frontend/package.json b/frontend/package.json index eed7f7e1..fca49bc3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -93,4 +93,4 @@ "tailwindcss": "^3.3.3", "typescript": "5.1.6" } -} +} \ No newline at end of file diff --git a/frontend/src/@types/index.ts b/frontend/src/@types/index.ts index c2eda636..e1f5fc29 100644 --- a/frontend/src/@types/index.ts +++ b/frontend/src/@types/index.ts @@ -111,3 +111,46 @@ export interface DisplayCategory { name: string; value: string; } + +export type PromptResponseType = "text" | "yesno" | "number" | "customOptions"; + +export interface Prompt { + uuid: string; + promptText: string; + responseType: PromptResponseType; + notificationConfig_days: NotificationConfigDays; + notificationConfig_startTime: string; + notificationConfig_endTime: string; + notificationConfig_countPerDay: number; + additionalMeta: PromptAdditionalMeta; +} + +export type PromptAdditionalMeta = { + category?: string; + isNotificationActive?: boolean; + customOptionText?: string; // ; separated list of options + questId?: string; +}; + +export type CreatePrompt = Omit & { + uuid?: string | null; + notificationConfig_days: NotificationConfigDays; +}; + +export type Days = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday"; + +export type NotificationConfigDays = Record; + +export interface IQuest { + title: string; + description: string; + config: string; + guid: string; + userGuid: string; + createdAt: string; + updatedAt: string; + joinCode: string; + organizerName?: string; + participants?: string[]; + prompts: Prompt[]; +} diff --git a/frontend/src/@types/nextauth.d.ts b/frontend/src/@types/nextauth.d.ts index 816b77f9..956d6c41 100644 --- a/frontend/src/@types/nextauth.d.ts +++ b/frontend/src/@types/nextauth.d.ts @@ -13,7 +13,7 @@ declare module "next-auth/jwt" { } declare module "next-auth" { - interface User extends IUser { } + interface User extends IUser {} interface Session extends DefaultSession { user?: User; } diff --git a/frontend/src/components/charts/line-chart.tsx b/frontend/src/components/charts/line-chart.tsx index 6965fca5..c60ce1ff 100644 --- a/frontend/src/components/charts/line-chart.tsx +++ b/frontend/src/components/charts/line-chart.tsx @@ -1,9 +1,8 @@ import dayjs from "dayjs"; -const ReactEcharts = dynamic(() => import('echarts-for-react'), { ssr: false }); +const ReactEcharts = dynamic(() => import("echarts-for-react"), { ssr: false }); import React, { FC } from "react"; import { DisplayCategory, FusionQuestDataset } from "~/@types"; -import dynamic from 'next/dynamic'; - +import dynamic from "next/dynamic"; export interface LineChartProps { seriesData: FusionQuestDataset[]; diff --git a/frontend/src/components/features/landing/blog-section/blog-section.stories.tsx b/frontend/src/components/features/landing/blog-section/blog-section.stories.tsx index 179229b1..fedd8833 100644 --- a/frontend/src/components/features/landing/blog-section/blog-section.stories.tsx +++ b/frontend/src/components/features/landing/blog-section/blog-section.stories.tsx @@ -3,8 +3,8 @@ import { BlogSection } from "./blog-section"; // This is the metadata for your story const meta: Meta = { - title: "UI/Landing/BlogSection", - component: BlogSection, + title: "UI/Landing/BlogSection", + component: BlogSection, }; export default meta; @@ -12,5 +12,5 @@ export default meta; type Story = StoryObj; export const Default: Story = { - args: {}, + args: {}, }; diff --git a/frontend/src/components/features/landing/blog-section/blog-section.tsx b/frontend/src/components/features/landing/blog-section/blog-section.tsx index 6993ba9e..694a678b 100644 --- a/frontend/src/components/features/landing/blog-section/blog-section.tsx +++ b/frontend/src/components/features/landing/blog-section/blog-section.tsx @@ -9,9 +9,9 @@ export function BlogSection({ posts }: any) { }; const cardStyles = { "&:hover": { - transform: "none", - } - }; + transform: "none", + }, + }; return (
diff --git a/frontend/src/components/features/landing/index.ts b/frontend/src/components/features/landing/index.ts index 4e75f839..4ecb1eb5 100644 --- a/frontend/src/components/features/landing/index.ts +++ b/frontend/src/components/features/landing/index.ts @@ -5,4 +5,4 @@ export * from "./offering-section/offering-section"; export * from "./faq-section/faq-section"; export * from "./testimonials-section/testimonials-section"; export * from "./team-section/team-section"; -export * from "./blog-section/blog-section" \ No newline at end of file +export * from "./blog-section/blog-section"; diff --git a/frontend/src/components/lab/experiment.tsx b/frontend/src/components/lab/experiment.tsx index 1d179ebb..706176b3 100644 --- a/frontend/src/components/lab/experiment.tsx +++ b/frontend/src/components/lab/experiment.tsx @@ -100,7 +100,11 @@ export const Experiment: FC = (experiment) => { window.addEventListener("message", (event) => { // IMPORTANT: Check the origin of the data! // You should probably not use '*', but restrict it to certain domains: - if (event.origin.startsWith("https://localhost:") || event.origin.startsWith("https://usefusion.app") || event.origin.startsWith("https://usefusion.ai")) { + if ( + event.origin.startsWith("https://localhost:") || + event.origin.startsWith("https://usefusion.app") || + event.origin.startsWith("https://usefusion.ai") + ) { console.log("event", event); if (typeof event.data === "string") { return; diff --git a/frontend/src/components/quest/addprompts.tsx b/frontend/src/components/quest/addprompts.tsx new file mode 100644 index 00000000..096fb808 --- /dev/null +++ b/frontend/src/components/quest/addprompts.tsx @@ -0,0 +1,163 @@ +import { useContext, useEffect, useState } from "react"; +import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Input } from "~/components/ui"; +import { categories, promptSelectionDays, responseTypes } from "~/config/data"; +import { Prompt, PromptResponseType } from "~/@types"; +import { TimePicker } from "./timepicker"; +import dayjs from "dayjs"; + +interface AddPromptModalProps { + prompt: Prompt; + setPrompt: (prompt: Prompt) => void; + onSave: (prompt: Prompt) => void; + onClose: () => void; +} + +export function getDayjsFromTimeString(timeString: string) { + // time is in the format "HH:mm", split up and convert to a dayjs object + const time = timeString.split(":"); + const hour = parseInt(time[0], 10); + const minute = parseInt(time[1], 10); + + return dayjs().startOf("day").add(hour, "hour").add(minute, "minute"); +} + +const AddPromptModal: React.FC = ({ prompt, setPrompt, onSave, onClose }) => { + const [promptText, setPromptText] = useState(prompt.promptText); + const [customOptions, setCustomOptions] = useState( + prompt.additionalMeta.customOptionText ? prompt.additionalMeta.customOptionText.split(";") : [] + ); + const [responseType, setResponseType] = useState(prompt.responseType); + const [category, setCategory] = useState(prompt.additionalMeta.category ?? null); + const [countPerDay, setCountPerDay] = useState(); + const [days, setDays] = useState(prompt.notificationConfig_days); + const [start, setStart] = useState(getDayjsFromTimeString("08:00")); + const [end, setEnd] = useState(getDayjsFromTimeString("22:00")); + + const updatePrompt = () => { + const updatedPrompt = { + ...prompt, + promptText, + responseType: responseType ?? "text", // Default to "text" if null + notificationConfig_days: days, + notificationConfig_startTime: start.format("HH:mm"), + notificationConfig_endTime: end.format("HH:mm"), + notificationConfig_countPerDay: countPerDay ?? 1, // Default to 1 if undefined + additionalMeta: { + category: category ?? "", + customOptionText: customOptions.join(";"), + }, // Initialize with an empty object + }; + console.log("updatedPrompt", updatedPrompt); + setPrompt(updatedPrompt); + onSave(updatedPrompt); + }; + + return ( + + + + Configure Prompt + + + {/* Choose Prompt Category */} +
+ + +
+ + {/* Prompt Details Component - Text, Response Type, */} +
+ setPromptText(e.target.value)} + /> +
+ + + + {/* Add Times Component */} + +
+
+ +
+
+
+ ); +}; + +export default AddPromptModal; diff --git a/frontend/src/components/quest/timepicker.tsx b/frontend/src/components/quest/timepicker.tsx new file mode 100644 index 00000000..541c0c0d --- /dev/null +++ b/frontend/src/components/quest/timepicker.tsx @@ -0,0 +1,111 @@ +import { Button, Dialog, DialogContent, DialogDescription, DialogTitle, Input } from "~/components/ui"; +import { useContext, useEffect, useState } from "react"; +import { NotificationConfigDays } from "~/@types"; +import dayjs from "dayjs"; +import { promptFrequencyData, promptSelectionDays } from "~/config/data"; +import { getDayjsFromTimeString } from "./utils"; + +export type TimePickerProps = { + start: dayjs.Dayjs; + setStart: (time: dayjs.Dayjs) => void; + end: dayjs.Dayjs; + setEnd: (time: dayjs.Dayjs) => void; + days?: NotificationConfigDays; + setDays: (days: NotificationConfigDays) => void; + setPromptCount?: (count: number) => void; + defaultPromptFrequencyLabel?: string | null; +}; + +export const TimePicker: React.FC = ({ + start, + setStart, + end, + setEnd, + days = promptSelectionDays, + setDays, + setPromptCount, + defaultPromptFrequencyLabel, +}) => { + const [frequency, setFrequency] = useState("1"); + + // support for choosing a single time + const [isSingleTime, setIsSingleTime] = useState(false); + useEffect(() => { + if (frequency === "1") { + setIsSingleTime(true); + setEnd(start.add(2, "minute")); + } else { + setIsSingleTime(false); + } + }, [frequency, start]); + + return ( + <> +
+ + + +
+ + setStart(getDayjsFromTimeString(e.target.value))} + className="block w-full mt-2 rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-indigo-500 dark:focus:ring-indigo-500" + /> +
+ + {!isSingleTime && ( +
+ + setEnd(getDayjsFromTimeString(e.target.value))} + className="block w-full mt-2 rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-indigo-500 dark:focus:ring-indigo-500" + /> +
+ )} + + +
+ + ); +}; diff --git a/frontend/src/components/quest/utils.ts b/frontend/src/components/quest/utils.ts new file mode 100644 index 00000000..9d068293 --- /dev/null +++ b/frontend/src/components/quest/utils.ts @@ -0,0 +1,10 @@ +import dayjs from "dayjs"; + +export function getDayjsFromTimeString(timeString: string) { + // time is in the format "HH:mm", split up and convert to a dayjs object + const time = timeString.split(":"); + const hour = parseInt(time[0], 10); + const minute = parseInt(time[1], 10); + + return dayjs().startOf("day").add(hour, "hour").add(minute, "minute"); +} diff --git a/frontend/src/config/data.ts b/frontend/src/config/data.ts new file mode 100644 index 00000000..05f830f3 --- /dev/null +++ b/frontend/src/config/data.ts @@ -0,0 +1,91 @@ +export const categories = [ + { + name: "Mental Health", + color: "#FFC0CB", + icon: "🧠", + }, + { + name: "Productivity", + color: "#FFD700", + icon: "👩‍💻", + }, + { + name: "Relationships", + color: "#00FFFF", + icon: "👫", + }, + { + name: "Health and Fitness", + color: "#00FF00", + icon: "🏃", + }, + { + name: "Spiritual Practice", + color: "#FFA500", + icon: "🧘", + }, + { + name: "Self-Care", + color: "#", + icon: "🧖", + }, + { + name: "Finance", + color: "#FF0000", + icon: "💸", + }, + { + name: "Personal Interest", + color: "#800080", + icon: "🤩", + }, + { + name: "Other", + color: "#000000", + icon: "📁", + }, +]; + +export const responseTypes = [ + { label: "Yes/No", value: "yesno" }, + { label: "Text", value: "text" }, + { label: "Number", value: "number" }, + { label: "Custom Options", value: "customOptions" }, +]; + +export const promptFrequencyData = [ + { + label: "Once", + value: "1", + }, + { + label: "Every 30 minutes", + value: "30", + }, + { + label: "Every hour", + value: "60", + }, + { + label: "Every two hours", + value: "120", + }, + { + label: "Every three hours", + value: "180", + }, + { + label: "Every four hours", + value: "240", + }, +]; + +export const promptSelectionDays = { + monday: true, + tuesday: true, + wednesday: true, + thursday: true, + friday: true, + saturday: true, + sunday: true, +}; diff --git a/frontend/src/hooks/useNeurosityState/useNeurosityState.ts b/frontend/src/hooks/useNeurosityState/useNeurosityState.ts index 0ca10527..0210cd47 100644 --- a/frontend/src/hooks/useNeurosityState/useNeurosityState.ts +++ b/frontend/src/hooks/useNeurosityState/useNeurosityState.ts @@ -73,7 +73,7 @@ export function useNeurosityState() { }, params: { redirectUri: window.location.origin + "/neurosity-callback", - } + }, }); console.log("Got response from API"); diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index d2ad1f53..0dad43d4 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -3,7 +3,7 @@ import type { AppProps } from "next/app"; import { SessionProvider } from "next-auth/react"; import { ThemeProvider } from "next-themes"; import React from "react"; -import { AppInsightsContext } from '@microsoft/applicationinsights-react-js'; +import { AppInsightsContext } from "@microsoft/applicationinsights-react-js"; import { QUERY_OPTIONS_DEFAULT } from "~/config"; import "../styles/globals.css"; import { gtw } from "~/utils"; @@ -36,17 +36,17 @@ button { - - - - -
- -
-
-
-
-
+ + + + +
+ +
+
+
+
+
); diff --git a/frontend/src/pages/auth/login.tsx b/frontend/src/pages/auth/login.tsx index 21f485c3..8679b2a1 100644 --- a/frontend/src/pages/auth/login.tsx +++ b/frontend/src/pages/auth/login.tsx @@ -50,7 +50,7 @@ const LoginPage = React.memo(() => { }; const useExistingAccount = async (privateKey: string) => { - appInsights.trackEvent({ name: 'use_existing_account', properties: { customProperty: 'value' } }); + appInsights.trackEvent({ name: "use_existing_account", properties: { customProperty: "value" } }); try { if (privateKey.length !== 64) { return; @@ -152,4 +152,4 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { return { props: { session }, }; -}; \ No newline at end of file +}; diff --git a/frontend/src/pages/blog/[slug].tsx b/frontend/src/pages/blog/[slug].tsx index 9b4173d4..1910eabf 100644 --- a/frontend/src/pages/blog/[slug].tsx +++ b/frontend/src/pages/blog/[slug].tsx @@ -45,7 +45,6 @@ export async function getStaticProps({ params }: any) { function BlogPost({ frontMatter, markdownBody, otherArticles }: any) { const [zoomedImage, setZoomedImage] = useState(null); - useEffect(() => { const images: NodeListOf = document.querySelectorAll("img[data-zoomable]"); @@ -67,7 +66,6 @@ function BlogPost({ frontMatter, markdownBody, otherArticles }: any) { }); }; }, []); - if (!frontMatter) return <>; @@ -98,7 +96,9 @@ function BlogPost({ frontMatter, markdownBody, otherArticles }: any) { />

{frontMatter.title}

-

By {frontMatter.authors[0].name}

+

+ By {frontMatter.authors[0].name} +

{dayjs(frontMatter.publishedDate).format("MMM DD, YYYY")}

diff --git a/frontend/src/pages/blog/index.tsx b/frontend/src/pages/blog/index.tsx index 309a5fda..15b4f01f 100644 --- a/frontend/src/pages/blog/index.tsx +++ b/frontend/src/pages/blog/index.tsx @@ -9,7 +9,7 @@ export async function getStaticProps() { return { props: { posts, - title: "NeuroFusion Blog", + title: "Blog | NeuroFusion", description: "Updates on our products & research.", }, }; diff --git a/frontend/src/pages/playground.tsx b/frontend/src/pages/playground.tsx index def9e8a2..424a6b3c 100644 --- a/frontend/src/pages/playground.tsx +++ b/frontend/src/pages/playground.tsx @@ -1,4 +1,3 @@ - import { GetServerSideProps, NextPage } from "next"; import { getServerSession } from "next-auth"; import React, { useState, useEffect } from "react"; @@ -39,15 +38,12 @@ const PlaygroundPage: NextPage = () => { const handleCloseCapabilities = () => { setShowCapabilitiesModal(false); - } + }; const handleCloseDataHandling = () => { - setShowDataHandlingModal(false) - } - // const handleRemoveOnboarding = () => { - // localStorage.removeItem("viewedOnboarding"); - // setShowCapabilitiesModal(true); - // }; + setShowDataHandlingModal(false); + }; + const experiments: IExperiment[] = [ { id: 3, @@ -131,7 +127,7 @@ const PlaygroundPage: NextPage = () => { {
{showCapabilitiesModal && ( - + )} {showDataHandlingModal && ( - + )}
@@ -186,17 +183,16 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { } return { - props: { session}, + props: { session }, }; }; interface CapabilitiesModalProps { onNext: () => void; onCancel: () => void; - } -const CapabilitiesModal: React.FC = ({ onNext, onCancel, }) => { +const CapabilitiesModal: React.FC = ({ onNext, onCancel }) => { return ( @@ -209,7 +205,9 @@ const CapabilitiesModal: React.FC = ({ onNext, onCancel,
- + @@ -222,7 +220,7 @@ const CapabilitiesModal: React.FC = ({ onNext, onCancel, interface DataHandlingModalProps { onPrevious: () => void; onGetStarted: () => void; - onClose : () =>void; + onClose: () => void; } const DataHandlingModal: React.FC = ({ onPrevious, onGetStarted, onClose }) => { diff --git a/frontend/src/pages/profile.tsx b/frontend/src/pages/profile.tsx index 2f86dd19..cad38fe8 100644 --- a/frontend/src/pages/profile.tsx +++ b/frontend/src/pages/profile.tsx @@ -15,7 +15,7 @@ const AccountPage: NextPage = () => { const [confirmDeleteAccount, setConfirmDeleteAccount] = React.useState(false); const deleteAccount = () => { - appInsights.trackEvent({ name: 'delete_account', properties: { customProperty: 'value' } }); + appInsights.trackEvent({ name: "delete_account", properties: { customProperty: "value" } }); deletePrivateKey(); signOut(); }; diff --git a/frontend/src/pages/quests.tsx b/frontend/src/pages/quests.tsx index 68d45982..8afdf2ef 100644 --- a/frontend/src/pages/quests.tsx +++ b/frontend/src/pages/quests.tsx @@ -1,26 +1,15 @@ import { GetServerSideProps, NextPage } from "next"; import { getServerSession } from "next-auth"; -import React from "react"; - +import React, { useState, useContext } from "react"; import { authOptions } from "./api/auth/[...nextauth]"; import { DashboardLayout, Meta } from "~/components/layouts"; import { Button, Dialog, DialogContent, DialogDescription, DialogTitle, Input } from "~/components/ui"; import { api } from "~/config"; import { useSession } from "next-auth/react"; import Link from "next/link"; - -interface IQuest { - title: string; - description: string; - config: string; - guid: string; - userGuid: string; - createdAt: string; - updatedAt: string; - joinCode: string; - organizerName?: string; - participants?: string[]; // userNpubs -} +import AddPromptModal from "~/components/quest/addprompts"; +import { IQuest, Prompt } from "~/@types"; +import { promptSelectionDays } from "~/config/data"; const QuestsPage: NextPage = () => { const session = useSession(); @@ -32,17 +21,17 @@ const QuestsPage: NextPage = () => { const [displayShareModal, setDisplayShareModal] = React.useState(false); const [savedQuests, setSavedQuests] = React.useState([]); const [activeQuest, setActiveQuest] = React.useState(null); + const [questSubscribers, setQuestSubscribers] = React.useState([]); const saveQuest = async () => { try { - // if activeView "edit" const res = await api.post( "/quest", { title: questTitle, description: questDescription, organizerName: questOrganizer, - config: questConfig, + config: JSON.stringify({ prompts: prompts }), }, { headers: { @@ -54,8 +43,9 @@ const QuestsPage: NextPage = () => { if (res.status === 201) { console.log("Quest saved successfully"); console.log(res.data); - // send user to the quest detail page - setSavedQuests([...savedQuests, res.data.quest]); + setSavedQuests([...savedQuests, { ...res.data.quest, prompts: JSON.parse(res.data.quest.config) }]); // Parse config back to prompts array + + console.log(res.data.quest); setActiveView("view"); } else { console.error("Failed to save quest"); @@ -73,7 +63,7 @@ const QuestsPage: NextPage = () => { title: questTitle, description: questDescription, organizerName: questOrganizer, - config: questConfig, + config: JSON.stringify({ prompts: prompts }), guid: activeQuest?.guid, }, { @@ -86,7 +76,6 @@ const QuestsPage: NextPage = () => { if (res.status === 200) { console.log("Quest edited successfully"); console.log(res.data); - // update the entry for quest with guid from savedQuests const updatedQuests = savedQuests.map((quest) => { if (quest.guid === res.data.quest.guid) { return res.data.quest; @@ -95,6 +84,7 @@ const QuestsPage: NextPage = () => { }); setSavedQuests(updatedQuests); + setActiveView("view"); } else { console.error("Failed to edit quest"); @@ -123,21 +113,16 @@ const QuestsPage: NextPage = () => { } }; - const [questSubscribers, setQuestSubscribers] = React.useState([]); const getQuestSubscribers = async (questId: string) => { try { - const res = await api.get( - "/quest/subscribers", - - { - params: { - questId, - }, - headers: { - Authorization: `Bearer ${session.data?.user?.authToken}`, - }, - } - ); + const res = await api.get("/quest/subscribers", { + params: { + questId, + }, + headers: { + Authorization: `Bearer ${session.data?.user?.authToken}`, + }, + }); if (res.status === 200) { console.log("Quest Subscribers fetched successfully"); @@ -162,7 +147,6 @@ const QuestsPage: NextPage = () => { console.log("subscribers", subscribers); if (subscribers) { - // update activeQuest with participants setQuestSubscribers(subscribers); } else { setQuestSubscribers([]); @@ -171,6 +155,59 @@ const QuestsPage: NextPage = () => { })(); }, [activeQuest]); + const [prompts, setPrompts] = useState([]); + const [activePrompt, setActivePrompt] = useState(null); + const [displayAddPromptModal, setDisplayAddPromptModal] = useState(false); + const [editingPromptIndex, setEditingPromptIndex] = useState(null); + + const handleAddPromptModal = () => { + const newPrompt: Prompt = { + uuid: "", + promptText: "", + responseType: "text", // Assuming "text" is a valid PromptResponseType + notificationConfig_days: promptSelectionDays, + notificationConfig_startTime: "", + notificationConfig_endTime: "", + notificationConfig_countPerDay: 0, // Assuming 0 is a valid default value for countPerDay + additionalMeta: {}, + }; + setEditingPromptIndex(null); + setActivePrompt(newPrompt); + }; + + const handleEditPrompt = (index: number) => { + const promptToEdit = prompts[index]; + console.log("promptToEdit", promptToEdit); + setEditingPromptIndex(index); + setActivePrompt(promptToEdit); + }; + + const handleDeletePrompt = (index: number) => { + setPrompts((prevPrompts) => prevPrompts.filter((_, i) => i !== index)); + }; + + // preload the prompts from questConfig + React.useEffect(() => { + if (questConfig) { + console.log("questConfig", questConfig); + // make sure it's valid + // handle based on if it's an array (old way) / or object with prompt key + const parsedConfig = JSON.parse(questConfig); + if (Array.isArray(parsedConfig)) { + setPrompts(parsedConfig); + } else if (parsedConfig && parsedConfig.prompts && Array.isArray(parsedConfig.prompts)) { + setPrompts(parsedConfig.prompts); + } + } + }, [questConfig]); + + React.useEffect(() => { + console.log("activePrompt", activePrompt); + if (activePrompt) { + setDisplayAddPromptModal(true); + } + }, [activePrompt]); + return ( { description: "Create and manage quests for your participants to run. Wearables. Behavior Tracking. Health Data.", }} - />{" "} + />

Quests

- {/* Two buttons, one for create another for view */}
+
+ + {/* Prompt Cards */} + {prompts.length > 0 && ( +
+
+ {prompts.map((prompt, index) => ( +
+

{prompt.promptText}

+

+ Days: + {Object.keys(prompt.notificationConfig_days) + .filter( + (day) => + prompt.notificationConfig_days[day as keyof typeof prompt.notificationConfig_days] + ) + .join(", ")} +

+

+ Time: {prompt.notificationConfig_startTime} - {prompt.notificationConfig_endTime} +

+

Frequency: {prompt.notificationConfig_countPerDay}

+ +
+ + +
+
+ ))} +
+
+ )} +