diff --git a/eslint.config.js b/eslint.config.js index 390b18c..e3ecdd7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,7 +28,8 @@ export default tseslint.config( "@typescript-eslint/no-unused-vars": "off", "react-refresh/only-export-components": "off", "react-hooks/exhaustive-deps": "off", - "@typescript-eslint/no-unused-expressions": "off" + "@typescript-eslint/no-unused-expressions": "off", + "no-debugger":"off" } } ); diff --git a/package-lock.json b/package-lock.json index 3cf0a49..2341009 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,10 @@ "version": "0.1.0", "dependencies": { "@apollo/client": "^3.11.5", - "@aut-labs/abi-types": "^0.0.86-dev", - "@aut-labs/connector": "^0.0.203", + "@aut-labs/abi-types": "^0.0.87-dev", + "@aut-labs/connector": "^0.0.205", "@aut-labs/d-aut": "^1.0.204-dev", - "@aut-labs/sdk": "^0.0.221-dev", + "@aut-labs/sdk": "^0.0.228-dev", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@marker.io/browser": "^0.19.0", @@ -137,14 +137,16 @@ } }, "node_modules/@aut-labs/abi-types": { - "version": "0.0.86-dev", - "resolved": "https://registry.npmjs.org/@aut-labs/abi-types/-/abi-types-0.0.86-dev.tgz", - "integrity": "sha512-I9VNsYN8smW516wPQeDWhJaw4OWiIc7WxRBIn2GkqKw4oYKaLbDRzTV2XXVrMlvVCo7V82V51SlV+fC0Hajq/w==" + "version": "0.0.87-dev", + "resolved": "https://registry.npmjs.org/@aut-labs/abi-types/-/abi-types-0.0.87-dev.tgz", + "integrity": "sha512-PDmfNSdzbQGcLiWuA7N4/ULLi3EFHvVyk3bAy55yHb24DLiq8wNWCmtngpWZK2u5/tjJ+X36pT7kGrtHS89Axg==", + "license": "ISC" }, "node_modules/@aut-labs/connector": { - "version": "0.0.203", - "resolved": "https://registry.npmjs.org/@aut-labs/connector/-/connector-0.0.203.tgz", - "integrity": "sha512-bUXlRb1YviVb6jpnTLnZs3HaZhABItep+knkbCsrKAAZcPH0bcsnJsjCfLOH2PsZX+BcTafxUrT2FDmy92aeZw==", + "version": "0.0.205", + "resolved": "https://registry.npmjs.org/@aut-labs/connector/-/connector-0.0.205.tgz", + "integrity": "sha512-TcbRTjHst2mM9SxZRRo0uKoY/dJ5xBuXpeDC8fc9R419nJxLm5cLLAleoNdXMrrSvSHjk7jJ27WEj3VpHGDqmA==", + "license": "MIT", "dependencies": { "@web3auth/base": "^8.0.0", "@web3auth/ethereum-provider": "^8.0.1", @@ -170,11 +172,12 @@ "integrity": "sha512-sDEu6yyvqu/KjqobI6nBoovrHWwhA4DoXPWObQLcGRs1Isuxg/d2HhP2/9k6Xz5zjFDo/w3MDI3XDvpVos20dg==" }, "node_modules/@aut-labs/sdk": { - "version": "0.0.221-dev", - "resolved": "https://registry.npmjs.org/@aut-labs/sdk/-/sdk-0.0.221-dev.tgz", - "integrity": "sha512-W6R0oneKaSvc9kYIicSoPvygP1c4UlWrWu884FtAPPrrRQpucEvo9NG2FzsKa/Ku+nKV6APf6YoG3Zjib1vLvQ==", + "version": "0.0.228-dev", + "resolved": "https://registry.npmjs.org/@aut-labs/sdk/-/sdk-0.0.228-dev.tgz", + "integrity": "sha512-PpgmuX07NHyXO1fMtdLcRV8VUc3Ve2r2+jk9H+c5WuidSu6+pkJXj/y6cQhUhSbb3/pKO8YlKS39qB3+ScRiWA==", + "license": "MIT", "dependencies": { - "@aut-labs/abi-types": "^0.0.86-dev", + "@aut-labs/abi-types": "^0.0.87-dev", "date-fns": "^2.29.3", "ethers": "^6.10.0" } diff --git a/package.json b/package.json index 641b512..cdf9471 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,10 @@ }, "dependencies": { "@apollo/client": "^3.11.5", - "@aut-labs/abi-types": "^0.0.86-dev", - "@aut-labs/connector": "^0.0.203", + "@aut-labs/abi-types": "^0.0.87-dev", + "@aut-labs/connector": "^0.0.205", "@aut-labs/d-aut": "^1.0.204-dev", - "@aut-labs/sdk": "^0.0.221-dev", + "@aut-labs/sdk": "^0.0.228-dev", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@marker.io/browser": "^0.19.0", diff --git a/src/App.tsx b/src/App.tsx index 0728fe5..347b0f3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { Route, Routes, Navigate } from "react-router-dom"; import { Box, useMediaQuery, useTheme } from "@mui/material"; import { useAppDispatch } from "@store/store.model"; import { updateWalletProviderState } from "@store/WalletProvider/WalletProvider"; +import useQueryTaskTypes from "@utils/hooks/useQueryTaskTypes"; import AutLoading from "@components/AutLoading"; import Web3DautConnect from "@components/DAutConnect"; import SWSnackbar from "./components/snackbar"; @@ -22,6 +23,13 @@ function App() { const theme = useTheme(); const mobile = useMediaQuery(theme.breakpoints.down("md")); + const { refetch } = useQueryTaskTypes({ + variables: { + skip: 0, + take: 1000 + } + }); + useEffect(() => { getAppConfig() .then(async (networks) => { diff --git a/src/api/aut.api.ts b/src/api/aut.api.ts index 686a05e..e7851a7 100644 --- a/src/api/aut.api.ts +++ b/src/api/aut.api.ts @@ -4,6 +4,7 @@ import { NetworkConfig } from "./models/network.config"; import { extractDomain } from "@utils/helpers"; import { useAutConnector, useWalletConnector } from "@aut-labs/connector"; import { useEffect, useState } from "react"; +import { AuthSig } from "@aut-labs/connector/lib/esm/aut-sig"; export const getAppConfig = (): Promise => { return axios @@ -11,6 +12,18 @@ export const getAppConfig = (): Promise => { .then((r) => r.data); }; +interface EncryptRequest { + autSig: AuthSig; + message: string; + hubAddress: string; +} + +export const encryptMessage = (body: EncryptRequest): Promise => { + return axios + .post(`${environment.apiUrl}/task/encrypt`, body) + .then((r) => r.data); +}; + export const useAuthenticatedApi = () => { const { state: { multiSigner, address } diff --git a/src/api/contributions.api.ts b/src/api/contributions.api.ts new file mode 100644 index 0000000..a3aadb4 --- /dev/null +++ b/src/api/contributions.api.ts @@ -0,0 +1,97 @@ +import { TaskContributionNFT } from "@aut-labs/sdk"; +import { BaseQueryApi, createApi } from "@reduxjs/toolkit/query/react"; +import { AuthSig } from "@aut-labs/connector/lib/esm/aut-sig"; +import axios from "axios"; +import { environment } from "./environment"; + +interface CommitRequest { + autSig: AuthSig; + message: string; + hubAddress: string; + contributionId: string; +} + +export const _commitContribution = async ( + body: CommitRequest +): Promise => { + const r = await axios.post( + `${environment.apiUrl}/task/contribution/commit`, + body + ); + return r.data; +}; + +const commitContribution = async ( + autSig: AuthSig, + contribution: TaskContributionNFT, + message: string, + hubAddress: string, + api: BaseQueryApi +) => { + try { + const response = _commitContribution({ + autSig: autSig, + message: message, + hubAddress: hubAddress, + contributionId: contribution.properties.id + }); + return { + data: response + }; + } catch (error) { + return { + error: error.message + }; + } +}; + +const commitAnyContribution = async ( + { + contribution, + autSig, + message, + hubAddress + }: { + contribution: TaskContributionNFT; + autSig: AuthSig; + message: string; + hubAddress: string; + }, + api: BaseQueryApi +) => { + return commitContribution(autSig, contribution, message, hubAddress, api); +}; + +export const contributionsApi = createApi({ + reducerPath: "contributionsApi", + baseQuery: (args, api) => { + const { url, body } = args; + if (url === "commitAnyContribution") { + return commitAnyContribution(body, api); + } + return { + data: "Test" + }; + }, + tagTypes: [], + endpoints: (builder) => ({ + commitAnyContribution: builder.mutation< + void, + { + autSig: AuthSig; + contribution: TaskContributionNFT; + message: string; + hubAddress: string; + } + >({ + query: (body) => { + return { + body, + url: "commitAnyContribution" + }; + } + }) + }) +}); + +export const { useCommitAnyContributionMutation } = contributionsApi; diff --git a/src/api/models/contribution-types/discord-gathering.model.ts b/src/api/models/contribution-types/discord-gathering.model.ts new file mode 100644 index 0000000..bdd8f74 --- /dev/null +++ b/src/api/models/contribution-types/discord-gathering.model.ts @@ -0,0 +1,45 @@ +import { + BaseNFTModel, + TaskContributionNFT, + TaskContributionProperties +} from "@aut-labs/sdk"; + +export class DiscordGatheringContributionProperties extends TaskContributionProperties { + channelId: string; + duration: number; + + constructor(data: DiscordGatheringContributionProperties) { + super(data); + this.channelId = data.channelId; + this.duration = data.duration; + } +} + +export class DiscordGatheringContribution< + T = DiscordGatheringContributionProperties +> extends TaskContributionNFT { + static getContributionNFT( + contribution: DiscordGatheringContribution + ): BaseNFTModel { + const taskContribution = new DiscordGatheringContribution(contribution); + return { + name: taskContribution.name, + description: taskContribution.description, + properties: { + duration: taskContribution.properties.duration, + channelId: taskContribution.properties.channelId + } + } as BaseNFTModel; + } + + constructor( + data: DiscordGatheringContribution = {} as DiscordGatheringContribution + ) { + super(data); + this.properties = new DiscordGatheringContributionProperties( + data.properties as DiscordGatheringContributionProperties + ) as T; + } + + contributionType? = "Discord Gatherings"; +} diff --git a/src/api/models/contribution-types/github-commit.model.ts b/src/api/models/contribution-types/github-commit.model.ts new file mode 100644 index 0000000..f9634d4 --- /dev/null +++ b/src/api/models/contribution-types/github-commit.model.ts @@ -0,0 +1,43 @@ +import { + BaseNFTModel, + TaskContributionNFT, + TaskContributionProperties +} from "@aut-labs/sdk"; + +export class GithubCommitContributionProperties extends TaskContributionProperties { + branch: string; + repository: string; + organisation: string; + constructor(data: GithubCommitContributionProperties) { + super(data); + this.branch = data.branch; + this.repository = data.repository; + this.organisation = data.organisation; + } +} + +export class GithubCommitContribution< + T = GithubCommitContributionProperties +> extends TaskContributionNFT { + static getContributionNFT( + contribution: GithubCommitContribution + ): BaseNFTModel { + const taskContribution = new GithubCommitContribution(contribution); + return { + name: taskContribution.name, + description: taskContribution.description, + properties: { + branch: taskContribution.properties.branch, + repository: taskContribution.properties.repository, + organisation: taskContribution.properties.organisation + } + } as BaseNFTModel; + } + constructor(data: GithubCommitContribution = {} as GithubCommitContribution) { + super(data); + this.properties = new GithubCommitContributionProperties( + data.properties as GithubCommitContributionProperties + ) as T; + } + contributionType? = "Commit"; +} diff --git a/src/api/models/contribution-types/github-pr.model.ts b/src/api/models/contribution-types/github-pr.model.ts new file mode 100644 index 0000000..111ca48 --- /dev/null +++ b/src/api/models/contribution-types/github-pr.model.ts @@ -0,0 +1,47 @@ +import { + BaseNFTModel, + TaskContributionNFT, + TaskContributionProperties +} from "@aut-labs/sdk"; + +export class GithubPullRequestContributionProperties extends TaskContributionProperties { + branch: string; + repository: string; + organisation: string; + constructor(data: GithubPullRequestContributionProperties) { + super(data); + this.branch = data.branch; + this.repository = data.repository; + this.organisation = data.organisation; + } +} + +export class GithubPullRequestContribution< + T = GithubPullRequestContributionProperties +> extends TaskContributionNFT { + static getContributionNFT( + contribution: GithubPullRequestContribution + ): BaseNFTModel { + const taskContribution = new GithubPullRequestContribution( + contribution + ); + return { + name: taskContribution.name, + description: taskContribution.description, + properties: { + branch: taskContribution.properties.branch, + repository: taskContribution.properties.repository, + organisation: taskContribution.properties.organisation + } + } as BaseNFTModel; + } + constructor( + data: GithubPullRequestContribution = {} as GithubPullRequestContribution + ) { + super(data); + this.properties = new GithubPullRequestContributionProperties( + data.properties as GithubPullRequestContributionProperties + ) as T; + } + contributionType? = "Pull Request"; +} diff --git a/src/api/models/contribution-types/join-discord.model.ts b/src/api/models/contribution-types/join-discord.model.ts new file mode 100644 index 0000000..e8fab20 --- /dev/null +++ b/src/api/models/contribution-types/join-discord.model.ts @@ -0,0 +1,44 @@ +import { + BaseNFTModel, + TaskContributionNFT, + TaskContributionProperties +} from "@aut-labs/sdk"; + +export class JoinDiscordContributionProperties extends TaskContributionProperties { + guildId: string; + inviteUrl: string; + constructor(data: JoinDiscordContributionProperties) { + super(data); + this.guildId = data.guildId; + this.inviteUrl = data.inviteUrl + } +} + +export class JoinDiscordContribution< + T = JoinDiscordContributionProperties +> extends TaskContributionNFT { + static getContributionNFT( + contribution: JoinDiscordContribution + ): BaseNFTModel { + const taskContribution = new JoinDiscordContribution(contribution); + return { + name: taskContribution.name, + description: taskContribution.description, + properties: { + guildId: taskContribution.properties.guildId, + inviteUrl: taskContribution.properties.inviteUrl + } + } as BaseNFTModel; + } + + constructor( + data: JoinDiscordContribution = {} as JoinDiscordContribution + ) { + super(data); + this.properties = new JoinDiscordContributionProperties( + data.properties as JoinDiscordContributionProperties + ) as T; + } + + contributionType? = "Join Discord"; +} diff --git a/src/api/models/contribution-types/open-task.model.ts b/src/api/models/contribution-types/open-task.model.ts new file mode 100644 index 0000000..115ee90 --- /dev/null +++ b/src/api/models/contribution-types/open-task.model.ts @@ -0,0 +1,44 @@ +import { + BaseNFTModel, + TaskContributionNFT, + TaskContributionProperties + } from "@aut-labs/sdk"; + +export class OpenTaskContributionProperties extends TaskContributionProperties { + attachmentRequired: boolean; + textRequired: boolean; + attachmentType: string; + constructor(data: OpenTaskContributionProperties) { + super(data); + this.attachmentRequired = data.attachmentRequired; + this.textRequired = data.textRequired; + this.attachmentType = data.attachmentType; + } + } + + export class OpenTaskContribution< + T = OpenTaskContributionProperties + > extends TaskContributionNFT { + static getContributionNFT( + contribution: OpenTaskContribution + ): BaseNFTModel { + const taskContribution = new OpenTaskContribution(contribution); + return { + name: taskContribution.name, + description: taskContribution.description, + properties: { + attachmentRequired: taskContribution.properties.attachmentRequired, + textRequired: taskContribution.properties.textRequired, + attachmentType: taskContribution.properties.attachmentType + } + } as BaseNFTModel; + } + constructor(data: OpenTaskContribution = {} as OpenTaskContribution) { + super(data); + this.properties = new OpenTaskContributionProperties( + data.properties as OpenTaskContributionProperties + ) as T; + } + + contributionType? = "Open Task"; + } \ No newline at end of file diff --git a/src/api/models/contribution-types/quiz.model.model.ts b/src/api/models/contribution-types/quiz.model.model.ts new file mode 100644 index 0000000..13fde8f --- /dev/null +++ b/src/api/models/contribution-types/quiz.model.model.ts @@ -0,0 +1,49 @@ +import { + BaseNFTModel, + TaskContributionNFT, + TaskContributionProperties +} from "@aut-labs/sdk"; + +export interface QuizQuestionsAndAnswers { + question: string; + questionType: string; + answers: { + value: string; + correct: boolean; + }[]; +} + +export class QuizTaskContributionProperties extends TaskContributionProperties { + questions: QuizQuestionsAndAnswers[]; + hash: string; + constructor(data: QuizTaskContributionProperties) { + super(data); + this.questions = data.questions; + this.hash = data.hash; + } +} + +export class QuizTaskContribution< + T = QuizTaskContributionProperties +> extends TaskContributionNFT { + static getContributionNFT( + contribution: QuizTaskContribution + ): BaseNFTModel { + const taskContribution = new QuizTaskContribution(contribution); + return { + name: taskContribution.name, + description: taskContribution.description, + properties: { + questions: taskContribution.properties.questions, + } + } as BaseNFTModel; + } + constructor(data: QuizTaskContribution = {} as QuizTaskContribution) { + super(data); + this.properties = new QuizTaskContributionProperties( + data.properties as QuizTaskContributionProperties + ) as T; + } + + contributionType? = "Quiz"; +} diff --git a/src/api/models/contribution-types/retweet.model.ts b/src/api/models/contribution-types/retweet.model.ts new file mode 100644 index 0000000..8e6d0f9 --- /dev/null +++ b/src/api/models/contribution-types/retweet.model.ts @@ -0,0 +1,38 @@ +import { + BaseNFTModel, + TaskContributionNFT, + TaskContributionProperties +} from "@aut-labs/sdk"; + +export class RetweetContributionProperties extends TaskContributionProperties { + tweetUrl: string; + constructor(data: RetweetContributionProperties) { + super(data); + this.tweetUrl = data.tweetUrl; + } +} + +export class RetweetContribution< + T = RetweetContributionProperties +> extends TaskContributionNFT { + static getContributionNFT( + contribution: RetweetContribution + ): BaseNFTModel { + const taskContribution = new RetweetContribution(contribution); + return { + name: taskContribution.name, + description: taskContribution.description, + properties: { + tweetUrl: taskContribution.properties.tweetUrl + } + } as BaseNFTModel; + } + constructor(data: RetweetContribution = {} as RetweetContribution) { + super(data); + this.properties = new RetweetContributionProperties( + data.properties as RetweetContributionProperties + ) as T; + } + + contributionType? = "Retweet"; +} diff --git a/src/api/models/contribution.model.ts b/src/api/models/contribution.model.ts new file mode 100644 index 0000000..432abee --- /dev/null +++ b/src/api/models/contribution.model.ts @@ -0,0 +1,56 @@ +import { BaseNFTModel, TaskContributionNFT } from "@aut-labs/sdk"; +import { OpenTaskContribution } from "./contribution-types/open-task.model"; +import { DiscordGatheringContribution } from "./contribution-types/discord-gathering.model"; +import { RetweetContribution } from "./contribution-types/retweet.model"; +import { JoinDiscordContribution } from "./contribution-types/join-discord.model"; +import { QuizTaskContribution } from "./contribution-types/quiz.model.model"; +import { TaskType } from "./task-type"; +import { GithubCommitContribution } from "./contribution-types/github-commit.model"; +import { GithubPullRequestContribution } from "./contribution-types/github-pr.model"; + +export const ContributionFactory = ( + metadata: BaseNFTModel, + contribution: any, + taskTypes: TaskType[] +) => { + const taskType = taskTypes.find( + (taskType) => taskType.taskId === contribution.taskId + ); + + if (!taskType) { + throw new Error("Task type not found"); + } + const taskName = taskType.metadata.properties.type; + const data = { + ...metadata, + properties: { + ...metadata.properties, + ...contribution + } + }; + switch (taskName) { + case "OpenTask": + return new OpenTaskContribution(data); + case "DiscordGatherings": + return new DiscordGatheringContribution(data); + case "TwitterRetweet": + return new RetweetContribution(data); + case "JoinDiscord": + return new JoinDiscordContribution(data); + case "Quiz": + return new QuizTaskContribution(data); + case "TwitterLike": + case "GitHubCommit": + return new GithubCommitContribution(data); + case "GitHubOpenPR": + return new GithubPullRequestContribution(data); + case "DiscordPolls": + case "TwitterFollow": + case "TwitterComment": + // throw new Error("Task type not implemented"); + return new TaskContributionNFT(data); + + default: + throw new Error("Task type not found"); + } +}; diff --git a/src/api/models/task-type.ts b/src/api/models/task-type.ts new file mode 100644 index 0000000..6db2423 --- /dev/null +++ b/src/api/models/task-type.ts @@ -0,0 +1,9 @@ +import { BaseNFTModel } from "@aut-labs/sdk"; + +export interface TaskType { + id: string; + metadataUri: string; + taskId: string; + creator: string; + metadata: BaseNFTModel; +} \ No newline at end of file diff --git a/src/assets/autos/move-right.svg b/src/assets/autos/move-right.svg index 4a24358..53e77f6 100644 --- a/src/assets/autos/move-right.svg +++ b/src/assets/autos/move-right.svg @@ -1,6 +1,6 @@ - - + + diff --git a/src/components/AutEditProfileDialog.tsx b/src/components/AutEditProfileDialog.tsx index be844c7..9b6a649 100644 --- a/src/components/AutEditProfileDialog.tsx +++ b/src/components/AutEditProfileDialog.tsx @@ -214,7 +214,6 @@ function AutEditProfileDialog(props: EditDialogProps) { // return social; // }); // // eslint-disable-next-line no-debugger - // debugger; await dispatch( updateProfile({ ...autID, diff --git a/src/index.tsx b/src/index.tsx index 51ac383..10a35bd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -45,7 +45,7 @@ root.render( diff --git a/src/pages/AutID/AutHub/AutHubContributionsTable.tsx b/src/pages/AutID/AutHub/AutHubContributionsTable.tsx index bd7a985..7607ff4 100644 --- a/src/pages/AutID/AutHub/AutHubContributionsTable.tsx +++ b/src/pages/AutID/AutHub/AutHubContributionsTable.tsx @@ -1,9 +1,8 @@ -import { memo, useEffect, useMemo } from "react"; +import { memo, useMemo } from "react"; import Box from "@mui/material/Box"; import ArrowIcon from "@assets/autos/move-right.svg?react"; import { - Link as BtnLink, Paper, Stack, SvgIcon, @@ -20,16 +19,16 @@ import { } from "@mui/material"; import { format } from "date-fns"; import { AutOsButton } from "@components/AutButton"; -import { Link } from "react-router-dom"; -import { useDispatch, useSelector } from "react-redux"; -import { - AllContributions, - setSelectedContribution, - updateContributionState -} from "@store/contributions/contributions.reducer"; -import { formatContributionType } from "@utils/format-contribution-type"; -import { TaskStatus } from "@store/model"; import useQueryContributions from "@utils/hooks/GetContributions"; +import { useCommitAnyContributionMutation } from "@api/contributions.api"; +import { useWalletConnector } from "@aut-labs/connector"; +import useQueryContributionCommits, { + ContributionCommit +} from "@utils/hooks/useQueryContributionCommits"; +import { TaskContributionNFT } from "@aut-labs/sdk"; +import { Link } from "react-router-dom"; +import { GithubCommitContribution } from "@api/models/contribution-types/github-commit.model"; +import useQueryHubPeriod from "@utils/hooks/useQueryHubPeriod"; const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}, &.${tableCellClasses.body}`]: { @@ -51,41 +50,27 @@ const StyledTableRow = styled(TableRow)(({ theme }) => ({ } })); -const generateRandomId = () => { - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { - const r = (Math.random() * 16) | 0, - v = c === "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -}; +interface TableListItemProps { + contribution: TaskContributionNFT & { contributionType?: string }; + commit: ContributionCommit; +} -const TableListItem = memo((data: any) => { - const dispatch = useDispatch(); - const { row } = data; +const TableListItem = memo(({ contribution, commit }: TableListItemProps) => { const theme = useTheme(); - const handleContributionClick = (contribution) => { - dispatch(setSelectedContribution(contribution)); - }; - - const contributionType = useMemo( - () => formatContributionType(row?.contributionType), - [row?.contributionType] - ); - const startDate = useMemo(() => { return format( - new Date(row?.properties?.startDate * 1000), + new Date(contribution?.properties?.startDate * 1000), "dd.MM.yy" ).toString(); - }, [row?.properties?.startDate]); + }, [contribution?.properties?.startDate]); const endDate = useMemo(() => { return format( - new Date(row?.properties?.endDate * 1000), + new Date(contribution?.properties?.endDate * 1000), "dd.MM.yy" ).toString(); - }, [row?.properties?.endDate]); + }, [contribution?.properties?.endDate]); return ( { - {row?.name} + {contribution?.name} - {row?.description} + {contribution?.description} @@ -133,13 +118,13 @@ const TableListItem = memo((data: any) => { }} > - {contributionType} + {contribution?.contributionType} - {`${row?.properties?.points || 0} ${row?.properties?.points === 1 ? "pt" : "pts"}`} + {`${contribution?.properties?.points || 0} ${contribution?.properties?.points === 1 ? "pt" : "pts"}`} @@ -173,7 +158,7 @@ const TableListItem = memo((data: any) => { alignItems: "center" }} > - {row?.status === TaskStatus.Created ? ( + {!commit ? ( { width: "100px" }} relative="path" - to={`contribution/${row?.id}`} + to={`contribution/${contribution?.properties?.id}`} component={Link} - onClick={() => handleContributionClick(row)} > Claim @@ -204,10 +188,11 @@ const TableListItem = memo((data: any) => { }, width: "100px" }} - disabled + to={`contribution/${contribution?.properties?.id}`} + component={Link} > - Completed + See Commit )} @@ -218,35 +203,56 @@ const TableListItem = memo((data: any) => { }); export const AutHubTasksTable = ({ header }) => { - const dispatch = useDispatch(); - const contributions = useSelector(AllContributions); + const { state } = useWalletConnector(); - const { - data, - loading: isLoading, - refetch - } = useQueryContributions({ + const { data, loading: isLoading } = useQueryContributions({ variables: { skip: 0, take: 1000 } }); - useEffect(() => { - if (!contributions.length) { - const updatedContributions = data?.map((item) => ({ - ...item, - contributionType: (item.properties as any) - .tweetUrl - ? "retweet" - : "open", - status: TaskStatus.Created, - id: generateRandomId() - })); - dispatch( - updateContributionState({ contributions: updatedContributions }) - ); + + const { data: commits, loading: isLoadingCommits } = + useQueryContributionCommits({ + skip: !state?.address, + variables: { + skip: 0, + take: 1000, + where: { + who: state?.address?.toLowerCase() + } } - }, [data]); + }); + + const contributionWithCommits = useMemo(() => { + return (data || []).map((contribution) => { + const commit = (commits || []).find( + (commit) => commit?.contribution?.id === contribution.properties.id + ); + return { + contribution, + commit + }; + }); + }, [data, commits]); + + if (data?.length) { + data.map((contribution) => { + console.log(contribution as GithubCommitContribution); + }); + } + const [ + commit, + { error, isError, isSuccess, isLoading: commitingContribution, reset } + ] = useCommitAnyContributionMutation(); + + // const commitContribution = (row) => { + // commit({ + // autSig: state.authSig, + // contribution: row, + // message: "secret" + // }); + // }; const theme = useTheme(); return ( @@ -327,7 +333,7 @@ export const AutHubTasksTable = ({ header }) => { End Date - + { - {contributions?.length ? ( + {contributionWithCommits?.length ? ( - {contributions?.map((row, index) => ( - - ))} + {contributionWithCommits?.map( + ({ contribution, commit }, index) => ( + + ) + )} ) : ( ({ display: "flex", @@ -136,6 +137,11 @@ const AutHubEdit = () => { const roleName = useSelector(RoleName(params.hubAddress)); const commitmentTemplate = useSelector(CommitmentTemplate(hubAddress)); + const { data: periodData } = useQueryHubPeriod(); + + const { pointsGiven, expectedPoints, endDate } = periodData || {}; + const initialScore = 100; + useEffect(() => { dispatch(updateAutState({ selectedHubAddress: params.hubAddress })); console.log( @@ -230,10 +236,6 @@ const AutHubEdit = () => { dispatch(setOpenCommitment(true)); }; - const nextPeriod = new Date("11/30/2024"); - const hubRep: number = 100; - const userRep: number = 100; - const socials = useMemo(() => { if (!selectedHub) return []; return socialsWithIcons(selectedHub?.properties?.socials); @@ -615,7 +617,7 @@ const AutHubEdit = () => { > Current Period Contribution - {userRep < hubRep ? ( + {pointsGiven < expectedPoints ? ( { period ends in @@ -684,7 +686,7 @@ const AutHubEdit = () => { lineHeight="48px" color="offWhite.main" > - {userRep} + {pointsGiven} { }} color="offWhite.dark" > - {hubRep} + {expectedPoints} - {userRep < hubRep && ( + {pointsGiven < expectedPoints && ( { } }} variant="determinate" - value={(userRep / hubRep) * 100} + value={ + (pointsGiven / (expectedPoints || 1)) * 100 + } /> { )} - {userRep === hubRep && ( + {pointsGiven === + expectedPoints && ( { )} - {userRep > hubRep && ( + {pointsGiven > expectedPoints && ( { color="offWhite.main" fontWeight="normal" > - 1.0 + {initialScore} { color="offWhite.main" fontWeight="normal" > - 100 + {pointsGiven} ({ display: "flex", @@ -61,6 +62,8 @@ export const PropertiesWrapper = styled(Box)(({ theme }) => ({ const { IconContainer } = EditContentElements; const HubListItem = memo(({ row }: { row: AutOSHub }) => { + const { data: periodData } = useQueryHubPeriod(); + const initialScore = 100; const theme = useTheme(); const { address } = useAccount(); const autID = useSelector(SelectedAutID); @@ -273,7 +276,8 @@ const HubListItem = memo(({ row }: { row: AutOSHub }) => { } > - { xs: "inherit", md: "transparent" } - }}> + }} + > - 1.0 + {initialScore} { color="offWhite.main" fontWeight="normal" > - 100 + {periodData?.pointsGiven || 0} { - let contribution = useSelector(SelectedContribution); - const contributions = useSelector(AllContributions); + const { state } = useWalletConnector(); const { id } = useParams<{ id: string }>(); + const { data, loading: isLoading } = useQueryContributions({ + variables: { + skip: 0, + take: 1000, + where: { + id + } + } + }); - if (!contribution) { - contribution = contributions.find((c) => c.id === id); - } - return ( - <> - {contribution?.contributionType === "open" && ( - - )} - {contribution?.contributionType === "discord" && ( - - )} - {contribution?.contributionType === "quiz" && ( - - )} - {contribution?.contributionType === "retweet" && ( - - )} - - ); + const { data: commits, loading: isLoadingCommits } = + useQueryContributionCommits({ + skip: !state?.address, + variables: { + skip: 0, + take: 1000, + where: { + who: state?.address?.toLowerCase(), + contribution_: { + id + } + } + } + }); + + const commit = useMemo(() => { + if (commits) { + return commits[0]; + } + return null; + }, [commits]); + + const contributionTemplate = useMemo(() => { + const contribution = data?.[0]; + if (!contribution) return null; + debugger; + if (contribution instanceof OpenTaskContribution) { + return ; + } else if (contribution instanceof QuizTaskContribution) { + return ; + } else if (contribution instanceof GithubCommitContribution) { + return ; + } else if (contribution instanceof GithubPullRequestContribution) { + return ; + } else if (contribution instanceof RetweetContribution) { + return ; + } else if (contribution instanceof JoinDiscordContribution) { + return ; + } else { + return "Contribution type not supported"; + } + }, [data, commit]); + + return <>{contributionTemplate}; }; export default Contributions; diff --git a/src/pages/Tasks/DiscordTask/DiscordTask.tsx b/src/pages/Tasks/DiscordTask/DiscordTask.tsx deleted file mode 100644 index ff497c1..0000000 --- a/src/pages/Tasks/DiscordTask/DiscordTask.tsx +++ /dev/null @@ -1,300 +0,0 @@ -import AutLoading from "@components/AutLoading"; -import { Box, Container, Stack, Typography } from "@mui/material"; -import { memo, useEffect, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { - useLocation, - useNavigate, - useParams, - useSearchParams -} from "react-router-dom"; -import TaskDetails from "../Shared/TaskDetails"; -import ErrorDialog from "@components/Dialog/ErrorPopup"; -import LoadingDialog from "@components/Dialog/LoadingPopup"; -import SuccessDialog from "@components/Dialog/SuccessPopup"; -import { StepperButton } from "@components/StepperButton"; -import { useAccount } from "wagmi"; -import { AutOsButton } from "@components/AutButton"; -import { TaskStatus } from "@store/model"; -import { useOAuth, useOAuthSocials } from "@components/OAuth"; -import { updateContributionById } from "@store/contributions/contributions.reducer"; -import SubmitDialog from "@components/Dialog/SubmitDialog"; - -const JoinDiscordTask = ({ contribution }: any) => { - const dispatch = useDispatch(); - const [searchParams] = useSearchParams(); - const [loading, setLoading] = useState(false); - const location = useLocation(); - const params = useParams(); - const { address: userAddress } = useAccount(); - // const isAdmin = useSelector(IsAdmin); - const isAdmin = false; - const { getAuthDiscord, authenticating } = useOAuthSocials(); - const [joinClicked, setJoinClicked] = useState(false); - const [openSubmitSuccess, setOpenSubmitSuccess] = useState(false); - const navigate = useNavigate(); - const { autAddress, hubAddress } = useParams(); - - // const { task, isLoading: isLoadingTasks } = useGetAllTasksPerQuestQuery( - // { - // userAddress, - // isAdmin, - // pluginAddress: searchParams.get( - // RequiredQueryParams.OnboardingQuestAddress - // ), - // questId: +searchParams.get(RequiredQueryParams.QuestId) - // }, - // { - // selectFromResult: ({ data, isLoading, isFetching }) => ({ - // isLoading: isLoading || isFetching, - // task: (data?.tasks || []).find((t) => { - // const [pluginType] = location.pathname.split("/").splice(-2); - // return ( - // t.taskId === +params?.taskId && - // PluginDefinitionType[pluginType] === - // taskTypes[t.taskType].pluginType - // ); - // }) - // }) - // } - // ); - - // const [ - // submitJoinDiscordTask, - // { isSuccess, error, isError, isLoading, reset } - // ] = useSubmitJoinDiscordTaskMutation(); - - const onSubmit = async () => { - setLoading(true); - await new Promise((resolve) => setTimeout(resolve, 3000)); - dispatch( - updateContributionById({ - id: contribution.id, - data: { - ...contribution, - status: TaskStatus.Submitted - } - }) - ); - setLoading(false); - setOpenSubmitSuccess(true); - // await getAuth( - // async (data) => { - // const { access_token } = data; - // submitJoinDiscordTask({ - // userAddress, - // task, - // bearerToken: access_token, - // onboardingPluginAddress: searchParams.get( - // RequiredQueryParams.OnboardingQuestAddress - // ) - // }); - // }, - // () => { - // setJoinClicked(false); - // } - // ); - }; - - // useEffect(() => { - // if (isSuccess) { - // setOpenSubmitSuccess(true); - // } - // }, [isSuccess]); - - return ( - - {/* reset()} open={isError} message={error} /> - */} - {/* - { - setOpenSubmitSuccess(false); - navigate({ - pathname: "/quest", - search: searchParams.toString() - }); - }} - > */} - - { - if (!loading) { - setOpenSubmitSuccess(false); - navigate({ - pathname: `/${autAddress}/hub/${hubAddress}`, - search: searchParams.toString() - }); - } - }} - > - - {contribution ? ( - <> - - - - {joinClicked && ( - onSubmit()} - > - - Confirm - - - )} - {!joinClicked && ( - { - setJoinClicked(true); - window.open( - contribution.metadata.properties["inviteUrl"], - "_blank" - ); - }} - > - - Join - - - )} - {/* */} - - {(contribution?.status === TaskStatus.Created || - contribution?.status === TaskStatus.Taken) && ( - - {" "} - - - Confirm - - - - )} - - - ) : ( - - )} - - ); -}; - -export default memo(JoinDiscordTask); diff --git a/src/pages/Tasks/DiscordTask/JoinDiscordTask.tsx b/src/pages/Tasks/DiscordTask/JoinDiscordTask.tsx new file mode 100644 index 0000000..0e20dcc --- /dev/null +++ b/src/pages/Tasks/DiscordTask/JoinDiscordTask.tsx @@ -0,0 +1,263 @@ +import { memo, useState } from "react"; +import { Card, CardContent, Container, Stack, Typography } from "@mui/material"; +import { useSearchParams, useParams, useNavigate } from "react-router-dom"; +import { TaskStatus } from "@store/model"; +import { AutOsButton } from "@components/AutButton"; +import TaskDetails from "../Shared/TaskDetails"; +import SubmitDialog from "@components/Dialog/SubmitDialog"; +import AutLoading from "@components/AutLoading"; +import { useOAuthSocials } from "@components/OAuth"; +import { useWalletConnector } from "@aut-labs/connector"; +import { ContributionCommit } from "@utils/hooks/useQueryContributionCommits"; +import { useCommitAnyContributionMutation } from "@api/contributions.api"; +import ErrorDialog from "@components/Dialog/ErrorPopup"; +import { JoinDiscordContribution } from "@api/models/contribution-types/join-discord.model"; + +const DiscordJoinContent = ({ + contribution, + commit +}: { + contribution: JoinDiscordContribution; + commit: ContributionCommit; +}) => { + const { state } = useWalletConnector(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const { autAddress, hubAddress } = useParams(); + const { getAuthDiscord } = useOAuthSocials(); + + const [commitContribution, { error, isError, isSuccess, isLoading, reset }] = + useCommitAnyContributionMutation(); + + const contributionSubmissionContent = (() => { + let userSubmit = null; + try { + userSubmit = JSON.parse(commit?.data || "{}"); + } catch (e) { + // pass + } + return userSubmit; + })(); + + const handleSubmit = async () => { + await getAuthDiscord( + async (data) => { + const { access_token } = data; + + const discordMessage = { + accessToken: access_token, + guildId: contribution?.properties?.guildId + }; + + commitContribution({ + autSig: state.authSig, + contribution, + message: JSON.stringify(discordMessage), + hubAddress + }); + }, + () => { + // Handle auth failure + } + ); + }; + + return ( + + reset()} + open={isError} + message={(error as { message: string })?.message || "An error occurred"} + /> + + { + if (!isLoading) { + reset(); + navigate({ + pathname: `/${autAddress}/hub/${hubAddress}`, + search: searchParams.toString() + }); + } + }} + /> + + {!commit ? ( + + + + {contribution?.description} + + + + Please join the Discord server and authenticate to verify your + contribution + + + + + window.open( + contribution?.properties?.inviteUrl, + "_blank" + ) + } + sx={{ + width: "160px" + }} + > + + Join Discord + + + + + + Submit + + + + + + ) : ( + + + + + {contributionSubmissionContent?.discordServer} + + + Discord Server + + + + + {contributionSubmissionContent?.authenticatedUser} + + + Authenticated User + + + + + )} + + ); +}; + +const JoinDiscordTask = ({ + contribution, + commit +}: { + contribution: JoinDiscordContribution; + commit: ContributionCommit; +}) => { + return ( + + {contribution ? ( + <> + + + + ) : ( + + )} + + ); +}; + +export default memo(JoinDiscordTask); diff --git a/src/pages/Tasks/GithubTask/GithubCommitTask.tsx b/src/pages/Tasks/GithubTask/GithubCommitTask.tsx new file mode 100644 index 0000000..865a48e --- /dev/null +++ b/src/pages/Tasks/GithubTask/GithubCommitTask.tsx @@ -0,0 +1,243 @@ +import { memo, useState } from "react"; +import { Card, CardContent, Container, Stack, Typography } from "@mui/material"; +import { useSearchParams, useParams, useNavigate } from "react-router-dom"; +import { TaskStatus } from "@store/model"; +import { AutOsButton } from "@components/AutButton"; +import TaskDetails from "../Shared/TaskDetails"; +import SubmitDialog from "@components/Dialog/SubmitDialog"; +import AutLoading from "@components/AutLoading"; +import { useOAuthSocials } from "@components/OAuth"; +import { useWalletConnector } from "@aut-labs/connector"; +import { ContributionCommit } from "@utils/hooks/useQueryContributionCommits"; +import { GithubCommitContribution } from "@api/models/contribution-types/github-commit.model"; +import { useCommitAnyContributionMutation } from "@api/contributions.api"; +import ErrorDialog from "@components/Dialog/ErrorPopup"; + +const GithubCommitContent = ({ + contribution, + commit +}: { + contribution: GithubCommitContribution; + commit: ContributionCommit; +}) => { + const { state } = useWalletConnector(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const { autAddress, hubAddress } = useParams(); + const { getAuthGithub } = useOAuthSocials(); + + const [commitContribution, { error, isError, isSuccess, isLoading, reset }] = + useCommitAnyContributionMutation(); + + const contributionSubmissionContent = (() => { + let userSubmit = null; + try { + userSubmit = JSON.parse(commit?.data || "{}"); + } catch (e) { + // pass + } + return userSubmit; + })(); + + const handleSubmit = async () => { + await getAuthGithub( + async (data) => { + const { access_token } = data; + + const commitMessage = { + accessToken: access_token, + owner: contribution?.properties?.organisation, + branch: contribution?.properties?.branch, + repo: contribution?.properties?.repository + }; + + commitContribution({ + autSig: state.authSig, + contribution, + message: JSON.stringify(commitMessage), + hubAddress + }); + }, + () => { + // Handle auth failure + } + ); + }; + + return ( + + reset()} + open={isError} + message={(error as { message: string })?.message || "An error occurred"} + /> + + { + if (!isLoading) { + reset(); + navigate({ + pathname: `/${autAddress}/hub/${hubAddress}`, + search: searchParams.toString() + }); + } + }} + /> + + {!commit ? ( + + + + {contribution?.description} + + + + Please authenticate with Github to verify your contribution + + + + + Submit + + + + + ) : ( + + + + + {contributionSubmissionContent?.repo} + + + Repository + + + + + {contributionSubmissionContent?.authenticatedUser} + + + Authenticated User + + + + + )} + + ); +}; + +const GithubCommitTask = ({ + contribution, + commit +}: { + contribution: GithubCommitContribution; + commit: ContributionCommit; +}) => { + return ( + + {contribution ? ( + <> + + + + ) : ( + + )} + + ); +}; + +export default memo(GithubCommitTask); diff --git a/src/pages/Tasks/GithubTask/GithubPRTask.tsx b/src/pages/Tasks/GithubTask/GithubPRTask.tsx new file mode 100644 index 0000000..19111d3 --- /dev/null +++ b/src/pages/Tasks/GithubTask/GithubPRTask.tsx @@ -0,0 +1,254 @@ +import { memo, useState } from "react"; +import { + Card, + CardContent, + Container, + Link, + Stack, + Typography +} from "@mui/material"; +import { useSearchParams, useParams, useNavigate } from "react-router-dom"; +import { TaskStatus } from "@store/model"; +import { AutOsButton } from "@components/AutButton"; +import TaskDetails from "../Shared/TaskDetails"; +import SubmitDialog from "@components/Dialog/SubmitDialog"; +import AutLoading from "@components/AutLoading"; +import { useOAuthSocials } from "@components/OAuth"; +import { useWalletConnector } from "@aut-labs/connector"; +import { ContributionCommit } from "@utils/hooks/useQueryContributionCommits"; +import { GithubPullRequestContribution } from "@api/models/contribution-types/github-pr.model"; +import { useCommitAnyContributionMutation } from "@api/contributions.api"; +import ErrorDialog from "@components/Dialog/ErrorPopup"; + +const GithubPRContent = ({ + contribution, + commit +}: { + contribution: GithubPullRequestContribution; + commit: ContributionCommit; +}) => { + const { state } = useWalletConnector(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const { autAddress, hubAddress } = useParams(); + const { getAuthGithub } = useOAuthSocials(); + + const [commitContribution, { error, isError, isSuccess, isLoading, reset }] = + useCommitAnyContributionMutation(); + + if (error || isError) { + // debugger; + } + + const contributionSubmissionContent = (() => { + let userSubmit = null; + try { + userSubmit = JSON.parse(commit?.data || "{}"); + } catch (e) { + // pass + } + return userSubmit; + })(); + + const handleSubmit = async () => { + await getAuthGithub( + async (data) => { + const { access_token } = data; + + const commitMessage = { + accessToken: access_token, + owner: contribution?.properties?.organisation, + branch: contribution?.properties?.branch, + repo: contribution?.properties?.repository + }; + + commitContribution({ + autSig: state.authSig, + contribution, + message: JSON.stringify(commitMessage), + hubAddress + }); + }, + () => { + // Handle auth failure + } + ); + }; + + return ( + + reset()} + open={isError} + message={(error as { message: string })?.message || "An error occurred"} + /> + + { + if (!isLoading) { + reset(); + navigate({ + pathname: `/${autAddress}/hub/${hubAddress}`, + search: searchParams.toString() + }); + } + }} + /> + + {!commit ? ( + + + + {contribution?.description} + + + + Please authenticate with Github to verify your contribution + + + + + Submit + + + + + ) : ( + + + + + {contributionSubmissionContent?.repo} + + + Repository + + + + + {contributionSubmissionContent?.authenticatedUser} + + + Authenticated User + + + + + )} + + ); +}; + +const GithubPRTask = ({ + contribution, + commit +}: { + contribution: GithubPullRequestContribution; + commit: ContributionCommit; +}) => { + return ( + + {contribution ? ( + <> + + + + ) : ( + + )} + + ); +}; + +export default memo(GithubPRTask); diff --git a/src/pages/Tasks/OpenTask/OpenTask.tsx b/src/pages/Tasks/OpenTask/OpenTask.tsx index 398f1c2..9ea75fd 100644 --- a/src/pages/Tasks/OpenTask/OpenTask.tsx +++ b/src/pages/Tasks/OpenTask/OpenTask.tsx @@ -1,7 +1,3 @@ -/* eslint-disable no-useless-escape */ -/* eslint-disable max-len */ - -import AutLoading from "@components/AutLoading"; import { Box, Card, @@ -16,24 +12,22 @@ import { AutTextField } from "@theme/field-text-styles"; import { memo, useEffect, useMemo, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { useSearchParams, useParams, useNavigate } from "react-router-dom"; -import TaskDetails from "../Shared/TaskDetails"; -import SuccessDialog from "@components/Dialog/SuccessPopup"; -import { useAccount } from "wagmi"; import { FormHelperText } from "@components/Fields/AutFields"; import { ipfsCIDToHttpUrl } from "@utils/ipfs"; import { TaskFileUpload } from "@components/FileUpload"; import { AutOsButton } from "@components/AutButton"; import { TaskStatus } from "@store/model"; -import LoadingDialog from "@components/Dialog/LoadingPopup"; -import { updateContributionById } from "@store/contributions/contributions.reducer"; -import { useDispatch } from "react-redux"; import SubmitDialog from "@components/Dialog/SubmitDialog"; +import { OpenTaskContribution } from "@api/models/contribution-types/open-task.model"; +import { ContributionCommit } from "@utils/hooks/useQueryContributionCommits"; +import { useCommitAnyContributionMutation } from "@api/contributions.api"; +import { useWalletConnector } from "@aut-labs/connector"; +import ErrorDialog from "@components/Dialog/ErrorPopup"; +import TaskDetails from "../Shared/TaskDetails"; interface UserSubmitContentProps { - contribution: any; - userAddress: string; - isLoadingTasks: boolean; - submission?: any; + contribution: OpenTaskContribution; + commit: ContributionCommit; } const errorTypes = { @@ -42,87 +36,183 @@ const errorTypes = { const UserSubmitContent = ({ contribution, - userAddress, - isLoadingTasks + commit }: UserSubmitContentProps) => { - const [loading, setLoading] = useState(false); - const dispatch = useDispatch(); + const { state } = useWalletConnector(); const [searchParams] = useSearchParams(); const [initialized, setInitialized] = useState(false); const navigate = useNavigate(); - const [openSubmitSuccess, setOpenSubmitSuccess] = useState(false); - const { control, handleSubmit, formState, setValue } = useForm({ + const { control, handleSubmit, formState, setValue, getValues } = useForm({ mode: "onChange", defaultValues: { - openTask: null, - attachment: null + description: "", + attachment: "" } }); const { autAddress, hubAddress } = useParams(); + const contributionSubmissionContent = useMemo(() => { + let userSubmit = null; + try { + userSubmit = JSON.parse(commit.data); + } catch (e) { + // pass + } + return userSubmit; + }, [commit]); + + console.log(contributionSubmissionContent, 'contributionSubmissionContent'); + useEffect(() => { - if (!initialized && contribution) { - setValue("openTask", contribution.submission?.description); + if (!initialized && contributionSubmissionContent) { + let userSubmit = { + description: "", + attachment: "" + }; + try { + userSubmit = JSON.parse(commit.data); + } catch (e) { + // pass + } + setValue("description", userSubmit.description); + setValue("attachment", userSubmit.attachment); setInitialized(true); } - }, [initialized, contribution]); + }, [initialized, contributionSubmissionContent]); - // const [submitTask, { isSuccess, error, isError, isLoading, reset }] = - // useSubmitOpenTaskMutation(); + console.log(contributionSubmissionContent, 'contributionSubmissionContent'); - // useEffect(() => { - // if (isSuccess) { - // setOpenSubmitSuccess(true); - // } - // }, [isSuccess]); + const [commitContribution, { error, isError, isSuccess, isLoading, reset }] = + useCommitAnyContributionMutation(); - const onSubmit = async (values) => { - setLoading(true); - await new Promise((resolve) => setTimeout(resolve, 3000)); - dispatch( - updateContributionById({ - id: contribution?.id, - data: { - ...contribution, - status: TaskStatus.Submitted - } - }) - ); - setLoading(false); - setOpenSubmitSuccess(true); - - // submitTask({ - // userAddress, - // file: values.attachment, - // task: { - // ...task, - // submission: { - // name: "Open task submission", - // description: values.openTask, - // properties: {} as any - // } - // }, - // onboardingQuestAddress: searchParams.get( - // RequiredQueryParams.OnboardingQuestAddress - // ), - // pluginAddress: plugin.pluginAddress, - // pluginDefinitionId: plugin.pluginDefinitionId - // }); + const onSubmit = () => { + const description = getValues("description"); + const attachment = getValues("attachment"); + commitContribution({ + autSig: state.authSig, + contribution, + message: JSON.stringify({ description, attachment }), + hubAddress + }); }; - // const attachmentType = useMemo(() => { - // // @ts-ignore - // return task.properties.attachmentType; - // }, [task]); + const textRequiredTemplate = useMemo(() => { + if (!contribution.properties.textRequired) return null; + return ( + { + return ( + + ); + }} + /> + ); + }, [contribution.properties.textRequired]); - // const textRequired = useMemo(() => { - // // @ts-ignore - // return task.properties.textRequired; - // }, [task]); + const attachmentTypeTemplate = useMemo(() => { + const attachmentType = contribution.properties.attachmentType; + if (!attachmentType) return null; - const textRequired = contribution?.properties?.textRequired; - const attachmentType = contribution?.properties?.attachmentType; + if (attachmentType === "url") { + return ( + <> + + URL + + { + return ( + + } + /> + ); + }} + /> + + ); + } else if (attachmentType === "image" || attachmentType === "text") { + return ( + <> + + {`Upload a file ${ + attachmentType === "image" + ? "(.png, .jpg, .jpeg)" + : "(.doc, .docx, .txt, .pdf)" + }`} + + { + return ( +
+ { + if (file) { + onChange(file); + } else { + onChange(null); + } + }} + /> +
+ ); + }} + /> + + ); + } + }, [contribution.properties.attachmentType]); return ( - {/* reset()} open={isError} message={error} /> - */} - {/* */} + reset()} + open={isError} + message={error || "An error occurred"} + /> { - if (!loading) { - setOpenSubmitSuccess(false); + if (!isLoading) { + reset(); navigate({ pathname: `/${autAddress}/hub/${hubAddress}`, search: searchParams.toString() @@ -165,8 +257,7 @@ const UserSubmitContent = ({ }} > - {contribution?.status === TaskStatus.Created || - contribution?.status === TaskStatus.Taken ? ( + {!commit ? ( {contribution?.description} - {textRequired && ( - { - return ( - - ); - }} - /> - )} - {attachmentType === "url" && ( - <> - - URL - - { - return ( - - } - /> - ); - }} - /> - - )} - {(attachmentType === "image" || attachmentType === "text") && ( - <> - - {`Upload a file ${ - attachmentType === "image" - ? "(.png, .jpg, .jpeg)" - : "(.doc, .docx, .txt, .pdf)" - }`} - - { - return ( -
- { - if (file) { - onChange(file); - } else { - onChange(null); - } - }} - /> -
- ); - }} - /> - - )} + {textRequiredTemplate} + {attachmentTypeTemplate}
) : ( - {(contribution as any)?.status === TaskStatus.Finished && ( + {/* this will be implemented once give contribution is completed */} + {/* {contribution?.status === TaskStatus.Finished && ( - )} + )} */} - {contribution?.description} + {contributionSubmissionContent?.description} - Contribution Description + Description - { - // @ts-ignore - task?.properties?.attachmentRequired && ( - - + + - - Open attachment - - - - Attachment File - - - ) - } + Open attachment + + + + Attachment File + +
+ )} )} @@ -393,8 +364,7 @@ const UserSubmitContent = ({ } }} > - {(contribution?.status === TaskStatus.Created || - contribution?.status === TaskStatus.Taken) && ( + {!commit && ( - {" "} - Confirm + Submit @@ -431,48 +397,13 @@ const UserSubmitContent = ({ ); }; -const OpenTask = ({ contribution }: any) => { - const [searchParams] = useSearchParams(); - const { address: userAddress } = useAccount(); - - const params = useParams(); - - // const { task, submission, isLoading } = useGetAllTasksPerQuestQuery( - // { - // userAddress, - // isAdmin: false, - // pluginAddress: searchParams.get( - // RequiredQueryParams.OnboardingQuestAddress - // ), - // questId: +searchParams.get(RequiredQueryParams.QuestId) - // }, - // { - // selectFromResult: ({ data, isLoading, isFetching }) => ({ - // isLoading: isLoading || isFetching, - // submission: (data?.submissions || []).find((t) => { - // const [pluginType] = location.pathname.split("/").splice(-2); - // return ( - // t.submitter === userAddress && - // t.taskId === +params?.taskId && - // PluginDefinitionType[pluginType] === - // taskTypes[t.taskType].pluginType - // ); - // }), - // task: (data?.tasks || []).find((t) => { - // const [pluginType] = location.pathname.split("/").splice(-2); - // return ( - // t.taskId === +params?.taskId && - // PluginDefinitionType[pluginType] === - // taskTypes[t.taskType].pluginType - // ); - // }) - // }) - // } - // ); - - const isLoading = false; - const submission = null; - +const OpenTask = ({ + contribution, + commit +}: { + contribution: OpenTaskContribution; + commit: ContributionCommit; +}) => { return ( { position: "relative" }} > - {contribution ? ( - <> - - - - ) : ( - - )} + + ); }; diff --git a/src/pages/Tasks/QuizTask/QuizTask.tsx b/src/pages/Tasks/QuizTask/QuizTask.tsx index 6e969f1..12735c8 100644 --- a/src/pages/Tasks/QuizTask/QuizTask.tsx +++ b/src/pages/Tasks/QuizTask/QuizTask.tsx @@ -1,6 +1,5 @@ import AutLoading from "@components/AutLoading"; import ErrorDialog from "@components/Dialog/ErrorPopup"; -import LoadingDialog from "@components/Dialog/LoadingPopup"; import { Box, Card, @@ -12,18 +11,20 @@ import { styled, Typography } from "@mui/material"; -import { memo, useEffect, useState } from "react"; -import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form"; -import { useDispatch, useSelector } from "react-redux"; +import { Fragment, memo, useEffect, useMemo, useState } from "react"; +import { Controller, useForm, useWatch } from "react-hook-form"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; -import SuccessDialog from "@components/Dialog/SuccessPopup"; -import { useAccount } from "wagmi"; import TaskDetails from "../Shared/TaskDetails"; -import { StepperButton } from "@components/StepperButton"; import { AutOsButton } from "@components/AutButton"; import { TaskStatus } from "@store/model"; -import { updateContributionById } from "@store/contributions/contributions.reducer"; import SubmitDialog from "@components/Dialog/SubmitDialog"; +import { + QuizQuestionsAndAnswers, + QuizTaskContribution +} from "@api/models/contribution-types/quiz.model.model"; +import { ContributionCommit } from "@utils/hooks/useQueryContributionCommits"; +import { useCommitAnyContributionMutation } from "@api/contributions.api"; +import { useWalletConnector } from "@aut-labs/connector"; export const taskStatuses: any = { [TaskStatus.Created]: { @@ -63,30 +64,24 @@ const GridRow = styled(Box)({ gridGap: "8px" }); -const Row = styled(Box)({ - display: "flex", - justifyContent: "flex-end", - width: "100%" -}); +const alphabetize = { + 0: "A", + 1: "B", + 2: "C", + 3: "D" +}; -const Answers = memo(({ control, questionIndex, answers, taskStatus }: any) => { +const Answers = memo(({ control, questionIndex, answers, disabled }: any) => { const values = useWatch({ name: `questions[${questionIndex}].answers`, control }); - const alphabetize = { - 0: "A", - 1: "B", - 2: "C", - 3: "D" - }; - return ( {answers.map((answer, index) => { return ( - <> + {answer?.value && ( { {answer?.value} - v.correct) - }} - render={({ field: { name, value, onChange } }) => { - return ( - - ); - }} - /> + {disabled ? ( + + ) : ( + v.correct) + }} + render={({ field: { name, value, onChange } }) => { + return ( + + ); + }} + /> + )} )} - - ); - })} - - ); -}); - -const AnswersAdminView = memo(({ questionIndex, answers }: any) => { - return ( - - {answers.map((answer, index) => { - return ( - - - - {answer?.value} - - + ); })} ); }); -const QuizTask = ({ contribution }: any) => { +const QuizTask = ({ + contribution, + commit +}: { + contribution: QuizTaskContribution; + commit: ContributionCommit; +}) => { + const navigate = useNavigate(); + const { state } = useWalletConnector(); const [searchParams] = useSearchParams(); - const [loading, setLoading] = useState(false); - const dispatch = useDispatch(); - // const isAdmin = useSelector(IsAdmin); - const isAdmin = false; - const { address: userAddress } = useAccount(); - const params = useParams(); const [initialized, setInitialized] = useState(false); - const navigate = useNavigate(); - const [openSubmitSuccess, setOpenSubmitSuccess] = useState(false); const { autAddress, hubAddress } = useParams(); - - // const { task, isLoading: isLoadingTasks } = useGetAllTasksPerQuestQuery( - // { - // userAddress, - // isAdmin, - // pluginAddress: searchParams.get( - // RequiredQueryParams.OnboardingQuestAddress - // ), - // questId: +searchParams.get(RequiredQueryParams.QuestId) - // }, - // { - // selectFromResult: ({ data, isLoading, isFetching }) => ({ - // isLoading: isLoading || isFetching, - // task: (data?.tasks || []).find((t) => { - // const [pluginType] = location.pathname.split("/").splice(-2); - // return ( - // t.taskId === +params?.taskId && - // PluginDefinitionType[pluginType] === - // taskTypes[t.taskType].pluginType - // ); - // }) - // }) - // } - // ); - const { control, handleSubmit, getValues, setValue, formState } = useForm({ mode: "onChange", defaultValues: { @@ -223,54 +170,52 @@ const QuizTask = ({ contribution }: any) => { } }); - const { fields, append, remove } = useFieldArray({ - control, - name: "questions" - }); + const questionsWithUserAnswers = useMemo(() => { + let answers: QuizQuestionsAndAnswers[] = []; + try { + answers = JSON.parse(commit.data); + } catch (e) { + // pass + } + if (answers.length === 0 || !commit) + return contribution.properties.questions; + return contribution.properties.questions.map((question) => { + const userAnswer = answers.find( + (answer) => answer.question === question.question + ); + return { + ...question, + answers: question.answers.map((answer) => { + const userAnswerValue = userAnswer.answers.find( + (a) => a.value === answer.value + ); + return { + ...answer, + correct: userAnswerValue?.correct + }; + }) + }; + }); + }, [contribution, commit]); useEffect(() => { - if (!initialized && contribution) { - setValue( - "questions", - (contribution as any)?.properties?.questions - ); + if (!initialized && questionsWithUserAnswers?.length) { + setValue("questions", questionsWithUserAnswers); setInitialized(true); } - }, [initialized, contribution]); + }, [initialized, questionsWithUserAnswers]); - // const [submitTask, { isSuccess, error, isError, isLoading, reset }] = - // useSubmitQuizTaskMutation(); + const [commitContribution, { error, isError, isSuccess, isLoading, reset }] = + useCommitAnyContributionMutation(); - // useEffect(() => { - // if (isSuccess) { - // setOpenSubmitSuccess(true); - // } - // }, [isSuccess]); - - const onSubmit = async () => { - setLoading(true); - await new Promise((resolve) => setTimeout(resolve, 3000)); - dispatch( - updateContributionById({ - id: contribution.id, - data: { - ...contribution, - status: TaskStatus.Submitted - } - }) - ); - setLoading(false); - setOpenSubmitSuccess(true); - // submitTask({ - // userAddress, - // task, - // questionsAndAnswers: values.questions, - // onboardingQuestAddress: searchParams.get( - // RequiredQueryParams.OnboardingQuestAddress - // ), - // pluginAddress: plugin.pluginAddress, - // pluginDefinitionId: plugin.pluginDefinitionId - // }); + const onSubmit = () => { + const answers = getValues("questions"); + commitContribution({ + autSig: state.authSig, + contribution, + message: JSON.stringify(answers), + hubAddress + }); }; return ( @@ -285,39 +230,26 @@ const QuizTask = ({ contribution }: any) => { position: "relative" }} > - {/* reset()} open={isError} message={error} /> - */} - {/* - { - setOpenSubmitSuccess(false); - navigate({ - pathname: "/quest", - search: searchParams.toString() - }); - }} - > */} - reset()} + open={isError} + message={error || "An error occurred"} + /> + { - if (!loading) { - setOpenSubmitSuccess(false); + if (!isLoading) { + reset(); navigate({ pathname: `/${autAddress}/hub/${hubAddress}`, search: searchParams.toString() @@ -327,7 +259,8 @@ const QuizTask = ({ contribution }: any) => { > {contribution ? ( <> - + + { } }} > - {( - (contribution as any)?.metadata?.properties?.questions as any[] - )?.map((question, questionIndex) => ( + {questionsWithUserAnswers.map((question, questionIndex) => ( { control={control} answers={question?.answers} questionIndex={questionIndex} - taskStatus={contribution?.status} + // taskStatus={contribution?.status} + disabled={!!commit} > ))} - - - - Confirm - - - + + + Confirm + + +
+ )} ) : ( diff --git a/src/pages/Tasks/Shared/TaskDetails.tsx b/src/pages/Tasks/Shared/TaskDetails.tsx index b5dbfc7..bae61fe 100644 --- a/src/pages/Tasks/Shared/TaskDetails.tsx +++ b/src/pages/Tasks/Shared/TaskDetails.tsx @@ -1,90 +1,341 @@ import { Box, Button, - CircularProgress, + Chip, Stack, + styled, + SvgIcon, + Tooltip, Typography } from "@mui/material"; import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; import { memo } from "react"; -import { Link, useParams, useSearchParams } from "react-router-dom"; -import { format } from "date-fns"; -import { getContributionTypeSubtitle } from "@utils/format-contribution-type"; +import { Link, useParams } from "react-router-dom"; +import { TaskContributionNFT } from "@aut-labs/sdk"; +import ArrowIcon from "@assets/autos/move-right.svg?react"; +import InfoIcon from "@assets/autos/info-icon.svg?react"; +import theme from "@theme/theme"; +import { + borderRadius, + display, + margin, + maxWidth, + textAlign, + width +} from "@mui/system"; +import { max } from "date-fns"; -const TaskDetails = ({ task }: any) => { - const [searchParams] = useSearchParams(); - const { hubAddress, autAddress } = useParams(); - const isLoading = false; +const TaskTopWrapper = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + padding: "16px", + borderBottom: "1px solid", + borderColor: "inherit", + [theme.breakpoints.down("md")]: { + flexDirection: "column", + justifyContent: "center", + alignItems: "flex-start", + gap: "20px" + } +})); +const TaskBottomWrapper = styled(Box)(({ theme }) => ({ + display: "grid", + gridTemplateColumns: "1fr 1fr 1fr", + borderColor: "inherit", - const contributionType = getContributionTypeSubtitle( - task?.contributionType - ); + [theme.breakpoints.down("md")]: { + display: "flex", + flexDirection: "column" + } +})); + +const PropertiesWrapper = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + padding: "8px", + [theme.breakpoints.down("md")]: { + padding: "4px 0" + } +})); + +const TaskDetails = ({ + contribution +}: { + contribution: TaskContributionNFT & { contributionType?: string }; +}) => { + const { hubAddress, autAddress } = useParams(); + console.log(contribution); return ( <> - {isLoading ? ( - - ) : ( + + + + - - - - {task?.name} - - + + {contribution?.description} + + + + + + + + {new Date( + parseInt( + contribution?.properties?.startDate?.toString() || "" + ) * 1000 + ).toLocaleDateString()}{" "} + + + + {new Date( + parseInt( + contribution?.properties?.endDate?.toString() || "" + ) * 1000 + ).toLocaleDateString()}{" "} + + + + + + + {contribution?.properties.points}{" "} + {contribution.properties.points > 1 ? "points" : "point"} + + + + + + + + - - {contributionType} - + + + + {contribution?.properties.quantity}{" "} + {contribution.properties.quantity > 1 + ? "completions" + : "completion"} + + + + + + + - {/* */} + {/* + + */} + + + - )} + + {/* + + */} + ); }; diff --git a/src/pages/Tasks/TwitterTask/TwitterTask.tsx b/src/pages/Tasks/TwitterTask/TwitterTask.tsx index 2eef2df..7c5c271 100644 --- a/src/pages/Tasks/TwitterTask/TwitterTask.tsx +++ b/src/pages/Tasks/TwitterTask/TwitterTask.tsx @@ -1,6 +1,5 @@ -import { memo, useState } from "react"; +import { memo } from "react"; import { - Box, Card, CardContent, Container, @@ -9,84 +8,64 @@ import { Typography } from "@mui/material"; import { useSearchParams, useParams, useNavigate } from "react-router-dom"; -import { useDispatch } from "react-redux"; import { TaskStatus } from "@store/model"; -import { updateContributionById } from "@store/contributions/contributions.reducer"; import { AutOsButton } from "@components/AutButton"; import TaskDetails from "../Shared/TaskDetails"; import SubmitDialog from "@components/Dialog/SubmitDialog"; import AutLoading from "@components/AutLoading"; import { useOAuthSocials } from "@components/OAuth"; -import { useAccount } from "wagmi"; -import { useMutation } from "@tanstack/react-query"; -import axios from "axios"; -import { environment } from "@api/environment"; +import { useWalletConnector } from "@aut-labs/connector"; +import { ContributionCommit } from "@utils/hooks/useQueryContributionCommits"; +import { useCommitAnyContributionMutation } from "@api/contributions.api"; +import ErrorDialog from "@components/Dialog/ErrorPopup"; +import { RetweetContribution } from "@api/models/contribution-types/retweet.model"; -const TwitterSubmitContent = ({ contribution, userAddress }) => { - const [loading, setLoading] = useState(false); - const dispatch = useDispatch(); +const TwitterSubmitContent = ({ + contribution, + commit +}: { + contribution: RetweetContribution; + commit: ContributionCommit; +}) => { + const { state } = useWalletConnector(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); - const [openSubmitSuccess, setOpenSubmitSuccess] = useState(false); const { autAddress, hubAddress } = useParams(); const { getAuthX } = useOAuthSocials(); - const { mutateAsync: verifyTetweetTask } = useMutation({ - mutationFn: (verifyRetweetRequest) => { - return axios - .post( - `${environment.apiUrl}/task/twitter/retweet`, - verifyRetweetRequest - ) - .then((res) => res.data); + const [commitContribution, { error, isError, isSuccess, isLoading, reset }] = + useCommitAnyContributionMutation(); + + const contributionSubmissionContent = (() => { + let userSubmit = null; + try { + userSubmit = JSON.parse(commit?.data || "{}"); + } catch (e) { + // pass } - }); + return userSubmit; + })(); + + console.log(contributionSubmissionContent); const handleSubmit = async () => { await getAuthX( async (data) => { const { access_token } = data; - setLoading(true); - try { - await verifyTetweetTask( - { - accessToken: access_token, - contributionId: contribution?.id, - tweetUrl: contribution.properties?.tweetUrl - }, - { - onSuccess: (response) => { - dispatch( - updateContributionById({ - id: contribution?.id, - data: { - ...contribution, - status: TaskStatus.Submitted, - submission: { - properties: { - twitterToken: access_token - } - } - } - }) - ); - setOpenSubmitSuccess(true); - setLoading(false); - }, - onError: (res) => { - console.error("Failed to submit contribution:", res); - } - } - ); - } catch (error) { - setLoading(false); - // Handle error case - console.error("Failed to submit contribution:", error); - } + const tweetMessage = { + accessToken: access_token, + tweetUrl: contribution.properties?.tweetUrl + }; + + commitContribution({ + autSig: state.authSig, + contribution, + message: JSON.stringify(tweetMessage), + hubAddress + }); }, () => { - setLoading(false); // Handle auth failure } ); @@ -107,21 +86,27 @@ const TwitterSubmitContent = ({ contribution, userAddress }) => { } }} > + reset()} + open={isError} + message={(error as { message: string })?.message || "An error occurred"} + /> + { - if (!loading) { - setOpenSubmitSuccess(false); + if (!isLoading) { + reset(); navigate({ pathname: `/${autAddress}/hub/${hubAddress}`, search: searchParams.toString() @@ -130,8 +115,7 @@ const TwitterSubmitContent = ({ contribution, userAddress }) => { }} /> - {contribution?.status === TaskStatus.Created || - contribution?.status === TaskStatus.Taken ? ( + {!commit ? ( { padding: "32px" }} > - {contribution?.description} - + */} { > @@ -176,11 +160,11 @@ const TwitterSubmitContent = ({ contribution, userAddress }) => { - Please authenticate with Twitter to verify your contribution + Please authenticate with Twitter to verify your contribution. { }} > - Claim + Submit @@ -201,8 +185,9 @@ const TwitterSubmitContent = ({ contribution, userAddress }) => { ) : ( { alignItems: "center" }} > + + + + {contribution.properties?.tweetUrl} + + + + Tweet Url + + - {contribution?.description} + {contributionSubmissionContent?.authenticatedUser} - Contribution Description + Authenticated User @@ -234,9 +239,13 @@ const TwitterSubmitContent = ({ contribution, userAddress }) => { ); }; -const TwitterTask = ({ contribution }) => { - const { address: userAddress } = useAccount(); - +const TwitterTask = ({ + contribution, + commit +}: { + contribution: RetweetContribution; + commit: ContributionCommit; +}) => { return ( { > {contribution ? ( <> - - + + ) : ( diff --git a/src/redux/contributions/contributions.reducer.ts b/src/redux/contributions/contributions.reducer.ts index 16b34ed..f875041 100644 --- a/src/redux/contributions/contributions.reducer.ts +++ b/src/redux/contributions/contributions.reducer.ts @@ -3,6 +3,7 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { set } from "date-fns"; import { TaskStatus } from "@store/model"; import { attach } from "@react-three/fiber/dist/declarations/src/core/utils"; +import { TaskType } from "@api/models/task-type"; export const createLink = createAsyncThunk( "contributions/create", @@ -137,13 +138,15 @@ export interface ContributionState { errorMessage: string; selectedContribution: any; contributions: any[]; + taskTypes: TaskType[]; } const initialState: ContributionState = { status: ResultState.Idle, errorMessage: "", selectedContribution: null, - contributions: [] + contributions: [], + taskTypes: [], }; export const contributionSlice = createSlice({ @@ -200,4 +203,6 @@ export const AllContributions = (state) => export const ContributionErrorMessage = (state) => state.contribution.errorMessage as string; +export const TaskTypes = (state) => state.contribution.taskTypes as TaskType[]; + export default contributionSlice.reducer; diff --git a/src/redux/reducers.ts b/src/redux/reducers.ts index 23617c2..7e5b6cb 100644 --- a/src/redux/reducers.ts +++ b/src/redux/reducers.ts @@ -5,6 +5,7 @@ import walletProvideReducer from "./WalletProvider/WalletProvider"; import pluginsReducer from "./plugins/plugins.reducer"; import interactionsReducer from "./interactions/interactions.reducer"; import contributionsReducer from "./contributions/contributions.reducer"; +import { contributionsApi } from "@api/contributions.api"; export const reducers = combineReducers({ ui: uiSliceReducer, @@ -12,7 +13,8 @@ export const reducers = combineReducers({ plugin: pluginsReducer, interaction: interactionsReducer, contribution: contributionsReducer, - walletProvider: walletProvideReducer + walletProvider: walletProvideReducer, + [contributionsApi.reducerPath]: contributionsApi.reducer, }); const rootReducer = (state, action) => { diff --git a/src/redux/store.ts b/src/redux/store.ts index 26db637..08318b9 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -2,12 +2,13 @@ import { configureStore } from "@reduxjs/toolkit"; import logger from "redux-logger"; import { reducers } from "./reducers"; +import { contributionsApi } from "@api/contributions.api"; export const store = configureStore({ middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false - }).concat(logger), + }).concat(logger, contributionsApi.middleware), reducer: reducers }); diff --git a/src/utils/format-contribution-type.tsx b/src/utils/format-contribution-type.tsx index f7dd1ad..86a3461 100644 --- a/src/utils/format-contribution-type.tsx +++ b/src/utils/format-contribution-type.tsx @@ -10,6 +10,8 @@ export const formatContributionType = (type: string) => { return "GitHub"; case "retweet": return "Retweet"; + case "commit": + return "Commit"; default: return type; } @@ -27,6 +29,8 @@ export const getContributionTypeSubtitle = (type: string) => { return "GitHub Task"; case "retweet": return "Retweet Task"; + case "commit": + return "Commit Task"; default: return type; } diff --git a/src/utils/hooks/GetContributions.tsx b/src/utils/hooks/GetContributions.tsx index 022ab6a..530fca8 100644 --- a/src/utils/hooks/GetContributions.tsx +++ b/src/utils/hooks/GetContributions.tsx @@ -1,72 +1,100 @@ -/* eslint-disable no-debugger */ - import AutSDK, { BaseNFTModel, fetchMetadata, Hub, - TaskContribution, - TaskContributionNFT + TaskContributionNFT, + TaskFactoryContract } from "@aut-labs/sdk"; -import { QueryFunctionOptions, QueryResult } from "@apollo/client"; -import { useEffect, useState } from "react"; +import { + gql, + QueryFunctionOptions, + QueryResult, + useQuery +} from "@apollo/client"; +import { useEffect, useMemo, useState } from "react"; import { environment } from "@api/environment"; -import { AutOSHub } from "@api/models/hub.model"; import { useSelector } from "react-redux"; -import { Select } from "@mui/material"; -import { SelectedHub, SelectedHubAddress } from "@store/aut/aut.reducer"; -import { RootState } from "@react-three/fiber"; +import { SelectedHubAddress, SelectedHub } from "@store/aut/aut.reducer"; +import { TaskType } from "@api/models/task-type"; +import { TaskTypes } from "@store/contributions/contributions.reducer"; +import { ContributionFactory } from "@api/models/contribution.model"; +import { useParams } from "react-router-dom"; -const fetchOnChainContributions = async ( - hubData: AutOSHub -): Promise => { - const sdk = await AutSDK.getInstance(); - const hubService: Hub = sdk.initService(Hub, hubData.properties.address); - const taskFactory = await hubService.getTaskFactory(); - const ids = (await taskFactory.functions.contributionIds()) as string[]; +const GET_HUB_CONTRIBUTIONS = gql` + query GetContributions($skip: Int, $first: Int, $where: Contribution_filter) { + contributions(skip: $skip, first: $first, where: $where) { + id + taskId + role + startDate + endDate + points + quantity + uri + } + } +`; - const contributions = await Promise.all( - ids.map(async (id) => { - const _contribution = await taskFactory.functions.getContributionById(id); - const contribution = TaskContribution.mapFromTuple(_contribution as any); - const { uri } = await taskFactory.functions.getDescriptionById( - contribution.descriptionId - ); - const metadata = await fetchMetadata>( - uri, - environment.ipfsGatewayUrl - ); - return { - ...metadata, - properties: { - ...metadata.properties, - ...contribution - } - }; - }) - ); - return contributions; +const contributionsMetadata = ( + contributions: any[], + taskFactory: TaskFactoryContract, + taskTypes: TaskType[] +) => { + return contributions.map(async (contribution) => { + let metadata = await fetchMetadata>( + contribution.uri, + environment.ipfsGatewayUrl + ); + metadata = metadata || ({ properties: {} } as BaseNFTModel); + return ContributionFactory(metadata, contribution, taskTypes); + }); }; const useQueryContributions = (props: QueryFunctionOptions = {}) => { - const [contributions, setContributions] = useState([]); - const [loadingMetadata, setLoadingMetadata] = useState(false); + const taskTypes = useSelector(TaskTypes); const selectedHubAddress = useSelector(SelectedHubAddress); - const hubData = useSelector(SelectedHub(selectedHubAddress)); + const { hubAddress: _hubAddress } = useParams<{ hubAddress: string }>(); + + const hubAddress = useMemo(() => { + return selectedHubAddress || _hubAddress; + }, [_hubAddress, selectedHubAddress]); + const { data, loading, ...rest } = useQuery(GET_HUB_CONTRIBUTIONS, { + skip: !hubAddress && !taskTypes.length, + fetchPolicy: "cache-and-network", + variables: { + ...props.variables, + where: { + hubAddress: hubAddress, + ...(props.variables?.where || {}) + } + } + // ...props + }); + + const [contributions, setContributions] = useState([]); + const [loadingMetadata, setLoadingMetadata] = useState(true); + useEffect(() => { - if (hubData) { + if (hubAddress && data?.contributions?.length && taskTypes.length) { const fetch = async () => { + const sdk = await AutSDK.getInstance(); + const hubService: Hub = sdk.initService(Hub, hubAddress); + const taskFactory = await hubService.getTaskFactory(); setLoadingMetadata(true); - const contributions = await fetchOnChainContributions(hubData); + const contributions = await Promise.all( + contributionsMetadata(data?.contributions, taskFactory, taskTypes) + ); setLoadingMetadata(false); setContributions(contributions); }; fetch(); } - }, [hubData]); + }, [hubAddress, data, taskTypes]); return { data: contributions || [], - loading: loadingMetadata + ...rest, + loading: loading || loadingMetadata } as QueryResult; }; diff --git a/src/utils/hooks/useQueryContributionCommits.tsx b/src/utils/hooks/useQueryContributionCommits.tsx new file mode 100644 index 0000000..707cb5a --- /dev/null +++ b/src/utils/hooks/useQueryContributionCommits.tsx @@ -0,0 +1,106 @@ +import { Hub, TaskContributionProperties } from "@aut-labs/sdk"; +import { + gql, + QueryFunctionOptions, + QueryResult, + useQuery +} from "@apollo/client"; +import { useEffect, useState } from "react"; +import { environment } from "@api/environment"; +import axios from "axios"; +import { useWalletConnector } from "@aut-labs/connector"; + +export interface ContributionCommit { + id: string; + hub: Hub; + data: any; + who: string; + contribution: Partial; + dataDecrypted?: boolean; +} + +export interface DecryptResponseData { + isSuccess: boolean; + data?: string; + error?: string; +} + +const GET_CONTRIBUTION_COMMITS = gql` + query GetContributionCommits( + $skip: Int + $first: Int + $where: ContributionCommit_filter + ) { + contributionCommits(skip: $skip, first: $first, where: $where) { + id + hub + data + who + contribution { + id + } + } + } +`; + +export const decodeHashesOfCommits = async ({ + hashes, + autSig +}): Promise => { + return axios + .post(`${environment.apiUrl}/task/contribution/viewByHashes`, { + hashes, + autSig + }) + .then((r) => r.data); +}; + +const useQueryContributionCommits = ( + props: QueryFunctionOptions = {} +) => { + const { state } = useWalletConnector(); + const { data, loading, ...rest } = useQuery(GET_CONTRIBUTION_COMMITS, { + fetchPolicy: "cache-and-network", + ...props + }); + + const [commits, setCommits] = useState([]); + + useEffect(() => { + if (data?.contributionCommits && !!state?.authSig) { + const hashes = data.contributionCommits.map( + (commit: ContributionCommit) => commit.data + ); + const fetch = async () => { + try { + const decodedCommits = await decodeHashesOfCommits({ + hashes, + autSig: state.authSig + }); + const commits = data.contributionCommits.map( + (commit: ContributionCommit, i: number) => { + const data = decodedCommits[i]; + return { + ...commit, + data: data?.data || commit.data, + dataDecrypted: data?.isSuccess + }; + } + ); + setCommits(commits); + } catch (e) { + setCommits(data?.contributionCommits); + } + }; + fetch(); + } + }, [data, state?.authSig]); + + return { + data: commits || [], + ...rest, + loading: loading + } as QueryResult; +}; + +export default useQueryContributionCommits; diff --git a/src/utils/hooks/useQueryHubPeriod.tsx b/src/utils/hooks/useQueryHubPeriod.tsx new file mode 100644 index 0000000..fb05339 --- /dev/null +++ b/src/utils/hooks/useQueryHubPeriod.tsx @@ -0,0 +1,86 @@ +import { useQuery } from "@tanstack/react-query"; +import { useSelector } from "react-redux"; +import AutSDK, { Hub } from "@aut-labs/sdk"; +import { SelectedAutID, SelectedHubAddress } from "@store/aut/aut.reducer"; +import { useParams } from "react-router-dom"; +import { useMemo } from "react"; +import { useWalletConnector } from "@aut-labs/connector"; +import { AutOSAutID } from "@api/models/aut.model"; + +export interface HubPeriodData { + hub: string; + who: string; + periodId: number; + startDate: Date; + endDate: Date; + pointsGiven: number; + score: number; + performance: number; + expectedPoints: number; +} + +const fetchHubPeriodData = async ( + hubAddress: string, + who: string, + autID: AutOSAutID +): Promise => { + const sdk = await AutSDK.getInstance(); + const hubService: Hub = sdk.initService(Hub, hubAddress); + const taskManager = await hubService.getTaskManager(); + const participationScore = await hubService.getParticipationScoreFactory(); + const periodId = await taskManager.functions.currentPeriodId(); + const selectedHub = autID.joinedHub(hubAddress); + + const [startDate, endDate, pointsGiven, participation, expectedPoints] = + await Promise.all([ + taskManager.functions.currentPeriodStart(), + taskManager.functions.currentPeriodEnd(), + taskManager.functions.getMemberPointsGiven(who, Number(periodId)), + participationScore.functions.memberParticipations(who, Number(periodId)), // {score: number; performance: number} + participationScore.functions.calcExpectedPoints( + +selectedHub.commitment, + Number(periodId) + ) + ]); + + return { + hub: hubAddress, + who, + periodId: Number(periodId), + startDate: new Date(Number(startDate) * 1000), + endDate: new Date(Number(endDate) * 1000), + pointsGiven: Number(pointsGiven), + score: Number(participation.score), + performance: Number(participation.performance), + expectedPoints: Number(expectedPoints) + }; +}; + +const useQueryHubPeriod = () => { + const selectedHubAddress = useSelector(SelectedHubAddress); + const autID = useSelector(SelectedAutID); + const { state } = useWalletConnector(); + const { hubAddress: _hubAddress } = useParams<{ hubAddress: string }>(); + + const hubAddress = useMemo(() => { + return selectedHubAddress || _hubAddress; + }, [_hubAddress, selectedHubAddress]); + + const { + data: periodData, + isLoading: loadingMetadata, + error + } = useQuery({ + queryKey: ["hubPeriodData", hubAddress], + queryFn: () => fetchHubPeriodData(hubAddress, state.address, autID), + enabled: !!hubAddress && !!state?.address + }); + + return { + data: periodData, + loading: loadingMetadata, + error + }; +}; + +export default useQueryHubPeriod; diff --git a/src/utils/hooks/useQueryTaskTypes.tsx b/src/utils/hooks/useQueryTaskTypes.tsx new file mode 100644 index 0000000..7d3d867 --- /dev/null +++ b/src/utils/hooks/useQueryTaskTypes.tsx @@ -0,0 +1,74 @@ +import { BaseNFTModel, fetchMetadata } from "@aut-labs/sdk"; +import { + gql, + QueryFunctionOptions, + QueryResult, + useQuery +} from "@apollo/client"; +import { useEffect, useState } from "react"; +import { environment } from "@api/environment"; +import { useDispatch } from "react-redux"; +import { TaskType } from "@api/models/task-type"; +import { updateContributionState } from "@store/contributions/contributions.reducer"; + +const GET_TASK_TYPES = gql` + query GetTaskTypes($skip: Int, $first: Int, $where: HubAdmin_filter) { + tasks(skip: $skip, first: $first, where: $where) { + id + metadataUri + taskId + creator + } + } +`; + +const fetchTaskTypesMetadata = (taskTypes: TaskType[]) => { + return taskTypes.map(async ({ id, metadataUri, taskId, creator }) => { + let metadata = await fetchMetadata>( + metadataUri, + environment.ipfsGatewayUrl + ); + metadata = metadata || ({ properties: {} } as BaseNFTModel); + return { + id, + metadataUri, + taskId, + creator, + metadata + }; + }); +}; + +const useQueryTaskTypes = (props: QueryFunctionOptions = {}) => { + const dispatch = useDispatch(); + const { data, loading, ...rest } = useQuery(GET_TASK_TYPES, { + fetchPolicy: "cache-and-network", + ...props + }); + + const [taskTypes, setTaskTypes] = useState([]); + const [loadingMetadata, setLoadingMetadata] = useState(false); + + useEffect(() => { + if (data?.tasks?.length) { + const fetch = async () => { + setLoadingMetadata(true); + const taskTypes = await Promise.all([ + ...fetchTaskTypesMetadata(data?.tasks) + ]); + dispatch(updateContributionState({ taskTypes })); + setLoadingMetadata(false); + setTaskTypes(taskTypes); + }; + fetch(); + } + }, [data]); + + return { + data: taskTypes || [], + ...rest, + loading: loadingMetadata || loading + } as QueryResult; +}; + +export default useQueryTaskTypes;