@ansiversa/components 0.0.142 → 0.0.144
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
|
@@ -35,6 +35,7 @@ export { default as AvTableToolbar } from './src/AvTableToolbar.astro';
|
|
|
35
35
|
export { default as AvTablePagination } from './src/AvTablePagination.astro';
|
|
36
36
|
export { default as QuizSummary } from './src/Summary/QuizSummary.astro';
|
|
37
37
|
export { default as FlashNoteSummary } from './src/Summary/FlashNoteSummary.astro';
|
|
38
|
+
export { default as StudyPlannerSummary } from './src/Summary/StudyPlannerSummary.astro';
|
|
38
39
|
export { default as ResumeBuilderSummary } from './src/Summary/ResumeBuilderSummary.astro';
|
|
39
40
|
export { default as PortfolioCreatorSummary } from './src/Summary/PortfolioCreatorSummary.astro';
|
|
40
41
|
export { default as AvImageUploader } from "./src/components/media/AvImageUploader.astro";
|
package/package.json
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { AvButton, AvCard } from "@ansiversa/components";
|
|
3
|
+
import type { StudyPlannerDashboardSummaryV1 } from "./types";
|
|
4
|
+
import { buildAppUrl } from "../utils/appUrls";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
summary: StudyPlannerDashboardSummaryV1;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { summary } = Astro.props as Props;
|
|
11
|
+
|
|
12
|
+
const formatDateTime = (value: string | null, fallback = "Not yet") => {
|
|
13
|
+
if (!value) return fallback;
|
|
14
|
+
const parsed = new Date(value);
|
|
15
|
+
return Number.isNaN(parsed.getTime()) ? fallback : parsed.toLocaleString();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const appBaseUrl = buildAppUrl(summary.appId);
|
|
19
|
+
const plansUrl = buildAppUrl(summary.appId, "/plans");
|
|
20
|
+
const trackOpenPayload = JSON.stringify({
|
|
21
|
+
id: summary.appId,
|
|
22
|
+
key: summary.appId,
|
|
23
|
+
name: "Study Planner",
|
|
24
|
+
description: "Study Planner",
|
|
25
|
+
url: appBaseUrl,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const lastPlan = summary.recent.recentPlans[0];
|
|
29
|
+
const lastTask = summary.recent.recentTasks[0];
|
|
30
|
+
const lastLog = summary.recent.recentLogs[0];
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<section class="av-auth-stack-lg">
|
|
34
|
+
<div class="av-form-row">
|
|
35
|
+
<div class="av-auth-stack-xxs">
|
|
36
|
+
<p class="av-text-soft">Last plan: {formatDateTime(lastPlan?.updatedAt ?? null)}</p>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="av-row-wrap">
|
|
39
|
+
<AvButton
|
|
40
|
+
href={appBaseUrl}
|
|
41
|
+
size="sm"
|
|
42
|
+
onclick={`window.dispatchEvent(new CustomEvent('ansiversa:app-opened', { detail: ${trackOpenPayload} }))`}
|
|
43
|
+
>
|
|
44
|
+
Open App ->
|
|
45
|
+
</AvButton>
|
|
46
|
+
<AvButton href={plansUrl} size="sm" variant="ghost">View plans</AvButton>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="av-grid-auto av-grid-auto--260">
|
|
51
|
+
<AvCard variant="soft" className="av-card--fullheight">
|
|
52
|
+
<div class="av-auth-stack-xxs">
|
|
53
|
+
<p class="av-card-heading">Plans</p>
|
|
54
|
+
<h3 class="av-app-card-title">{summary.totals.plansTotal}</h3>
|
|
55
|
+
<p class="av-text-soft">{summary.totals.plansActive} active</p>
|
|
56
|
+
</div>
|
|
57
|
+
</AvCard>
|
|
58
|
+
<AvCard variant="soft" className="av-card--fullheight">
|
|
59
|
+
<div class="av-auth-stack-xxs">
|
|
60
|
+
<p class="av-card-heading">Tasks</p>
|
|
61
|
+
<h3 class="av-app-card-title">{summary.totals.tasksTotal}</h3>
|
|
62
|
+
<p class="av-text-soft">{summary.totals.tasksCompleted} completed</p>
|
|
63
|
+
</div>
|
|
64
|
+
</AvCard>
|
|
65
|
+
<AvCard variant="soft" className="av-card--fullheight">
|
|
66
|
+
<div class="av-auth-stack-xxs">
|
|
67
|
+
<p class="av-card-heading">Due today</p>
|
|
68
|
+
<h3 class="av-app-card-title">{summary.totals.tasksDueToday}</h3>
|
|
69
|
+
<p class="av-text-soft">{summary.totals.logsThisWeek} logs this week</p>
|
|
70
|
+
</div>
|
|
71
|
+
</AvCard>
|
|
72
|
+
<AvCard variant="soft" className="av-card--fullheight">
|
|
73
|
+
<div class="av-auth-stack-xxs">
|
|
74
|
+
<p class="av-card-heading">Bookmarks</p>
|
|
75
|
+
<h3 class="av-app-card-title">{summary.totals.bookmarksTotal}</h3>
|
|
76
|
+
<p class="av-text-soft">Last log: {formatDateTime(lastLog?.createdAt ?? null)}</p>
|
|
77
|
+
</div>
|
|
78
|
+
</AvCard>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="av-grid-auto av-grid-auto--260">
|
|
82
|
+
<AvCard variant="soft" className="av-card--fullheight">
|
|
83
|
+
<div class="av-auth-stack-xxs">
|
|
84
|
+
<p class="av-card-heading">Recent plan</p>
|
|
85
|
+
<p class="av-text-strong">{lastPlan?.title ?? "No recent plans"}</p>
|
|
86
|
+
</div>
|
|
87
|
+
</AvCard>
|
|
88
|
+
<AvCard variant="soft" className="av-card--fullheight">
|
|
89
|
+
<div class="av-auth-stack-xxs">
|
|
90
|
+
<p class="av-card-heading">Recent task</p>
|
|
91
|
+
<p class="av-text-strong">{lastTask?.title ?? "No recent tasks"}</p>
|
|
92
|
+
</div>
|
|
93
|
+
</AvCard>
|
|
94
|
+
</div>
|
|
95
|
+
</section>
|
package/src/Summary/types.ts
CHANGED
|
@@ -64,3 +64,35 @@ export type PortfolioDashboardSummaryV1 = {
|
|
|
64
64
|
};
|
|
65
65
|
completionHint?: number;
|
|
66
66
|
};
|
|
67
|
+
|
|
68
|
+
export type StudyPlannerDashboardSummaryV1 = {
|
|
69
|
+
appId: "study-planner";
|
|
70
|
+
version: 1;
|
|
71
|
+
updatedAt: string;
|
|
72
|
+
totals: {
|
|
73
|
+
plansTotal: number;
|
|
74
|
+
plansActive: number;
|
|
75
|
+
tasksTotal: number;
|
|
76
|
+
tasksCompleted: number;
|
|
77
|
+
tasksDueToday: number;
|
|
78
|
+
logsThisWeek: number;
|
|
79
|
+
bookmarksTotal: number;
|
|
80
|
+
};
|
|
81
|
+
recent: {
|
|
82
|
+
recentPlans: Array<{
|
|
83
|
+
id: number;
|
|
84
|
+
title: string;
|
|
85
|
+
updatedAt: string | null;
|
|
86
|
+
}>;
|
|
87
|
+
recentTasks: Array<{
|
|
88
|
+
id: number;
|
|
89
|
+
title: string;
|
|
90
|
+
updatedAt: string | null;
|
|
91
|
+
}>;
|
|
92
|
+
recentLogs: Array<{
|
|
93
|
+
id: number;
|
|
94
|
+
title: string;
|
|
95
|
+
createdAt: string | null;
|
|
96
|
+
}>;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import AvButton from "../../AvButton.astro";
|
|
3
3
|
import AvCard from "../../AvCard.astro";
|
|
4
4
|
import AvDrawer from "../../AvDrawer.astro";
|
|
5
|
+
import AvEmptyState from "../../AvEmptyState.astro";
|
|
6
|
+
import AvIcon from "../../AvIcon.astro";
|
|
5
7
|
import AvInput from "../../AvInput.astro";
|
|
6
8
|
import AvLoading from "../../AvLoading.astro";
|
|
7
9
|
import AvSelect from "../../AvSelect.astro";
|
|
@@ -50,29 +52,43 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
50
52
|
<p class="av-table-toolbar__subtitle av-m-0">Manage frequently asked questions.</p>
|
|
51
53
|
<div class="av-admin-toolbar-actions">
|
|
52
54
|
<AvButton
|
|
53
|
-
size="
|
|
55
|
+
size="md"
|
|
54
56
|
variant="ghost"
|
|
55
57
|
type="button"
|
|
58
|
+
title="Refresh FAQs"
|
|
59
|
+
aria-label="Refresh FAQs"
|
|
56
60
|
@click.prevent="fetchFaqs()"
|
|
57
61
|
:disabled="loading || saving"
|
|
58
62
|
>
|
|
59
|
-
|
|
63
|
+
<AvIcon name="refresh" size="md" />
|
|
60
64
|
</AvButton>
|
|
61
65
|
<AvButton
|
|
62
|
-
size="
|
|
66
|
+
size="md"
|
|
63
67
|
variant="primary"
|
|
64
68
|
type="button"
|
|
69
|
+
title="Add FAQ"
|
|
70
|
+
aria-label="Add FAQ"
|
|
65
71
|
@click.prevent="openCreate()"
|
|
66
72
|
:disabled="loading || saving"
|
|
67
73
|
>
|
|
68
|
-
|
|
74
|
+
<AvIcon name="plus" size="md" />
|
|
69
75
|
</AvButton>
|
|
70
76
|
</div>
|
|
71
77
|
</div>
|
|
72
78
|
|
|
73
|
-
<div class="av-admin-filters"
|
|
74
|
-
<div class="av-admin-filter av-admin-filter--
|
|
75
|
-
<
|
|
79
|
+
<div class="av-admin-filters">
|
|
80
|
+
<div class="av-admin-filter av-admin-filter--search">
|
|
81
|
+
<AvInput
|
|
82
|
+
name="faq-search"
|
|
83
|
+
type="search"
|
|
84
|
+
placeholder="Search by question, category, or audience..."
|
|
85
|
+
x-model="q"
|
|
86
|
+
@input="setQuery($event.target.value)"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="av-admin-filter av-admin-filter--dropdown" x-show="showAudienceToggle" x-cloak>
|
|
91
|
+
<AvSelect name="faq-audience" aria-label="Filter by audience" x-model="audience" @change="setAudience($event.target.value)">
|
|
76
92
|
<option value="user">User</option>
|
|
77
93
|
<option value="admin">Admin</option>
|
|
78
94
|
</AvSelect>
|
|
@@ -92,9 +108,13 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
92
108
|
</AvCard>
|
|
93
109
|
|
|
94
110
|
<template x-if="!loading && !faqs.length">
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
<AvEmptyState
|
|
112
|
+
headline="No FAQs match your search"
|
|
113
|
+
description="Try another search term or adjust filters."
|
|
114
|
+
ctaLabel="Add FAQ"
|
|
115
|
+
x-bind:ctaDisabled="loading || saving"
|
|
116
|
+
@cta="openCreate()"
|
|
117
|
+
/>
|
|
98
118
|
</template>
|
|
99
119
|
|
|
100
120
|
<template x-if="faqs.length">
|
|
@@ -102,11 +122,10 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
102
122
|
<AvTable stickyHeader>
|
|
103
123
|
<thead>
|
|
104
124
|
<tr>
|
|
105
|
-
<th
|
|
125
|
+
<th>Reorder</th>
|
|
106
126
|
<th x-show="showAudienceColumn" x-cloak>Audience</th>
|
|
107
127
|
<th x-show="showCategoryColumn" x-cloak>Category</th>
|
|
108
128
|
<th>Question</th>
|
|
109
|
-
<th>Answer</th>
|
|
110
129
|
<th>Published</th>
|
|
111
130
|
<th x-show="showUpdatedColumn" x-cloak>Updated</th>
|
|
112
131
|
<th class="av-table__th-actions">Actions</th>
|
|
@@ -118,7 +137,6 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
118
137
|
<tr>
|
|
119
138
|
<td>
|
|
120
139
|
<div class="av-faq-manager__order-cell">
|
|
121
|
-
<span class="av-faq-manager__order-pill" x-text="faq.sort_order"></span>
|
|
122
140
|
<AvButton
|
|
123
141
|
size="sm"
|
|
124
142
|
variant="ghost"
|
|
@@ -126,6 +144,7 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
126
144
|
@click.prevent="moveFaq(index, -1)"
|
|
127
145
|
:disabled="loading || saving || index === 0 || !faq.id || !faqs[index - 1]?.id"
|
|
128
146
|
aria-label="Move up"
|
|
147
|
+
title="Move up"
|
|
129
148
|
>
|
|
130
149
|
↑
|
|
131
150
|
</AvButton>
|
|
@@ -136,6 +155,7 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
136
155
|
@click.prevent="moveFaq(index, 1)"
|
|
137
156
|
:disabled="loading || saving || index === faqs.length - 1 || !faq.id || !faqs[index + 1]?.id"
|
|
138
157
|
aria-label="Move down"
|
|
158
|
+
title="Move down"
|
|
139
159
|
>
|
|
140
160
|
↓
|
|
141
161
|
</AvButton>
|
|
@@ -150,9 +170,6 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
150
170
|
<td>
|
|
151
171
|
<div class="av-table__cell-strong" x-text="faq.question || '-'" ></div>
|
|
152
172
|
</td>
|
|
153
|
-
<td class="av-table__cell-muted">
|
|
154
|
-
<span x-text="previewAnswer(faq.answer_md)"></span>
|
|
155
|
-
</td>
|
|
156
173
|
<td>
|
|
157
174
|
<AvButton
|
|
158
175
|
size="sm"
|
|
@@ -246,17 +263,6 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
246
263
|
<p class="av-faq-manager__field-error" role="status" x-text="validationErrors.category"></p>
|
|
247
264
|
</template>
|
|
248
265
|
|
|
249
|
-
<AvInput
|
|
250
|
-
label="Sort order"
|
|
251
|
-
type="number"
|
|
252
|
-
min="1"
|
|
253
|
-
name="sort_order"
|
|
254
|
-
x-model.number="draftSortOrder"
|
|
255
|
-
/>
|
|
256
|
-
<template x-if="validationErrors.sort_order">
|
|
257
|
-
<p class="av-faq-manager__field-error" role="status" x-text="validationErrors.sort_order"></p>
|
|
258
|
-
</template>
|
|
259
|
-
|
|
260
266
|
<div class="av-faq-manager__publish-toggle">
|
|
261
267
|
<label class="av-label" for="faq-draft-published">Published</label>
|
|
262
268
|
<input
|
|
@@ -303,11 +309,12 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
303
309
|
drawerError: "",
|
|
304
310
|
draftId: null,
|
|
305
311
|
draftAudience: "user",
|
|
312
|
+
q: "",
|
|
306
313
|
draftCategory: "",
|
|
307
314
|
draftQuestion: "",
|
|
308
315
|
draftAnswerMd: "",
|
|
309
|
-
draftSortOrder: 0,
|
|
310
316
|
draftIsPublished: true,
|
|
317
|
+
_queryTimer: null,
|
|
311
318
|
limits: {
|
|
312
319
|
questionMin: 3,
|
|
313
320
|
questionMax: 160,
|
|
@@ -354,7 +361,6 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
354
361
|
const questionLen = this.draftQuestionCount;
|
|
355
362
|
const answerLen = this.draftAnswerCount;
|
|
356
363
|
const categoryLen = this.draftCategoryCount;
|
|
357
|
-
const sortOrder = Number(this.draftSortOrder);
|
|
358
364
|
|
|
359
365
|
if (questionLen < this.limits.questionMin || questionLen > this.limits.questionMax) {
|
|
360
366
|
errors.question = `Question must be ${this.limits.questionMin}-${this.limits.questionMax} characters`;
|
|
@@ -368,10 +374,6 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
368
374
|
errors.category = `Category must be ≤ ${this.limits.categoryMax} characters`;
|
|
369
375
|
}
|
|
370
376
|
|
|
371
|
-
if (!Number.isInteger(sortOrder) || sortOrder < 1) {
|
|
372
|
-
errors.sort_order = "Sort order must be 1 or greater";
|
|
373
|
-
}
|
|
374
|
-
|
|
375
377
|
if (this.draftAudience !== "user" && this.draftAudience !== "admin") {
|
|
376
378
|
errors.audience = "Audience must be user or admin";
|
|
377
379
|
}
|
|
@@ -439,12 +441,6 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
439
441
|
};
|
|
440
442
|
},
|
|
441
443
|
|
|
442
|
-
previewAnswer(value) {
|
|
443
|
-
const text = typeof value === "string" ? value.trim() : "";
|
|
444
|
-
if (!text) return "-";
|
|
445
|
-
return text.length > 120 ? `${text.slice(0, 120)}...` : text;
|
|
446
|
-
},
|
|
447
|
-
|
|
448
444
|
formatUpdatedAt(value) {
|
|
449
445
|
if (!value) return "-";
|
|
450
446
|
const date = new Date(value);
|
|
@@ -457,6 +453,19 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
457
453
|
this.fetchFaqs();
|
|
458
454
|
},
|
|
459
455
|
|
|
456
|
+
setQuery(value) {
|
|
457
|
+
this.q = typeof value === "string" ? value : "";
|
|
458
|
+
if (this._queryTimer) {
|
|
459
|
+
window.clearTimeout(this._queryTimer);
|
|
460
|
+
this._queryTimer = null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
this._queryTimer = window.setTimeout(() => {
|
|
464
|
+
this.fetchFaqs();
|
|
465
|
+
this._queryTimer = null;
|
|
466
|
+
}, 250);
|
|
467
|
+
},
|
|
468
|
+
|
|
460
469
|
clearMessages() {
|
|
461
470
|
this.error = "";
|
|
462
471
|
this.notice = "";
|
|
@@ -467,8 +476,13 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
467
476
|
this.clearMessages();
|
|
468
477
|
|
|
469
478
|
try {
|
|
479
|
+
const searchParams = new URLSearchParams();
|
|
480
|
+
searchParams.set("audience", this.audience);
|
|
481
|
+
const trimmedQuery = this.trimValue(this.q);
|
|
482
|
+
if (trimmedQuery) searchParams.set("q", trimmedQuery);
|
|
483
|
+
|
|
470
484
|
const response = await fetch(
|
|
471
|
-
this.endpoint(`/api/admin/faqs.json
|
|
485
|
+
this.endpoint(`/api/admin/faqs.json?${searchParams.toString()}`),
|
|
472
486
|
{
|
|
473
487
|
method: "GET",
|
|
474
488
|
credentials: "include",
|
|
@@ -503,7 +517,6 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
503
517
|
this.draftCategory = "";
|
|
504
518
|
this.draftQuestion = "";
|
|
505
519
|
this.draftAnswerMd = "";
|
|
506
|
-
this.draftSortOrder = this.faqs.length + 1;
|
|
507
520
|
this.draftIsPublished = true;
|
|
508
521
|
this.drawerError = "";
|
|
509
522
|
},
|
|
@@ -523,7 +536,6 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
523
536
|
this.draftCategory = faq?.category ?? "";
|
|
524
537
|
this.draftQuestion = faq?.question ?? "";
|
|
525
538
|
this.draftAnswerMd = faq?.answer_md ?? "";
|
|
526
|
-
this.draftSortOrder = Number.isFinite(Number(faq?.sort_order)) ? Number(faq.sort_order) : 0;
|
|
527
539
|
this.draftIsPublished = faq?.is_published !== false;
|
|
528
540
|
this.drawerError = "";
|
|
529
541
|
this.drawerOpen = true;
|
|
@@ -536,13 +548,11 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
536
548
|
},
|
|
537
549
|
|
|
538
550
|
buildPayload() {
|
|
539
|
-
const parsedSortOrder = Number.parseInt(String(this.draftSortOrder ?? ""), 10);
|
|
540
551
|
return {
|
|
541
552
|
audience: this.draftAudience === "admin" ? "admin" : "user",
|
|
542
553
|
category: String(this.draftCategory || "").trim(),
|
|
543
554
|
question: String(this.draftQuestion || "").trim(),
|
|
544
555
|
answer_md: String(this.draftAnswerMd || "").trim(),
|
|
545
|
-
sort_order: Number.isInteger(parsedSortOrder) ? parsedSortOrder : 0,
|
|
546
556
|
is_published: Boolean(this.draftIsPublished),
|
|
547
557
|
};
|
|
548
558
|
},
|
|
@@ -742,20 +752,6 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
742
752
|
align-items: center;
|
|
743
753
|
}
|
|
744
754
|
|
|
745
|
-
.av-faq-manager__order-pill {
|
|
746
|
-
min-width: 2rem;
|
|
747
|
-
height: 2rem;
|
|
748
|
-
display: inline-flex;
|
|
749
|
-
align-items: center;
|
|
750
|
-
justify-content: center;
|
|
751
|
-
border-radius: 9999px;
|
|
752
|
-
border: 1px solid rgba(148, 163, 184, 0.35);
|
|
753
|
-
color: rgba(226, 232, 240, 0.95);
|
|
754
|
-
font-size: 0.78rem;
|
|
755
|
-
line-height: 1;
|
|
756
|
-
padding: 0 0.5rem;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
755
|
.av-faq-manager__publish-toggle {
|
|
760
756
|
display: flex;
|
|
761
757
|
align-items: center;
|