Skip to content

Commit

Permalink
Quest Experience: Add Modal for creating & updating prompts (#229)
Browse files Browse the repository at this point in the history
* feat: added a modal for onboarding flow on the web ✨

* feat: completed the ui for how the modal should look :saprkles:

* feat: made use of usecontext for passing down data in the modals ✨

* feat: displaying added prompts on the quest page+ using the prompts to save and edit quests ✨

* feat: added buttons for the user to edit and delete individual prompts  ✨

* operations: linting ♻️

* operations: linting ♻️

* 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 <[email protected]>
  • Loading branch information
abena07 and oreHGA authored Jul 12, 2024
1 parent 6a099e6 commit 00a354c
Show file tree
Hide file tree
Showing 29 changed files with 646 additions and 119 deletions.
2 changes: 2 additions & 0 deletions frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
{
Expand Down
10 changes: 5 additions & 5 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
};
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,4 @@
"tailwindcss": "^3.3.3",
"typescript": "5.1.6"
}
}
}
43 changes: 43 additions & 0 deletions frontend/src/@types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Prompt, "notificationConfig_days" | "uuid"> & {
uuid?: string | null;
notificationConfig_days: NotificationConfigDays;
};

export type Days = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";

export type NotificationConfigDays = Record<Days, boolean>;

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[];
}
2 changes: 1 addition & 1 deletion frontend/src/@types/nextauth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/components/charts/line-chart.tsx
Original file line number Diff line number Diff line change
@@ -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[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { BlogSection } from "./blog-section";

// This is the metadata for your story
const meta: Meta<typeof BlogSection> = {
title: "UI/Landing/BlogSection",
component: BlogSection,
title: "UI/Landing/BlogSection",
component: BlogSection,
};

export default meta;

type Story = StoryObj<typeof BlogSection>;

export const Default: Story = {
args: {},
args: {},
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ export function BlogSection({ posts }: any) {
};
const cardStyles = {
"&:hover": {
transform: "none",
}
};
transform: "none",
},
};

return (
<section title="News and Updates" aria-labelledby="" className="font-body mb-24">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/features/landing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
export * from "./blog-section/blog-section";
6 changes: 5 additions & 1 deletion frontend/src/components/lab/experiment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ export const Experiment: FC<IExperiment> = (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;
Expand Down
163 changes: 163 additions & 0 deletions frontend/src/components/quest/addprompts.tsx
Original file line number Diff line number Diff line change
@@ -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<AddPromptModalProps> = ({ prompt, setPrompt, onSave, onClose }) => {
const [promptText, setPromptText] = useState(prompt.promptText);
const [customOptions, setCustomOptions] = useState<string[]>(
prompt.additionalMeta.customOptionText ? prompt.additionalMeta.customOptionText.split(";") : []
);
const [responseType, setResponseType] = useState<PromptResponseType | null>(prompt.responseType);
const [category, setCategory] = useState<string | null>(prompt.additionalMeta.category ?? null);
const [countPerDay, setCountPerDay] = useState<number | undefined>();
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 (
<Dialog open={true} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Configure Prompt</DialogTitle>
</DialogHeader>
<DialogDescription>
{/* Choose Prompt Category */}
<div className="list-disc mt-2">
<label
htmlFor="categorySelect"
className="my-2 block text-sm font-medium text-gray-900 dark:text-white mt-4"
>
Category
</label>
<select
id="categorySelect"
value={category!}
onChange={(e) => {
const selectedCategory = e.target.value;
setCategory(selectedCategory);
console.log("Selected category: ", selectedCategory);
}}
className="block w-full 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"
>
<option value="" disabled>
Select a category
</option>
{categories.map((item, index) => (
<option key={index} value={item.name}>
{item.name}
</option>
))}
</select>
</div>

{/* Prompt Details Component - Text, Response Type, */}
<div className="mt-4">
<Input
label="Prompt Text"
type="text"
size="md"
fullWidth
placeholder="eg : are you feeling energetic"
value={promptText}
onChange={(e) => setPromptText(e.target.value)}
/>
</div>

<label htmlFor="activity" className="my-2 block text-sm font-medium text-gray-900 dark:text-white mt-4">
Response Type:
<select
value={responseType ?? ""}
onChange={(e) => {
setResponseType(e.target.value as PromptResponseType);
}}
id="activity"
className="block w-full 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"
>
<option value="" disabled>
Select a response type
</option>
{responseTypes.map((item, index) => (
<option key={index} value={item.value}>
{item.label}
</option>
))}
</select>
{/* TODO: Add Custom Options Component */}
{responseType === "customOptions" && (
<div>
<Input
label="Custom Options"
type="text"
size="md"
fullWidth
placeholder="eg: Good;Bad;Neutral"
value={customOptions.join(";")}
onChange={(e) => setCustomOptions(e.target.value.split(";"))}
/>

<div className="flex flex-wrap gap-2 mt-2">
{customOptions.map((option, index) => (
<span
key={index}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 border border-blue-800"
>
{option}
</span>
))}
</div>
</div>
)}
</label>

{/* Add Times Component */}
<TimePicker start={start} setStart={setStart} end={end} setEnd={setEnd} days={days} setDays={setDays} />
</DialogDescription>
<div className="mt-4 flex justify-end gap-4">
<Button disabled={false} onClick={updatePrompt}>
Save Prompt
</Button>
</div>
</DialogContent>
</Dialog>
);
};

export default AddPromptModal;
Loading

0 comments on commit 00a354c

Please sign in to comment.