Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat - Handle do exercise #1

Merged
merged 2 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"axios": "^1.7.2",
"clsx": "^2.1.1",
"pinia": "^2.1.7",
"primeicons": "^7.0.0",
"tailwind-merge": "^2.3.0",
"vue": "^3.4.21",
"vue-awesome-paginate": "^1.1.46",
"vue-router": "4",
Expand Down
6 changes: 5 additions & 1 deletion src/api/examsRepository.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import AxiosClient from "./axios";

const resource = "/category";
const resourceExam = "/exam";

export default {
get(classId: string, currentPage: number) {
return AxiosClient.get(`${resource}/${classId}/exams?page=${currentPage}`);
},
getOne(exerciseId: string) {
return AxiosClient.get(`/exam/${exerciseId}`);
return AxiosClient.get(`${resourceExam}/${exerciseId}`);
},
search(searchText: string, page: number) {
return AxiosClient.post('/search', {
keyword: searchText,
page: page || 1,
});
},
submit(formData: any) {
return AxiosClient.post(`${resourceExam}/submit`, formData);
}
}

22 changes: 22 additions & 0 deletions src/components/Button/ButtonUtils.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template lang="">
<button
:class="cn('border rounded px-4 py-2 hover:text-white flex items-center justify-center gap-2 transition ease-linear w-full md:w-fit', props.className)"
@click="props.func"
>
<slot/>
</button>
</template>

<script setup>
import { cn } from "@/utils/cn";

const props = defineProps({
className: {
type: String,
},
func: {
type: Function,
required: true
}
})
</script>
30 changes: 25 additions & 5 deletions src/components/DoExercise/Question.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
<template lang="">
<div class="mb-5">
<h1 v-html="props.name" class="mb-2"></h1>
<p v-for="(ans, idx) in props.answers" :key="ans.id" class="text-[1rem] leading-4 mb-4 px-1 flex items-center">

<h1
v-html="props.name"
:class="{'text-red-500' : props.warning}"
></h1>
<p v-for="(ans, idx) in props.answers" :key="ans.id" class="text-[1rem] leading-4 mb-4 px-1 flex items-center mt-2">
<input
:id="'question-'+props.questionId+'-'+idx"
type="radio"
:value="idx"
:value="ans.name"
v-model="checked"
class="w-4 h-4 bg-gray-100 border-gray-300 cursor-pointer focus:outline-none"
/>
Expand All @@ -21,8 +25,9 @@
</template>

<script setup>
import console from "console";
import { ref } from "vue";
import { ref, watch } from "vue";
const emit = defineEmits(["update-answer"]);

const props = defineProps({
name: {
type: String,
Expand All @@ -35,8 +40,23 @@ import { ref } from "vue";
questionId: {
type: Number,
required: true,
},
questionIdx: {
type: Number,
required: true,
},
pageIdx: {
type: Number,
required: true,
},
warning: {
type: Boolean,
}
});

const checked = ref();

watch(() => checked.value, ()=>{
emit("update-answer", checked.value, props.pageIdx, props.questionIdx)
})
</script>
20 changes: 20 additions & 0 deletions src/components/DoExercise/Quote.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template lang="">
<i class="block md:mb-8 mb-5 text-base md:text-xl md:px-8 px-2" v-html="quote">
</i>
</template>

<script setup>
import { ref, onMounted } from "vue";
const quote = ref("");

onMounted(async ()=> {
const API_KEY = "SltTB9eBPK6on57H/Vlygg==1GGt23U1uXtMedKH";
const response = await fetch("https://api.api-ninjas.com/v1/quotes?category=learning", {
headers: {
'x-api-key': API_KEY,
}
});
const quoteData = await response.json();
quote.value = `${quoteData[0].quote} - <b>${quoteData[0].author}</b>`;
});
</script>
73 changes: 73 additions & 0 deletions src/components/DoExercise/Results.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<template lang="">
<div class="text-center">
<h1 class="text-3xl md:text-4xl my-4 md:my-6 font-bold">
{{ isSubmitHaveNiceResult(props.results.score)?"Xin chúc mừng!":"Bạn có thể thử lại lần nữa."}}
</h1>
<p>Với <span class="text-green-500 font-bold">{{ props.results.correctAnswers }}/{{ props.results.totalQuestion }}</span> câu trả lời đúng. Điểm của bạn là</p>
<h1
:class="'text-4xl font-bold my-6 ' + (isSubmitHaveNiceResult(props.results.score)?'text-green-500':'text-red-400')"
>{{ props.results.score }}</h1>
<div class="flex md:flex-row flex-col gap-3 md:gap-5 flex-wrap md:my-10 my-5 justify-center items-center px-2">
<ButtonUtils
className="bg-[#f0f9eb] border-[#c2e7b0] hover:bg-[#67c23a] text-[#67c23a]"
:func="handleTryAgain"
>
<Icon icon="pi-sync"/>
Thử làm lại
</ButtonUtils>

<ButtonUtils
className="bg-[#fdf6ec] border-[#f5dab1] hover:bg-[#e6a23c] text-[#e6a23c]"
:func="handleShowResults"
>
<Icon icon="pi-file-check"/>
Xem đáp án chi tiết
</ButtonUtils>

<ButtonUtils
className="bg-[#f4f4f5] border-[#d3d4d6] hover:bg-[#909399] text-[#909399]"
:func="handleDoMore"
>
<Icon icon="pi-send"/>
Làm các đề khác
</ButtonUtils>
</div>

<Quote/>
</div>
</template>

<script setup>
import ButtonUtils from "@/components/Button/ButtonUtils.vue";
import Icon from "@/components/Icon.vue";
import Quote from "@/components/DoExercise/Quote.vue";
import router from "@/router";

const props = defineProps({
slug: {
type: String,
required: true,
},
results: {
type: Object,
required: true,
}
});

function isSubmitHaveNiceResult(score) {
return score >= 5;
}

function handleTryAgain() {
window.location.reload();
}

function handleDoMore() {
router.push({ name: 'exercise', params: { classId: props.slug } });
}

function handleShowResults() {
alert("This feature will show the results and implement later");
}

</script>
6 changes: 6 additions & 0 deletions src/utils/cn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
75 changes: 64 additions & 11 deletions src/views/DoExercisePage.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
<template lang="">
<div class="bg-slate-50 m-0 py-2 px-1">
<div class="md:w-9/12 rounded mx-auto box-shadow bg-white text-[#606266] flex flex-col">
<div v-if="!loadingStore.isLoading && examData">
<div v-if="!loadingStore.isLoading && examData && !isShowResults">
<Title :name="examData.value.exam.name"/>
<div class="md:px-4 px-2 text-base mb-8">
<i class="font-medium" v-html="examData.value.exam.pages[0].note">
</i>
<div class="my-3 px-3">
<Question
v-for="q in examData.value.exam.pages[0].questions"
:key="q.id"
:name="q.name"
:answers="q.answers"
:question-id="q.id"
/>
<div
v-for="(page, pageIdx) in examData.value.exam.pages"
>
<Question
v-for="(q, questionIdx) in page.questions"
:key="q.id"
:name="q.name"
:answers="q.answers"
:question-id="q.id"
:page-idx="pageIdx"
:question-idx="questionIdx"
:warning="q.warning==false||!isFirstSubmit?false:true"
@update-answer="handleUpdateAnswer"
/>
</div>
</div>
</div>

Expand All @@ -24,21 +32,32 @@
</button>
</div>
</div>

<div v-else-if="!loadingStore.isLoading && isShowResults">
<Results
:slug="examData.value.category.slug"
:results="resultsData.value"
/>
</div>
</div>
</div>
</template>

<script setup>
import { useRoute } from 'vue-router';
import { watch, reactive } from "vue";
import { watch, reactive, ref } from "vue";
import { RepositoryFactory } from "@/api/RepositoryFactory";
import { useLoadingStore } from "@/store/loading";
import { toast } from 'vue3-toastify';
import Title from "@/components/DoExercise/Title.vue";
import Question from "@/components/DoExercise/Question.vue";
import Results from "@/components/DoExercise/Results.vue";
import Icon from "@/components/Icon.vue";

const examData = reactive([]);
const resultsData = reactive([]);
const isShowResults = ref(false);
const isFirstSubmit = ref(false);
const loadingStore = useLoadingStore();
const ExamRepository = RepositoryFactory.get("exams");
const route = useRoute();
Expand All @@ -59,9 +78,43 @@
});
}, { immediate: true });

function submitExam() {
alert("This feature will implement later");
function handleUpdateAnswer(answer, pageId, questionId) {
examData.value.exam.pages[pageId].questions[questionId].answer = answer;
examData.value.exam.pages[pageId].questions[questionId].warning = false;
}

async function submitExam() {
isFirstSubmit.value = true;

// check all questions have answer from user
for (const page of examData.value.exam.pages) {
for (const q of page.questions){
if (q.warning==null || q.warning==true || !q.answer || q.answer.length <= 0) {
return toast.warning("Vui lòng trả lời hết các câu hỏi (đang bị bôi đỏ) trước khi nộp bài.", {
toastStyle: {
fontSize: '14px',
},
position: toast.POSITION.BOTTOM_RIGHT,
});
}
}
}

// submit answer
loadingStore.changeLoadingState(true);
const { data } = await ExamRepository.submit(examData.value.exam);
loadingStore.changeLoadingState(false);

resultsData.value = data;
isShowResults.value = true;
toast.success("Nộp bài thành công", {
toastStyle: {
fontSize: '14px',
},
position: toast.POSITION.BOTTOM_RIGHT,
});
}

</script>

<style lang="scss">
Expand Down
24 changes: 24 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz"
integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==

"@babel/runtime@^7.24.1":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
dependencies:
regenerator-runtime "^0.14.0"

"@commitlint/cli@^19.3.0":
version "19.3.0"
resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-19.3.0.tgz#44e6da9823a01f0cdcc43054bbefdd2c6c5ddf39"
Expand Down Expand Up @@ -789,6 +796,11 @@ cliui@^8.0.1:
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"

clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==

color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
Expand Down Expand Up @@ -1680,6 +1692,11 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"

regenerator-runtime@^0.14.0:
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==

require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
Expand Down Expand Up @@ -1865,6 +1882,13 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==

tailwind-merge@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.3.0.tgz#27d2134fd00a1f77eca22bcaafdd67055917d286"
integrity sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==
dependencies:
"@babel/runtime" "^7.24.1"

tailwindcss@^3.4.4:
version "3.4.4"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz"
Expand Down
Loading