@ansiversa/components 0.0.146 → 0.0.148
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/README.md
CHANGED
|
@@ -24,3 +24,10 @@ Inside any Ansiversa app (`web`, `admin`, or mini-apps):
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
npm install @ansiversa/components
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Environment
|
|
30
|
+
|
|
31
|
+
- `PUBLIC_ANSIVERSA_PARENT_ORIGIN`: parent web origin used for shared footer legal/help links.
|
|
32
|
+
- Production example: `PUBLIC_ANSIVERSA_PARENT_ORIGIN=https://ansiversa.com`
|
|
33
|
+
- Local example: `PUBLIC_ANSIVERSA_PARENT_ORIGIN=http://localhost:2000`
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import AvButton from "../../AvButton.astro";
|
|
3
3
|
import AvCard from "../../AvCard.astro";
|
|
4
|
+
import AvConfirmDialog from "../../AvConfirmDialog.astro";
|
|
4
5
|
import AvDrawer from "../../AvDrawer.astro";
|
|
5
6
|
import AvEmptyState from "../../AvEmptyState.astro";
|
|
6
7
|
import AvIcon from "../../AvIcon.astro";
|
|
@@ -191,17 +192,21 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
191
192
|
type="button"
|
|
192
193
|
@click.prevent="openEdit(faq)"
|
|
193
194
|
:disabled="loading || saving"
|
|
195
|
+
x-bind:title="`Edit FAQ: ${faq.question || 'Untitled'}`"
|
|
196
|
+
x-bind:aria-label="`Edit FAQ: ${faq.question || 'Untitled'}`"
|
|
194
197
|
>
|
|
195
|
-
|
|
198
|
+
<AvIcon name="edit" size="sm" />
|
|
196
199
|
</AvButton>
|
|
197
200
|
<AvButton
|
|
198
201
|
size="sm"
|
|
199
202
|
variant="ghost"
|
|
200
203
|
type="button"
|
|
201
|
-
@click.prevent="
|
|
204
|
+
@click.prevent="openDeleteConfirm(faq)"
|
|
202
205
|
:disabled="loading || saving || !faq.id"
|
|
206
|
+
x-bind:title="`Delete FAQ: ${faq.question || 'Untitled'}`"
|
|
207
|
+
x-bind:aria-label="`Delete FAQ: ${faq.question || 'Untitled'}`"
|
|
203
208
|
>
|
|
204
|
-
|
|
209
|
+
<AvIcon name="trash" size="sm" />
|
|
205
210
|
</AvButton>
|
|
206
211
|
</div>
|
|
207
212
|
</td>
|
|
@@ -289,6 +294,17 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
289
294
|
</div>
|
|
290
295
|
</template>
|
|
291
296
|
|
|
297
|
+
<AvConfirmDialog
|
|
298
|
+
id="faq-delete-dialog"
|
|
299
|
+
variant="danger"
|
|
300
|
+
headline="Delete this FAQ?"
|
|
301
|
+
description="This action cannot be undone."
|
|
302
|
+
confirmLabel="Delete"
|
|
303
|
+
cancelLabel="Cancel"
|
|
304
|
+
@av-confirm="confirmDeleteFaq()"
|
|
305
|
+
@av-cancel="pendingDeleteFaqId = null; pendingDeleteFaqQuestion = ''"
|
|
306
|
+
/>
|
|
307
|
+
|
|
292
308
|
<AvLoading x-show="loading" x-cloak label="Loading FAQs..." />
|
|
293
309
|
</div>
|
|
294
310
|
|
|
@@ -314,6 +330,8 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
314
330
|
draftQuestion: "",
|
|
315
331
|
draftAnswerMd: "",
|
|
316
332
|
draftIsPublished: true,
|
|
333
|
+
pendingDeleteFaqId: null,
|
|
334
|
+
pendingDeleteFaqQuestion: "",
|
|
317
335
|
_queryTimer: null,
|
|
318
336
|
limits: {
|
|
319
337
|
questionMin: 3,
|
|
@@ -605,9 +623,22 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
605
623
|
}
|
|
606
624
|
},
|
|
607
625
|
|
|
608
|
-
|
|
626
|
+
openDeleteConfirm(faq) {
|
|
609
627
|
if (!faq?.id) return;
|
|
610
|
-
|
|
628
|
+
this.pendingDeleteFaqId = String(faq.id);
|
|
629
|
+
this.pendingDeleteFaqQuestion = String(faq.question || "").trim();
|
|
630
|
+
const dialogTitle = document.getElementById("faq-delete-dialog-title");
|
|
631
|
+
if (dialogTitle) {
|
|
632
|
+
dialogTitle.textContent = this.pendingDeleteFaqQuestion
|
|
633
|
+
? `Delete FAQ '${this.pendingDeleteFaqQuestion}'?`
|
|
634
|
+
: "Delete this FAQ?";
|
|
635
|
+
}
|
|
636
|
+
window.AvDialog?.open("faq-delete-dialog");
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
async confirmDeleteFaq() {
|
|
640
|
+
if (!this.pendingDeleteFaqId) return;
|
|
641
|
+
const faqId = this.pendingDeleteFaqId;
|
|
611
642
|
|
|
612
643
|
this.saving = true;
|
|
613
644
|
this.error = "";
|
|
@@ -615,7 +646,7 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
615
646
|
|
|
616
647
|
try {
|
|
617
648
|
const response = await fetch(
|
|
618
|
-
this.endpoint(`/api/admin/faqs/${encodeURIComponent(
|
|
649
|
+
this.endpoint(`/api/admin/faqs/${encodeURIComponent(faqId)}.json`),
|
|
619
650
|
{
|
|
620
651
|
method: "DELETE",
|
|
621
652
|
credentials: "include",
|
|
@@ -633,6 +664,8 @@ const initialAudience = defaultAudience === "admin" ? "admin" : "user";
|
|
|
633
664
|
} catch (deleteError) {
|
|
634
665
|
this.error = deleteError?.message || "Failed to delete FAQ.";
|
|
635
666
|
} finally {
|
|
667
|
+
this.pendingDeleteFaqId = null;
|
|
668
|
+
this.pendingDeleteFaqQuestion = "";
|
|
636
669
|
this.saving = false;
|
|
637
670
|
}
|
|
638
671
|
},
|
|
@@ -4,6 +4,7 @@ import AvFooter from "../AvFooter.astro";
|
|
|
4
4
|
import AvMiniAppBar from "./AvMiniAppBar.astro";
|
|
5
5
|
import AvNavbar from "../AvNavbar.astro";
|
|
6
6
|
import AvNavbarActions from "../AvNavbarActions.astro";
|
|
7
|
+
import { buildParentUrl } from "../lib/urls/parentOrigin";
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
title?: string;
|
|
@@ -99,10 +100,10 @@ const ROOT_URL = rawDomain.match(/^https?:\/\//i)
|
|
|
99
100
|
<!-- Footer -->
|
|
100
101
|
<AvFooter
|
|
101
102
|
links={[
|
|
102
|
-
{ label: "Terms", href: "/terms" },
|
|
103
|
-
{ label: "Privacy", href: "/privacy" },
|
|
104
|
-
{ label: "FAQ", href: "/faq" },
|
|
105
|
-
{ label: "Contact", href: "/contact" }
|
|
103
|
+
{ label: "Terms", href: buildParentUrl("/terms") },
|
|
104
|
+
{ label: "Privacy", href: buildParentUrl("/privacy") },
|
|
105
|
+
{ label: "FAQ", href: buildParentUrl("/faq") },
|
|
106
|
+
{ label: "Contact", href: buildParentUrl("/contact") }
|
|
106
107
|
]}
|
|
107
108
|
/>
|
|
108
109
|
</body>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const DEFAULT_PARENT_ORIGIN = "http://localhost:2000";
|
|
2
|
+
const PRODUCTION_PARENT_ORIGIN = "https://ansiversa.com";
|
|
3
|
+
|
|
4
|
+
const trimTrailingSlash = (value: string) => value.trim().replace(/\/+$/, "");
|
|
5
|
+
|
|
6
|
+
const normalizeOrigin = (value: string) => {
|
|
7
|
+
const normalized = trimTrailingSlash(value);
|
|
8
|
+
if (!normalized) return "";
|
|
9
|
+
try {
|
|
10
|
+
return new URL(normalized).toString().replace(/\/+$/, "");
|
|
11
|
+
} catch {
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const isAnsiversaHostname = (hostname: string) =>
|
|
17
|
+
hostname === "ansiversa.com" || hostname.endsWith(".ansiversa.com");
|
|
18
|
+
|
|
19
|
+
export const resolveParentOrigin = () => {
|
|
20
|
+
const envOrigin = normalizeOrigin(import.meta.env.PUBLIC_ANSIVERSA_PARENT_ORIGIN ?? "");
|
|
21
|
+
if (envOrigin) return envOrigin;
|
|
22
|
+
|
|
23
|
+
if (typeof window !== "undefined") {
|
|
24
|
+
const host = window.location.hostname.toLowerCase();
|
|
25
|
+
if (isAnsiversaHostname(host)) {
|
|
26
|
+
return PRODUCTION_PARENT_ORIGIN;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return DEFAULT_PARENT_ORIGIN;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const buildParentUrl = (path: string) => {
|
|
34
|
+
const normalizedPath = `/${String(path ?? "").replace(/^\/+/, "")}`;
|
|
35
|
+
return `${resolveParentOrigin()}${normalizedPath}`;
|
|
36
|
+
};
|