payload-quiz-plugin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +35 -0
- package/README.md +413 -0
- package/dist/client.d.mts +303 -0
- package/dist/client.d.ts +303 -0
- package/dist/client.js +769 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +725 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +434 -0
- package/dist/index.d.ts +434 -0
- package/dist/index.js +923 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +902 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Alexandr Studio
|
|
4
|
+
|
|
5
|
+
You are free to:
|
|
6
|
+
- Share — copy and redistribute the material in any medium or format
|
|
7
|
+
- Adapt — remix, transform, and build upon the material
|
|
8
|
+
|
|
9
|
+
Under the following terms:
|
|
10
|
+
|
|
11
|
+
1. Attribution — You must give appropriate credit to "Alexandr Studio", provide a
|
|
12
|
+
link to the license, and indicate if changes were made. You may do so in any
|
|
13
|
+
reasonable manner, but not in any way that suggests the licensor endorses you
|
|
14
|
+
or your use.
|
|
15
|
+
|
|
16
|
+
2. NonCommercial — You may not use the material for commercial purposes. You may
|
|
17
|
+
not sell this software or include it in a product or service that you sell.
|
|
18
|
+
|
|
19
|
+
3. ShareAlike — If you remix, transform, or build upon the material, you must
|
|
20
|
+
distribute your contributions under the same license as the original.
|
|
21
|
+
|
|
22
|
+
4. No additional restrictions — You may not apply legal terms or technological
|
|
23
|
+
measures that legally restrict others from doing anything the license permits.
|
|
24
|
+
|
|
25
|
+
DISCLAIMER:
|
|
26
|
+
|
|
27
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
28
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
29
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
30
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
31
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
32
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
33
|
+
SOFTWARE.
|
|
34
|
+
|
|
35
|
+
Full license text: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
|
package/README.md
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# Payload Quiz Plugin
|
|
2
|
+
|
|
3
|
+
A comprehensive quiz and test system plugin for [Payload CMS](https://payloadcms.com) v3. Create timed quizzes with multiple choice questions, automatic grading, and detailed results.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Collections**: Questions, Tests, and Certificate Types
|
|
8
|
+
- **Multiple Choice Support**: Single or multiple correct answers per question
|
|
9
|
+
- **Timed Tests**: Configurable time limits with automatic submission
|
|
10
|
+
- **Results Tracking**: Detailed results with question-by-question review
|
|
11
|
+
- **i18n Support**: Built-in English and German translations, easily extendable
|
|
12
|
+
- **Customizable**: Override collections, add custom fields, and configure features
|
|
13
|
+
- **React Components**: Ready-to-use Quiz UI components for the frontend
|
|
14
|
+
- **SEO Ready**: Built-in SEO fields for tests using @payloadcms/plugin-seo
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install payload-quiz-plugin
|
|
20
|
+
# or
|
|
21
|
+
pnpm add payload-quiz-plugin
|
|
22
|
+
# or
|
|
23
|
+
yarn add payload-quiz-plugin
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Peer Dependencies
|
|
27
|
+
|
|
28
|
+
This plugin requires the following peer dependencies:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install payload @payloadcms/richtext-lexical
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Optional (for SEO fields):
|
|
35
|
+
```bash
|
|
36
|
+
npm install @payloadcms/plugin-seo
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### 1. Add the Plugin to Payload Config
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// payload.config.ts
|
|
45
|
+
import { buildConfig } from 'payload'
|
|
46
|
+
import { quizPlugin } from 'payload-quiz-plugin'
|
|
47
|
+
|
|
48
|
+
export default buildConfig({
|
|
49
|
+
// ... your config
|
|
50
|
+
plugins: [
|
|
51
|
+
quizPlugin({
|
|
52
|
+
i18n: {
|
|
53
|
+
enabled: true,
|
|
54
|
+
defaultLocale: 'en',
|
|
55
|
+
},
|
|
56
|
+
admin: {
|
|
57
|
+
group: 'Quiz', // Admin panel group name
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Create Quiz UI Components
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// app/tests/[slug]/page.tsx
|
|
68
|
+
import { getPayload } from 'payload'
|
|
69
|
+
import { QuizClient } from './QuizClient'
|
|
70
|
+
|
|
71
|
+
export default async function TestPage({ params }) {
|
|
72
|
+
const payload = await getPayload({ config: configPromise })
|
|
73
|
+
|
|
74
|
+
const test = await payload.findByID({
|
|
75
|
+
collection: 'tests',
|
|
76
|
+
id: params.slug,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const questions = await payload.find({
|
|
80
|
+
collection: 'questions',
|
|
81
|
+
where: {
|
|
82
|
+
certificateTypes: {
|
|
83
|
+
contains: test.certificateType,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return <QuizClient test={test} questions={questions.docs} />
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// app/tests/[slug]/QuizClient.tsx
|
|
94
|
+
'use client'
|
|
95
|
+
|
|
96
|
+
import {
|
|
97
|
+
QuizProvider,
|
|
98
|
+
useQuiz,
|
|
99
|
+
QuizTimer,
|
|
100
|
+
QuizProgress,
|
|
101
|
+
QuestionCard,
|
|
102
|
+
QuizResults,
|
|
103
|
+
} from 'payload-quiz-plugin/client'
|
|
104
|
+
|
|
105
|
+
export function QuizClient({ test, questions }) {
|
|
106
|
+
return (
|
|
107
|
+
<QuizProvider questions={questions} timeLimit={test.timeLimit}>
|
|
108
|
+
<QuizContent test={test} />
|
|
109
|
+
</QuizProvider>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function QuizContent({ test }) {
|
|
114
|
+
const {
|
|
115
|
+
state,
|
|
116
|
+
currentQuestion,
|
|
117
|
+
selectAnswer,
|
|
118
|
+
deselectAnswer,
|
|
119
|
+
nextQuestion,
|
|
120
|
+
prevQuestion,
|
|
121
|
+
finishQuiz,
|
|
122
|
+
calculateResults,
|
|
123
|
+
} = useQuiz()
|
|
124
|
+
|
|
125
|
+
// Render your quiz UI using these hooks and components
|
|
126
|
+
return (
|
|
127
|
+
<div>
|
|
128
|
+
<QuizTimer timeRemaining={state.timeRemaining} />
|
|
129
|
+
<QuizProgress
|
|
130
|
+
currentIndex={state.currentQuestionIndex}
|
|
131
|
+
totalQuestions={state.questions.length}
|
|
132
|
+
/>
|
|
133
|
+
{currentQuestion && (
|
|
134
|
+
<QuestionCard
|
|
135
|
+
question={currentQuestion}
|
|
136
|
+
selectedChoiceIds={state.answers.get(currentQuestion.id) || []}
|
|
137
|
+
onSelectChoice={selectAnswer}
|
|
138
|
+
onDeselectChoice={deselectAnswer}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Configuration
|
|
147
|
+
|
|
148
|
+
### Plugin Options
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
interface QuizPluginConfig {
|
|
152
|
+
i18n?: {
|
|
153
|
+
/** Enable i18n support (default: true) */
|
|
154
|
+
enabled?: boolean
|
|
155
|
+
/** Default locale (default: 'en') */
|
|
156
|
+
defaultLocale?: string
|
|
157
|
+
/** Custom translations */
|
|
158
|
+
translations?: Record<string, QuizTranslations>
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
collections?: {
|
|
162
|
+
/** Override Questions collection config */
|
|
163
|
+
questions?: Partial<CollectionConfig>
|
|
164
|
+
/** Override Tests collection config */
|
|
165
|
+
tests?: Partial<CollectionConfig>
|
|
166
|
+
/** Override CertificateTypes collection config */
|
|
167
|
+
certificateTypes?: Partial<CollectionConfig>
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
admin?: {
|
|
171
|
+
/** Admin panel group name (default: 'Quiz') */
|
|
172
|
+
group?: string
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
features?: {
|
|
176
|
+
/** Enable certificate types (default: true) */
|
|
177
|
+
certificateTypes?: boolean
|
|
178
|
+
/** Enable tests archive block (default: true) */
|
|
179
|
+
testsArchiveBlock?: boolean
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Custom Translations
|
|
185
|
+
|
|
186
|
+
Add your own translations or override existing ones:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
quizPlugin({
|
|
190
|
+
i18n: {
|
|
191
|
+
enabled: true,
|
|
192
|
+
translations: {
|
|
193
|
+
fr: {
|
|
194
|
+
tests: {
|
|
195
|
+
title: 'Tests',
|
|
196
|
+
startButton: 'Commencer le test',
|
|
197
|
+
finishButton: 'Terminer',
|
|
198
|
+
// ... more translations
|
|
199
|
+
},
|
|
200
|
+
results: {
|
|
201
|
+
congratulations: 'Félicitations !',
|
|
202
|
+
// ... more translations
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Collection Overrides
|
|
211
|
+
|
|
212
|
+
Customize the collections with additional fields or settings:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
quizPlugin({
|
|
216
|
+
collections: {
|
|
217
|
+
questions: {
|
|
218
|
+
access: {
|
|
219
|
+
read: () => true,
|
|
220
|
+
create: ({ req }) => req.user?.role === 'admin',
|
|
221
|
+
},
|
|
222
|
+
fields: [
|
|
223
|
+
// Additional fields
|
|
224
|
+
{
|
|
225
|
+
name: 'difficulty',
|
|
226
|
+
type: 'select',
|
|
227
|
+
options: ['easy', 'medium', 'hard'],
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Collections
|
|
236
|
+
|
|
237
|
+
### Questions
|
|
238
|
+
|
|
239
|
+
| Field | Type | Description |
|
|
240
|
+
|-------|------|-------------|
|
|
241
|
+
| `question` | textarea | The question text (localized) |
|
|
242
|
+
| `certificateTypes` | relationship | Certificate types this question belongs to |
|
|
243
|
+
| `requiredAnswers` | number | Number of correct answers to select (1 for single choice) |
|
|
244
|
+
| `choices` | array | Answer choices with text, isCorrect flag, and optional explanation |
|
|
245
|
+
| `explanation` | richText | General explanation shown after answering |
|
|
246
|
+
|
|
247
|
+
### Tests
|
|
248
|
+
|
|
249
|
+
| Field | Type | Description |
|
|
250
|
+
|-------|------|-------------|
|
|
251
|
+
| `title` | text | Test name (localized) |
|
|
252
|
+
| `certificateType` | relationship | Certificate type for question filtering |
|
|
253
|
+
| `questionCount` | number | Number of questions to include |
|
|
254
|
+
| `timeLimit` | number | Time limit in minutes |
|
|
255
|
+
| `passMark` | number | Percentage required to pass |
|
|
256
|
+
| `allowGoBack` | checkbox | Allow revisiting previous questions |
|
|
257
|
+
| `requireAllAnswered` | checkbox | Require all questions before submitting |
|
|
258
|
+
| `description` | richText | Test description (localized) |
|
|
259
|
+
| `instructions` | richText | Test instructions (localized) |
|
|
260
|
+
| `meta` | group | SEO fields (title, description, image) |
|
|
261
|
+
|
|
262
|
+
### Certificate Types
|
|
263
|
+
|
|
264
|
+
| Field | Type | Description |
|
|
265
|
+
|-------|------|-------------|
|
|
266
|
+
| `title` | text | Full certificate name (localized) |
|
|
267
|
+
| `shortName` | text | Abbreviated name (e.g., "PSM-I") |
|
|
268
|
+
| `description` | textarea | Brief description |
|
|
269
|
+
| `slug` | text | URL-friendly identifier |
|
|
270
|
+
|
|
271
|
+
## Components
|
|
272
|
+
|
|
273
|
+
### Server-Side Imports
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// For payload.config.ts and server components
|
|
277
|
+
import {
|
|
278
|
+
quizPlugin,
|
|
279
|
+
getQuizPluginConfig,
|
|
280
|
+
createQuestionsCollection,
|
|
281
|
+
createTestsCollection,
|
|
282
|
+
createCertificateTypesCollection,
|
|
283
|
+
TestsArchiveBlock,
|
|
284
|
+
createTestsArchiveBlock,
|
|
285
|
+
createTranslator,
|
|
286
|
+
getTranslations,
|
|
287
|
+
} from 'payload-quiz-plugin'
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Client-Side Imports
|
|
291
|
+
|
|
292
|
+
**IMPORTANT**: All React components with hooks must be imported from `payload-quiz-plugin/client` to ensure proper `"use client"` directive handling.
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// For client components only - do NOT import these from 'payload-quiz-plugin'
|
|
296
|
+
import {
|
|
297
|
+
QuizProvider,
|
|
298
|
+
useQuiz,
|
|
299
|
+
QuizTimer,
|
|
300
|
+
QuizProgress,
|
|
301
|
+
QuestionCard,
|
|
302
|
+
ChoiceOption,
|
|
303
|
+
QuizResults,
|
|
304
|
+
TestCard,
|
|
305
|
+
cn, // Tailwind class merge utility
|
|
306
|
+
} from 'payload-quiz-plugin/client'
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### QuizProvider
|
|
310
|
+
|
|
311
|
+
Wraps your quiz UI and manages state:
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
<QuizProvider questions={questions} timeLimit={30}>
|
|
315
|
+
{children}
|
|
316
|
+
</QuizProvider>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### useQuiz Hook
|
|
320
|
+
|
|
321
|
+
Access quiz state and actions:
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
const {
|
|
325
|
+
state, // Current quiz state
|
|
326
|
+
currentQuestion, // Current question object
|
|
327
|
+
isFirstQuestion, // Boolean
|
|
328
|
+
isLastQuestion, // Boolean
|
|
329
|
+
answeredCount, // Number of answered questions
|
|
330
|
+
selectAnswer, // (choiceId: string) => void
|
|
331
|
+
deselectAnswer, // (choiceId: string) => void
|
|
332
|
+
nextQuestion, // () => void
|
|
333
|
+
prevQuestion, // () => void
|
|
334
|
+
goToQuestion, // (index: number) => void
|
|
335
|
+
finishQuiz, // () => void
|
|
336
|
+
calculateResults, // (passMark: number) => QuizResult
|
|
337
|
+
} = useQuiz()
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### TestCard
|
|
341
|
+
|
|
342
|
+
Display a test card with optional custom media component:
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
<TestCard
|
|
346
|
+
doc={test}
|
|
347
|
+
className="h-full"
|
|
348
|
+
MediaComponent={({ resource, size }) => (
|
|
349
|
+
<YourMediaComponent resource={resource} size={size} />
|
|
350
|
+
)}
|
|
351
|
+
/>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Blocks
|
|
355
|
+
|
|
356
|
+
### TestsArchiveBlock
|
|
357
|
+
|
|
358
|
+
A Payload block for displaying a grid of tests:
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// In your page collection
|
|
362
|
+
import { TestsArchiveBlock } from 'payload-quiz-plugin'
|
|
363
|
+
|
|
364
|
+
export const Pages = {
|
|
365
|
+
slug: 'pages',
|
|
366
|
+
fields: [
|
|
367
|
+
{
|
|
368
|
+
name: 'layout',
|
|
369
|
+
type: 'blocks',
|
|
370
|
+
blocks: [
|
|
371
|
+
TestsArchiveBlock,
|
|
372
|
+
// ... other blocks
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Styling
|
|
380
|
+
|
|
381
|
+
The components use Tailwind CSS classes and are designed to work with shadcn/ui-style theming. They use CSS variables like:
|
|
382
|
+
|
|
383
|
+
- `--background`, `--foreground`
|
|
384
|
+
- `--card`, `--card-foreground`
|
|
385
|
+
- `--primary`, `--primary-foreground`
|
|
386
|
+
- `--muted`, `--muted-foreground`
|
|
387
|
+
- `--border`
|
|
388
|
+
|
|
389
|
+
Make sure your project has these CSS variables defined.
|
|
390
|
+
|
|
391
|
+
## TypeScript
|
|
392
|
+
|
|
393
|
+
Full TypeScript support with exported types:
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
import type {
|
|
397
|
+
QuizPluginConfig,
|
|
398
|
+
QuizQuestion,
|
|
399
|
+
QuizTest,
|
|
400
|
+
QuizResult,
|
|
401
|
+
QuestionResult,
|
|
402
|
+
QuizChoice,
|
|
403
|
+
TestCardData,
|
|
404
|
+
} from 'payload-quiz-plugin'
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## License
|
|
408
|
+
|
|
409
|
+
MIT
|
|
410
|
+
|
|
411
|
+
## Contributing
|
|
412
|
+
|
|
413
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|