@ansiversa/components 0.0.140 → 0.0.142
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 +1 -0
- package/package.json +1 -1
- package/src/components/Admin/FaqManager.astro +79 -37
- package/src/components/Bookmarks/AvBookmarkButton.astro +93 -0
- package/src/components/Bookmarks/AvBookmarksEmpty.astro +5 -0
- package/src/components/Bookmarks/AvBookmarksList.astro +83 -0
- package/src/components/Bookmarks/index.ts +3 -0
- package/src/layouts/AvMiniAppBar.astro +12 -1
- package/src/layouts/WebLayout.astro +3 -1
package/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export { default as PortfolioCreatorSummary } from './src/Summary/PortfolioCreat
|
|
|
40
40
|
export { default as AvImageUploader } from "./src/components/media/AvImageUploader.astro";
|
|
41
41
|
export { default as AvAiAssist } from "./src/components/Ai/AvAiAssist.astro";
|
|
42
42
|
export { default as FaqManager } from "./src/components/Admin/FaqManager.astro";
|
|
43
|
+
export { AvBookmarkButton, AvBookmarksEmpty, AvBookmarksList } from "./src/components/Bookmarks";
|
|
43
44
|
export { AppLogo } from "./src/Logo";
|
|
44
45
|
export type { AppLogoProps } from "./src/Logo";
|
|
45
46
|
export { default as ResumeBuilderShell } from './src/resume-templates/ResumeBuilderShell.astro';
|
package/package.json
CHANGED
|
@@ -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="1"
|
|
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>
|
|
@@ -658,41 +662,70 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
658
662
|
}
|
|
659
663
|
},
|
|
660
664
|
|
|
661
|
-
|
|
662
|
-
|
|
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);
|
|
685
|
+
|
|
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;
|
|
663
700
|
|
|
664
|
-
const
|
|
665
|
-
|
|
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) {
|
|
666
704
|
this.error = "Sort order must be 1 or greater.";
|
|
667
705
|
return;
|
|
668
706
|
}
|
|
669
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
|
+
|
|
670
716
|
this.saving = true;
|
|
671
717
|
this.error = "";
|
|
672
718
|
this.notice = "";
|
|
673
719
|
|
|
674
720
|
try {
|
|
675
|
-
|
|
676
|
-
this.
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
credentials: "include",
|
|
680
|
-
headers: {
|
|
681
|
-
"Content-Type": "application/json",
|
|
682
|
-
},
|
|
683
|
-
body: JSON.stringify({ sort_order: nextSortOrder }),
|
|
684
|
-
},
|
|
685
|
-
);
|
|
686
|
-
|
|
687
|
-
const responsePayload = await this.parseJson(response);
|
|
688
|
-
|
|
689
|
-
if (!response.ok) {
|
|
690
|
-
throw new Error(this.mapError(response.status, responsePayload, "Failed to update sort order."));
|
|
691
|
-
}
|
|
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
|
+
]);
|
|
692
725
|
|
|
693
|
-
this.notice = "
|
|
694
|
-
await this.fetchFaqs();
|
|
726
|
+
this.notice = "Order updated.";
|
|
695
727
|
} catch (sortError) {
|
|
728
|
+
this.faqs = snapshot;
|
|
696
729
|
this.error = sortError?.message || "Failed to update sort order.";
|
|
697
730
|
} finally {
|
|
698
731
|
this.saving = false;
|
|
@@ -709,9 +742,18 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
709
742
|
align-items: center;
|
|
710
743
|
}
|
|
711
744
|
|
|
712
|
-
.av-faq-manager__order-
|
|
713
|
-
width:
|
|
714
|
-
|
|
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;
|
|
715
757
|
}
|
|
716
758
|
|
|
717
759
|
.av-faq-manager__publish-toggle {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
type Size = "sm" | "md";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
active: boolean;
|
|
6
|
+
activeExpr?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
activeTitle?: string;
|
|
9
|
+
size?: Size;
|
|
10
|
+
onClick?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
active,
|
|
15
|
+
activeExpr,
|
|
16
|
+
title = "Save bookmark",
|
|
17
|
+
activeTitle = "Remove bookmark",
|
|
18
|
+
size = "md",
|
|
19
|
+
onClick,
|
|
20
|
+
} = Astro.props as Props;
|
|
21
|
+
|
|
22
|
+
const iconPx = size === "sm" ? 16 : 20;
|
|
23
|
+
const checkPx = size === "sm" ? 10 : 12;
|
|
24
|
+
const resolvedActiveExpr = activeExpr ?? (active ? "true" : "false");
|
|
25
|
+
const escapeSingleQuotes = (value: string) => value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
26
|
+
const tooltipExpr = `(${resolvedActiveExpr}) ? '${escapeSingleQuotes(activeTitle)}' : '${escapeSingleQuotes(title)}'`;
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
class="av-bookmark-btn"
|
|
32
|
+
:class={`(${resolvedActiveExpr}) ? 'is-active' : ''`}
|
|
33
|
+
x-on:click.stop.prevent={onClick}
|
|
34
|
+
title={active ? activeTitle : title}
|
|
35
|
+
:title={tooltipExpr}
|
|
36
|
+
aria-label={title}
|
|
37
|
+
:aria-label={`(${resolvedActiveExpr}) ? ${JSON.stringify(activeTitle)} : ${JSON.stringify(title)}`}
|
|
38
|
+
:aria-pressed={resolvedActiveExpr}
|
|
39
|
+
>
|
|
40
|
+
<span class="av-bookmark-btn__icon" :class={`(${resolvedActiveExpr}) ? 'is-hidden' : ''`} aria-hidden="true">
|
|
41
|
+
<svg width={iconPx} height={iconPx} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
42
|
+
<path d="M19 21 12 16 5 21V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
|
43
|
+
</svg>
|
|
44
|
+
</span>
|
|
45
|
+
<span class="av-bookmark-btn__icon" :class={`(${resolvedActiveExpr}) ? '' : 'is-hidden'`} aria-hidden="true">
|
|
46
|
+
<svg width={iconPx} height={iconPx} viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
|
47
|
+
<path d="M19 21 12 16 5 21V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
|
48
|
+
</svg>
|
|
49
|
+
<svg class="av-bookmark-btn__check" width={checkPx} height={checkPx} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
50
|
+
<path d="m5 13 4 4L19 7" />
|
|
51
|
+
</svg>
|
|
52
|
+
</span>
|
|
53
|
+
</button>
|
|
54
|
+
|
|
55
|
+
<style>
|
|
56
|
+
.av-bookmark-btn {
|
|
57
|
+
position: relative;
|
|
58
|
+
display: inline-flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
width: 2rem;
|
|
62
|
+
height: 2rem;
|
|
63
|
+
border: 1px solid rgba(148, 163, 184, 0.35);
|
|
64
|
+
border-radius: 999px;
|
|
65
|
+
background: rgba(15, 23, 42, 0.65);
|
|
66
|
+
color: #94a3b8;
|
|
67
|
+
transition: all 0.2s ease;
|
|
68
|
+
}
|
|
69
|
+
.av-bookmark-btn:hover {
|
|
70
|
+
border-color: rgba(14, 165, 233, 0.6);
|
|
71
|
+
color: #7dd3fc;
|
|
72
|
+
}
|
|
73
|
+
.av-bookmark-btn.is-active {
|
|
74
|
+
border-color: rgba(14, 165, 233, 0.8);
|
|
75
|
+
color: #38bdf8;
|
|
76
|
+
background: rgba(14, 116, 144, 0.25);
|
|
77
|
+
}
|
|
78
|
+
.av-bookmark-btn__icon {
|
|
79
|
+
position: absolute;
|
|
80
|
+
inset: 0;
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
transition: opacity 0.15s ease;
|
|
85
|
+
}
|
|
86
|
+
.av-bookmark-btn__icon.is-hidden {
|
|
87
|
+
opacity: 0;
|
|
88
|
+
}
|
|
89
|
+
.av-bookmark-btn__check {
|
|
90
|
+
position: absolute;
|
|
91
|
+
color: #0f172a;
|
|
92
|
+
}
|
|
93
|
+
</style>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AvCard from "../../AvCard.astro";
|
|
3
|
+
import AvBookmarkButton from "./AvBookmarkButton.astro";
|
|
4
|
+
|
|
5
|
+
type RightSlot = {
|
|
6
|
+
active: boolean;
|
|
7
|
+
activeExpr?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
activeTitle?: string;
|
|
10
|
+
size?: "sm" | "md";
|
|
11
|
+
onClick?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type Item = {
|
|
15
|
+
title: string;
|
|
16
|
+
subtitle?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
href?: string;
|
|
19
|
+
rightSlot?: RightSlot;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
items: Item[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { items } = Astro.props as Props;
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<div class="av-bookmarks-list">
|
|
30
|
+
{items.map((item) => (
|
|
31
|
+
<AvCard className="av-bookmarks-list__card">
|
|
32
|
+
<div class="av-bookmarks-list__row">
|
|
33
|
+
<div>
|
|
34
|
+
{item.href ? (
|
|
35
|
+
<a class="av-bookmarks-list__title" href={item.href}>{item.title}</a>
|
|
36
|
+
) : (
|
|
37
|
+
<p class="av-bookmarks-list__title">{item.title}</p>
|
|
38
|
+
)}
|
|
39
|
+
{item.subtitle ? <p class="av-bookmarks-list__meta">{item.subtitle}</p> : null}
|
|
40
|
+
{item.description ? <p class="av-bookmarks-list__desc">{item.description}</p> : null}
|
|
41
|
+
</div>
|
|
42
|
+
{item.rightSlot ? (
|
|
43
|
+
<AvBookmarkButton
|
|
44
|
+
active={item.rightSlot.active}
|
|
45
|
+
activeExpr={item.rightSlot.activeExpr}
|
|
46
|
+
title={item.rightSlot.title}
|
|
47
|
+
activeTitle={item.rightSlot.activeTitle}
|
|
48
|
+
size={item.rightSlot.size ?? "sm"}
|
|
49
|
+
onClick={item.rightSlot.onClick}
|
|
50
|
+
/>
|
|
51
|
+
) : null}
|
|
52
|
+
</div>
|
|
53
|
+
</AvCard>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<style>
|
|
58
|
+
.av-bookmarks-list {
|
|
59
|
+
display: grid;
|
|
60
|
+
gap: 0.85rem;
|
|
61
|
+
}
|
|
62
|
+
.av-bookmarks-list__row {
|
|
63
|
+
display: flex;
|
|
64
|
+
justify-content: space-between;
|
|
65
|
+
align-items: flex-start;
|
|
66
|
+
gap: 0.75rem;
|
|
67
|
+
}
|
|
68
|
+
.av-bookmarks-list__title {
|
|
69
|
+
color: #e2e8f0;
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
text-decoration: none;
|
|
72
|
+
}
|
|
73
|
+
.av-bookmarks-list__meta {
|
|
74
|
+
font-size: 0.75rem;
|
|
75
|
+
color: #7dd3fc;
|
|
76
|
+
margin-top: 0.35rem;
|
|
77
|
+
}
|
|
78
|
+
.av-bookmarks-list__desc {
|
|
79
|
+
margin-top: 0.4rem;
|
|
80
|
+
color: #94a3b8;
|
|
81
|
+
font-size: 0.82rem;
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
@@ -6,9 +6,10 @@ import { MINI_APP_REGISTRY } from "./miniAppRegistry";
|
|
|
6
6
|
interface Props {
|
|
7
7
|
appKey: string;
|
|
8
8
|
links?: MiniAppLink[];
|
|
9
|
+
bookmarksHref?: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
const { appKey, links } = Astro.props as Props;
|
|
12
|
+
const { appKey, links, bookmarksHref } = Astro.props as Props;
|
|
12
13
|
|
|
13
14
|
const normalizedAppKey = typeof appKey === "string" ? appKey.trim() : "";
|
|
14
15
|
const meta = normalizedAppKey ? MINI_APP_REGISTRY[normalizedAppKey] : undefined;
|
|
@@ -19,6 +20,16 @@ let menuLinks = links ?? meta?.links ?? [];
|
|
|
19
20
|
if (!menuLinks.length) {
|
|
20
21
|
menuLinks = [{ label: "Home", href: "/" }];
|
|
21
22
|
}
|
|
23
|
+
|
|
24
|
+
const normalizedBookmarksHref = typeof bookmarksHref === "string" ? bookmarksHref.trim() : "";
|
|
25
|
+
if (normalizedBookmarksHref.length > 0) {
|
|
26
|
+
const hasBookmarksItem = menuLinks.some(
|
|
27
|
+
(item) => item.href === normalizedBookmarksHref || item.label.toLowerCase() === "bookmarks",
|
|
28
|
+
);
|
|
29
|
+
if (!hasBookmarksItem) {
|
|
30
|
+
menuLinks = [...menuLinks, { label: "Bookmarks", href: normalizedBookmarksHref }];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
22
33
|
---
|
|
23
34
|
|
|
24
35
|
<div class="av-mini-app-bar">
|
|
@@ -11,6 +11,7 @@ interface Props {
|
|
|
11
11
|
notificationUnreadCount?: number;
|
|
12
12
|
miniAppKey?: string;
|
|
13
13
|
links?: { label: string; href: string }[];
|
|
14
|
+
bookmarksHref?: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const {
|
|
@@ -19,6 +20,7 @@ const {
|
|
|
19
20
|
notificationUnreadCount = 0,
|
|
20
21
|
miniAppKey,
|
|
21
22
|
links,
|
|
23
|
+
bookmarksHref,
|
|
22
24
|
} = Astro.props;
|
|
23
25
|
|
|
24
26
|
const user = (Astro.locals as { user?: {
|
|
@@ -87,7 +89,7 @@ const ROOT_URL = rawDomain.match(/^https?:\/\//i)
|
|
|
87
89
|
<AvNavbarActions user={user} notificationUnreadCount={notificationUnreadCount} />
|
|
88
90
|
</AvNavbar>
|
|
89
91
|
|
|
90
|
-
{miniAppKey && <AvMiniAppBar appKey={miniAppKey} links={links} />}
|
|
92
|
+
{miniAppKey && <AvMiniAppBar appKey={miniAppKey} links={links} bookmarksHref={bookmarksHref} />}
|
|
91
93
|
|
|
92
94
|
<!-- Main content -->
|
|
93
95
|
<main class="av-main">
|