Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Prevent task duplication when 'Save all tasks' is clicked multip… #998

Merged
merged 7 commits into from
Dec 22, 2024
341 changes: 194 additions & 147 deletions app/admin/quests/create/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"use client";

import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, {
useCallback,
useEffect,
useMemo,
useState,
useRef,
} from "react";
import styles from "@styles/admin.module.css";
import { useRouter } from "next/navigation";
import { AdminService } from "@services/authService";
Expand All @@ -23,6 +29,7 @@ import Typography from "@components/UI/typography/typography";
import { TEXT_TYPE } from "@constants/typography";

export default function Page() {
const isSaving = useRef(false);
const router = useRouter();
const [currentPage, setCurrentPage] = useState(0);
const [questId, setQuestId] = useState<number>(0);
Expand Down Expand Up @@ -247,161 +254,201 @@ export default function Page() {
}, [questInput, boostInput, nfturi]);

const handleCreateTask = useCallback(async () => {
setButtonLoading(true);
steps.map(async (step) => {
if (step.type === "Quiz") {
if (
step.data.quiz_name?.length === 0 ||
step.data.quiz_desc?.length === 0 ||
step.data.quiz_intro?.length === 0 ||
step.data.quiz_cta?.length === 0 ||
step.data.quiz_help_link?.length === 0
) {
showNotification("Please fill all fields for Quiz", "info");
return;
}
const response = await AdminService.createQuiz({
quest_id: questId,
name: step.data.quiz_name,
desc: step.data.quiz_desc,
intro: step.data.quiz_intro,
cta: step.data.quiz_cta,
help_link: step.data.quiz_help_link,
});
for (const question of step.data.questions) {
try {
await AdminService.createQuizQuestion({
quiz_id: response.quiz_id,
question: question.question,
options: question.options,
correct_answers: question.correct_answers,
});
} catch (error) {
console.error("Error executing promise:", error);
if (isSaving.current) return;
try {
isSaving.current = true;
setButtonLoading(true);
const unsavedSteps = steps.filter((step) => !step.data.id);
let failedQuestions = [];
for (const step of unsavedSteps) {
if (step.type === "Quiz") {
if (
step.data.quiz_name?.length === 0 ||
step.data.quiz_desc?.length === 0 ||
step.data.quiz_intro?.length === 0 ||
step.data.quiz_cta?.length === 0 ||
step.data.quiz_help_link?.length === 0
) {
showNotification("Please fill all fields for Quiz", "info");
continue;
}
}
}
if (step.type === "TwitterFw") {
if (
step.data.twfw_name?.length === 0 ||
step.data.twfw_desc?.length === 0 ||
step.data.twfw_username?.length === 0
) {
showNotification("Please fill all fields for Twitter Follow", "info");
return;
}
await AdminService.createTwitterFw({
quest_id: questId,
name: step.data.twfw_name,
desc: step.data.twfw_desc,
username: step.data.twfw_username,
});
} else if (step.type === "TwitterRw") {
if (
step.data.twrw_name?.length === 0 ||
step.data.twrw_desc?.length === 0 ||
step.data.twrw_post_link?.length === 0
) {
showNotification(
"Please fill all fields for Twitter Retweet",
"info"
);
return;
}
await AdminService.createTwitterRw({
quest_id: questId,
name: step.data.twrw_name,
desc: step.data.twrw_desc,
post_link: step.data.twrw_post_link,
});
} else if (step.type === "Discord") {
if (
step.data.dc_name?.length === 0 ||
step.data.dc_desc?.length === 0 ||
step.data.dc_invite_link?.length === 0 ||
step.data.dc_guild_id?.length === 0
) {
showNotification("Please fill all fields for Discord", "info");
return;
}
await AdminService.createDiscord({
quest_id: questId,
name: step.data.dc_name,
desc: step.data.dc_desc,
invite_link: step.data.dc_invite_link,
guild_id: step.data.dc_guild_id,
});
} else if (step.type === "Custom") {
if (
step.data.custom_name?.length === 0 ||
step.data.custom_desc?.length === 0 ||
step.data.custom_cta?.length === 0 ||
step.data.custom_href?.length === 0 ||
step.data.custom_api?.length === 0
) {
showNotification("Please fill all fields for Discord", "info");
return;
}
await AdminService.createCustom({
quest_id: questId,
name: step.data.custom_name,
desc: step.data.custom_desc,
cta: step.data.custom_cta,
href: step.data.custom_href,
api: step.data.custom_api,
});
} else if (step.type === "Domain") {
await AdminService.createDomain({
quest_id: questId,
name: step.data.domain_name,
desc: step.data.domain_desc,
});
} else if (step.type === "Balance") {
try {
await AdminService.createBalance({
const response = await AdminService.createQuiz({
quest_id: questId,
name: step.data.balance_name,
desc: step.data.balance_desc,
contracts: step.data.balance_contracts,
cta: step.data.balance_cta,
href: step.data.balance_href,
name: step.data.quiz_name,
desc: step.data.quiz_desc,
intro: step.data.quiz_intro,
cta: step.data.quiz_cta,
help_link: step.data.quiz_help_link,
});
} catch (error) {
console.error("Error while creating balance task:", error);
}
} else if (step.type === "CustomApi") {
try {
await AdminService.createCustomApi({

if (response) {
for (const question of step.data.questions) {
try {
await AdminService.createQuizQuestion({
quiz_id: response.quiz_id,
question: question.question,
options: question.options,
correct_answers: question.correct_answers,
});
} catch (error) {
console.error("Error executing promise:", error);
failedQuestions.push(question.question);
}
}
if (failedQuestions.length > 0) {
showNotification(
`Failed to create ${failedQuestions.length} questions. Please review and try again.`,
"warning"
);
}
step.data.id = response.id;
}
} else if (step.type === "TwitterFw") {
if (
step.data.twfw_name?.length === 0 ||
step.data.twfw_desc?.length === 0 ||
step.data.twfw_username?.length === 0
) {
showNotification(
"Please fill all fields for Twitter Follow",
"info"
);
continue;
}
const response = await AdminService.createTwitterFw({
quest_id: questId,
name: step.data.api_name,
desc: step.data.api_desc,
api_url: step.data.api_url,
cta: step.data.api_cta,
href: step.data.api_href,
regex: step.data.api_regex,
name: step.data.twfw_name,
desc: step.data.twfw_desc,
username: step.data.twfw_username,
});
} catch (error) {
console.error("Error while creating balance task:", error);
}
} else if (step.type === "Contract") {
try {
await AdminService.createContract({
if (response) step.data.id = response.id;
} else if (step.type === "TwitterRw") {
if (
step.data.twrw_name?.length === 0 ||
step.data.twrw_desc?.length === 0 ||
step.data.twrw_post_link?.length === 0
) {
showNotification(
"Please fill all fields for Twitter Retweet",
"info"
);
continue;
}
const response = await AdminService.createTwitterRw({
quest_id: questId,
name: step.data.twrw_name,
desc: step.data.twrw_desc,
post_link: step.data.twrw_post_link,
});
if (response) step.data.id = response.id;
} else if (step.type === "Discord") {
if (
step.data.dc_name?.length === 0 ||
step.data.dc_desc?.length === 0 ||
step.data.dc_invite_link?.length === 0 ||
step.data.dc_guild_id?.length === 0
) {
showNotification("Please fill all fields for Discord", "info");
continue;
}
const response = await AdminService.createDiscord({
quest_id: questId,
name: step.data.contract_name,
desc: step.data.contract_desc,
href: step.data.contract_href,
cta: step.data.contract_cta,
calls: JSON.parse(step.data.contract_calls),
name: step.data.dc_name,
desc: step.data.dc_desc,
invite_link: step.data.dc_invite_link,
guild_id: step.data.dc_guild_id,
});
} catch (error) {
console.error("Error while creating contract task:", error);
showNotification(`Error adding ${step.type} task: ${error}`, "error");
if (response) step.data.id = response.id;
} else if (step.type === "Custom") {
if (
step.data.custom_name?.length === 0 ||
step.data.custom_desc?.length === 0 ||
step.data.custom_cta?.length === 0 ||
step.data.custom_href?.length === 0 ||
step.data.custom_api?.length === 0
) {
showNotification("Please fill all fields for Custom", "info");
continue;
}
const response = await AdminService.createCustom({
quest_id: questId,
name: step.data.custom_name,
desc: step.data.custom_desc,
cta: step.data.custom_cta,
href: step.data.custom_href,
api: step.data.custom_api,
});
if (response) step.data.id = response.id;
} else if (step.type === "Domain") {
const response = await AdminService.createDomain({
quest_id: questId,
name: step.data.domain_name,
desc: step.data.domain_desc,
});
if (response) step.data.id = response.id;
} else if (step.type === "Balance") {
Comment on lines +382 to +389
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add validation for Domain task type

The Domain task type lacks field validation unlike other task types. This could lead to creation of incomplete tasks.

Add validation similar to other task types:

 } else if (step.type === "Domain") {
+  if (
+    step.data.domain_name?.length === 0 ||
+    step.data.domain_desc?.length === 0
+  ) {
+    showNotification("Please fill all fields for Domain", "info");
+    continue;
+  }
   const response = await AdminService.createDomain({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (step.type === "Domain") {
const response = await AdminService.createDomain({
quest_id: questId,
name: step.data.domain_name,
desc: step.data.domain_desc,
});
if (response) step.data.id = response.id;
} else if (step.type === "Balance") {
} else if (step.type === "Domain") {
if (
step.data.domain_name?.length === 0 ||
step.data.domain_desc?.length === 0
) {
showNotification("Please fill all fields for Domain", "info");
continue;
}
const response = await AdminService.createDomain({
quest_id: questId,
name: step.data.domain_name,
desc: step.data.domain_desc,
});
if (response) step.data.id = response.id;
} else if (step.type === "Balance") {

try {
const response = await AdminService.createBalance({
quest_id: questId,
name: step.data.balance_name,
desc: step.data.balance_desc,
contracts: step.data.balance_contracts,
cta: step.data.balance_cta,
href: step.data.balance_href,
});
if (response) step.data.id = response.id;
} catch (error) {
console.error("Error while creating balance task:", error);
}
} else if (step.type === "CustomApi") {
Comment on lines +389 to +403
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Standardize error handling for Balance and CustomApi tasks

The error handling for Balance and CustomApi tasks only logs to console, while Contract tasks show notifications. This inconsistency could lead to poor user experience.

Add user notifications:

 } catch (error) {
   console.error("Error while creating balance task:", error);
+  showNotification(
+    `Error adding Balance task: ${error}`,
+    "error"
+  );
 }
 } catch (error) {
   console.error("Error while creating CustomApi task:", error);
+  showNotification(
+    `Error adding CustomApi task: ${error}`,
+    "error"
+  );
 }

Also applies to: 403-418

try {
const response = await AdminService.createCustomApi({
quest_id: questId,
name: step.data.api_name,
desc: step.data.api_desc,
api_url: step.data.api_url,
cta: step.data.api_cta,
href: step.data.api_href,
regex: step.data.api_regex,
});
if (response) step.data.id = response.id;
} catch (error) {
console.error("Error while creating CustomApi task:", error);
}
} else if (step.type === "Contract") {
try {
const response = await AdminService.createContract({
quest_id: questId,
name: step.data.contract_name,
desc: step.data.contract_desc,
href: step.data.contract_href,
cta: step.data.contract_cta,
calls: (() => {
try {
return JSON.parse(step.data.contract_calls);
} catch (error) {
showNotification("Invalid contract calls format", "error");
throw error;
}
})(),
});
if (response) step.data.id = response.id;
} catch (error) {
console.error("Error while creating contract task:", error);
showNotification(
`Error adding ${step.type} task: ${error}`,
"error"
);
}
}
}
});
setButtonLoading(false);
setCurrentPage((prev) => prev + 1);
}, [steps]);
setSteps([...steps]);
setCurrentPage((prev) => prev + 1);
} finally {
isSaving.current = false;
setButtonLoading(false);
}
}, [steps, questId]);

const handleRemoveStep = useCallback(
(index: number) => {
Expand Down