@ansiversa/components 0.0.141 → 0.0.143
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 +2 -0
- package/package.json +1 -1
- package/src/Summary/StudyPlannerSummary.astro +95 -0
- package/src/Summary/types.ts +32 -0
- 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/src/utils/appUrls.ts +1 -0
package/index.ts
CHANGED
|
@@ -35,11 +35,13 @@ 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";
|
|
41
42
|
export { default as AvAiAssist } from "./src/components/Ai/AvAiAssist.astro";
|
|
42
43
|
export { default as FaqManager } from "./src/components/Admin/FaqManager.astro";
|
|
44
|
+
export { AvBookmarkButton, AvBookmarksEmpty, AvBookmarksList } from "./src/components/Bookmarks";
|
|
43
45
|
export { AppLogo } from "./src/Logo";
|
|
44
46
|
export type { AppLogoProps } from "./src/Logo";
|
|
45
47
|
export { default as ResumeBuilderShell } from './src/resume-templates/ResumeBuilderShell.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
|
+
};
|
|
@@ -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">
|