@ansiversa/components 0.0.100 → 0.0.101

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/index.ts CHANGED
@@ -32,5 +32,7 @@ export { default as AvConfirmDialog } from './src/AvConfirmDialog.astro';
32
32
  export { default as AvTable } from './src/AvTable.astro';
33
33
  export { default as AvTableToolbar } from './src/AvTableToolbar.astro';
34
34
  export { default as AvTablePagination } from './src/AvTablePagination.astro';
35
+ export { default as QuizSummary } from './src/Summary/QuizSummary.astro';
35
36
 
36
37
  export * from "./src/alpine";
38
+ export * from "./src/Summary/types";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ansiversa/components",
3
- "version": "0.0.100",
3
+ "version": "0.0.101",
4
4
  "description": "Shared UI components and layouts for the Ansiversa ecosystem",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,124 @@
1
+ ---
2
+ import { AvButton, AvCard } from "@ansiversa/components";
3
+ import type { QuizDashboardSummaryV1 } from "./types";
4
+
5
+ interface Props {
6
+ summary: QuizDashboardSummaryV1;
7
+ }
8
+
9
+ const { summary } = Astro.props as Props;
10
+
11
+ const formatPct = (value: number) => `${value.toFixed(1)}%`;
12
+ const formatDateTime = (value: string | null) => {
13
+ if (!value) return "No attempts yet";
14
+ const date = new Date(value);
15
+ return Number.isNaN(date.getTime()) ? "No attempts yet" : date.toLocaleString();
16
+ };
17
+ ---
18
+
19
+ <section class="av-auth-stack-lg">
20
+ <div class="av-form-row">
21
+ <div class="av-auth-stack-xxs">
22
+ <div class="av-kicker">
23
+ <span>Quiz</span>
24
+ </div>
25
+ <h2 class="av-card-heading">Quiz summary</h2>
26
+ <p class="av-text-soft">Last attempt: {formatDateTime(summary.kpis.lastAttemptAt)}</p>
27
+ </div>
28
+ <div class="av-row-wrap">
29
+ <AvButton href="/quiz" size="sm">Open Quiz</AvButton>
30
+ <AvButton href="/results" size="sm" variant="ghost">View Results</AvButton>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="av-grid-auto av-grid-auto--260">
35
+ <AvCard variant="soft" className="av-card--fullheight">
36
+ <div class="av-auth-stack-xxs">
37
+ <p class="av-card-heading">Attempts</p>
38
+ <h3 class="av-app-card-title">{summary.kpis.attemptsTotal}</h3>
39
+ <p class="av-text-soft">All time</p>
40
+ </div>
41
+ </AvCard>
42
+ <AvCard variant="soft" className="av-card--fullheight">
43
+ <div class="av-auth-stack-xxs">
44
+ <p class="av-card-heading">Attempts (7d)</p>
45
+ <h3 class="av-app-card-title">{summary.kpis.attemptsLast7d}</h3>
46
+ <p class="av-text-soft">Last 7 days</p>
47
+ </div>
48
+ </AvCard>
49
+ <AvCard variant="soft" className="av-card--fullheight">
50
+ <div class="av-auth-stack-xxs">
51
+ <p class="av-card-heading">Avg Score</p>
52
+ <h3 class="av-app-card-title">{formatPct(summary.kpis.avgScorePctAllTime)}</h3>
53
+ <p class="av-text-soft">All attempts</p>
54
+ </div>
55
+ </AvCard>
56
+ <AvCard variant="soft" className="av-card--fullheight">
57
+ <div class="av-auth-stack-xxs">
58
+ <p class="av-card-heading">Best Score</p>
59
+ <h3 class="av-app-card-title">{formatPct(summary.kpis.bestScorePctAllTime)}</h3>
60
+ <p class="av-text-soft">All time best</p>
61
+ </div>
62
+ </AvCard>
63
+ </div>
64
+
65
+ <div class="av-section-grid av-section-grid--2">
66
+ <AvCard>
67
+ <div class="av-auth-stack-md">
68
+ <div class="av-form-row">
69
+ <div>
70
+ <h3 class="av-card-heading">Recent attempts</h3>
71
+ <p class="av-text-soft">Last 5</p>
72
+ </div>
73
+ </div>
74
+
75
+ {summary.recentAttempts.length === 0 ? (
76
+ <p class="av-text-soft">No recent attempts yet.</p>
77
+ ) : (
78
+ <ul class="av-list">
79
+ {summary.recentAttempts.map((attempt) => (
80
+ <li class="av-form-row">
81
+ <div class="av-auth-stack-xxs">
82
+ <strong>{attempt.platformName} / {attempt.subjectName} / {attempt.topicName}</strong>
83
+ <p class="av-text-soft">
84
+ {attempt.roadmapName} / {new Date(attempt.createdAt).toLocaleString()}
85
+ </p>
86
+ </div>
87
+ <span class="av-badge av-badge-soft">
88
+ {attempt.score.mark}/{attempt.score.total} ({formatPct(attempt.score.pct)})
89
+ </span>
90
+ </li>
91
+ ))}
92
+ </ul>
93
+ )}
94
+ </div>
95
+ </AvCard>
96
+
97
+ <AvCard>
98
+ <div class="av-auth-stack-md">
99
+ <div class="av-form-row">
100
+ <div>
101
+ <h3 class="av-card-heading">Top roadmaps</h3>
102
+ <p class="av-text-soft">All time</p>
103
+ </div>
104
+ </div>
105
+
106
+ {summary.topRoadmaps.length === 0 ? (
107
+ <p class="av-text-soft">No roadmap stats yet.</p>
108
+ ) : (
109
+ <ul class="av-list">
110
+ {summary.topRoadmaps.map((roadmap) => (
111
+ <li class="av-form-row">
112
+ <div class="av-auth-stack-xxs">
113
+ <strong>{roadmap.roadmapName}</strong>
114
+ <p class="av-text-soft">{roadmap.attempts} attempts</p>
115
+ </div>
116
+ <span class="av-badge av-badge-soft">{formatPct(roadmap.avgScorePct)}</span>
117
+ </li>
118
+ ))}
119
+ </ul>
120
+ )}
121
+ </div>
122
+ </AvCard>
123
+ </div>
124
+ </section>
@@ -0,0 +1,29 @@
1
+ export type QuizDashboardSummaryV1 = {
2
+ version: 1;
3
+ appId: "quiz";
4
+ userId: number;
5
+ updatedAt: string;
6
+ kpis: {
7
+ attemptsTotal: number;
8
+ attemptsLast7d: number;
9
+ avgScorePctAllTime: number;
10
+ bestScorePctAllTime: number;
11
+ lastAttemptAt: string | null;
12
+ };
13
+ recentAttempts: Array<{
14
+ id: number;
15
+ createdAt: string;
16
+ platformName: string;
17
+ subjectName: string;
18
+ topicName: string;
19
+ roadmapName: string;
20
+ level: "E" | "M" | "D" | null;
21
+ score: { mark: number; total: number; pct: number };
22
+ }>;
23
+ topRoadmaps: Array<{
24
+ roadmapId: number;
25
+ roadmapName: string;
26
+ attempts: number;
27
+ avgScorePct: number;
28
+ }>;
29
+ };