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(quizzes): added regex and landing page #1

Merged
merged 1 commit into from
Mar 13, 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: 1 addition & 1 deletion components/app-header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const links = [
<template>
<div class="border-b border-gray-800">
<UContainer class="flex justify-between items-center h-16">
<UButton label="Deviz" color="gray" variant="ghost" class="text-xl font-bold">
<UButton label="Deviz" color="gray" variant="ghost" class="text-xl font-bold" to="/">
<template #leading>
<UIcon name="i-tabler-terminal-2" class="w-8 h-8 text-primary" />
</template>
Expand Down
11 changes: 11 additions & 0 deletions components/grid-background.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{ fadeBottom?: boolean }>(), { fadeBottom: true })
</script>

<template>
<div class="w-full bg-[linear-gradient(to_right,#80808010_1px,transparent_1px),linear-gradient(to_bottom,#80808010_1px,transparent_1px)] bg-[size:48px_48px] pt-20">
<slot />

<div v-if="props.fadeBottom" class="bg-gradient-to-t dark:from-gray-900 from-white to-transparent h-24 mt-24" />
</div>
</template>
4 changes: 2 additions & 2 deletions layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<div class="w-full min-h-screen dark:bg-gray-900 text-sm relative font-sans flex flex-col">
<app-header />

<UContainer class="flex-1">
<div class="flex-1 pb-16">
<slot />
</UContainer>
</div>

<app-footer />
</div>
Expand Down
2 changes: 1 addition & 1 deletion nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
extends: ['src/modules/quiz', 'src/modules/http-status'],
extends: ['src/modules/quiz', 'src/modules/http-status', 'src/modules/regex'],

site: {
url: 'https://deviz.corentin.tech',
Expand Down
63 changes: 63 additions & 0 deletions pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script setup lang="ts">
const quizzes = [
{
title: 'HTTP Status Quiz',
description: 'Test your knowledge of HTTP status codes and their meaning.',
to: '/http-status-quiz',
icon: 'i-tabler-server',
difficulty: 'hard',
},
{
title: 'Regex Tokens Quiz',
description: 'Test your knowledge of regex tokens with this online quiz.',
to: '/regex-tokens-quiz',
icon: 'i-tabler-regex',
difficulty: 'easy',
},
];
</script>

<template>
<grid-background>
<div class="flex flex-col items-center justify-center">
<div class="py-24 sm:py-32 relative md:py-32 !pb-0">
<div class="mx-auto px-4 sm:px-6 lg:px-8 gap-16 sm:gap-y-24 flex flex-col max-w-5xl">
<div class="text-center">
<h1 class="text-5xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-6xl">Online <span class="text-primary">quizzes</span> for devs</h1>
<p class="mt-6 text-lg tracking-tight text-gray-600 dark:text-gray-300">
Test your knowledge of dev stuff with these online quizzes. <br class="hidden sm:block" />Choose a quiz and start answering questions.
</p>
<div class="mt-10 flex flex-wrap gap-x-6 gap-y-3 justify-center">
<UButton to="/http-status-quiz" label="HTTP Status Quiz" color="primary" size="lg" icon="i-tabler-arrow-right" trailing />
</div>
</div>
</div>
</div>
</div>
</grid-background>

<UContainer class="flex flex-col gap-4 max-w-4xl">
<UCard v-for="(quiz, index) in quizzes" :key="index" :to="quiz.to" :icon="quiz.icon">
<div class="flex sm:items-center gap-4 flex-col sm:flex-row">
<div class="flex-1">
<div class="font-bold text-base truncate">{{ quiz.title }}</div>
<div class="text-gray-500 text-sm my-1">{{ quiz.description }}</div>
<div class="flex items-center text-gray-500 text-sm">
<div class="flex gap-1">
<UIcon class="w-4 h-4" dynamic name="i-tabler-star-filled" />
<UIcon class="w-4 h-4" dynamic :name="quiz.difficulty === 'easy' ? 'i-tabler-star' : 'i-tabler-star-filled'" />
<UIcon class="w-4 h-4" dynamic :name="quiz.difficulty === 'easy' || quiz.difficulty === 'medium' ? 'i-tabler-star' : 'i-tabler-star-filled'" />
</div>
<div>
<span class="mx-2">-</span><span class="capitalize">{{ quiz.difficulty }}</span>
</div>
</div>
</div>

<div class="flex-shrink-0">
<UButton to="/http-status-quiz" label="Start this quiz" icon="i-tabler-arrow-right" trailing color="white" size="lg" />
</div>
</div>
</UCard>
</UContainer>
</template>
15 changes: 15 additions & 0 deletions src/modules/http-status/pages/http-status-quiz.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import { getHttpQuestions } from '../http-status.usecases';

defineOgImageComponent('base-og-image', {
title: 'HTTP Status Quiz',
description: 'Test your knowledge of HTTP status codes with this online quiz.',
icon: 'i-tabler-terminal-2',
});
</script>

<template>
<div class="flex flex-col items-center justify-center">
<quiz :questions-generator="getHttpQuestions" />
</div>
</template>
79 changes: 0 additions & 79 deletions src/modules/http-status/pages/index.vue

This file was deleted.

72 changes: 72 additions & 0 deletions src/modules/quiz/components/quiz.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<div class="flex flex-col items-center max-w-md text-center mt-12">
<div v-if="isFinished">
<score-panel :score="score" :total="questionCount" />

<UButton @click="reset" size="lg" color="primary" trailingIcon="i-tabler-arrow-right"> Try again </UButton>
</div>

<ClientOnly v-else>
<UProgress :value="progress" class="w-full mb-4" />
<div class="text-gray-400 mb-4">Question {{ currentQuestionIndex + 1 }} of {{ questionCount }}</div>

<h1 class="text-xl text-neutral-300">{{ currentQuestion.question }}</h1>
<h2 class="text-4xl font-bold my-10">{{ currentQuestion.heading }}</h2>

<div class="flex flex-col gap-1 w-full">
<UButton
v-for="(answer, index) in currentQuestion.answers"
:key="index"
:onClick="() => selectAnswer({ answer })"
:disabled="isAnswered"
size="lg"
class="transition"
:color="isAnswered ? (answer.isCorrect ? 'green' : selectedAnswer === answer ? 'red' : 'primary') : 'primary'"
>
{{ isAnswered ? answer.explanation ?? answer.label : answer.label }}
</UButton>
</div>

<div v-if="isAnswered" class="mt-8 flex flex-col gap-8 w-full">
<UCard class="text-left w-full" v-if="currentQuestion.explanation">
<div class="font-bold mb-2">
{{ currentQuestion.explanation.title }}
</div>

<div class="text-gray-400">
{{ currentQuestion.explanation.description }}
</div>
</UCard>

<div class="flex justify-end">
<UButton @click="goToNextQuestion" size="lg" color="orange" trailingIcon="i-tabler-arrow-right"> Next </UButton>
</div>
</div>

<template #fallback>
<h1 class="sr-only">HTTP Status codes quiz</h1>
<p class="sr-only">Test your knowledge of HTTP status codes with this online quiz.</p>

<USkeleton class="h-4 w-[300px]" />
<USkeleton class="h-4 w-[150px] mt-4" />
<USkeleton class="h-8 w-[100px] my-10" />
<USkeleton class="h-6 w-[300px] mb-1" />
<USkeleton class="h-6 w-[300px] mb-1" />
<USkeleton class="h-6 w-[300px] mb-1" />
<USkeleton class="h-6 w-[300px] mb-1" />
</template>
</ClientOnly>
</div>
</template>

<script setup lang="ts">
import type { QuestionsGenerator } from '../quiz.types';

const props = withDefaults(defineProps<{ questionsGenerator?: QuestionsGenerator; questionCount?: number }>(), { questionsGenerator: () => [], questionCount: 10 });
const { questionsGenerator, questionCount } = toRefs(props);

const { currentQuestion, selectAnswer, isAnswered, selectedAnswer, goToNextQuestion, isFinished, reset, score, progress, currentQuestionIndex } = useQuiz({
questionsGenerator,
questionCount,
});
</script>
31 changes: 10 additions & 21 deletions src/modules/quiz/components/score-panel.vue
Original file line number Diff line number Diff line change
@@ -1,33 +1,22 @@
<script setup lang="ts">
import { getFeedbackSentence } from '../quiz.models';

const props = defineProps<{ score: number; total: number }>();
const { score, total } = toRefs(props);

const percentage = computed(() => (score.value / total.value) * 100);

const sentence = computed(() => {
if (percentage.value === 100) {
return 'Congratulations! You got a perfect score!';
}

if (percentage.value > 80) {
return 'Great job!';
}

if (percentage.value > 50) {
return 'Good effort!';
}

if (percentage.value > 25) {
return 'You can do better!';
}

return 'If you would have answered randomly, you would have scored better!';
});
const sentence = computed(() => getFeedbackSentence({ scorePercentage: percentage.value }));
</script>

<template>
<div>
<h1 class="text-3xl font-bold">You scored {{ score }}/{{ total }}</h1>
<p class="text-lg my-8 text-gray-400">{{ sentence }}</p>
<Icon name="i-tabler-star" class="text-7xl text-primary" v-if="percentage > 80" />
<Icon name="i-tabler-mood-happy" class="text-7xl text-primary" v-else-if="percentage > 60" />
<Icon name="i-tabler-mood-annoyed" class="text-7xl text-primary" v-else-if="percentage > 0" />
<Icon name="i-tabler-poo" class="text-7xl text-primary" v-else />

<h1 class="text-3xl font-bold mt-4">You scored {{ score }}/{{ total }}</h1>
<p class="text-lg my-4 text-gray-400">{{ sentence }}</p>
</div>
</template>
11 changes: 7 additions & 4 deletions src/modules/quiz/composables/useQuiz.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { get } from '@vueuse/core';
import type { Question, QuestionAnswer } from '../quiz.types';
import type { Question, QuestionAnswer, QuestionsGenerator } from '../quiz.types';

export { useQuiz };

function useQuiz({ questionsBuilder: createQuestions, questionCount = 10 }: { questionsBuilder: (args: { questionCount: number }) => Question[]; questionCount?: MaybeRef<number> }) {
const questions = ref<Question[]>(createQuestions({ questionCount: get(questionCount) }));
function useQuiz({ questionsGenerator, questionCount: rawQuestionCount = 10 }: { questionsGenerator: MaybeRef<QuestionsGenerator>; questionCount?: MaybeRef<number> }) {
const createQuestions = get(questionsGenerator);
const questionCount = get(rawQuestionCount);

const questions = ref<Question[]>(createQuestions({ questionCount }));
const currentQuestionIndex = ref(0);
const currentQuestion = computed<Question>(() => questions.value[currentQuestionIndex.value]);
const selectedAnswer = ref<QuestionAnswer | undefined>(undefined);
Expand Down Expand Up @@ -32,7 +35,7 @@ function useQuiz({ questionsBuilder: createQuestions, questionCount = 10 }: { qu
};

const reset = () => {
questions.value = createQuestions({ questionCount: get(questionCount) });
questions.value = createQuestions({ questionCount });
currentQuestionIndex.value = 0;
selectedAnswer.value = undefined;
state.value = 'answering';
Expand Down
31 changes: 31 additions & 0 deletions src/modules/quiz/quiz.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export const feedbackSentencesPerScore = {
25: [
'If you would have answered randomly, you would have scored better!',
'Seems like your expertise lies outside the realm of this quiz!',
'Did you try using your lucky guess? Oh wait, we need actual knowledge here!',
'Even a broken clock is right twice a day, but you? Not so much.',
"Your answers were as close to correct as I am to winning a Grammy. Spoiler: I can't sing.",
'You might want to brush up on your knowledge before the next quiz!',
],
50: [
"Not too shabby, but not too shiny either. You're comfortably mediocre!",
"You're halfway to genius or halfway to clueless. Glass half full?",
"Looks like you guessed half of them right. Or actually knew. We'll never tell.",
"A solid C for effort, but in the game of knowledge, it's win or learn.",
"{score-percentage}? In a 'Did I turn off the oven?' test, that would be worrying.",
],
75: [
"Impressive! You're pretty smart, or a very good guesser.",
"80%? You're the kind of person who reads instruction manuals, aren't you?",
"You've nearly aced it. Missed by just a hair...or five questions.",
'Close to perfection, but just human enough to keep things interesting.',
"You're like a human encyclopedia! Just a few pages short.",
],
100: [
'{score-percentage}%? Show-off! But seriously, impressive work.',
"Perfection achieved! Are you sure you didn't cheat? Just kidding, we're proud!",
"You didn't just ace it; you rewrote the book on it.",
'All hail the quiz master! Your throne awaits.',
"Congratulations! You're the unicorn of quiz takers - mythical and legendary.",
],
} as const;
15 changes: 15 additions & 0 deletions src/modules/quiz/quiz.models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import _ from 'lodash';
import { feedbackSentencesPerScore } from './quiz.constants';

export { getFeedbackSentence };

function getFeedbackSentence({ scorePercentage }: { scorePercentage: number }) {
const clampedScore = scorePercentage <= 25 ? 25 : scorePercentage <= 50 ? 50 : scorePercentage <= 75 ? 75 : 100;
const feedbackSentences = feedbackSentencesPerScore[clampedScore];

const sentenceTemplate = _.sample(feedbackSentences);

const sentence = _.replace(sentenceTemplate, '{score-percentage}', scorePercentage.toString());

return sentence;
}
2 changes: 2 additions & 0 deletions src/modules/quiz/quiz.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ export type Question = {
description: string;
};
};

export type QuestionsGenerator = (options: { questionCount: number }) => Question[];
Empty file.
Loading
Loading