Skip to content

Commit

Permalink
feat: edit quest & display quest info on mobile
Browse files Browse the repository at this point in the history
  • Loading branch information
oreHGA committed Apr 30, 2024
1 parent 8f3812d commit 361280e
Show file tree
Hide file tree
Showing 16 changed files with 429 additions and 150 deletions.
61 changes: 54 additions & 7 deletions frontend/src/pages/quests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@ const QuestsPage: NextPage = () => {
const [questTitle, setQuestTitle] = React.useState<string>("");
const [questDescription, setQuestDescription] = React.useState<string>("");
const [questConfig, setQuestConfig] = React.useState<string>("");
const [activeView, setActiveView] = React.useState<"create" | "view">("create");
const [activeView, setActiveView] = React.useState<"create" | "view" | "edit">("create");
const [displayShareModal, setDisplayShareModal] = React.useState<boolean>(false);
const [savedQuests, setSavedQuests] = React.useState<IQuest[]>([]);
const [activeQuest, setActiveQuest] = React.useState<IQuest | null>(null);

const saveQuest = async () => {
try {
// if activeView "edit"
const res = await api.post(
"/quests",
"/quest",
{
title: questTitle,
description: questDescription,
Expand All @@ -59,6 +60,37 @@ const QuestsPage: NextPage = () => {
}
};

const editQuest = async () => {
try {
const res = await api.post(
"/quest/edit",
{
title: questTitle,
description: questDescription,
config: questConfig,
guid: activeQuest?.guid,
},
{
headers: {
Authorization: `Bearer ${session.data?.user?.authToken}`,
},
}
);

if (res.status === 200) {
console.log("Quest edited successfully");
console.log(res.data);
// send user to the quest detail page
setSavedQuests([...savedQuests, res.data.quest]);
setActiveView("view");
} else {
console.error("Failed to edit quest");
}
} catch (error) {
console.error("Failed to edit quest", error);
}
};

const getSavedQuests = async () => {
try {
const res = await api.get("/quests", {
Expand Down Expand Up @@ -96,10 +128,10 @@ const QuestsPage: NextPage = () => {
View Quests
</Button>
<Button onClick={() => setActiveView("create")} intent={activeView == "create" ? "primary" : "integration"}>
Configure Quest
Create Quest
</Button>
</div>
{activeView === "create" && (
{["create", "edit"].includes(activeView) && (
<>
<p className="mb-5 mt-5 text-lg dark:text-slate-400">
Create a new quest that you want other participants to run
Expand Down Expand Up @@ -140,7 +172,19 @@ const QuestsPage: NextPage = () => {
onChange={(e) => setQuestConfig(e.target.value)}
/>

<Button type="submit" size="lg" fullWidth className="mt-4" onClick={saveQuest}>
<Button
type="submit"
size="lg"
fullWidth
className="mt-4"
onClick={async () => {
if (activeView === "edit") {
await editQuest();
} else {
await saveQuest();
}
}}
>
Save Quest
</Button>
</div>
Expand Down Expand Up @@ -179,7 +223,10 @@ const QuestsPage: NextPage = () => {
intent="ghost"
onClick={() => {
setActiveQuest(quest);
setActiveView("create");
setQuestTitle(quest.title);
setQuestDescription(quest.description);
setQuestConfig(quest.config);
setActiveView("edit");
}}
>
Edit
Expand All @@ -196,7 +243,7 @@ const QuestsPage: NextPage = () => {
<DialogContent>
<DialogTitle>Share Quest</DialogTitle>
<DialogDescription>
Share the code with participants to join this quest using the Fusion mobile app
Share the code with your participants to join this quest using the Fusion mobile app
</DialogDescription>
<div className="flex flex-col space-y-4">
<Input label="Join Code" type="text" size="lg" value={activeQuest?.joinCode} readOnly />
Expand Down
10 changes: 10 additions & 0 deletions mobile/src/@types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,13 @@ export interface StreakEntry {
timestamp: number;
score: number;
}

export interface Quest {
title: string;
description: string;
guid: string;
startDate?: number;
endDate?: number;
prompts?: Prompt[];
additionalMeta?: object;
}
1 change: 1 addition & 0 deletions mobile/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from "./chat-bubble";
export * from "./headers";
export * from "./modal";
export * from "./calendar-picker";
export * from "./quests";
2 changes: 2 additions & 0 deletions mobile/src/components/prompts/headers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export * from "./edit-prompt-header";
export * from "./quick-add-prompt-header";
export * from "./insights-header";
export * from "./home-header";
export * from "./quests-header";
export * from "./quest-detail-header";
32 changes: 32 additions & 0 deletions mobile/src/components/prompts/headers/quest-detail-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useNavigation } from "@react-navigation/native";
import React from "react";
import { Text, View } from "react-native";

import { Button } from "../../button";
import { LeftArrow, VerticalMenu } from "../../icons";

export const QuestDetailHeader = () => {
const navigation = useNavigation();

const handleGoBack = () => {
navigation.goBack();
};

return (
<View className="flex flex-row p-5 justify-between flex-nowrap bg-dark">
<Button
variant="ghost"
size="icon"
leftIcon={<LeftArrow width={32} height={32} />}
onPress={handleGoBack}
/>
<Text className="font-sans text-base text-white">Quest</Text>
<Button
variant="ghost"
size="icon"
leftIcon={<VerticalMenu />}
onPress={() => {}}
/>
</View>
);
};
33 changes: 33 additions & 0 deletions mobile/src/components/prompts/headers/quests-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import RNBottomSheet from "@gorhom/bottom-sheet";
import React from "react";
import { Text, View } from "react-native";

import { Button } from "../../button";

import { Plus } from "~/components/icons";
import { JoinQuestSheet } from "~/components/quests";

export const QuestsHeader = () => {
const bottomSheetRef = React.useRef<RNBottomSheet>(null);
const handleExpandSheet = React.useCallback(
() => bottomSheetRef.current?.expand(),
[]
);
return (
<View className="flex flex-row p-5 justify-between flex-nowrap bg-dark">
<Text className="font-sans-bold text-[26px] text-white">Quests</Text>

<View className="flex flex-row">
<Button
variant="ghost"
size="icon"
rounded
className="bg-white/10 ml-1"
leftIcon={<Plus />}
onPress={handleExpandSheet}
/>
</View>
<JoinQuestSheet bottomSheetRef={bottomSheetRef} />
</View>
);
};
1 change: 1 addition & 0 deletions mobile/src/components/quests/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./join-quest-sheet";
108 changes: 108 additions & 0 deletions mobile/src/components/quests/join-quest-sheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import RNBottomSheet from "@gorhom/bottom-sheet";
import { Portal } from "@gorhom/portal";
import { useNavigation } from "@react-navigation/native";
import axios from "axios";
import Constants from "expo-constants";
import * as WebBrowser from "expo-web-browser";
import { FC, RefObject, useContext, useState } from "react";
import { View, Pressable, Text, Alert } from "react-native";

import { BottomSheet } from "../bottom-sheet/bottom-sheet";
import { Input } from "../input";
import { PromptOption } from "../prompts/prompt-option/prompt-option";

import { Prompt } from "~/@types";
import { AccountContext } from "~/contexts";

interface AddPromptSheetProps {
bottomSheetRef: RefObject<RNBottomSheet>;
selectedCategory?: string;
}

export const JoinQuestSheet: FC<AddPromptSheetProps> = ({ bottomSheetRef }) => {
const accountContext = useContext(AccountContext);
const navigation = useNavigation();

let fusionBackendUrl = "";
if (Constants.expoConfig?.extra) {
fusionBackendUrl = Constants.expoConfig.extra.fusionBackendUrl;
}

const [joinCode, setJoinCode] = useState("");

const handleJoinQuest = async () => {
// call api to fetch the quest if it's still active
try {
const res = await axios.get(`${fusionBackendUrl}/api/quest/getByCode`, {
params: {
joinCode,
},
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accountContext?.userApiToken}`,
},
});

if (res.data) {
// navigate to quest screen
const questRes = res.data.quest;
console.log(questRes.config);
const questPrompts = JSON.parse(questRes.config) as Prompt[];
console.log("prompts object", questPrompts);
// build the quest object and navigate to the quest screen
bottomSheetRef.current?.close();
navigation.navigate("QuestNavigator", {
screen: "QuestDetailScreen",
params: {
quest: {
title: questRes.title,
description: questRes.description,
guid: questRes.guid,
prompts: questPrompts,
},
},
});
} else {
Alert.alert("Quest not found");
}
} catch (err) {
if (axios.isAxiosError(err) && err.response?.status === 404) {
Alert.alert("Join code is invalid. Please confirm & retry");
} else {
Alert.alert("An error occurred. Please try again later");
}
}
};

const _handlePressButtonAsync = async () => {
const result = await WebBrowser.openBrowserAsync(
"https://usefusion.ai/blog"
);
};

return (
<Portal>
<BottomSheet ref={bottomSheetRef} snapPoints={["42.5%"]}>
<View className="flex flex-1 w-full justify-center gap-y-10 flex-col p-5">
<View className="flex flex-col gap-y-2.5">
{/* add input ask user to enter code */}
<Input
value={joinCode}
onChangeText={setJoinCode}
label="Quests are currently invite only. Your organizer will send you one"
className="mb-5"
placeholder="Enter join code"
/>

<PromptOption text="Join Quest" onPress={handleJoinQuest} />
<Pressable onPress={_handlePressButtonAsync}>
<Text className="font-sans text-white underline">
Don't have a code?
</Text>
</Pressable>
</View>
</View>
</BottomSheet>
</Portal>
);
};
16 changes: 13 additions & 3 deletions mobile/src/navigation/quest-navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import {
createNativeStackNavigator,
} from "@react-navigation/native-stack";

import { Logo } from "~/components/logo";
import { Quest } from "~/@types";
import { QuestsHeader, QuestDetailHeader } from "~/components";
import { QuestDetailScreen } from "~/pages";
import { QuestsScreen } from "~/pages/quests";

export type QuestStackParamList = {
QuestsScreen: undefined;
QuestDetailScreen: {
quest: Quest;
};
};

export type HealthScreenNavigationProp =
export type QuestScreenNavigationProp =
NativeStackNavigationProp<QuestStackParamList>;

const Stack = createNativeStackNavigator<QuestStackParamList>();
Expand All @@ -21,7 +26,12 @@ export const QuestStack = () => {
<Stack.Screen
name="QuestsScreen"
component={QuestsScreen}
options={{ headerTitle: () => <Logo /> }}
options={{ header: () => <QuestsHeader /> }}
/>
<Stack.Screen
name="QuestDetailScreen"
component={QuestDetailScreen}
options={{ header: () => <QuestDetailHeader /> }}
/>
</Stack.Navigator>
);
Expand Down
11 changes: 6 additions & 5 deletions mobile/src/navigation/tab-navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { SvgProps } from "react-native-svg";
import { HomeStack } from "./home-navigator";
import { InsightsStack } from "./insights-navigator";
import { PromptStack } from "./prompt-navigator";
import { QuestStack } from "./quest-navigator";

import {
Home as HomeIcon,
Expand Down Expand Up @@ -68,11 +69,11 @@ const tabs: TabType[] = [
component: InsightsStack,
label: "Insights",
},
// {
// name: "QuestNavigator",
// component: QuestStack,
// label: "Quests",
// },
{
name: "QuestNavigator",
component: QuestStack,
label: "Quests",
},
// {
// name: "Community",
// component: CommunityStack,
Expand Down
4 changes: 3 additions & 1 deletion mobile/src/navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { CommunityStackParamList } from "./community-navigator";
import { HealthStackParamList } from "./health-navigator";
import { InsightsStackParamList } from "./insights-navigator";
import { PromptStackParamList } from "./prompt-navigator";
import { QuestStackParamList } from "./quest-navigator";

export type RootStackParamList = PromptStackParamList &
CommunityStackParamList &
HealthStackParamList &
InsightsStackParamList;
InsightsStackParamList &
QuestStackParamList;

export type RouteProp<T extends keyof RootStackParamList> = NRouteProp<
RootStackParamList,
Expand Down
2 changes: 2 additions & 0 deletions mobile/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export * from "./settings";
export * from "./chat";
export * from "./community";
export * from "./prompt-responses";
export * from "./quests";
export * from "./quest-detail";
Loading

0 comments on commit 361280e

Please sign in to comment.