@ansiversa/components 0.0.138 → 0.0.140
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>
|
|
@@ -121,7 +121,7 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
121
121
|
<input
|
|
122
122
|
class="av-input av-faq-manager__order-input"
|
|
123
123
|
type="number"
|
|
124
|
-
min="
|
|
124
|
+
min="1"
|
|
125
125
|
x-model.number="faq.sort_order"
|
|
126
126
|
:disabled="loading || saving"
|
|
127
127
|
aria-label="Sort order"
|
|
@@ -202,6 +202,12 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
202
202
|
<AvDrawer title="Manage FAQ" description="Create or update a frequently asked question.">
|
|
203
203
|
<form class="av-auth-stack-md" @submit.prevent="submitDrawer()">
|
|
204
204
|
<AvInput label="Question" name="question" required placeholder="Enter question" x-model="draftQuestion" />
|
|
205
|
+
<div class="av-faq-manager__field-meta">
|
|
206
|
+
<span class="av-faq-manager__field-counter" x-text="`Question: ${draftQuestionCount}/160`"></span>
|
|
207
|
+
</div>
|
|
208
|
+
<template x-if="validationErrors.question">
|
|
209
|
+
<p class="av-faq-manager__field-error" role="status" x-text="validationErrors.question"></p>
|
|
210
|
+
</template>
|
|
205
211
|
|
|
206
212
|
<AvTextarea
|
|
207
213
|
label="Answer (Markdown)"
|
|
@@ -211,6 +217,12 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
211
217
|
placeholder="Write answer in markdown"
|
|
212
218
|
x-model="draftAnswerMd"
|
|
213
219
|
/>
|
|
220
|
+
<div class="av-faq-manager__field-meta">
|
|
221
|
+
<span class="av-faq-manager__field-counter" x-text="`Answer: ${draftAnswerCount}/2000`"></span>
|
|
222
|
+
</div>
|
|
223
|
+
<template x-if="validationErrors.answer_md">
|
|
224
|
+
<p class="av-faq-manager__field-error" role="status" x-text="validationErrors.answer_md"></p>
|
|
225
|
+
</template>
|
|
214
226
|
|
|
215
227
|
<div x-show="showAudienceToggle" x-cloak>
|
|
216
228
|
<AvSelect label="Audience" name="draft-audience" x-model="draftAudience">
|
|
@@ -218,16 +230,28 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
218
230
|
<option value="admin">Admin</option>
|
|
219
231
|
</AvSelect>
|
|
220
232
|
</div>
|
|
233
|
+
<template x-if="validationErrors.audience">
|
|
234
|
+
<p class="av-faq-manager__field-error" role="status" x-text="validationErrors.audience"></p>
|
|
235
|
+
</template>
|
|
221
236
|
|
|
222
237
|
<AvInput label="Category (optional)" name="category" placeholder="General" x-model="draftCategory" />
|
|
238
|
+
<div class="av-faq-manager__field-meta">
|
|
239
|
+
<span class="av-faq-manager__field-counter" x-text="`Category: ${draftCategoryCount}/40`"></span>
|
|
240
|
+
</div>
|
|
241
|
+
<template x-if="validationErrors.category">
|
|
242
|
+
<p class="av-faq-manager__field-error" role="status" x-text="validationErrors.category"></p>
|
|
243
|
+
</template>
|
|
223
244
|
|
|
224
245
|
<AvInput
|
|
225
246
|
label="Sort order"
|
|
226
247
|
type="number"
|
|
227
|
-
min="
|
|
248
|
+
min="1"
|
|
228
249
|
name="sort_order"
|
|
229
250
|
x-model.number="draftSortOrder"
|
|
230
251
|
/>
|
|
252
|
+
<template x-if="validationErrors.sort_order">
|
|
253
|
+
<p class="av-faq-manager__field-error" role="status" x-text="validationErrors.sort_order"></p>
|
|
254
|
+
</template>
|
|
231
255
|
|
|
232
256
|
<div class="av-faq-manager__publish-toggle">
|
|
233
257
|
<label class="av-label" for="faq-draft-published">Published</label>
|
|
@@ -246,7 +270,7 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
246
270
|
|
|
247
271
|
<div slot="footer">
|
|
248
272
|
<AvButton variant="ghost" type="button" @click.prevent="closeDrawer()" :disabled="saving">Cancel</AvButton>
|
|
249
|
-
<AvButton type="button" @click.prevent="submitDrawer()" :disabled="saving">
|
|
273
|
+
<AvButton type="button" @click.prevent="submitDrawer()" :disabled="saving || !isDraftValid">
|
|
250
274
|
<span x-show="!saving" x-text="drawerMode === 'edit' ? 'Update' : 'Create'"></span>
|
|
251
275
|
<span x-show="saving">Saving...</span>
|
|
252
276
|
</AvButton>
|
|
@@ -280,6 +304,13 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
280
304
|
draftAnswerMd: "",
|
|
281
305
|
draftSortOrder: 0,
|
|
282
306
|
draftIsPublished: true,
|
|
307
|
+
limits: {
|
|
308
|
+
questionMin: 3,
|
|
309
|
+
questionMax: 160,
|
|
310
|
+
answerMin: 3,
|
|
311
|
+
answerMax: 2000,
|
|
312
|
+
categoryMax: 40,
|
|
313
|
+
},
|
|
283
314
|
|
|
284
315
|
get showAudienceColumn() {
|
|
285
316
|
if (this.showAudienceToggle) return true;
|
|
@@ -294,6 +325,66 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
294
325
|
return this.faqs.some((faq) => Boolean(faq.updated_at));
|
|
295
326
|
},
|
|
296
327
|
|
|
328
|
+
trimValue(value) {
|
|
329
|
+
return typeof value === "string" ? value.trim() : "";
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
trimLength(value) {
|
|
333
|
+
return this.trimValue(value).length;
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
get draftQuestionCount() {
|
|
337
|
+
return this.trimLength(this.draftQuestion);
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
get draftAnswerCount() {
|
|
341
|
+
return this.trimLength(this.draftAnswerMd);
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
get draftCategoryCount() {
|
|
345
|
+
return this.trimLength(this.draftCategory);
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
get validationErrors() {
|
|
349
|
+
const errors = {};
|
|
350
|
+
const questionLen = this.draftQuestionCount;
|
|
351
|
+
const answerLen = this.draftAnswerCount;
|
|
352
|
+
const categoryLen = this.draftCategoryCount;
|
|
353
|
+
const sortOrder = Number(this.draftSortOrder);
|
|
354
|
+
|
|
355
|
+
if (questionLen < this.limits.questionMin || questionLen > this.limits.questionMax) {
|
|
356
|
+
errors.question = `Question must be ${this.limits.questionMin}-${this.limits.questionMax} characters`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (answerLen < this.limits.answerMin || answerLen > this.limits.answerMax) {
|
|
360
|
+
errors.answer_md = `Answer must be ${this.limits.answerMin}-${this.limits.answerMax} characters`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (categoryLen > this.limits.categoryMax) {
|
|
364
|
+
errors.category = `Category must be ≤ ${this.limits.categoryMax} characters`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!Number.isInteger(sortOrder) || sortOrder < 1) {
|
|
368
|
+
errors.sort_order = "Sort order must be 1 or greater";
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (this.draftAudience !== "user" && this.draftAudience !== "admin") {
|
|
372
|
+
errors.audience = "Audience must be user or admin";
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return errors;
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
get isDraftValid() {
|
|
379
|
+
return Object.keys(this.validationErrors).length === 0;
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
firstValidationError() {
|
|
383
|
+
const errors = this.validationErrors;
|
|
384
|
+
const firstKey = Object.keys(errors)[0];
|
|
385
|
+
return firstKey ? errors[firstKey] : "";
|
|
386
|
+
},
|
|
387
|
+
|
|
297
388
|
init() {
|
|
298
389
|
this.draftAudience = this.audience;
|
|
299
390
|
this.fetchFaqs();
|
|
@@ -441,17 +532,23 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
441
532
|
},
|
|
442
533
|
|
|
443
534
|
buildPayload() {
|
|
535
|
+
const parsedSortOrder = Number.parseInt(String(this.draftSortOrder ?? ""), 10);
|
|
444
536
|
return {
|
|
445
537
|
audience: this.draftAudience === "admin" ? "admin" : "user",
|
|
446
538
|
category: String(this.draftCategory || "").trim(),
|
|
447
539
|
question: String(this.draftQuestion || "").trim(),
|
|
448
540
|
answer_md: String(this.draftAnswerMd || "").trim(),
|
|
449
|
-
sort_order: Number(
|
|
541
|
+
sort_order: Number.isInteger(parsedSortOrder) ? parsedSortOrder : 0,
|
|
450
542
|
is_published: Boolean(this.draftIsPublished),
|
|
451
543
|
};
|
|
452
544
|
},
|
|
453
545
|
|
|
454
546
|
async submitDrawer() {
|
|
547
|
+
if (!this.isDraftValid) {
|
|
548
|
+
this.drawerError = this.firstValidationError() || "Please correct the highlighted fields.";
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
455
552
|
const payload = this.buildPayload();
|
|
456
553
|
|
|
457
554
|
if (!payload.question || !payload.answer_md) {
|
|
@@ -565,8 +662,8 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
565
662
|
if (!faq?.id) return;
|
|
566
663
|
|
|
567
664
|
const nextSortOrder = Number(faq.sort_order);
|
|
568
|
-
if (!Number.
|
|
569
|
-
this.error = "Sort order must be
|
|
665
|
+
if (!Number.isInteger(nextSortOrder) || nextSortOrder < 1) {
|
|
666
|
+
this.error = "Sort order must be 1 or greater.";
|
|
570
667
|
return;
|
|
571
668
|
}
|
|
572
669
|
|
|
@@ -628,4 +725,23 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
628
725
|
width: 1rem;
|
|
629
726
|
height: 1rem;
|
|
630
727
|
}
|
|
728
|
+
|
|
729
|
+
.av-faq-manager__field-meta {
|
|
730
|
+
display: flex;
|
|
731
|
+
justify-content: flex-end;
|
|
732
|
+
margin-top: -0.35rem;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
.av-faq-manager__field-counter {
|
|
736
|
+
font-size: 0.78rem;
|
|
737
|
+
line-height: 1.2;
|
|
738
|
+
color: rgba(148, 163, 184, 0.9);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.av-faq-manager__field-error {
|
|
742
|
+
margin: -0.2rem 0 0;
|
|
743
|
+
font-size: 0.78rem;
|
|
744
|
+
line-height: 1.3;
|
|
745
|
+
color: rgba(244, 63, 94, 0.95);
|
|
746
|
+
}
|
|
631
747
|
</style>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { AppLogo } from "@ansiversa/components";
|
|
2
3
|
import type { MiniAppLink } from "./miniAppRegistry";
|
|
3
4
|
import { MINI_APP_REGISTRY } from "./miniAppRegistry";
|
|
4
5
|
|
|
@@ -9,8 +10,10 @@ interface Props {
|
|
|
9
10
|
|
|
10
11
|
const { appKey, links } = Astro.props as Props;
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
13
|
+
const normalizedAppKey = typeof appKey === "string" ? appKey.trim() : "";
|
|
14
|
+
const meta = normalizedAppKey ? MINI_APP_REGISTRY[normalizedAppKey] : undefined;
|
|
15
|
+
const appName = meta?.name ?? (normalizedAppKey || "App");
|
|
16
|
+
const showLogo = normalizedAppKey.length > 0;
|
|
14
17
|
|
|
15
18
|
let menuLinks = links ?? meta?.links ?? [];
|
|
16
19
|
if (!menuLinks.length) {
|
|
@@ -21,7 +24,10 @@ if (!menuLinks.length) {
|
|
|
21
24
|
<div class="av-mini-app-bar">
|
|
22
25
|
<div class="av-mini-app-bar__inner">
|
|
23
26
|
<div class="av-mini-app-bar__title" title={appName}>
|
|
24
|
-
<span class="av-mini-app-bar__name">
|
|
27
|
+
<span class="av-mini-app-bar__name flex items-center gap-2">
|
|
28
|
+
{showLogo && <AppLogo appId={normalizedAppKey} size="sm" />}
|
|
29
|
+
<span class="truncate">{appName}</span>
|
|
30
|
+
</span>
|
|
25
31
|
</div>
|
|
26
32
|
|
|
27
33
|
<div class="av-mini-app-bar__menu" x-data="{ open: false }">
|