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

#132 - offline quizzes frontend #133

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions app/Http/Requests/UpdateQuizRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public function prepareForValidation(): void
if ($this->has("isPublic")) {
$this->merge(["is_public" => $this->input("isPublic")]);
}

if ($this->has("isLocal")) {
$this->merge(["is_local" => $this->input("isLocal")]);
}
}

/**
Expand All @@ -35,6 +39,7 @@ public function rules(): array
"title" => ["required", "string", "max:255"],
"scheduled_at" => ["date", "after:now"],
"is_public" => ["boolean"],
"is_local" => ["boolean"],
"duration" => ["integer", "min:1", "max:2147483647"],
"description" => ["string", "nullable"],
"questions" => ["array"],
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Resources/QuizResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function toArray($request): array
"isPublic" => $this->is_public,
"canBeLocked" => $this->canBeLocked,
"canBeUnlocked" => $this->canBeUnlocked,
"questions" => $this->is_local ? [] : QuestionResource::collection($this->questions),
"questions" => QuestionResource::collection($this->questions),
"isUserAssigned" => $this->isUserAssigned($request->user()),
"isRankingPublished" => $this->isRankingPublished,
"isLocal" => $this->is_local,
Expand Down
1 change: 1 addition & 0 deletions app/Http/Resources/UserQuizResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public function toArray(Request $request): array
return [
"id" => $this->id,
"title" => $this->quiz->title,
"description" => $this->quiz->description,
"createdAt" => $this->created_at,
"updatedAt" => $this->updated_at,
"closedAt" => $this->closed_at,
Expand Down
1 change: 1 addition & 0 deletions app/Models/Quiz.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class Quiz extends Model
"ranking_published_at",
"description",
"is_public",
"is_local",
];
protected $guarded = [];

Expand Down
6 changes: 3 additions & 3 deletions database/seeders/AdminSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public function run(): void
"firstname" => "Example",
"surname" => "Super Admin",
"email_verified_at" => Carbon::now(),
"password" => Hash::make("password"),
"password" => Hash::make("interns2024b"),
"remember_token" => Str::random(10),
"school_id" => $school->id,
],
Expand All @@ -48,7 +48,7 @@ public function run(): void
"firstname" => "Example",
"surname" => "Admin",
"email_verified_at" => Carbon::now(),
"password" => Hash::make("password"),
"password" => Hash::make("interns2024b"),
"remember_token" => Str::random(10),
"school_id" => $school->id,
],
Expand All @@ -61,7 +61,7 @@ public function run(): void
"firstname" => "Example",
"surname" => "User",
"email_verified_at" => Carbon::now(),
"password" => Hash::make("password"),
"password" => Hash::make("interns2024b"),
"remember_token" => Str::random(10),
"school_id" => School::factory()->create()->id,
],
Expand Down
1 change: 1 addition & 0 deletions resources/js/Helpers/vDynamicInputWidth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function calculateDynamicWidth(input: HTMLInputElement, binding?: DirectiveBindi
context.font = `${fontWeight} ${fontSize} ${fontFamily}`
const width = context.measureText(input.value || input.placeholder).width

input.style.minWidth = `${width}px`
input.style.width = `clamp(1.1rem,${width}px,100%)`
}

Expand Down
10 changes: 5 additions & 5 deletions resources/js/Helpers/vDynamicTextAreaHeight.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { type DirectiveBinding } from 'vue'

function initDynamicHeightCalc(input: HTMLTextAreaElement & { _calculateDynamicHeight: () => void }, binding?: DirectiveBinding<boolean>) {
input.style.resize = 'none'
input._calculateDynamicHeight = () => calculateDynamicHeight(input, binding)

document.fonts.addEventListener('loadingdone', input._calculateDynamicHeight)
input.addEventListener('transitionend', input._calculateDynamicHeight)

input._calculateDynamicHeight()
input.style.resize = 'none'
}

function calculateDynamicHeight(input: HTMLTextAreaElement, binding?: DirectiveBinding<boolean>){
Expand All @@ -17,19 +17,19 @@ function calculateDynamicHeight(input: HTMLTextAreaElement, binding?: DirectiveB

// Reset the height to a minimal value to refresh scrollHeight.
// This ensures the textarea will shrink when text is removed.
input.style.height = '5px'
input.style.height = `${input.scrollHeight}px`
input.style.height = '0'
input.style.height = `${input.scrollHeight+1}px`
}

function removeDynamicHeightCalc(input: HTMLTextAreaElement & { _calculateDynamicHeight: () => void }) {
document.fonts.removeEventListener('loadingdone', input._calculateDynamicHeight)
input.removeEventListener('transitionend', input._calculateDynamicHeight)
}

const vDynamicInputHeight = {
const vDynamicTextAreaHeight = {
mounted: initDynamicHeightCalc,
updated: calculateDynamicHeight,
unmounted: removeDynamicHeightCalc,
}

export default vDynamicInputHeight
export default vDynamicTextAreaHeight
48 changes: 27 additions & 21 deletions resources/js/Pages/User/Dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ const history = computed(() => props.userQuizzes.filter(userQuiz => userQuiz.clo
v-for="quiz in started"
:key="quiz.id"
:title="quiz.title"
:description="quiz.description"
:time="quiz.scheduledAt"
>
<FormButton
v-if="!quiz.isLocal"
button-class="min-w-32 w-full 2xs:w-fit"
class="w-full 2xs:w-fit"
method="post"
Expand All @@ -60,29 +62,32 @@ const history = computed(() => props.userQuizzes.filter(userQuiz => userQuiz.clo
v-for="quiz in scheduled"
:key="quiz.id"
:title="quiz.title"
:description="quiz.description"
:time="quiz.scheduledAt"
>
<FormButton
v-if="!quiz.isUserAssigned"
button-class="min-w-32 w-full 2xs:w-fit"
class="w-full 2xs:w-fit"
method="post"
:href="`/quizzes/${quiz.id}/assign`"
preserve-scroll
>
Zapisz się
</FormButton>

<FormButton
v-else
button-class="min-w-32 w-full 2xs:w-fit"
class="w-full 2xs:w-fit"
disabled
method="post"
:href="`/quizzes/${quiz.id}/assign`"
>
Zapisano
</FormButton>
<template v-if="!quiz.isLocal">
<FormButton
v-if="!quiz.isUserAssigned"
button-class="min-w-32 w-full 2xs:w-fit"
class="w-full 2xs:w-fit"
method="post"
:href="`/quizzes/${quiz.id}/assign`"
preserve-scroll
>
Zapisz się
</FormButton>

<FormButton
v-else
button-class="min-w-32 w-full 2xs:w-fit"
class="w-full 2xs:w-fit"
disabled
method="post"
:href="`/quizzes/${quiz.id}/assign`"
>
Zapisano
</FormButton>
</template>
</QuizItem>

<div v-if="scheduled.length == 0 && started.length == 0">
Expand All @@ -99,6 +104,7 @@ const history = computed(() => props.userQuizzes.filter(userQuiz => userQuiz.clo
v-for="userQuiz in history"
:key="userQuiz.id"
:title="userQuiz.title"
:description="userQuiz.description"
:time="userQuiz.closedAt"
>
<LinkButton
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/Common/Header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ watch(open, isOpen => document.body.style.overflow = isOpen ? 'hidden' : '')
:class="{'bg-primary/[.02] backdrop-blur-md pointer-events-auto':open}"
>
<div
class="bg-white h-full top-0 absolute left-full duration-200 flex flex-col gap-4 p-4 min-w-[50%] scale-95 overflow-y-auto"
class="bg-white h-full top-0 absolute left-full duration-200 flex flex-col gap-4 p-4 min-w-80 scale-95 overflow-y-auto"
:class="{'-translate-x-full shadow-lg !scale-100':open}"
>
<div class="flex justify-between option !py-3">
Expand Down
7 changes: 6 additions & 1 deletion resources/js/components/Crud/CrudInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const props = defineProps<{
error?: string
password?: boolean
large?: boolean
row?: boolean
column?: boolean
format?: (value: T) => any
} & InputProps>()

Expand All @@ -28,10 +30,12 @@ function handleInput() {
<template>
<div
class="flex gap-1 duration-200 min-h-7"
:class="{ 'text-sm text-gray-600' : !selected && !editing && !large, 'text-lg h-8': large }"
:class="{ 'text-sm text-gray-600' : !selected && !editing && !large, 'text-lg min-h-8': large }"
>
<InputWrapper
:label
:row
:column
:hide-content="!value && !editing"
:error="error"
:hide-error="!editing"
Expand All @@ -41,6 +45,7 @@ function handleInput() {
<b
v-if="!editing"
class="row-start-1 col-start-1"
:class="{'text-nowrap truncate': !selected}"
>
{{ format ? format(value) : value }}
</b>
Expand Down
7 changes: 6 additions & 1 deletion resources/js/components/Dashboard/QuizItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@ import dayjs from 'dayjs'

const props = defineProps<{
title: string
description?: string
time?: string
}>()
</script>

<template>
<div class="flex flex-col gap-5 2xs:flex-row 2xs:items-center rounded-xl bg-white shadow border p-5 justify-between">
<div class="flex flex-col gap-5 2xs:flex-row 2xs:items-center rounded-xl bg-white/70 shadow border p-5 justify-between">
<div class="flex flex-col gap-2">
<p class="font-bold text-lg text-primary">
{{ title }}
</p>

<p class="text-gray-600 text-sm">
{{ description }}
</p>

<p class="text-gray-600 font-medium text-sm">
{{ props.time ? dayjs(props.time).fromNow() : '' }}
</p>
</div>
Expand Down
2 changes: 2 additions & 0 deletions resources/js/components/QuizzesPanel/AnswerComponent.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import CheckDynamicIcon from '@/components/Icons/CheckDynamicIcon.vue'
import vDynamicTextAreaHeight from '@/Helpers/vDynamicTextAreaHeight'
import { XMarkIcon } from '@heroicons/vue/24/outline'

defineProps<{ editing: boolean }>()
Expand Down Expand Up @@ -29,6 +30,7 @@ const emit = defineEmits<{ delete: [answer:Answer], setCorrect: [answer:Answer]}
<textarea
v-else
v-model="answer.text"
v-dynamic-text-area-height
placeholder="Wpisz odpowiedź"
class="h-12 w-full p-2 bg-transparent outline-none border-b border-primary/30 focus:border-primary/60"
/>
Expand Down
2 changes: 2 additions & 0 deletions resources/js/components/QuizzesPanel/QuestionComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { vAutoAnimate } from '@formkit/auto-animate'
import ExpansionToggleDynamicIcon from '@/components/Icons/ExpansionToggleDynamicIcon.vue'
import AnswerComponent from '@/components/QuizzesPanel/AnswerComponent.vue'
import getKey from '@/Helpers/KeysManager'
import vDynamicTextAreaHeight from '@/Helpers/vDynamicTextAreaHeight'

defineProps<{ editing: boolean, index: number, questionsTotal: number, error: string }>()
const emit = defineEmits<{ copy: [question:Question], delete: [index:number, question:Question] }>()
Expand Down Expand Up @@ -71,6 +72,7 @@ function setCorrectAnswer(currentAnswer: Answer) {
<textarea
v-else
v-model="question.text"
v-dynamic-text-area-height
placeholder="Wpisz pytanie"
class="h-12 w-full bg-transparent outline-none border-b border-primary/30 focus:border-primary/60"
/>
Expand Down
4 changes: 2 additions & 2 deletions resources/js/components/QuizzesPanel/QuizComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ function toggleEditing(isEditing: boolean){
:errors="errors"
/>

<template v-if="selected">
<template v-if="selected && !quiz.isLocal">
<InputWrapper
v-for="(question, idx) of quiz.questions"
:key="getKey(question)"
Expand All @@ -131,7 +131,7 @@ function toggleEditing(isEditing: boolean){
</template>

<button
v-if="editing"
v-if="editing && !quiz.isLocal"
class="icon-button px-2"
@click="addQuestion"
>
Expand Down
45 changes: 44 additions & 1 deletion resources/js/components/QuizzesPanel/QuizHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import CustomDatepicker from '@/components/Common/CustomDatepicker.vue'
import { formatDate } from '@/Helpers/Format'
import CrudInput from '@/components/Crud/CrudInput.vue'
import { computed } from 'vue'
import vDynamicTextAreaHeight from '@/Helpers/vDynamicTextAreaHeight'

defineProps<{
editing: boolean
Expand All @@ -12,10 +13,11 @@ defineProps<{
}>()
const quiz = defineModel<Quiz>({ required: true })
const publicAvailabityString = computed(() => quiz.value.isPublic ? 'Dostępny dla wszystkich' : 'Dostępny tylko dla zaproszonych')
const quizModeString = computed(() => quiz.value.isLocal ? 'Offline' : 'Online')
</script>

<template>
<div class="flex flex-col w-full px-2">
<div class="flex flex-col flex-1 w-full px-2">
<CrudInput
v-model="quiz.title"
name="title"
Expand Down Expand Up @@ -65,5 +67,46 @@ const publicAvailabityString = computed(() => quiz.value.isPublic ? 'Dostępny d
{{ publicAvailabityString }}
</button>
</CrudInput>

<CrudInput
v-model="quizModeString"
label="Tryb:"
:error="errors.is_local"
:editing
:selected
>
<button
class="font-bold text-primary border-b border-primary/30 hover:border-primary transition-colors"
@click="quiz.isLocal = !quiz.isLocal"
>
{{ quizModeString }}
</button>
</CrudInput>

<template v-if="quiz.description || editing">
<CrudInput
v-model="quiz.description"
label="Opis:"
:error="errors.is_public"
:editing
:selected
>
<div />
</CrudInput>

<textarea
v-show="editing"
v-model="quiz.description"
v-dynamic-text-area-height
name="name"
autocomplete="off"
class="bg-transparent outline-none font-bold"
:disabled="!editing"
:class="{
'border-b border-b-primary/30 transition-colors hover:border-b-primary/60 text-primary' : editing,
'border-b-red' : errors.name
}"
/>
</template>
</div>
</template>
4 changes: 2 additions & 2 deletions resources/js/components/QuizzesPanel/QuizNavbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ const questionsHaveOneCorrectAnswer = computed(
)

const assertions = computed<Record<string, [boolean, string]>>(() => ({
hasCorrectAnswers: [questionsHaveOneCorrectAnswer.value, 'Żadne pytanie nie zawiera zaznaczonej prawidłowej odpowiedzi.'],
hasQuestions: [quiz.value.questions.length > 0, 'Test nie zawiera żadnego pytania.'],
hasCorrectAnswers: [questionsHaveOneCorrectAnswer.value || quiz.value.isLocal, 'Żadne pytanie nie zawiera zaznaczonej prawidłowej odpowiedzi.'],
hasQuestions: [quiz.value.questions.length > 0 || quiz.value.isLocal, 'Test nie zawiera żadnego pytania.'],
duration: [!!quiz.value.duration, 'Czas trwania testu nie jest ustawiony.'],
startTimeNotReached: [props.startTimeNotReached, 'Czas rozpoczęcia testu upłynął.'],
}))
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/UserQuiz/UserQuestion.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const emit = defineEmits<{ answer: [question: UserQuestion, answerId: number] }>
</script>

<template>
<div class="rounded-xl bg-white shadow border flex flex-col justify-between gap-5 p-5 pb-6">
<div class="rounded-xl bg-white/70 shadow border flex flex-col justify-between gap-5 p-5 pb-6">
<div class="flex flex-col gap-3">
<b class="text-primary text-lg">
Pytanie: {{ index + 1 }}/{{ questionsTotal }}
Expand Down
Loading
Loading