Skip to content

Commit

Permalink
#68 - Student interface for answering tests frontend (#73)
Browse files Browse the repository at this point in the history
* create button components

* update favicon

* clean up route files structure

* create layouts

* fix code style

* add mobile version

* make mobile version hidden by default

* change logout button to text

* fix button styles

* remove 'mój'

* fix linter errors

* fix Admin tests

* fix user tests

* create guest layout

* fix dashboard

* fix profile title

* fix user tests

* move user crud to admin folder

* refactor routes for admin/user CRUDs

* fix status messages

* Add user quiz seeder, ranking test, attribute for quiz submission that returns amount of points for user

* Improve seeder

* Remove unused imports

* fix layouts

* Add ranking controller

* implement user dashboard

* add mobile version

* add non-content message

* disable button while processing

* add tests for quiz

* move user submission check to model

* remove extra semicolon

* Add ranking resource

* fix code style

* changed status to be gender-neutral

* fix tests

* Modify policy for ranking
Add ranking_published_at to quizzes table

* Modify QuizPolicy
- User can/cannot view ranking if he is/isn't in the ranking

* Modify QuizPolicy
- User can/cannot view ranking if he is/isn't in the ranking
Modify tests

* create quiz page

* fix code style

* preserve scroll

* hide closed quizzes

* remove extra semicolon

* add time left

* fix code style

* add timer

* fix timer

* center close submission text

* fix code style

* Fix github warnings with linter

* Fix route in verify email

* Add action for publishing and unpublishing quiz

* Add return type

* Apply suggestions from code review

Co-authored-by: Aleksandra Kozubal <104600942+AleksandraKozubal@users.noreply.github.com>

* Remove not existing policy from AuthServiceProvider

* fix code style

* rename migration

* Create action for publishing and unpublishing ranking

* create MessageBox component

* add ziggy

* fix code style

* handle timeout

* create quiz result page

* fix auth test

* fix time & radio

* change FormButton to LinkButton

* fix code style

* add title

* add route parameter

* fix code style

* fix code style

* implement backend for viewing quiz result

* fix tests

* import carbon

* rename page

* remove redeclared methods

* change default timezone

* fix tests

* rename Verify-Email to VerifyEmail

* remove ziggy

* import watch

* import watch

* fix result route url

* fix seeder

* fix code style

* Migrate button from useForm to router

* remove ziggy

* improve isClosed function

* fix code style

* improve 404 text

* remove unused import

* fix code style

* fix code style

* Apply suggestions from code review

* fixed js warnings

* Remove unused files

* move logic of closing submissions into action

* fix code style

* Apply suggestions from code review

Co-authored-by: Dawid Rudnik <48356242+dawidrudnik@users.noreply.github.com>

* improve message box title

* remove polish-plurals

* fix code style

* catch axios errors

* change md:w-x/12 to md:max-w

* rename FakeRadio to AnswerResult

* convert TimeLeft component into function

* fix code style

* fix linter error

* fix Timer

* make time left sticky

* Revert "make time left sticky"

This reverts commit f1be288.

* fix messageboxes

* fix code style

* remove scroll.y logging

* Update resources/js/components/Common/FormButton.vue

Co-authored-by: Dawid Rudnik <48356242+dawidrudnik@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Dawid Rudnik <48356242+dawidrudnik@users.noreply.github.com>

* fix calculation of submission duration

* change class to buttonClass

* fix code style

* fix code style

* remove class from props

* fix id warning

* remove useMessageBox function

* fix code style

* Update resources/js/components/Common/LinkButton.vue

Co-authored-by: Dawid Rudnik <48356242+dawidrudnik@users.noreply.github.com>

* Update resources/js/Pages/User/Quiz.vue

Co-authored-by: Dawid Rudnik <48356242+dawidrudnik@users.noreply.github.com>

* remove empty file

---------

Co-authored-by: Dominik Prabucki <dominikprabucki.2002@gmail.com>
Co-authored-by: Dominikaninn <130690231+PrabuckiDominik@users.noreply.github.com>
Co-authored-by: Aleksandra Kozubal <104600942+AleksandraKozubal@users.noreply.github.com>
Co-authored-by: Dawid Rudnik <48356242+dawidrudnik@users.noreply.github.com>
  • Loading branch information
5 people authored Sep 11, 2024
1 parent 51f1262 commit 3b5b366
Show file tree
Hide file tree
Showing 49 changed files with 666 additions and 90 deletions.
17 changes: 17 additions & 0 deletions app/Actions/CloseQuizSubmissionAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Models\QuizSubmission;
use Carbon\Carbon;

class CloseQuizSubmissionAction
{
public function execute(QuizSubmission $submission): void
{
$submission->closed_at = Carbon::now();
$submission->save();
}
}
11 changes: 10 additions & 1 deletion app/Http/Controllers/QuizSubmissionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,29 @@

namespace App\Http\Controllers;

use App\Actions\CloseQuizSubmissionAction;
use App\Http\Resources\QuizSubmissionResource;
use App\Models\QuizSubmission;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;

class QuizSubmissionController extends Controller
{
public function show(QuizSubmission $quizSubmission): Response
{
$quizSubmission->load(["answerRecords.question.answers", "quiz"]);
$quizSubmission->load(["answerRecords.question.answers"]);

return Inertia::render("User/Quiz", ["submission" => QuizSubmissionResource::make($quizSubmission)]);
}

public function close(QuizSubmission $quizSubmission, CloseQuizSubmissionAction $action): RedirectResponse
{
$action->execute($quizSubmission);

return redirect()->route("submissions.result", $quizSubmission->id)->with("status", "Test został oddany.");
}

public function result(QuizSubmission $quizSubmission): Response
{
$quizSubmission->load(["answerRecords.question.answers", "quiz"]);
Expand Down
1 change: 1 addition & 0 deletions app/Http/Resources/QuizSubmissionResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public function toArray(Request $request): array
"name" => $this->quiz->name,
"createdAt" => $this->created_at,
"updatedAt" => $this->updated_at,
"openedAt" => $this->quiz->scheduled_at,
"closedAt" => $this->closed_at,
"closed" => $this->isClosed,
"quiz" => $this->quiz_id,
Expand Down
2 changes: 1 addition & 1 deletion app/Models/AnswerRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
* @property bool $isCorrect
* @property QuizSubmission $quizSubmission
* @property Question $question
* @property Answer $answer
* @property ?Answer $answer
*/
class AnswerRecord extends Model
{
Expand Down
5 changes: 5 additions & 0 deletions app/Policies/QuizSubmissionPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public function view(User $user, QuizSubmission $quizSubmission): bool
return $user->id === $quizSubmission->user_id;
}

public function close(User $user, QuizSubmission $quizSubmission): bool
{
return $user->id === $quizSubmission->user_id && !$quizSubmission->isClosed;
}

public function result(User $user, QuizSubmission $quizSubmission): bool
{
return $user->id === $quizSubmission->user_id && $quizSubmission->isClosed;
Expand Down
1 change: 1 addition & 0 deletions database/seeders/DatabaseSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function run(): void
]);

Quiz::factory()->locked()->count(5)->create(["scheduled_at" => Carbon::now()->addMonth()]);
Quiz::factory()->locked()->create(["name" => "6 Minutes", "scheduled_at" => Carbon::now(), "duration" => 6]);

$quiz = Quiz::factory()->locked()->create(["name" => "Test Quiz", "scheduled_at" => Carbon::now(), "duration" => 2]);
$questions = Question::factory()->count(10)->create(["quiz_id" => $quiz->id]);
Expand Down
95 changes: 95 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@inertiajs/vue3": "^1.2.0",
"@tailwindcss/forms": "^0.5.8",
"@tailwindcss/typography": "^0.5.15",
"@vueuse/core": "^11.0.3",
"dayjs": "^1.11.13",
"laravel-vite-plugin": "^1.0.5",
"lodash": "^4.17.21",
Expand Down
13 changes: 13 additions & 0 deletions resources/js/Helpers/Plurals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const usePlurals = (singular: string, pluralNominativ: string, pluralGenitive: string) => (value: number) => {
value = Math.abs(value)

if (value === 1) {
return singular
}

if (value % 10 >= 2 && value % 10 <= 4 && (value % 100 < 10 || value % 100 >= 20)) {
return pluralNominativ
}

return pluralGenitive
}
46 changes: 46 additions & 0 deletions resources/js/Helpers/Time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {type TimeObject} from '@/Types/TimeObject'
import dayjs, { type Dayjs } from 'dayjs'
import {usePlurals} from '@/Helpers/Plurals'

export function calcSecondsBetweenDates(from: string | number | Dayjs = 0, to: string | number | Dayjs = 0): number {
return dayjs(from).diff(dayjs(to), 's')
}

export function calcSecondsLeftToDate(date: string | number | Dayjs = 0): number {
return Math.max(calcSecondsBetweenDates(date, dayjs()), 0)
}

export function secondsToHour(seconds: number): TimeObject {
return {
'h': Math.floor(seconds / 3600),
'm': Math.floor(seconds % 3600 / 60),
's': seconds % 60,
}
}

const translateLeft = usePlurals('Pozostała', 'Pozostały', 'Pozostało')
const translateSecondsLeft = usePlurals('sekunda', 'sekundy', 'sekund')
const translateMinutesLeft = usePlurals('minuta', 'minuty', 'minut')
const translateHoursLeft = usePlurals('godzina', 'godziny', 'godzin')

export function timeToString(time: TimeObject, withLeft = false): string {
const { s, m, h } = time

if (h <= 0 && m <= 0) {
return `${withLeft ? translateLeft(s) : ''} ${s} ${translateSecondsLeft(s)}`.trimStart()
}

if (h <= 0 && m < 10 && s > 0 ) {
return `${withLeft ? translateLeft(m) : ''} ${m} ${translateMinutesLeft(m)} i ${s} ${translateSecondsLeft(s)}`.trimStart()
}

if (h <= 0) {
return `${withLeft ? translateLeft(m) : ''} ${m} ${translateMinutesLeft(m)}`.trimStart()
}

if (h <= 0 && m > 0) {
return `${withLeft ? translateLeft(h) : ''} ${h} ${translateHoursLeft(h)} i ${m} ${translateMinutesLeft(m)}`.trimStart()
}

return `${withLeft ? translateLeft(h) : ''} ${h} ${translateHoursLeft(h)}`.trimStart()
}
17 changes: 17 additions & 0 deletions resources/js/Helpers/Timer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {computed, ref} from 'vue'
import {calcSecondsLeftToDate, secondsToHour, timeToString} from '@/Helpers/Time'

export function useTimer(to: string | number, timeout: () => void) {
const left = ref(calcSecondsLeftToDate(to))

const interval = setInterval(() => {
left.value = Math.max(0, left.value - 1)

if (left.value === 0) {
timeout()
clearInterval(interval)
}
}, 1000)

return computed(() => timeToString(secondsToHour(left.value), true))
}
1 change: 0 additions & 1 deletion resources/js/Layouts/AdminLayout.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
import type {Page} from '@/Types/Page'
import BaseLayout from '@/Layouts/BaseLayout.vue'
import {type PageProps} from '@/Types/PageProps'
Expand Down
1 change: 0 additions & 1 deletion resources/js/Layouts/BaseLayout.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
import type {Page} from '@/Types/Page'
import Header from '@/components/Common/Header.vue'
import Footer from '@/components/Common/Footer.vue'
Expand Down
1 change: 0 additions & 1 deletion resources/js/Layouts/GuestLayout.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
import BaseLayout from '@/Layouts/BaseLayout.vue'
import type {PageProps} from '@/Types/PageProps'
Expand Down
1 change: 0 additions & 1 deletion resources/js/Layouts/UserLayout.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
import type {Page} from '@/Types/Page'
import BaseLayout from '@/Layouts/BaseLayout.vue'
import type {PageProps} from '@/Types/PageProps'
Expand Down
12 changes: 11 additions & 1 deletion resources/js/Pages/Admin/Quizzes.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
<script setup lang="ts">
import {Head} from '@inertiajs/vue3'
import {type Quiz} from '@/Types/Quiz'
import LinkButton from '@/components/Common/LinkButton.vue'
defineProps<{ quizzes: Quiz[] }>()
</script>

<template>
<Head>
<title>Testy</title>
</Head>

Quizzes - CRUD

<div class="w-4/5">
<div v-for="quiz in quizzes" :key="quiz.id" class="m-4 bg-white w-100 rounded-2xl p-4 border shadow flex gap-4 items-center justify-between">
<b>{{ quiz.name }}</b>
<LinkButton :href="`/admin/quizzes/${quiz.id}/ranking`" small>Ranking</LinkButton>
</div>
</div>
</template>
22 changes: 5 additions & 17 deletions resources/js/Pages/Admin/Ranking.vue
Original file line number Diff line number Diff line change
@@ -1,37 +1,25 @@
<script setup lang="ts">
import {computed, ref} from 'vue'
import { defineProps } from 'vue'
import type { QuizRankingProps} from '@/Types/Ranking'
import { useForm } from '@inertiajs/vue3'
import FormButton from '@/components/Common/FormButton.vue'
const props = defineProps<QuizRankingProps>()
const form = useForm({})
const quiz = ref(props.quiz)
const rankings = ref(props.rankings)
const sortedRankings = computed(() => {
return [...rankings.value].sort((a, b) => b.points - a.points)
})
function publish() {
form.post(`/admin/quizzes/${quiz.value.id}/ranking/publish`)
}
function unpublish() {
form.post(`/admin/quizzes/${quiz.value.id}/ranking/unpublish`)
}
</script>

<template>
<div>
<h1>Ranking Quizu: {{ quiz.name }}</h1>
<form @submit.prevent="publish">
<button type="submit">Publikuj</button>
</form>
<form @submit.prevent="unpublish">
<button type="submit">Wycofaj publikacje</button>
</form>
<div class="flex gap-4">
<FormButton method="post" :href="`/admin/quizzes/${quiz.id}/ranking/publish`" small preserve-scroll>Publikuj</FormButton>
<FormButton method="post" :href="`/admin/quizzes/${quiz.id}/ranking/unpublish`" small preserve-scroll>Wycofaj publikacje</FormButton>
</div>
<table>
<thead>
<tr>
Expand Down
1 change: 0 additions & 1 deletion resources/js/Pages/Auth/VerifyEmail.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
import {useForm} from '@inertiajs/vue3'
const form = useForm({})
Expand Down
1 change: 0 additions & 1 deletion resources/js/Pages/Home.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script lang="ts" setup>
import {ref, provide, watch} from 'vue'
import Footer from '@/components/Common/Footer.vue'
import AuthBanner from '@/components/Home/AuthBanner.vue'
Expand Down
Loading

0 comments on commit 3b5b366

Please sign in to comment.