diff --git a/src/app/[locale]/(interactive)/interactive/voting/page.client.tsx b/src/app/[locale]/(interactive)/interactive/voting/page.client.tsx new file mode 100644 index 00000000..4d0d1f2a --- /dev/null +++ b/src/app/[locale]/(interactive)/interactive/voting/page.client.tsx @@ -0,0 +1,181 @@ +'use client' +import { client } from '@/app/appwrite' +import { createVote } from '@/utils/actions/interactive/createVote' +import { useEffect, useState } from 'react' +import { RealtimeResponseEvent } from 'appwrite' +import { Interactive } from '@/utils/types/models' + +export default function VotingClient({ + questionId, + votes, + forwardedFor, +}: { + questionId: number + votes: Interactive.VotesAnswersType + forwardedFor: string +}) { + const [votedQuestions, setVotedQuestions] = useState({}) + const [selectedQuestionIndex, setSelectedQuestionIndex] = useState(questionId) + const [selectedOptionIndex, setSelectedOptionIndex] = useState(null) + + const loadVotedQuestions = () => { + let newVotedQuestions = { ...votedQuestions } + votes.documents.forEach((vote) => { + if (vote.ipAddress === forwardedFor) { + newVotedQuestions[vote.questionId] = Number(vote.optionId) + } + }) + setVotedQuestions(newVotedQuestions) + } + + console.log(votedQuestions) + + // Call this function when the component mounts + useEffect(() => { + loadVotedQuestions() + }, []) + + useEffect(() => { + if (votedQuestions[selectedQuestionIndex]) { + setSelectedOptionIndex(votedQuestions[selectedQuestionIndex]) + } + }, [selectedQuestionIndex, votedQuestions]) + + interface QuestionType extends RealtimeResponseEvent { + questionId: string + } + + client.subscribe( + ['databases.interactive.collections.questions.documents.main'], + (response: QuestionType) => { + setSelectedQuestionIndex(response.payload.questionId) + } + ) + + const questions = [ + { + question: 'You come across a fork in the road. Do you:', + options: [ + 'Take the left path, which seems well-traveled.', + 'Take the right path, which appears less traveled but possibly more interesting.', + 'Consult your map or ask a local for advice.', + ], + }, + { + question: 'You encounter a wounded traveler on the roadside. Do you:', + options: [ + 'Offer immediate assistance, tending to their wounds.', + "Approach cautiously, unsure if it's a trap.", + 'Ignore them and continue on your journey, not wanting to get involved.', + ], + }, + { + question: 'A merchant offers to sell you a mysterious potion. Do you:', + options: [ + 'Haggle for a lower price before buying it.', + "Ask for more information about the potion's effects.", + 'Decline the offer, wary of unknown substances.', + ], + }, + { + question: 'You stumble upon a hidden cave entrance. Do you:', + options: [ + 'Investigate further, curious about what treasures it might hold.', + 'Proceed cautiously, wary of potential dangers lurking inside.', + 'Mark the location on your map for later exploration and continue on your current path.', + ], + }, + { + question: + 'A group of bandits blocks your path, demanding payment for safe passage. Do you:', + options: [ + 'Negotiate with them to find a peaceful resolution.', + 'Refuse to pay and prepare to fight if necessary.', + 'Attempt to sneak past them unnoticed.', + ], + }, + { + question: 'You discover an ancient tome in a forgotten library. Do you:', + options: [ + 'Spend time deciphering its contents to gain knowledge or power.', + 'Leave it undisturbed, fearing potential consequences.', + 'Take the tome with you to study later.', + ], + }, + { + question: 'You encounter a magical creature in distress. Do you:', + options: [ + 'Rush to aid the creature, believing it may reward you for your kindness.', + 'Approach cautiously, unsure of its intentions.', + 'Ignore the creature and continue on your journey, not wanting to risk danger.', + ], + }, + ] + + const handleVote = async (questionIndex, optionIndex) => { + const voteKey = `${questionIndex}-${optionIndex}` + + if (votedQuestions[questionIndex]) { + alert('You have already voted on this question.') + return + } + + // Check if the user has already voted on this question + const userHasVoted = votes.documents.some( + (vote) => + vote.questionId === questionIndex && vote.ipAddress === forwardedFor + ) + + if (userHasVoted) { + alert('You have already voted on this question.') + return + } + + // Update the state immediately + setVotedQuestions((prevState) => { + return { ...prevState, [selectedQuestionIndex]: optionIndex } + }) + + // Update the selected option immediately + setSelectedOptionIndex(optionIndex) + + // Then send the vote to the server + const vote = await createVote(selectedQuestionIndex, optionIndex) + } + + console.log(selectedOptionIndex) + + //console.log(votedQuestions) + + return ( +
+
+ {questions[selectedQuestionIndex].question && ( +

+ {questions[selectedQuestionIndex].question} +

+ )} + {questions[selectedQuestionIndex].options.map((option, optionIndex) => ( + + ))} +
+
+ ) +} diff --git a/src/app/[locale]/(interactive)/interactive/voting/page.tsx b/src/app/[locale]/(interactive)/interactive/voting/page.tsx new file mode 100644 index 00000000..7fa4b2e9 --- /dev/null +++ b/src/app/[locale]/(interactive)/interactive/voting/page.tsx @@ -0,0 +1,23 @@ +import PageLayout from '@/components/pageLayout' +import VotingClient from '@/app/[locale]/(interactive)/interactive/voting/page.client' +import { getVotes } from '@/utils/server-api/interactive/votes/getVotes' +import { headers } from 'next/headers' +import { getQuestionId } from '@/utils/server-api/interactive/votes/getQuestionId' + +export const runtime = 'edge' + +export default async function VotingPage() { + const questionId = await getQuestionId() + const votes = await getVotes() + const forwardedFor = headers().get('x-forwarded-for') + + return ( + + + + ) +} diff --git a/src/app/[locale]/(interactive)/layout.tsx b/src/app/[locale]/(interactive)/layout.tsx new file mode 100644 index 00000000..a4f26c20 --- /dev/null +++ b/src/app/[locale]/(interactive)/layout.tsx @@ -0,0 +1,9 @@ +import Header from '@/components/header/header-server' + +export default function LocaleLayout({ children, params: { locale } }) { + return ( +
+
{children}
+
+ ) +} diff --git a/src/utils/actions/interactive/createVote.ts b/src/utils/actions/interactive/createVote.ts new file mode 100644 index 00000000..cf99b904 --- /dev/null +++ b/src/utils/actions/interactive/createVote.ts @@ -0,0 +1,15 @@ +'use server' +import { createAdminClient } from '@/app/appwrite-session' +import { ID } from 'node-appwrite' +import { headers } from 'next/headers' + +export async function createVote(questionId: number, optionId: number) { + const forwardedFor = headers().get('x-forwarded-for') + + const { databases } = await createAdminClient() + return await databases.createDocument('interactive', 'answers', ID.unique(), { + questionId: `${questionId}`, + optionId: `${optionId}`, + ipAddress: forwardedFor || null, + }) +} diff --git a/src/utils/server-api/interactive/votes/getQuestionId.ts b/src/utils/server-api/interactive/votes/getQuestionId.ts new file mode 100644 index 00000000..cf15ee6c --- /dev/null +++ b/src/utils/server-api/interactive/votes/getQuestionId.ts @@ -0,0 +1,18 @@ +import { createAdminClient } from '@/app/appwrite-session' +import { Interactive } from '@/utils/types/models' + +/** + * This function is used to get the answers of the votes of Lighthase's EF Panel. + * @returns The question ID. + * @example + * const questionId = await getQuestionId() + */ +export async function getQuestionId(): Promise { + const { databases } = await createAdminClient() + const data: Interactive.VotesQuestionId = await databases + .getDocument('interactive', 'questions', 'main') + .catch((error) => { + return error + }) + return data.questionId +} diff --git a/src/utils/server-api/interactive/votes/getVotes.ts b/src/utils/server-api/interactive/votes/getVotes.ts new file mode 100644 index 00000000..bfdc07cd --- /dev/null +++ b/src/utils/server-api/interactive/votes/getVotes.ts @@ -0,0 +1,17 @@ +import { createAdminClient } from '@/app/appwrite-session' +import { Interactive } from '@/utils/types/models' + +/** + * This function is used to get the answers of the votes of Lighthase's EF Panel. + * @returns The answers from the votes. + * @example + * const votes = await getVotes() + */ +export async function getVotes(): Promise { + const { databases } = await createAdminClient() + return await databases + .listDocuments('interactive', 'answers') + .catch((error) => { + return error + }) +} diff --git a/src/utils/types/models.ts b/src/utils/types/models.ts index 22a5b1c2..4f858301 100644 --- a/src/utils/types/models.ts +++ b/src/utils/types/models.ts @@ -117,3 +117,32 @@ export namespace Gallery { mimeType: string } } + +export namespace Interactive { + /** + * This data is returned from the API by calling the interactive endpoint. + * @see InteractiveDocumentsType + * @interface + * @since 2.0.0 + */ + export interface VotesAnswersType { + total: number + documents: VotesAnswersDocumentsType[] + } + + /** + * This data is returned from the API within the `documents` array. + * @see InteractiveType + * @interface + * @since 2.0.0 + */ + export interface VotesAnswersDocumentsType extends Models.Document { + ipAddress: string | null + questionId: number + optionId: number + } + + export interface VotesQuestionId extends Models.Document { + questionId: number + } +}