@ansiversa/components 0.0.139 → 0.0.141
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/package.json
CHANGED
package/src/Logo/AppLogo.astro
CHANGED
|
@@ -4,6 +4,7 @@ import AppLogoFlashNote from "./logos/AppLogoFlashNote.astro";
|
|
|
4
4
|
import AppLogoPortfolioCreator from "./logos/AppLogoPortfolioCreator.astro";
|
|
5
5
|
import AppLogoQuiz from "./logos/AppLogoQuiz.astro";
|
|
6
6
|
import AppLogoResumeBuilder from "./logos/AppLogoResumeBuilder.astro";
|
|
7
|
+
import AppLogoStudyPlanner from "./logos/AppLogoStudyPlanner.astro";
|
|
7
8
|
|
|
8
9
|
export type AppLogoProps = {
|
|
9
10
|
appId: string;
|
|
@@ -25,6 +26,7 @@ const logoRegistry = {
|
|
|
25
26
|
"resume-builder": AppLogoResumeBuilder,
|
|
26
27
|
"portfolio-creator": AppLogoPortfolioCreator,
|
|
27
28
|
flashnote: AppLogoFlashNote,
|
|
29
|
+
"study-planner": AppLogoStudyPlanner,
|
|
28
30
|
} as const;
|
|
29
31
|
|
|
30
32
|
const Logo = logoRegistry[appId as keyof typeof logoRegistry] ?? AppLogoDefault;
|
package/src/Logo/index.ts
CHANGED
|
@@ -4,4 +4,5 @@ export { default as AppLogoFlashNote } from "./logos/AppLogoFlashNote.astro";
|
|
|
4
4
|
export { default as AppLogoPortfolioCreator } from "./logos/AppLogoPortfolioCreator.astro";
|
|
5
5
|
export { default as AppLogoQuiz } from "./logos/AppLogoQuiz.astro";
|
|
6
6
|
export { default as AppLogoResumeBuilder } from "./logos/AppLogoResumeBuilder.astro";
|
|
7
|
+
export { default as AppLogoStudyPlanner } from "./logos/AppLogoStudyPlanner.astro";
|
|
7
8
|
export type { AppLogoProps } from "./AppLogo.astro";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { AppLogoGlyphProps } from "./AppLogoQuiz.astro";
|
|
3
|
+
|
|
4
|
+
const { size = 18, class: className, title } = Astro.props as AppLogoGlyphProps;
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<svg
|
|
8
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
+
viewBox="0 0 24 24"
|
|
10
|
+
width={size}
|
|
11
|
+
height={size}
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
stroke-width="1.75"
|
|
15
|
+
stroke-linecap="round"
|
|
16
|
+
stroke-linejoin="round"
|
|
17
|
+
class={className}
|
|
18
|
+
aria-hidden={title ? undefined : "true"}
|
|
19
|
+
role={title ? "img" : "presentation"}
|
|
20
|
+
>
|
|
21
|
+
{title ? <title>{title}</title> : null}
|
|
22
|
+
<rect x="3.5" y="4.5" width="17" height="16" rx="2.5" />
|
|
23
|
+
<path d="M8 2.8v3.4" />
|
|
24
|
+
<path d="M16 2.8v3.4" />
|
|
25
|
+
<path d="M3.5 9h17" />
|
|
26
|
+
<path d="m9.4 14 1.9 1.9 3.4-3.5" />
|
|
27
|
+
</svg>
|
|
@@ -118,22 +118,26 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
118
118
|
<tr>
|
|
119
119
|
<td>
|
|
120
120
|
<div class="av-faq-manager__order-cell">
|
|
121
|
-
<
|
|
122
|
-
class="av-input av-faq-manager__order-input"
|
|
123
|
-
type="number"
|
|
124
|
-
min="0"
|
|
125
|
-
x-model.number="faq.sort_order"
|
|
126
|
-
:disabled="loading || saving"
|
|
127
|
-
aria-label="Sort order"
|
|
128
|
-
/>
|
|
121
|
+
<span class="av-faq-manager__order-pill" x-text="faq.sort_order"></span>
|
|
129
122
|
<AvButton
|
|
130
123
|
size="sm"
|
|
131
124
|
variant="ghost"
|
|
132
125
|
type="button"
|
|
133
|
-
@click.prevent="
|
|
134
|
-
:disabled="loading || saving || !faq.id"
|
|
126
|
+
@click.prevent="moveFaq(index, -1)"
|
|
127
|
+
:disabled="loading || saving || index === 0 || !faq.id || !faqs[index - 1]?.id"
|
|
128
|
+
aria-label="Move up"
|
|
135
129
|
>
|
|
136
|
-
|
|
130
|
+
↑
|
|
131
|
+
</AvButton>
|
|
132
|
+
<AvButton
|
|
133
|
+
size="sm"
|
|
134
|
+
variant="ghost"
|
|
135
|
+
type="button"
|
|
136
|
+
@click.prevent="moveFaq(index, 1)"
|
|
137
|
+
:disabled="loading || saving || index === faqs.length - 1 || !faq.id || !faqs[index + 1]?.id"
|
|
138
|
+
aria-label="Move down"
|
|
139
|
+
>
|
|
140
|
+
↓
|
|
137
141
|
</AvButton>
|
|
138
142
|
</div>
|
|
139
143
|
</td>
|
|
@@ -202,6 +206,12 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
202
206
|
<AvDrawer title="Manage FAQ" description="Create or update a frequently asked question.">
|
|
203
207
|
<form class="av-auth-stack-md" @submit.prevent="submitDrawer()">
|
|
204
208
|
<AvInput label="Question" name="question" required placeholder="Enter question" x-model="draftQuestion" />
|
|
209
|
+
<div class="av-faq-manager__field-meta">
|
|
210
|
+
<span class="av-faq-manager__field-counter" x-text="`Question: ${draftQuestionCount}/160`"></span>
|
|
211
|
+
</div>
|
|
212
|
+
<template x-if="validationErrors.question">
|
|
213
|
+
<p class="av-faq-manager__field-error" role="status" x-text="validationErrors.question"></p>
|
|
214
|
+
</template>
|
|
205
215
|
|
|
206
216
|
<AvTextarea
|
|
207
217
|
label="Answer (Markdown)"
|
|
@@ -211,6 +221,12 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
211
221
|
placeholder="Write answer in markdown"
|
|
212
222
|
x-model="draftAnswerMd"
|
|
213
223
|
/>
|
|
224
|
+
<div class="av-faq-manager__field-meta">
|
|
225
|
+
<span class="av-faq-manager__field-counter" x-text="`Answer: ${draftAnswerCount}/2000`"></span>
|
|
226
|
+
</div>
|
|
227
|
+
<template x-if="validationErrors.answer_md">
|
|
228
|
+
<p class="av-faq-manager__field-error" role="status" x-text="validationErrors.answer_md"></p>
|
|
229
|
+
</template>
|
|
214
230
|
|
|
215
231
|
<div x-show="showAudienceToggle" x-cloak>
|
|
216
232
|
<AvSelect label="Audience" name="draft-audience" x-model="draftAudience">
|
|
@@ -218,16 +234,28 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
218
234
|
<option value="admin">Admin</option>
|
|
219
235
|
</AvSelect>
|
|
220
236
|
</div>
|
|
237
|
+
<template x-if="validationErrors.audience">
|
|
238
|
+
<p class="av-faq-manager__field-error" role="status" x-text="validationErrors.audience"></p>
|
|
239
|
+
</template>
|
|
221
240
|
|
|
222
241
|
<AvInput label="Category (optional)" name="category" placeholder="General" x-model="draftCategory" />
|
|
242
|
+
<div class="av-faq-manager__field-meta">
|
|
243
|
+
<span class="av-faq-manager__field-counter" x-text="`Category: ${draftCategoryCount}/40`"></span>
|
|
244
|
+
</div>
|
|
245
|
+
<template x-if="validationErrors.category">
|
|
246
|
+
<p class="av-faq-manager__field-error" role="status" x-text="validationErrors.category"></p>
|
|
247
|
+
</template>
|
|
223
248
|
|
|
224
249
|
<AvInput
|
|
225
250
|
label="Sort order"
|
|
226
251
|
type="number"
|
|
227
|
-
min="
|
|
252
|
+
min="1"
|
|
228
253
|
name="sort_order"
|
|
229
254
|
x-model.number="draftSortOrder"
|
|
230
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>
|
|
231
259
|
|
|
232
260
|
<div class="av-faq-manager__publish-toggle">
|
|
233
261
|
<label class="av-label" for="faq-draft-published">Published</label>
|
|
@@ -246,7 +274,7 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
246
274
|
|
|
247
275
|
<div slot="footer">
|
|
248
276
|
<AvButton variant="ghost" type="button" @click.prevent="closeDrawer()" :disabled="saving">Cancel</AvButton>
|
|
249
|
-
<AvButton type="button" @click.prevent="submitDrawer()" :disabled="saving">
|
|
277
|
+
<AvButton type="button" @click.prevent="submitDrawer()" :disabled="saving || !isDraftValid">
|
|
250
278
|
<span x-show="!saving" x-text="drawerMode === 'edit' ? 'Update' : 'Create'"></span>
|
|
251
279
|
<span x-show="saving">Saving...</span>
|
|
252
280
|
</AvButton>
|
|
@@ -280,6 +308,13 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
280
308
|
draftAnswerMd: "",
|
|
281
309
|
draftSortOrder: 0,
|
|
282
310
|
draftIsPublished: true,
|
|
311
|
+
limits: {
|
|
312
|
+
questionMin: 3,
|
|
313
|
+
questionMax: 160,
|
|
314
|
+
answerMin: 3,
|
|
315
|
+
answerMax: 2000,
|
|
316
|
+
categoryMax: 40,
|
|
317
|
+
},
|
|
283
318
|
|
|
284
319
|
get showAudienceColumn() {
|
|
285
320
|
if (this.showAudienceToggle) return true;
|
|
@@ -294,6 +329,66 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
294
329
|
return this.faqs.some((faq) => Boolean(faq.updated_at));
|
|
295
330
|
},
|
|
296
331
|
|
|
332
|
+
trimValue(value) {
|
|
333
|
+
return typeof value === "string" ? value.trim() : "";
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
trimLength(value) {
|
|
337
|
+
return this.trimValue(value).length;
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
get draftQuestionCount() {
|
|
341
|
+
return this.trimLength(this.draftQuestion);
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
get draftAnswerCount() {
|
|
345
|
+
return this.trimLength(this.draftAnswerMd);
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
get draftCategoryCount() {
|
|
349
|
+
return this.trimLength(this.draftCategory);
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
get validationErrors() {
|
|
353
|
+
const errors = {};
|
|
354
|
+
const questionLen = this.draftQuestionCount;
|
|
355
|
+
const answerLen = this.draftAnswerCount;
|
|
356
|
+
const categoryLen = this.draftCategoryCount;
|
|
357
|
+
const sortOrder = Number(this.draftSortOrder);
|
|
358
|
+
|
|
359
|
+
if (questionLen < this.limits.questionMin || questionLen > this.limits.questionMax) {
|
|
360
|
+
errors.question = `Question must be ${this.limits.questionMin}-${this.limits.questionMax} characters`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (answerLen < this.limits.answerMin || answerLen > this.limits.answerMax) {
|
|
364
|
+
errors.answer_md = `Answer must be ${this.limits.answerMin}-${this.limits.answerMax} characters`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (categoryLen > this.limits.categoryMax) {
|
|
368
|
+
errors.category = `Category must be ≤ ${this.limits.categoryMax} characters`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (!Number.isInteger(sortOrder) || sortOrder < 1) {
|
|
372
|
+
errors.sort_order = "Sort order must be 1 or greater";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (this.draftAudience !== "user" && this.draftAudience !== "admin") {
|
|
376
|
+
errors.audience = "Audience must be user or admin";
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return errors;
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
get isDraftValid() {
|
|
383
|
+
return Object.keys(this.validationErrors).length === 0;
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
firstValidationError() {
|
|
387
|
+
const errors = this.validationErrors;
|
|
388
|
+
const firstKey = Object.keys(errors)[0];
|
|
389
|
+
return firstKey ? errors[firstKey] : "";
|
|
390
|
+
},
|
|
391
|
+
|
|
297
392
|
init() {
|
|
298
393
|
this.draftAudience = this.audience;
|
|
299
394
|
this.fetchFaqs();
|
|
@@ -441,17 +536,23 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
441
536
|
},
|
|
442
537
|
|
|
443
538
|
buildPayload() {
|
|
539
|
+
const parsedSortOrder = Number.parseInt(String(this.draftSortOrder ?? ""), 10);
|
|
444
540
|
return {
|
|
445
541
|
audience: this.draftAudience === "admin" ? "admin" : "user",
|
|
446
542
|
category: String(this.draftCategory || "").trim(),
|
|
447
543
|
question: String(this.draftQuestion || "").trim(),
|
|
448
544
|
answer_md: String(this.draftAnswerMd || "").trim(),
|
|
449
|
-
sort_order: Number(
|
|
545
|
+
sort_order: Number.isInteger(parsedSortOrder) ? parsedSortOrder : 0,
|
|
450
546
|
is_published: Boolean(this.draftIsPublished),
|
|
451
547
|
};
|
|
452
548
|
},
|
|
453
549
|
|
|
454
550
|
async submitDrawer() {
|
|
551
|
+
if (!this.isDraftValid) {
|
|
552
|
+
this.drawerError = this.firstValidationError() || "Please correct the highlighted fields.";
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
455
556
|
const payload = this.buildPayload();
|
|
456
557
|
|
|
457
558
|
if (!payload.question || !payload.answer_md) {
|
|
@@ -561,41 +662,70 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
561
662
|
}
|
|
562
663
|
},
|
|
563
664
|
|
|
564
|
-
|
|
565
|
-
|
|
665
|
+
normalizeSortOrder(value, fallbackOrder) {
|
|
666
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
667
|
+
if (Number.isInteger(parsed) && parsed >= 1) return parsed;
|
|
668
|
+
return Math.max(1, Number.parseInt(String(fallbackOrder ?? 1), 10) || 1);
|
|
669
|
+
},
|
|
670
|
+
|
|
671
|
+
async patchSortOrder(id, sortOrder) {
|
|
672
|
+
const response = await fetch(
|
|
673
|
+
this.endpoint(`/api/admin/faqs/${encodeURIComponent(String(id))}.json`),
|
|
674
|
+
{
|
|
675
|
+
method: "PATCH",
|
|
676
|
+
credentials: "include",
|
|
677
|
+
headers: {
|
|
678
|
+
"Content-Type": "application/json",
|
|
679
|
+
},
|
|
680
|
+
body: JSON.stringify({ sort_order: sortOrder }),
|
|
681
|
+
},
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
const responsePayload = await this.parseJson(response);
|
|
566
685
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
686
|
+
if (!response.ok) {
|
|
687
|
+
throw new Error(this.mapError(response.status, responsePayload, "Failed to update sort order."));
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
|
|
691
|
+
async moveFaq(index, direction) {
|
|
692
|
+
const currentIndex = Number(index);
|
|
693
|
+
const nextIndex = currentIndex + Number(direction);
|
|
694
|
+
if (!Number.isInteger(currentIndex) || !Number.isInteger(nextIndex)) return;
|
|
695
|
+
if (nextIndex < 0 || nextIndex >= this.faqs.length) return;
|
|
696
|
+
|
|
697
|
+
const currentFaq = this.faqs[currentIndex];
|
|
698
|
+
const nextFaq = this.faqs[nextIndex];
|
|
699
|
+
if (!currentFaq?.id || !nextFaq?.id) return;
|
|
700
|
+
|
|
701
|
+
const currentOrder = this.normalizeSortOrder(currentFaq.sort_order, currentIndex + 1);
|
|
702
|
+
const nextOrder = this.normalizeSortOrder(nextFaq.sort_order, nextIndex + 1);
|
|
703
|
+
if (currentOrder < 1 || nextOrder < 1) {
|
|
704
|
+
this.error = "Sort order must be 1 or greater.";
|
|
570
705
|
return;
|
|
571
706
|
}
|
|
572
707
|
|
|
708
|
+
const snapshot = this.faqs.map((item) => ({ ...item }));
|
|
709
|
+
const reordered = this.faqs.map((item) => ({ ...item }));
|
|
710
|
+
[reordered[currentIndex], reordered[nextIndex]] = [reordered[nextIndex], reordered[currentIndex]];
|
|
711
|
+
reordered[nextIndex].sort_order = currentOrder;
|
|
712
|
+
reordered[currentIndex].sort_order = nextOrder;
|
|
713
|
+
|
|
714
|
+
this.faqs = reordered;
|
|
715
|
+
|
|
573
716
|
this.saving = true;
|
|
574
717
|
this.error = "";
|
|
575
718
|
this.notice = "";
|
|
576
719
|
|
|
577
720
|
try {
|
|
578
|
-
|
|
579
|
-
this.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
credentials: "include",
|
|
583
|
-
headers: {
|
|
584
|
-
"Content-Type": "application/json",
|
|
585
|
-
},
|
|
586
|
-
body: JSON.stringify({ sort_order: nextSortOrder }),
|
|
587
|
-
},
|
|
588
|
-
);
|
|
589
|
-
|
|
590
|
-
const responsePayload = await this.parseJson(response);
|
|
591
|
-
|
|
592
|
-
if (!response.ok) {
|
|
593
|
-
throw new Error(this.mapError(response.status, responsePayload, "Failed to update sort order."));
|
|
594
|
-
}
|
|
721
|
+
await Promise.all([
|
|
722
|
+
this.patchSortOrder(reordered[currentIndex].id, reordered[currentIndex].sort_order),
|
|
723
|
+
this.patchSortOrder(reordered[nextIndex].id, reordered[nextIndex].sort_order),
|
|
724
|
+
]);
|
|
595
725
|
|
|
596
|
-
this.notice = "
|
|
597
|
-
await this.fetchFaqs();
|
|
726
|
+
this.notice = "Order updated.";
|
|
598
727
|
} catch (sortError) {
|
|
728
|
+
this.faqs = snapshot;
|
|
599
729
|
this.error = sortError?.message || "Failed to update sort order.";
|
|
600
730
|
} finally {
|
|
601
731
|
this.saving = false;
|
|
@@ -612,9 +742,18 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
612
742
|
align-items: center;
|
|
613
743
|
}
|
|
614
744
|
|
|
615
|
-
.av-faq-manager__order-
|
|
616
|
-
width:
|
|
617
|
-
|
|
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;
|
|
618
757
|
}
|
|
619
758
|
|
|
620
759
|
.av-faq-manager__publish-toggle {
|
|
@@ -628,4 +767,23 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
628
767
|
width: 1rem;
|
|
629
768
|
height: 1rem;
|
|
630
769
|
}
|
|
770
|
+
|
|
771
|
+
.av-faq-manager__field-meta {
|
|
772
|
+
display: flex;
|
|
773
|
+
justify-content: flex-end;
|
|
774
|
+
margin-top: -0.35rem;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.av-faq-manager__field-counter {
|
|
778
|
+
font-size: 0.78rem;
|
|
779
|
+
line-height: 1.2;
|
|
780
|
+
color: rgba(148, 163, 184, 0.9);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.av-faq-manager__field-error {
|
|
784
|
+
margin: -0.2rem 0 0;
|
|
785
|
+
font-size: 0.78rem;
|
|
786
|
+
line-height: 1.3;
|
|
787
|
+
color: rgba(244, 63, 94, 0.95);
|
|
788
|
+
}
|
|
631
789
|
</style>
|