@medialane/ui 0.98.0 → 0.99.1
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/dist/components/community-rewards-section.cjs +12 -9
- package/dist/components/community-rewards-section.cjs.map +1 -1
- package/dist/components/community-rewards-section.js +12 -9
- package/dist/components/community-rewards-section.js.map +1 -1
- package/dist/components/rewards/badge-shelf.cjs +2 -0
- package/dist/components/rewards/badge-shelf.cjs.map +1 -1
- package/dist/components/rewards/badge-shelf.d.cts +6 -1
- package/dist/components/rewards/badge-shelf.d.ts +6 -1
- package/dist/components/rewards/badge-shelf.js +1 -0
- package/dist/components/rewards/badge-shelf.js.map +1 -1
- package/dist/components/rewards/badge-unlock-toast-content.cjs +49 -0
- package/dist/components/rewards/badge-unlock-toast-content.cjs.map +1 -0
- package/dist/components/rewards/badge-unlock-toast-content.d.cts +11 -0
- package/dist/components/rewards/badge-unlock-toast-content.d.ts +11 -0
- package/dist/components/rewards/badge-unlock-toast-content.js +25 -0
- package/dist/components/rewards/badge-unlock-toast-content.js.map +1 -0
- package/dist/components/rewards/journey-path.cjs +77 -0
- package/dist/components/rewards/journey-path.cjs.map +1 -0
- package/dist/components/rewards/journey-path.d.cts +26 -0
- package/dist/components/rewards/journey-path.d.ts +26 -0
- package/dist/components/rewards/journey-path.js +53 -0
- package/dist/components/rewards/journey-path.js.map +1 -0
- package/dist/components/rewards/leaderboard-table.cjs +1 -1
- package/dist/components/rewards/leaderboard-table.cjs.map +1 -1
- package/dist/components/rewards/leaderboard-table.js +1 -1
- package/dist/components/rewards/leaderboard-table.js.map +1 -1
- package/dist/components/rewards/level-badge.cjs +1 -3
- package/dist/components/rewards/level-badge.cjs.map +1 -1
- package/dist/components/rewards/level-badge.d.cts +4 -2
- package/dist/components/rewards/level-badge.d.ts +4 -2
- package/dist/components/rewards/level-badge.js +1 -3
- package/dist/components/rewards/level-badge.js.map +1 -1
- package/dist/components/rewards/level-up-celebration.cjs +71 -0
- package/dist/components/rewards/level-up-celebration.cjs.map +1 -0
- package/dist/components/rewards/level-up-celebration.d.cts +16 -0
- package/dist/components/rewards/level-up-celebration.d.ts +16 -0
- package/dist/components/rewards/level-up-celebration.js +47 -0
- package/dist/components/rewards/level-up-celebration.js.map +1 -0
- package/dist/components/rewards/reward-toast.cjs +20 -2
- package/dist/components/rewards/reward-toast.cjs.map +1 -1
- package/dist/components/rewards/reward-toast.d.cts +6 -2
- package/dist/components/rewards/reward-toast.d.ts +6 -2
- package/dist/components/rewards/reward-toast.js +20 -2
- package/dist/components/rewards/reward-toast.js.map +1 -1
- package/dist/components/rewards/use-rewards-celebrations.cjs +70 -0
- package/dist/components/rewards/use-rewards-celebrations.cjs.map +1 -0
- package/dist/components/rewards/use-rewards-celebrations.d.cts +25 -0
- package/dist/components/rewards/use-rewards-celebrations.d.ts +25 -0
- package/dist/components/rewards/use-rewards-celebrations.js +46 -0
- package/dist/components/rewards/use-rewards-celebrations.js.map +1 -0
- package/dist/components/rewards/xp-toast-content.cjs +23 -18
- package/dist/components/rewards/xp-toast-content.cjs.map +1 -1
- package/dist/components/rewards/xp-toast-content.d.cts +7 -1
- package/dist/components/rewards/xp-toast-content.d.ts +7 -1
- package/dist/components/rewards/xp-toast-content.js +23 -18
- package/dist/components/rewards/xp-toast-content.js.map +1 -1
- package/dist/index.cjs +14 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/rewards/leaderboard-table.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../../utils/cn.js\";\nimport { shortenAddress } from \"../../utils/address.js\";\nimport { LevelBadge } from \"./level-badge.js\";\n\nexport interface LeaderboardEntryLike {\n /** Kept for API compatibility with the backend's ordered response — not\n * rendered. This is a scoreboard, not a ranking: no \"#1/#2/#3\", no podium\n * colors. Order in the array is still the sort order the caller gets. */\n rank: number;\n address: string;\n publicId?: string | null;\n totalXp: number;\n currentLevel: number;\n currentLevelName: string;\n badgeColor: string;\n}\n\nexport interface LeaderboardTableProps {\n entries: LeaderboardEntryLike[];\n /** The viewer's address — its row gets highlighted. */\n highlightAddress?: string | null;\n /** Render the address cell (e.g. a profile link). Defaults to a shortened address. */\n renderAddress?: (address: string) => ReactNode;\n className?: string;\n}\n\n/** Scoreboard rows: address, level chip, points. No position number or\n * podium coloring — this celebrates participation, not competition. */\nexport function LeaderboardTable({ entries, highlightAddress, renderAddress, className }: LeaderboardTableProps) {\n return (\n <div className={cn(\"overflow-hidden rounded-xl border border-border\", className)}>\n {entries.map((e) => {\n const isViewer = highlightAddress != null && e.address === highlightAddress;\n return (\n <div\n key={e.address}\n className={cn(\n \"flex items-center gap-3 border-b border-border/60 bg-card px-3 py-2.5 last:border-b-0 sm:px-4\",\n isViewer && \"bg-primary/10\"\n )}\n >\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium\">\n {renderAddress ? renderAddress(e.address) : shortenAddress(e.address)}\n {isViewer && <span className=\"ml-1.5 text-xs text-muted-foreground\">(you)</span>}\n </span>\n <LevelBadge level={e.currentLevel} name={e.currentLevelName} badgeColor={e.badgeColor} size=\"sm\" className=\"hidden sm:inline-flex\" />\n <span className=\"shrink-0 text-sm font-semibold tabular-nums\"
|
|
1
|
+
{"version":3,"sources":["../../../src/components/rewards/leaderboard-table.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../../utils/cn.js\";\nimport { shortenAddress } from \"../../utils/address.js\";\nimport { LevelBadge } from \"./level-badge.js\";\n\nexport interface LeaderboardEntryLike {\n /** Kept for API compatibility with the backend's ordered response — not\n * rendered. This is a scoreboard, not a ranking: no \"#1/#2/#3\", no podium\n * colors. Order in the array is still the sort order the caller gets. */\n rank: number;\n address: string;\n publicId?: string | null;\n totalXp: number;\n currentLevel: number;\n currentLevelName: string;\n badgeColor: string;\n}\n\nexport interface LeaderboardTableProps {\n entries: LeaderboardEntryLike[];\n /** The viewer's address — its row gets highlighted. */\n highlightAddress?: string | null;\n /** Render the address cell (e.g. a profile link). Defaults to a shortened address. */\n renderAddress?: (address: string) => ReactNode;\n className?: string;\n}\n\n/** Scoreboard rows: address, level chip, points. No position number or\n * podium coloring — this celebrates participation, not competition. */\nexport function LeaderboardTable({ entries, highlightAddress, renderAddress, className }: LeaderboardTableProps) {\n return (\n <div className={cn(\"overflow-hidden rounded-xl border border-border\", className)}>\n {entries.map((e) => {\n const isViewer = highlightAddress != null && e.address === highlightAddress;\n return (\n <div\n key={e.address}\n className={cn(\n \"flex items-center gap-3 border-b border-border/60 bg-card px-3 py-2.5 last:border-b-0 sm:px-4\",\n isViewer && \"bg-primary/10\"\n )}\n >\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium\">\n {renderAddress ? renderAddress(e.address) : shortenAddress(e.address)}\n {isViewer && <span className=\"ml-1.5 text-xs text-muted-foreground\">(you)</span>}\n </span>\n <LevelBadge level={e.currentLevel} name={e.currentLevelName} badgeColor={e.badgeColor} size=\"sm\" className=\"hidden sm:inline-flex\" />\n <span className=\"shrink-0 text-sm font-semibold tabular-nums\">\n {e.totalXp.toLocaleString()}\n <span className=\"ml-1 text-xs font-normal text-muted-foreground\">XP</span>\n </span>\n </div>\n );\n })}\n </div>\n );\n}\n\nexport interface LeaderboardWidgetProps {\n entries: LeaderboardEntryLike[];\n title?: string;\n /** Link to the full scoreboard (\"/rewards\"). */\n href?: string;\n renderAddress?: (address: string) => ReactNode;\n className?: string;\n}\n\n/** Compact card for homepage/discover rails — same no-ranking scoreboard rows. */\nexport function LeaderboardWidget({ entries, title = \"Community Rewards\", href, renderAddress, className }: LeaderboardWidgetProps) {\n if (entries.length === 0) return null;\n return (\n <section className={cn(\"rounded-xl border border-border bg-card p-4 sm:p-5\", className)}>\n <div className=\"mb-3 flex items-center justify-between\">\n <h2 className=\"text-base font-black\">{title}</h2>\n {href && (\n <a href={href} className=\"text-xs font-semibold text-muted-foreground active:opacity-70\">\n View scoreboard →\n </a>\n )}\n </div>\n <ol className=\"space-y-2.5\">\n {entries.map((e) => (\n <li key={e.address} className=\"flex items-center gap-2.5\">\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium\">\n {renderAddress ? renderAddress(e.address) : shortenAddress(e.address)}\n </span>\n <LevelBadge level={e.currentLevel} name={e.currentLevelName} badgeColor={e.badgeColor} size=\"sm\" />\n </li>\n ))}\n </ol>\n </section>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA0CY;AAzCZ,gBAAmB;AACnB,qBAA+B;AAC/B,yBAA2B;AA0BpB,SAAS,iBAAiB,EAAE,SAAS,kBAAkB,eAAe,UAAU,GAA0B;AAC/G,SACE,4CAAC,SAAI,eAAW,cAAG,mDAAmD,SAAS,GAC5E,kBAAQ,IAAI,CAAC,MAAM;AAClB,UAAM,WAAW,oBAAoB,QAAQ,EAAE,YAAY;AAC3D,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,eAAW;AAAA,UACT;AAAA,UACA,YAAY;AAAA,QACd;AAAA,QAEA;AAAA,uDAAC,UAAK,WAAU,+CACb;AAAA,4BAAgB,cAAc,EAAE,OAAO,QAAI,+BAAe,EAAE,OAAO;AAAA,YACnE,YAAY,4CAAC,UAAK,WAAU,wCAAuC,mBAAK;AAAA,aAC3E;AAAA,UACA,4CAAC,iCAAW,OAAO,EAAE,cAAc,MAAM,EAAE,kBAAkB,YAAY,EAAE,YAAY,MAAK,MAAK,WAAU,yBAAwB;AAAA,UACnI,6CAAC,UAAK,WAAU,+CACb;AAAA,cAAE,QAAQ,eAAe;AAAA,YAC1B,4CAAC,UAAK,WAAU,kDAAiD,gBAAE;AAAA,aACrE;AAAA;AAAA;AAAA,MAdK,EAAE;AAAA,IAeT;AAAA,EAEJ,CAAC,GACH;AAEJ;AAYO,SAAS,kBAAkB,EAAE,SAAS,QAAQ,qBAAqB,MAAM,eAAe,UAAU,GAA2B;AAClI,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,SACE,6CAAC,aAAQ,eAAW,cAAG,sDAAsD,SAAS,GACpF;AAAA,iDAAC,SAAI,WAAU,0CACb;AAAA,kDAAC,QAAG,WAAU,wBAAwB,iBAAM;AAAA,MAC3C,QACC,4CAAC,OAAE,MAAY,WAAU,iEAAgE,oCAEzF;AAAA,OAEJ;AAAA,IACA,4CAAC,QAAG,WAAU,eACX,kBAAQ,IAAI,CAAC,MACZ,6CAAC,QAAmB,WAAU,6BAC5B;AAAA,kDAAC,UAAK,WAAU,+CACb,0BAAgB,cAAc,EAAE,OAAO,QAAI,+BAAe,EAAE,OAAO,GACtE;AAAA,MACA,4CAAC,iCAAW,OAAO,EAAE,cAAc,MAAM,EAAE,kBAAkB,YAAY,EAAE,YAAY,MAAK,MAAK;AAAA,SAJ1F,EAAE,OAKX,CACD,GACH;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -20,7 +20,7 @@ function LeaderboardTable({ entries, highlightAddress, renderAddress, className
|
|
|
20
20
|
/* @__PURE__ */ jsx(LevelBadge, { level: e.currentLevel, name: e.currentLevelName, badgeColor: e.badgeColor, size: "sm", className: "hidden sm:inline-flex" }),
|
|
21
21
|
/* @__PURE__ */ jsxs("span", { className: "shrink-0 text-sm font-semibold tabular-nums", children: [
|
|
22
22
|
e.totalXp.toLocaleString(),
|
|
23
|
-
"
|
|
23
|
+
/* @__PURE__ */ jsx("span", { className: "ml-1 text-xs font-normal text-muted-foreground", children: "XP" })
|
|
24
24
|
] })
|
|
25
25
|
]
|
|
26
26
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/rewards/leaderboard-table.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../../utils/cn.js\";\nimport { shortenAddress } from \"../../utils/address.js\";\nimport { LevelBadge } from \"./level-badge.js\";\n\nexport interface LeaderboardEntryLike {\n /** Kept for API compatibility with the backend's ordered response — not\n * rendered. This is a scoreboard, not a ranking: no \"#1/#2/#3\", no podium\n * colors. Order in the array is still the sort order the caller gets. */\n rank: number;\n address: string;\n publicId?: string | null;\n totalXp: number;\n currentLevel: number;\n currentLevelName: string;\n badgeColor: string;\n}\n\nexport interface LeaderboardTableProps {\n entries: LeaderboardEntryLike[];\n /** The viewer's address — its row gets highlighted. */\n highlightAddress?: string | null;\n /** Render the address cell (e.g. a profile link). Defaults to a shortened address. */\n renderAddress?: (address: string) => ReactNode;\n className?: string;\n}\n\n/** Scoreboard rows: address, level chip, points. No position number or\n * podium coloring — this celebrates participation, not competition. */\nexport function LeaderboardTable({ entries, highlightAddress, renderAddress, className }: LeaderboardTableProps) {\n return (\n <div className={cn(\"overflow-hidden rounded-xl border border-border\", className)}>\n {entries.map((e) => {\n const isViewer = highlightAddress != null && e.address === highlightAddress;\n return (\n <div\n key={e.address}\n className={cn(\n \"flex items-center gap-3 border-b border-border/60 bg-card px-3 py-2.5 last:border-b-0 sm:px-4\",\n isViewer && \"bg-primary/10\"\n )}\n >\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium\">\n {renderAddress ? renderAddress(e.address) : shortenAddress(e.address)}\n {isViewer && <span className=\"ml-1.5 text-xs text-muted-foreground\">(you)</span>}\n </span>\n <LevelBadge level={e.currentLevel} name={e.currentLevelName} badgeColor={e.badgeColor} size=\"sm\" className=\"hidden sm:inline-flex\" />\n <span className=\"shrink-0 text-sm font-semibold tabular-nums\"
|
|
1
|
+
{"version":3,"sources":["../../../src/components/rewards/leaderboard-table.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { cn } from \"../../utils/cn.js\";\nimport { shortenAddress } from \"../../utils/address.js\";\nimport { LevelBadge } from \"./level-badge.js\";\n\nexport interface LeaderboardEntryLike {\n /** Kept for API compatibility with the backend's ordered response — not\n * rendered. This is a scoreboard, not a ranking: no \"#1/#2/#3\", no podium\n * colors. Order in the array is still the sort order the caller gets. */\n rank: number;\n address: string;\n publicId?: string | null;\n totalXp: number;\n currentLevel: number;\n currentLevelName: string;\n badgeColor: string;\n}\n\nexport interface LeaderboardTableProps {\n entries: LeaderboardEntryLike[];\n /** The viewer's address — its row gets highlighted. */\n highlightAddress?: string | null;\n /** Render the address cell (e.g. a profile link). Defaults to a shortened address. */\n renderAddress?: (address: string) => ReactNode;\n className?: string;\n}\n\n/** Scoreboard rows: address, level chip, points. No position number or\n * podium coloring — this celebrates participation, not competition. */\nexport function LeaderboardTable({ entries, highlightAddress, renderAddress, className }: LeaderboardTableProps) {\n return (\n <div className={cn(\"overflow-hidden rounded-xl border border-border\", className)}>\n {entries.map((e) => {\n const isViewer = highlightAddress != null && e.address === highlightAddress;\n return (\n <div\n key={e.address}\n className={cn(\n \"flex items-center gap-3 border-b border-border/60 bg-card px-3 py-2.5 last:border-b-0 sm:px-4\",\n isViewer && \"bg-primary/10\"\n )}\n >\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium\">\n {renderAddress ? renderAddress(e.address) : shortenAddress(e.address)}\n {isViewer && <span className=\"ml-1.5 text-xs text-muted-foreground\">(you)</span>}\n </span>\n <LevelBadge level={e.currentLevel} name={e.currentLevelName} badgeColor={e.badgeColor} size=\"sm\" className=\"hidden sm:inline-flex\" />\n <span className=\"shrink-0 text-sm font-semibold tabular-nums\">\n {e.totalXp.toLocaleString()}\n <span className=\"ml-1 text-xs font-normal text-muted-foreground\">XP</span>\n </span>\n </div>\n );\n })}\n </div>\n );\n}\n\nexport interface LeaderboardWidgetProps {\n entries: LeaderboardEntryLike[];\n title?: string;\n /** Link to the full scoreboard (\"/rewards\"). */\n href?: string;\n renderAddress?: (address: string) => ReactNode;\n className?: string;\n}\n\n/** Compact card for homepage/discover rails — same no-ranking scoreboard rows. */\nexport function LeaderboardWidget({ entries, title = \"Community Rewards\", href, renderAddress, className }: LeaderboardWidgetProps) {\n if (entries.length === 0) return null;\n return (\n <section className={cn(\"rounded-xl border border-border bg-card p-4 sm:p-5\", className)}>\n <div className=\"mb-3 flex items-center justify-between\">\n <h2 className=\"text-base font-black\">{title}</h2>\n {href && (\n <a href={href} className=\"text-xs font-semibold text-muted-foreground active:opacity-70\">\n View scoreboard →\n </a>\n )}\n </div>\n <ol className=\"space-y-2.5\">\n {entries.map((e) => (\n <li key={e.address} className=\"flex items-center gap-2.5\">\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium\">\n {renderAddress ? renderAddress(e.address) : shortenAddress(e.address)}\n </span>\n <LevelBadge level={e.currentLevel} name={e.currentLevelName} badgeColor={e.badgeColor} size=\"sm\" />\n </li>\n ))}\n </ol>\n </section>\n );\n}\n"],"mappings":"AA0CY,SAEe,KAFf;AAzCZ,SAAS,UAAU;AACnB,SAAS,sBAAsB;AAC/B,SAAS,kBAAkB;AA0BpB,SAAS,iBAAiB,EAAE,SAAS,kBAAkB,eAAe,UAAU,GAA0B;AAC/G,SACE,oBAAC,SAAI,WAAW,GAAG,mDAAmD,SAAS,GAC5E,kBAAQ,IAAI,CAAC,MAAM;AAClB,UAAM,WAAW,oBAAoB,QAAQ,EAAE,YAAY;AAC3D,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,WAAW;AAAA,UACT;AAAA,UACA,YAAY;AAAA,QACd;AAAA,QAEA;AAAA,+BAAC,UAAK,WAAU,+CACb;AAAA,4BAAgB,cAAc,EAAE,OAAO,IAAI,eAAe,EAAE,OAAO;AAAA,YACnE,YAAY,oBAAC,UAAK,WAAU,wCAAuC,mBAAK;AAAA,aAC3E;AAAA,UACA,oBAAC,cAAW,OAAO,EAAE,cAAc,MAAM,EAAE,kBAAkB,YAAY,EAAE,YAAY,MAAK,MAAK,WAAU,yBAAwB;AAAA,UACnI,qBAAC,UAAK,WAAU,+CACb;AAAA,cAAE,QAAQ,eAAe;AAAA,YAC1B,oBAAC,UAAK,WAAU,kDAAiD,gBAAE;AAAA,aACrE;AAAA;AAAA;AAAA,MAdK,EAAE;AAAA,IAeT;AAAA,EAEJ,CAAC,GACH;AAEJ;AAYO,SAAS,kBAAkB,EAAE,SAAS,QAAQ,qBAAqB,MAAM,eAAe,UAAU,GAA2B;AAClI,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,SACE,qBAAC,aAAQ,WAAW,GAAG,sDAAsD,SAAS,GACpF;AAAA,yBAAC,SAAI,WAAU,0CACb;AAAA,0BAAC,QAAG,WAAU,wBAAwB,iBAAM;AAAA,MAC3C,QACC,oBAAC,OAAE,MAAY,WAAU,iEAAgE,oCAEzF;AAAA,OAEJ;AAAA,IACA,oBAAC,QAAG,WAAU,eACX,kBAAQ,IAAI,CAAC,MACZ,qBAAC,QAAmB,WAAU,6BAC5B;AAAA,0BAAC,UAAK,WAAU,+CACb,0BAAgB,cAAc,EAAE,OAAO,IAAI,eAAe,EAAE,OAAO,GACtE;AAAA,MACA,oBAAC,cAAW,OAAO,EAAE,cAAc,MAAM,EAAE,kBAAkB,YAAY,EAAE,YAAY,MAAK,MAAK;AAAA,SAJ1F,EAAE,OAKX,CACD,GACH;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -37,6 +37,7 @@ function LevelBadge({ level, name, badgeColor, size = "md", className }) {
|
|
|
37
37
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
38
38
|
"span",
|
|
39
39
|
{
|
|
40
|
+
title: `Level ${level}`,
|
|
40
41
|
className: (0, import_cn.cn)(
|
|
41
42
|
"inline-flex items-center rounded-full border font-semibold tracking-tight whitespace-nowrap",
|
|
42
43
|
sizeClasses[size],
|
|
@@ -45,9 +46,6 @@ function LevelBadge({ level, name, badgeColor, size = "md", className }) {
|
|
|
45
46
|
style: { borderColor: `${badgeColor}60`, backgroundColor: `${badgeColor}18`, color: badgeColor },
|
|
46
47
|
children: [
|
|
47
48
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("rounded-full shrink-0", dotSizes[size]), style: { backgroundColor: badgeColor } }),
|
|
48
|
-
"Lv.",
|
|
49
|
-
level,
|
|
50
|
-
" ",
|
|
51
49
|
name
|
|
52
50
|
]
|
|
53
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/rewards/level-badge.tsx"],"sourcesContent":["import { cn } from \"../../utils/cn.js\";\n\nexport interface LevelBadgeProps {\n level: number;\n name: string;\n badgeColor: string;\n size?: \"sm\" | \"md\" | \"lg\";\n className?: string;\n}\n\n/** Color-coded level chip (\"Lv.6 Voyager\"). Pure presentation — the
|
|
1
|
+
{"version":3,"sources":["../../../src/components/rewards/level-badge.tsx"],"sourcesContent":["import { cn } from \"../../utils/cn.js\";\n\nexport interface LevelBadgeProps {\n level: number;\n name: string;\n badgeColor: string;\n size?: \"sm\" | \"md\" | \"lg\";\n className?: string;\n}\n\n/** Color-coded level chip — shows the level's title only (\"Voyager\"), not\n * the raw number (\"Lv.6 Voyager\" read as UI noise). The level is still\n * available on hover via the native title tooltip. Pure presentation — the\n * consuming app supplies level data from the rewards API. */\nexport function LevelBadge({ level, name, badgeColor, size = \"md\", className }: LevelBadgeProps) {\n const sizeClasses = {\n sm: \"h-5 px-1.5 gap-1 text-[10px]\",\n md: \"h-6 px-2 gap-1.5 text-xs\",\n lg: \"h-8 px-3 gap-2 text-sm\",\n };\n\n const dotSizes = {\n sm: \"h-1.5 w-1.5\",\n md: \"h-2 w-2\",\n lg: \"h-2.5 w-2.5\",\n };\n\n return (\n <span\n title={`Level ${level}`}\n className={cn(\n \"inline-flex items-center rounded-full border font-semibold tracking-tight whitespace-nowrap\",\n sizeClasses[size],\n className\n )}\n style={{ borderColor: `${badgeColor}60`, backgroundColor: `${badgeColor}18`, color: badgeColor }}\n >\n <span className={cn(\"rounded-full shrink-0\", dotSizes[size])} style={{ backgroundColor: badgeColor }} />\n {name}\n </span>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA4BI;AA5BJ,gBAAmB;AAcZ,SAAS,WAAW,EAAE,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU,GAAoB;AAC/F,QAAM,cAAc;AAAA,IAClB,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAEA,QAAM,WAAW;AAAA,IACf,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,SAAS,KAAK;AAAA,MACrB,eAAW;AAAA,QACT;AAAA,QACA,YAAY,IAAI;AAAA,QAChB;AAAA,MACF;AAAA,MACA,OAAO,EAAE,aAAa,GAAG,UAAU,MAAM,iBAAiB,GAAG,UAAU,MAAM,OAAO,WAAW;AAAA,MAE/F;AAAA,oDAAC,UAAK,eAAW,cAAG,yBAAyB,SAAS,IAAI,CAAC,GAAG,OAAO,EAAE,iBAAiB,WAAW,GAAG;AAAA,QACrG;AAAA;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -7,8 +7,10 @@ interface LevelBadgeProps {
|
|
|
7
7
|
size?: "sm" | "md" | "lg";
|
|
8
8
|
className?: string;
|
|
9
9
|
}
|
|
10
|
-
/** Color-coded level chip ("
|
|
11
|
-
*
|
|
10
|
+
/** Color-coded level chip — shows the level's title only ("Voyager"), not
|
|
11
|
+
* the raw number ("Lv.6 Voyager" read as UI noise). The level is still
|
|
12
|
+
* available on hover via the native title tooltip. Pure presentation — the
|
|
13
|
+
* consuming app supplies level data from the rewards API. */
|
|
12
14
|
declare function LevelBadge({ level, name, badgeColor, size, className }: LevelBadgeProps): react_jsx_runtime.JSX.Element;
|
|
13
15
|
|
|
14
16
|
export { LevelBadge, type LevelBadgeProps };
|
|
@@ -7,8 +7,10 @@ interface LevelBadgeProps {
|
|
|
7
7
|
size?: "sm" | "md" | "lg";
|
|
8
8
|
className?: string;
|
|
9
9
|
}
|
|
10
|
-
/** Color-coded level chip ("
|
|
11
|
-
*
|
|
10
|
+
/** Color-coded level chip — shows the level's title only ("Voyager"), not
|
|
11
|
+
* the raw number ("Lv.6 Voyager" read as UI noise). The level is still
|
|
12
|
+
* available on hover via the native title tooltip. Pure presentation — the
|
|
13
|
+
* consuming app supplies level data from the rewards API. */
|
|
12
14
|
declare function LevelBadge({ level, name, badgeColor, size, className }: LevelBadgeProps): react_jsx_runtime.JSX.Element;
|
|
13
15
|
|
|
14
16
|
export { LevelBadge, type LevelBadgeProps };
|
|
@@ -14,6 +14,7 @@ function LevelBadge({ level, name, badgeColor, size = "md", className }) {
|
|
|
14
14
|
return /* @__PURE__ */ jsxs(
|
|
15
15
|
"span",
|
|
16
16
|
{
|
|
17
|
+
title: `Level ${level}`,
|
|
17
18
|
className: cn(
|
|
18
19
|
"inline-flex items-center rounded-full border font-semibold tracking-tight whitespace-nowrap",
|
|
19
20
|
sizeClasses[size],
|
|
@@ -22,9 +23,6 @@ function LevelBadge({ level, name, badgeColor, size = "md", className }) {
|
|
|
22
23
|
style: { borderColor: `${badgeColor}60`, backgroundColor: `${badgeColor}18`, color: badgeColor },
|
|
23
24
|
children: [
|
|
24
25
|
/* @__PURE__ */ jsx("span", { className: cn("rounded-full shrink-0", dotSizes[size]), style: { backgroundColor: badgeColor } }),
|
|
25
|
-
"Lv.",
|
|
26
|
-
level,
|
|
27
|
-
" ",
|
|
28
26
|
name
|
|
29
27
|
]
|
|
30
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/rewards/level-badge.tsx"],"sourcesContent":["import { cn } from \"../../utils/cn.js\";\n\nexport interface LevelBadgeProps {\n level: number;\n name: string;\n badgeColor: string;\n size?: \"sm\" | \"md\" | \"lg\";\n className?: string;\n}\n\n/** Color-coded level chip (\"Lv.6 Voyager\"). Pure presentation — the
|
|
1
|
+
{"version":3,"sources":["../../../src/components/rewards/level-badge.tsx"],"sourcesContent":["import { cn } from \"../../utils/cn.js\";\n\nexport interface LevelBadgeProps {\n level: number;\n name: string;\n badgeColor: string;\n size?: \"sm\" | \"md\" | \"lg\";\n className?: string;\n}\n\n/** Color-coded level chip — shows the level's title only (\"Voyager\"), not\n * the raw number (\"Lv.6 Voyager\" read as UI noise). The level is still\n * available on hover via the native title tooltip. Pure presentation — the\n * consuming app supplies level data from the rewards API. */\nexport function LevelBadge({ level, name, badgeColor, size = \"md\", className }: LevelBadgeProps) {\n const sizeClasses = {\n sm: \"h-5 px-1.5 gap-1 text-[10px]\",\n md: \"h-6 px-2 gap-1.5 text-xs\",\n lg: \"h-8 px-3 gap-2 text-sm\",\n };\n\n const dotSizes = {\n sm: \"h-1.5 w-1.5\",\n md: \"h-2 w-2\",\n lg: \"h-2.5 w-2.5\",\n };\n\n return (\n <span\n title={`Level ${level}`}\n className={cn(\n \"inline-flex items-center rounded-full border font-semibold tracking-tight whitespace-nowrap\",\n sizeClasses[size],\n className\n )}\n style={{ borderColor: `${badgeColor}60`, backgroundColor: `${badgeColor}18`, color: badgeColor }}\n >\n <span className={cn(\"rounded-full shrink-0\", dotSizes[size])} style={{ backgroundColor: badgeColor }} />\n {name}\n </span>\n );\n}\n"],"mappings":"AA4BI,SASE,KATF;AA5BJ,SAAS,UAAU;AAcZ,SAAS,WAAW,EAAE,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU,GAAoB;AAC/F,QAAM,cAAc;AAAA,IAClB,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAEA,QAAM,WAAW;AAAA,IACf,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,SAAS,KAAK;AAAA,MACrB,WAAW;AAAA,QACT;AAAA,QACA,YAAY,IAAI;AAAA,QAChB;AAAA,MACF;AAAA,MACA,OAAO,EAAE,aAAa,GAAG,UAAU,MAAM,iBAAiB,GAAG,UAAU,MAAM,OAAO,WAAW;AAAA,MAE/F;AAAA,4BAAC,UAAK,WAAW,GAAG,yBAAyB,SAAS,IAAI,CAAC,GAAG,OAAO,EAAE,iBAAiB,WAAW,GAAG;AAAA,QACrG;AAAA;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
var level_up_celebration_exports = {};
|
|
21
|
+
__export(level_up_celebration_exports, {
|
|
22
|
+
LevelUpCelebration: () => LevelUpCelebration
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(level_up_celebration_exports);
|
|
25
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
26
|
+
var import_react = require("react");
|
|
27
|
+
var import_level_badge = require("./level-badge.js");
|
|
28
|
+
var import_cn = require("../../utils/cn.js");
|
|
29
|
+
const PARTICLE_COUNT = 14;
|
|
30
|
+
function LevelUpCelebration({ level, name, badgeColor, onDismiss, className }) {
|
|
31
|
+
(0, import_react.useEffect)(() => {
|
|
32
|
+
const timer = setTimeout(onDismiss, 3e3);
|
|
33
|
+
return () => clearTimeout(timer);
|
|
34
|
+
}, [onDismiss]);
|
|
35
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
36
|
+
"div",
|
|
37
|
+
{
|
|
38
|
+
role: "status",
|
|
39
|
+
"aria-live": "polite",
|
|
40
|
+
className: (0, import_cn.cn)(
|
|
41
|
+
"fixed inset-0 z-[200] flex items-center justify-center bg-background/40 backdrop-blur-sm",
|
|
42
|
+
className
|
|
43
|
+
),
|
|
44
|
+
onClick: onDismiss,
|
|
45
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative flex flex-col items-center gap-3 animate-in zoom-in-95 fade-in duration-300", children: [
|
|
46
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pointer-events-none absolute inset-0 -z-10", children: Array.from({ length: PARTICLE_COUNT }).map((_, i) => {
|
|
47
|
+
const angle = 360 / PARTICLE_COUNT * i;
|
|
48
|
+
const distance = 60 + i % 3 * 20;
|
|
49
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
50
|
+
"span",
|
|
51
|
+
{
|
|
52
|
+
className: "absolute left-1/2 top-1/2 h-1.5 w-1.5 rounded-full animate-in fade-in zoom-in duration-700",
|
|
53
|
+
style: {
|
|
54
|
+
backgroundColor: badgeColor,
|
|
55
|
+
transform: `rotate(${angle}deg) translate(${distance}px) rotate(-${angle}deg)`
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
i
|
|
59
|
+
);
|
|
60
|
+
}) }),
|
|
61
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-sm font-bold uppercase tracking-widest text-muted-foreground", children: "Level up" }),
|
|
62
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_level_badge.LevelBadge, { level, name, badgeColor, size: "lg" })
|
|
63
|
+
] })
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
68
|
+
0 && (module.exports = {
|
|
69
|
+
LevelUpCelebration
|
|
70
|
+
});
|
|
71
|
+
//# sourceMappingURL=level-up-celebration.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/rewards/level-up-celebration.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\nimport { LevelBadge } from \"./level-badge.js\";\nimport { cn } from \"../../utils/cn.js\";\n\nexport interface LevelUpCelebrationProps {\n level: number;\n name: string;\n badgeColor: string;\n onDismiss: () => void;\n className?: string;\n}\n\nconst PARTICLE_COUNT = 14;\n\n/** Auto-dismissing celebration burst for a level-up moment. Pure CSS\n * animation, no dependency — particles are positioned/rotated via inline\n * style so the same component works without a keyframe-per-particle\n * stylesheet entry. */\nexport function LevelUpCelebration({ level, name, badgeColor, onDismiss, className }: LevelUpCelebrationProps) {\n useEffect(() => {\n const timer = setTimeout(onDismiss, 3000);\n return () => clearTimeout(timer);\n }, [onDismiss]);\n\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n className={cn(\n \"fixed inset-0 z-[200] flex items-center justify-center bg-background/40 backdrop-blur-sm\",\n className\n )}\n onClick={onDismiss}\n >\n <div className=\"relative flex flex-col items-center gap-3 animate-in zoom-in-95 fade-in duration-300\">\n <div className=\"pointer-events-none absolute inset-0 -z-10\">\n {Array.from({ length: PARTICLE_COUNT }).map((_, i) => {\n const angle = (360 / PARTICLE_COUNT) * i;\n const distance = 60 + (i % 3) * 20;\n return (\n <span\n key={i}\n className=\"absolute left-1/2 top-1/2 h-1.5 w-1.5 rounded-full animate-in fade-in zoom-in duration-700\"\n style={{\n backgroundColor: badgeColor,\n transform: `rotate(${angle}deg) translate(${distance}px) rotate(-${angle}deg)`,\n }}\n />\n );\n })}\n </div>\n <p className=\"text-sm font-bold uppercase tracking-widest text-muted-foreground\">Level up</p>\n <LevelBadge level={level} name={name} badgeColor={badgeColor} size=\"lg\" />\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAoCM;AAlCN,mBAA0B;AAC1B,yBAA2B;AAC3B,gBAAmB;AAUnB,MAAM,iBAAiB;AAMhB,SAAS,mBAAmB,EAAE,OAAO,MAAM,YAAY,WAAW,UAAU,GAA4B;AAC7G,8BAAU,MAAM;AACd,UAAM,QAAQ,WAAW,WAAW,GAAI;AACxC,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,SAAS,CAAC;AAEd,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,aAAU;AAAA,MACV,eAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACA,SAAS;AAAA,MAET,uDAAC,SAAI,WAAU,wFACb;AAAA,oDAAC,SAAI,WAAU,8CACZ,gBAAM,KAAK,EAAE,QAAQ,eAAe,CAAC,EAAE,IAAI,CAAC,GAAG,MAAM;AACpD,gBAAM,QAAS,MAAM,iBAAkB;AACvC,gBAAM,WAAW,KAAM,IAAI,IAAK;AAChC,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,WAAU;AAAA,cACV,OAAO;AAAA,gBACL,iBAAiB;AAAA,gBACjB,WAAW,UAAU,KAAK,kBAAkB,QAAQ,eAAe,KAAK;AAAA,cAC1E;AAAA;AAAA,YALK;AAAA,UAMP;AAAA,QAEJ,CAAC,GACH;AAAA,QACA,4CAAC,OAAE,WAAU,qEAAoE,sBAAQ;AAAA,QACzF,4CAAC,iCAAW,OAAc,MAAY,YAAwB,MAAK,MAAK;AAAA,SAC1E;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface LevelUpCelebrationProps {
|
|
4
|
+
level: number;
|
|
5
|
+
name: string;
|
|
6
|
+
badgeColor: string;
|
|
7
|
+
onDismiss: () => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Auto-dismissing celebration burst for a level-up moment. Pure CSS
|
|
11
|
+
* animation, no dependency — particles are positioned/rotated via inline
|
|
12
|
+
* style so the same component works without a keyframe-per-particle
|
|
13
|
+
* stylesheet entry. */
|
|
14
|
+
declare function LevelUpCelebration({ level, name, badgeColor, onDismiss, className }: LevelUpCelebrationProps): react_jsx_runtime.JSX.Element;
|
|
15
|
+
|
|
16
|
+
export { LevelUpCelebration, type LevelUpCelebrationProps };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
interface LevelUpCelebrationProps {
|
|
4
|
+
level: number;
|
|
5
|
+
name: string;
|
|
6
|
+
badgeColor: string;
|
|
7
|
+
onDismiss: () => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Auto-dismissing celebration burst for a level-up moment. Pure CSS
|
|
11
|
+
* animation, no dependency — particles are positioned/rotated via inline
|
|
12
|
+
* style so the same component works without a keyframe-per-particle
|
|
13
|
+
* stylesheet entry. */
|
|
14
|
+
declare function LevelUpCelebration({ level, name, badgeColor, onDismiss, className }: LevelUpCelebrationProps): react_jsx_runtime.JSX.Element;
|
|
15
|
+
|
|
16
|
+
export { LevelUpCelebration, type LevelUpCelebrationProps };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { LevelBadge } from "./level-badge.js";
|
|
5
|
+
import { cn } from "../../utils/cn.js";
|
|
6
|
+
const PARTICLE_COUNT = 14;
|
|
7
|
+
function LevelUpCelebration({ level, name, badgeColor, onDismiss, className }) {
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const timer = setTimeout(onDismiss, 3e3);
|
|
10
|
+
return () => clearTimeout(timer);
|
|
11
|
+
}, [onDismiss]);
|
|
12
|
+
return /* @__PURE__ */ jsx(
|
|
13
|
+
"div",
|
|
14
|
+
{
|
|
15
|
+
role: "status",
|
|
16
|
+
"aria-live": "polite",
|
|
17
|
+
className: cn(
|
|
18
|
+
"fixed inset-0 z-[200] flex items-center justify-center bg-background/40 backdrop-blur-sm",
|
|
19
|
+
className
|
|
20
|
+
),
|
|
21
|
+
onClick: onDismiss,
|
|
22
|
+
children: /* @__PURE__ */ jsxs("div", { className: "relative flex flex-col items-center gap-3 animate-in zoom-in-95 fade-in duration-300", children: [
|
|
23
|
+
/* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 -z-10", children: Array.from({ length: PARTICLE_COUNT }).map((_, i) => {
|
|
24
|
+
const angle = 360 / PARTICLE_COUNT * i;
|
|
25
|
+
const distance = 60 + i % 3 * 20;
|
|
26
|
+
return /* @__PURE__ */ jsx(
|
|
27
|
+
"span",
|
|
28
|
+
{
|
|
29
|
+
className: "absolute left-1/2 top-1/2 h-1.5 w-1.5 rounded-full animate-in fade-in zoom-in duration-700",
|
|
30
|
+
style: {
|
|
31
|
+
backgroundColor: badgeColor,
|
|
32
|
+
transform: `rotate(${angle}deg) translate(${distance}px) rotate(-${angle}deg)`
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
i
|
|
36
|
+
);
|
|
37
|
+
}) }),
|
|
38
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-bold uppercase tracking-widest text-muted-foreground", children: "Level up" }),
|
|
39
|
+
/* @__PURE__ */ jsx(LevelBadge, { level, name, badgeColor, size: "lg" })
|
|
40
|
+
] })
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
LevelUpCelebration
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=level-up-celebration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/rewards/level-up-celebration.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\nimport { LevelBadge } from \"./level-badge.js\";\nimport { cn } from \"../../utils/cn.js\";\n\nexport interface LevelUpCelebrationProps {\n level: number;\n name: string;\n badgeColor: string;\n onDismiss: () => void;\n className?: string;\n}\n\nconst PARTICLE_COUNT = 14;\n\n/** Auto-dismissing celebration burst for a level-up moment. Pure CSS\n * animation, no dependency — particles are positioned/rotated via inline\n * style so the same component works without a keyframe-per-particle\n * stylesheet entry. */\nexport function LevelUpCelebration({ level, name, badgeColor, onDismiss, className }: LevelUpCelebrationProps) {\n useEffect(() => {\n const timer = setTimeout(onDismiss, 3000);\n return () => clearTimeout(timer);\n }, [onDismiss]);\n\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n className={cn(\n \"fixed inset-0 z-[200] flex items-center justify-center bg-background/40 backdrop-blur-sm\",\n className\n )}\n onClick={onDismiss}\n >\n <div className=\"relative flex flex-col items-center gap-3 animate-in zoom-in-95 fade-in duration-300\">\n <div className=\"pointer-events-none absolute inset-0 -z-10\">\n {Array.from({ length: PARTICLE_COUNT }).map((_, i) => {\n const angle = (360 / PARTICLE_COUNT) * i;\n const distance = 60 + (i % 3) * 20;\n return (\n <span\n key={i}\n className=\"absolute left-1/2 top-1/2 h-1.5 w-1.5 rounded-full animate-in fade-in zoom-in duration-700\"\n style={{\n backgroundColor: badgeColor,\n transform: `rotate(${angle}deg) translate(${distance}px) rotate(-${angle}deg)`,\n }}\n />\n );\n })}\n </div>\n <p className=\"text-sm font-bold uppercase tracking-widest text-muted-foreground\">Level up</p>\n <LevelBadge level={level} name={name} badgeColor={badgeColor} size=\"lg\" />\n </div>\n </div>\n );\n}\n"],"mappings":";AAoCM,SAMQ,KANR;AAlCN,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAC3B,SAAS,UAAU;AAUnB,MAAM,iBAAiB;AAMhB,SAAS,mBAAmB,EAAE,OAAO,MAAM,YAAY,WAAW,UAAU,GAA4B;AAC7G,YAAU,MAAM;AACd,UAAM,QAAQ,WAAW,WAAW,GAAI;AACxC,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,SAAS,CAAC;AAEd,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,aAAU;AAAA,MACV,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MACA,SAAS;AAAA,MAET,+BAAC,SAAI,WAAU,wFACb;AAAA,4BAAC,SAAI,WAAU,8CACZ,gBAAM,KAAK,EAAE,QAAQ,eAAe,CAAC,EAAE,IAAI,CAAC,GAAG,MAAM;AACpD,gBAAM,QAAS,MAAM,iBAAkB;AACvC,gBAAM,WAAW,KAAM,IAAI,IAAK;AAChC,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,WAAU;AAAA,cACV,OAAO;AAAA,gBACL,iBAAiB;AAAA,gBACjB,WAAW,UAAU,KAAK,kBAAkB,QAAQ,eAAe,KAAK;AAAA,cAC1E;AAAA;AAAA,YALK;AAAA,UAMP;AAAA,QAEJ,CAAC,GACH;AAAA,QACA,oBAAC,OAAE,WAAU,qEAAoE,sBAAQ;AAAA,QACzF,oBAAC,cAAW,OAAc,MAAY,YAAwB,MAAK,MAAK;AAAA,SAC1E;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -31,11 +31,29 @@ function createRewardToast(getRewardsConfig) {
|
|
|
31
31
|
configPromise ?? (configPromise = getRewardsConfig());
|
|
32
32
|
return configPromise;
|
|
33
33
|
}
|
|
34
|
-
return function rewardToast(actionType) {
|
|
34
|
+
return function rewardToast(actionType, snapshot) {
|
|
35
35
|
loadConfig().then((config) => {
|
|
36
36
|
const action = config.actions.find((a) => a.type === actionType);
|
|
37
37
|
if (!action) return;
|
|
38
|
-
|
|
38
|
+
if (!snapshot) {
|
|
39
|
+
(0, import_sonner.toast)(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_xp_toast_content.XpToastContent, { xp: action.xp, label: action.label }), { duration: 3500 });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const level = config.levels.find((l) => l.level === snapshot.currentLevel);
|
|
43
|
+
const nextLevel = config.levels.find((l) => l.level === snapshot.currentLevel + 1);
|
|
44
|
+
(0, import_sonner.toast)(
|
|
45
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
46
|
+
import_xp_toast_content.XpToastContent,
|
|
47
|
+
{
|
|
48
|
+
xp: action.xp,
|
|
49
|
+
label: action.label,
|
|
50
|
+
totalXp: snapshot.totalXp,
|
|
51
|
+
levelXp: level?.xpRequired ?? 0,
|
|
52
|
+
nextLevelXp: nextLevel?.xpRequired ?? null
|
|
53
|
+
}
|
|
54
|
+
),
|
|
55
|
+
{ duration: 3500 }
|
|
56
|
+
);
|
|
39
57
|
}).catch(() => {
|
|
40
58
|
configPromise = null;
|
|
41
59
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/rewards/reward-toast.tsx"],"sourcesContent":["\"use client\";\n\nimport { toast } from \"sonner\";\nimport { XpToastContent } from \"./xp-toast-content.js\";\nimport type { ApiRewardsConfig } from \"@medialane/sdk\";\n\n/**\n * Optimistic XP feedback after a scoring action. Scores recompute on a\n * schedule, so this shows the action's configured value, never a balance.\n * Fire-and-forget: any failure is silent (rewards UI must never break a flow).\n *\n * Each app supplies its own `getRewardsConfig` (bound to its own\n * `MedialaneClient` singleton) — the caching/toast logic lives here once.\n */\nexport function createRewardToast(getRewardsConfig: () => Promise<ApiRewardsConfig>) {\n let configPromise: Promise<ApiRewardsConfig> | null = null;\n function loadConfig() {\n configPromise ??= getRewardsConfig();\n return configPromise;\n }\n\n return function rewardToast(actionType: string): void {\n loadConfig()\n .then((config) => {\n const action = config.actions.find((a) => a.type === actionType);\n if (!action) return;\n toast(<XpToastContent xp={action.xp} label={action.label} />, { duration: 3500 });\n })\n .catch(() => {\n configPromise = null; // retry on the next action\n });\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
1
|
+
{"version":3,"sources":["../../../src/components/rewards/reward-toast.tsx"],"sourcesContent":["\"use client\";\n\nimport { toast } from \"sonner\";\nimport { XpToastContent } from \"./xp-toast-content.js\";\nimport type { ApiRewardsConfig } from \"@medialane/sdk\";\n\nexport interface RewardToastSnapshot {\n totalXp: number;\n currentLevel: number;\n}\n\n/**\n * Optimistic XP feedback after a scoring action. Scores recompute on a\n * schedule, so this shows the action's configured value, never a balance.\n * Fire-and-forget: any failure is silent (rewards UI must never break a flow).\n *\n * Each app supplies its own `getRewardsConfig` (bound to its own\n * `MedialaneClient` singleton) — the caching/toast logic lives here once.\n */\nexport function createRewardToast(getRewardsConfig: () => Promise<ApiRewardsConfig>) {\n let configPromise: Promise<ApiRewardsConfig> | null = null;\n function loadConfig() {\n configPromise ??= getRewardsConfig();\n return configPromise;\n }\n\n /**\n * @param snapshot Optional — when provided, the toast additionally shows\n * a progress bar toward the next level. Omit to get today's plain toast.\n */\n return function rewardToast(actionType: string, snapshot?: RewardToastSnapshot): void {\n loadConfig()\n .then((config) => {\n const action = config.actions.find((a) => a.type === actionType);\n if (!action) return;\n\n if (!snapshot) {\n toast(<XpToastContent xp={action.xp} label={action.label} />, { duration: 3500 });\n return;\n }\n\n const level = config.levels.find((l) => l.level === snapshot.currentLevel);\n const nextLevel = config.levels.find((l) => l.level === snapshot.currentLevel + 1);\n toast(\n <XpToastContent\n xp={action.xp}\n label={action.label}\n totalXp={snapshot.totalXp}\n levelXp={level?.xpRequired ?? 0}\n nextLevelXp={nextLevel?.xpRequired ?? null}\n />,\n { duration: 3500 }\n );\n })\n .catch(() => {\n configPromise = null; // retry on the next action\n });\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAqCgB;AAnChB,oBAAsB;AACtB,8BAA+B;AAgBxB,SAAS,kBAAkB,kBAAmD;AACnF,MAAI,gBAAkD;AACtD,WAAS,aAAa;AACpB,sCAAkB,iBAAiB;AACnC,WAAO;AAAA,EACT;AAMA,SAAO,SAAS,YAAY,YAAoB,UAAsC;AACpF,eAAW,EACR,KAAK,CAAC,WAAW;AAChB,YAAM,SAAS,OAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,UAAU;AAC/D,UAAI,CAAC,OAAQ;AAEb,UAAI,CAAC,UAAU;AACb,iCAAM,4CAAC,0CAAe,IAAI,OAAO,IAAI,OAAO,OAAO,OAAO,GAAI,EAAE,UAAU,KAAK,CAAC;AAChF;AAAA,MACF;AAEA,YAAM,QAAQ,OAAO,OAAO,KAAK,CAAC,MAAM,EAAE,UAAU,SAAS,YAAY;AACzE,YAAM,YAAY,OAAO,OAAO,KAAK,CAAC,MAAM,EAAE,UAAU,SAAS,eAAe,CAAC;AACjF;AAAA,QACE;AAAA,UAAC;AAAA;AAAA,YACC,IAAI,OAAO;AAAA,YACX,OAAO,OAAO;AAAA,YACd,SAAS,SAAS;AAAA,YAClB,SAAS,OAAO,cAAc;AAAA,YAC9B,aAAa,WAAW,cAAc;AAAA;AAAA,QACxC;AAAA,QACA,EAAE,UAAU,KAAK;AAAA,MACnB;AAAA,IACF,CAAC,EACA,MAAM,MAAM;AACX,sBAAgB;AAAA,IAClB,CAAC;AAAA,EACL;AACF;","names":[]}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ApiRewardsConfig } from '@medialane/sdk';
|
|
2
2
|
|
|
3
|
+
interface RewardToastSnapshot {
|
|
4
|
+
totalXp: number;
|
|
5
|
+
currentLevel: number;
|
|
6
|
+
}
|
|
3
7
|
/**
|
|
4
8
|
* Optimistic XP feedback after a scoring action. Scores recompute on a
|
|
5
9
|
* schedule, so this shows the action's configured value, never a balance.
|
|
@@ -8,6 +12,6 @@ import { ApiRewardsConfig } from '@medialane/sdk';
|
|
|
8
12
|
* Each app supplies its own `getRewardsConfig` (bound to its own
|
|
9
13
|
* `MedialaneClient` singleton) — the caching/toast logic lives here once.
|
|
10
14
|
*/
|
|
11
|
-
declare function createRewardToast(getRewardsConfig: () => Promise<ApiRewardsConfig>): (actionType: string) => void;
|
|
15
|
+
declare function createRewardToast(getRewardsConfig: () => Promise<ApiRewardsConfig>): (actionType: string, snapshot?: RewardToastSnapshot) => void;
|
|
12
16
|
|
|
13
|
-
export { createRewardToast };
|
|
17
|
+
export { type RewardToastSnapshot, createRewardToast };
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { ApiRewardsConfig } from '@medialane/sdk';
|
|
2
2
|
|
|
3
|
+
interface RewardToastSnapshot {
|
|
4
|
+
totalXp: number;
|
|
5
|
+
currentLevel: number;
|
|
6
|
+
}
|
|
3
7
|
/**
|
|
4
8
|
* Optimistic XP feedback after a scoring action. Scores recompute on a
|
|
5
9
|
* schedule, so this shows the action's configured value, never a balance.
|
|
@@ -8,6 +12,6 @@ import { ApiRewardsConfig } from '@medialane/sdk';
|
|
|
8
12
|
* Each app supplies its own `getRewardsConfig` (bound to its own
|
|
9
13
|
* `MedialaneClient` singleton) — the caching/toast logic lives here once.
|
|
10
14
|
*/
|
|
11
|
-
declare function createRewardToast(getRewardsConfig: () => Promise<ApiRewardsConfig>): (actionType: string) => void;
|
|
15
|
+
declare function createRewardToast(getRewardsConfig: () => Promise<ApiRewardsConfig>): (actionType: string, snapshot?: RewardToastSnapshot) => void;
|
|
12
16
|
|
|
13
|
-
export { createRewardToast };
|
|
17
|
+
export { type RewardToastSnapshot, createRewardToast };
|
|
@@ -8,11 +8,29 @@ function createRewardToast(getRewardsConfig) {
|
|
|
8
8
|
configPromise ?? (configPromise = getRewardsConfig());
|
|
9
9
|
return configPromise;
|
|
10
10
|
}
|
|
11
|
-
return function rewardToast(actionType) {
|
|
11
|
+
return function rewardToast(actionType, snapshot) {
|
|
12
12
|
loadConfig().then((config) => {
|
|
13
13
|
const action = config.actions.find((a) => a.type === actionType);
|
|
14
14
|
if (!action) return;
|
|
15
|
-
|
|
15
|
+
if (!snapshot) {
|
|
16
|
+
toast(/* @__PURE__ */ jsx(XpToastContent, { xp: action.xp, label: action.label }), { duration: 3500 });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const level = config.levels.find((l) => l.level === snapshot.currentLevel);
|
|
20
|
+
const nextLevel = config.levels.find((l) => l.level === snapshot.currentLevel + 1);
|
|
21
|
+
toast(
|
|
22
|
+
/* @__PURE__ */ jsx(
|
|
23
|
+
XpToastContent,
|
|
24
|
+
{
|
|
25
|
+
xp: action.xp,
|
|
26
|
+
label: action.label,
|
|
27
|
+
totalXp: snapshot.totalXp,
|
|
28
|
+
levelXp: level?.xpRequired ?? 0,
|
|
29
|
+
nextLevelXp: nextLevel?.xpRequired ?? null
|
|
30
|
+
}
|
|
31
|
+
),
|
|
32
|
+
{ duration: 3500 }
|
|
33
|
+
);
|
|
16
34
|
}).catch(() => {
|
|
17
35
|
configPromise = null;
|
|
18
36
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/rewards/reward-toast.tsx"],"sourcesContent":["\"use client\";\n\nimport { toast } from \"sonner\";\nimport { XpToastContent } from \"./xp-toast-content.js\";\nimport type { ApiRewardsConfig } from \"@medialane/sdk\";\n\n/**\n * Optimistic XP feedback after a scoring action. Scores recompute on a\n * schedule, so this shows the action's configured value, never a balance.\n * Fire-and-forget: any failure is silent (rewards UI must never break a flow).\n *\n * Each app supplies its own `getRewardsConfig` (bound to its own\n * `MedialaneClient` singleton) — the caching/toast logic lives here once.\n */\nexport function createRewardToast(getRewardsConfig: () => Promise<ApiRewardsConfig>) {\n let configPromise: Promise<ApiRewardsConfig> | null = null;\n function loadConfig() {\n configPromise ??= getRewardsConfig();\n return configPromise;\n }\n\n return function rewardToast(actionType: string): void {\n loadConfig()\n .then((config) => {\n const action = config.actions.find((a) => a.type === actionType);\n if (!action) return;\n toast(<XpToastContent xp={action.xp} label={action.label} />, { duration: 3500 });\n })\n .catch(() => {\n configPromise = null; // retry on the next action\n });\n };\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../../src/components/rewards/reward-toast.tsx"],"sourcesContent":["\"use client\";\n\nimport { toast } from \"sonner\";\nimport { XpToastContent } from \"./xp-toast-content.js\";\nimport type { ApiRewardsConfig } from \"@medialane/sdk\";\n\nexport interface RewardToastSnapshot {\n totalXp: number;\n currentLevel: number;\n}\n\n/**\n * Optimistic XP feedback after a scoring action. Scores recompute on a\n * schedule, so this shows the action's configured value, never a balance.\n * Fire-and-forget: any failure is silent (rewards UI must never break a flow).\n *\n * Each app supplies its own `getRewardsConfig` (bound to its own\n * `MedialaneClient` singleton) — the caching/toast logic lives here once.\n */\nexport function createRewardToast(getRewardsConfig: () => Promise<ApiRewardsConfig>) {\n let configPromise: Promise<ApiRewardsConfig> | null = null;\n function loadConfig() {\n configPromise ??= getRewardsConfig();\n return configPromise;\n }\n\n /**\n * @param snapshot Optional — when provided, the toast additionally shows\n * a progress bar toward the next level. Omit to get today's plain toast.\n */\n return function rewardToast(actionType: string, snapshot?: RewardToastSnapshot): void {\n loadConfig()\n .then((config) => {\n const action = config.actions.find((a) => a.type === actionType);\n if (!action) return;\n\n if (!snapshot) {\n toast(<XpToastContent xp={action.xp} label={action.label} />, { duration: 3500 });\n return;\n }\n\n const level = config.levels.find((l) => l.level === snapshot.currentLevel);\n const nextLevel = config.levels.find((l) => l.level === snapshot.currentLevel + 1);\n toast(\n <XpToastContent\n xp={action.xp}\n label={action.label}\n totalXp={snapshot.totalXp}\n levelXp={level?.xpRequired ?? 0}\n nextLevelXp={nextLevel?.xpRequired ?? null}\n />,\n { duration: 3500 }\n );\n })\n .catch(() => {\n configPromise = null; // retry on the next action\n });\n };\n}\n"],"mappings":";AAqCgB;AAnChB,SAAS,aAAa;AACtB,SAAS,sBAAsB;AAgBxB,SAAS,kBAAkB,kBAAmD;AACnF,MAAI,gBAAkD;AACtD,WAAS,aAAa;AACpB,sCAAkB,iBAAiB;AACnC,WAAO;AAAA,EACT;AAMA,SAAO,SAAS,YAAY,YAAoB,UAAsC;AACpF,eAAW,EACR,KAAK,CAAC,WAAW;AAChB,YAAM,SAAS,OAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,UAAU;AAC/D,UAAI,CAAC,OAAQ;AAEb,UAAI,CAAC,UAAU;AACb,cAAM,oBAAC,kBAAe,IAAI,OAAO,IAAI,OAAO,OAAO,OAAO,GAAI,EAAE,UAAU,KAAK,CAAC;AAChF;AAAA,MACF;AAEA,YAAM,QAAQ,OAAO,OAAO,KAAK,CAAC,MAAM,EAAE,UAAU,SAAS,YAAY;AACzE,YAAM,YAAY,OAAO,OAAO,KAAK,CAAC,MAAM,EAAE,UAAU,SAAS,eAAe,CAAC;AACjF;AAAA,QACE;AAAA,UAAC;AAAA;AAAA,YACC,IAAI,OAAO;AAAA,YACX,OAAO,OAAO;AAAA,YACd,SAAS,SAAS;AAAA,YAClB,SAAS,OAAO,cAAc;AAAA,YAC9B,aAAa,WAAW,cAAc;AAAA;AAAA,QACxC;AAAA,QACA,EAAE,UAAU,KAAK;AAAA,MACnB;AAAA,IACF,CAAC,EACA,MAAM,MAAM;AACX,sBAAgB;AAAA,IAClB,CAAC;AAAA,EACL;AACF;","names":[]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
var use_rewards_celebrations_exports = {};
|
|
21
|
+
__export(use_rewards_celebrations_exports, {
|
|
22
|
+
useRewardsCelebrations: () => useRewardsCelebrations
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(use_rewards_celebrations_exports);
|
|
25
|
+
var import_react = require("react");
|
|
26
|
+
const EMPTY = { leveledUpTo: null, newBadgeKeys: [] };
|
|
27
|
+
function storageKey(address) {
|
|
28
|
+
return `ml:rewards-seen:${address.toLowerCase()}`;
|
|
29
|
+
}
|
|
30
|
+
function readSeen(address) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = localStorage.getItem(storageKey(address));
|
|
33
|
+
return raw ? JSON.parse(raw) : null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function writeSeen(address, state) {
|
|
39
|
+
try {
|
|
40
|
+
localStorage.setItem(storageKey(address), JSON.stringify(state));
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function useRewardsCelebrations(address, rewards) {
|
|
45
|
+
const [state, setState] = (0, import_react.useState)(EMPTY);
|
|
46
|
+
const lastAddress = (0, import_react.useRef)(null);
|
|
47
|
+
(0, import_react.useEffect)(() => {
|
|
48
|
+
if (!address || !rewards) return;
|
|
49
|
+
const seen = readSeen(address);
|
|
50
|
+
const currentBadgeKeys = rewards.badges.map((b) => b.key);
|
|
51
|
+
if (!seen || lastAddress.current !== address) {
|
|
52
|
+
writeSeen(address, { level: rewards.currentLevel, badgeKeys: currentBadgeKeys });
|
|
53
|
+
lastAddress.current = address;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const leveledUpTo = rewards.currentLevel > seen.level ? rewards.currentLevel : null;
|
|
57
|
+
const newBadgeKeys = currentBadgeKeys.filter((k) => !seen.badgeKeys.includes(k));
|
|
58
|
+
if (leveledUpTo !== null || newBadgeKeys.length > 0) {
|
|
59
|
+
writeSeen(address, { level: rewards.currentLevel, badgeKeys: currentBadgeKeys });
|
|
60
|
+
setState({ leveledUpTo, newBadgeKeys });
|
|
61
|
+
}
|
|
62
|
+
}, [address, rewards]);
|
|
63
|
+
const dismiss = () => setState(EMPTY);
|
|
64
|
+
return { ...state, dismiss };
|
|
65
|
+
}
|
|
66
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
67
|
+
0 && (module.exports = {
|
|
68
|
+
useRewardsCelebrations
|
|
69
|
+
});
|
|
70
|
+
//# sourceMappingURL=use-rewards-celebrations.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/rewards/use-rewards-celebrations.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\ninterface RewardsSnapshot {\n currentLevel: number;\n badges: { key: string }[];\n}\n\ninterface CelebrationState {\n leveledUpTo: number | null;\n newBadgeKeys: string[];\n}\n\nconst EMPTY: CelebrationState = { leveledUpTo: null, newBadgeKeys: [] };\n\nfunction storageKey(address: string) {\n return `ml:rewards-seen:${address.toLowerCase()}`;\n}\n\ninterface SeenState {\n level: number;\n badgeKeys: string[];\n}\n\nfunction readSeen(address: string): SeenState | null {\n try {\n const raw = localStorage.getItem(storageKey(address));\n return raw ? (JSON.parse(raw) as SeenState) : null;\n } catch {\n return null;\n }\n}\n\nfunction writeSeen(address: string, state: SeenState) {\n try {\n localStorage.setItem(storageKey(address), JSON.stringify(state));\n } catch {\n // localStorage unavailable (private mode, quota) — celebrations just won't fire again this session\n }\n}\n\n/**\n * Diffs the current rewards snapshot against the last-seen state cached in\n * localStorage (per address) and reports what's new since last time. A\n * first-ever snapshot for an address seeds the cache silently — no\n * celebration on first sight, only on a detected increase/addition.\n * Call `dismiss()` to clear the returned state once the caller has shown it\n * (the storage baseline is written the moment new state is detected, not on\n * dismiss, so a level-up and a badge unlock arriving together can each\n * dismiss on their own timeline without racing the other's write).\n */\nexport function useRewardsCelebrations(\n address: string | null | undefined,\n rewards: RewardsSnapshot | null | undefined\n): CelebrationState & { dismiss: () => void } {\n const [state, setState] = useState<CelebrationState>(EMPTY);\n const lastAddress = useRef<string | null>(null);\n\n useEffect(() => {\n if (!address || !rewards) return;\n\n const seen = readSeen(address);\n const currentBadgeKeys = rewards.badges.map((b) => b.key);\n\n if (!seen || lastAddress.current !== address) {\n // First time we've ever seen this address (or a wallet switch) — seed silently.\n writeSeen(address, { level: rewards.currentLevel, badgeKeys: currentBadgeKeys });\n lastAddress.current = address;\n return;\n }\n\n const leveledUpTo = rewards.currentLevel > seen.level ? rewards.currentLevel : null;\n const newBadgeKeys = currentBadgeKeys.filter((k) => !seen.badgeKeys.includes(k));\n\n if (leveledUpTo !== null || newBadgeKeys.length > 0) {\n // Persist the new baseline immediately — a level-up and a badge unlock\n // can arrive in the same snapshot but dismiss independently (the\n // celebration overlay dismisses itself on a timer/tap; the badge\n // toasts fire-and-forget). Gating the write on either UI action\n // dismissing would let the other clear this state first and skip the\n // write. A page refresh mid-celebration correctly won't re-fire either.\n writeSeen(address, { level: rewards.currentLevel, badgeKeys: currentBadgeKeys });\n setState({ leveledUpTo, newBadgeKeys });\n }\n }, [address, rewards]);\n\n // Purely local UI state — the storage baseline is already written above,\n // the moment new state was detected. This only clears what's on screen.\n const dismiss = () => setState(EMPTY);\n\n return { ...state, dismiss };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,mBAA4C;AAY5C,MAAM,QAA0B,EAAE,aAAa,MAAM,cAAc,CAAC,EAAE;AAEtE,SAAS,WAAW,SAAiB;AACnC,SAAO,mBAAmB,QAAQ,YAAY,CAAC;AACjD;AAOA,SAAS,SAAS,SAAmC;AACnD,MAAI;AACF,UAAM,MAAM,aAAa,QAAQ,WAAW,OAAO,CAAC;AACpD,WAAO,MAAO,KAAK,MAAM,GAAG,IAAkB;AAAA,EAChD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,UAAU,SAAiB,OAAkB;AACpD,MAAI;AACF,iBAAa,QAAQ,WAAW,OAAO,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,EACjE,QAAQ;AAAA,EAER;AACF;AAYO,SAAS,uBACd,SACA,SAC4C;AAC5C,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAA2B,KAAK;AAC1D,QAAM,kBAAc,qBAAsB,IAAI;AAE9C,8BAAU,MAAM;AACd,QAAI,CAAC,WAAW,CAAC,QAAS;AAE1B,UAAM,OAAO,SAAS,OAAO;AAC7B,UAAM,mBAAmB,QAAQ,OAAO,IAAI,CAAC,MAAM,EAAE,GAAG;AAExD,QAAI,CAAC,QAAQ,YAAY,YAAY,SAAS;AAE5C,gBAAU,SAAS,EAAE,OAAO,QAAQ,cAAc,WAAW,iBAAiB,CAAC;AAC/E,kBAAY,UAAU;AACtB;AAAA,IACF;AAEA,UAAM,cAAc,QAAQ,eAAe,KAAK,QAAQ,QAAQ,eAAe;AAC/E,UAAM,eAAe,iBAAiB,OAAO,CAAC,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,CAAC;AAE/E,QAAI,gBAAgB,QAAQ,aAAa,SAAS,GAAG;AAOnD,gBAAU,SAAS,EAAE,OAAO,QAAQ,cAAc,WAAW,iBAAiB,CAAC;AAC/E,eAAS,EAAE,aAAa,aAAa,CAAC;AAAA,IACxC;AAAA,EACF,GAAG,CAAC,SAAS,OAAO,CAAC;AAIrB,QAAM,UAAU,MAAM,SAAS,KAAK;AAEpC,SAAO,EAAE,GAAG,OAAO,QAAQ;AAC7B;","names":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface RewardsSnapshot {
|
|
2
|
+
currentLevel: number;
|
|
3
|
+
badges: {
|
|
4
|
+
key: string;
|
|
5
|
+
}[];
|
|
6
|
+
}
|
|
7
|
+
interface CelebrationState {
|
|
8
|
+
leveledUpTo: number | null;
|
|
9
|
+
newBadgeKeys: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Diffs the current rewards snapshot against the last-seen state cached in
|
|
13
|
+
* localStorage (per address) and reports what's new since last time. A
|
|
14
|
+
* first-ever snapshot for an address seeds the cache silently — no
|
|
15
|
+
* celebration on first sight, only on a detected increase/addition.
|
|
16
|
+
* Call `dismiss()` to clear the returned state once the caller has shown it
|
|
17
|
+
* (the storage baseline is written the moment new state is detected, not on
|
|
18
|
+
* dismiss, so a level-up and a badge unlock arriving together can each
|
|
19
|
+
* dismiss on their own timeline without racing the other's write).
|
|
20
|
+
*/
|
|
21
|
+
declare function useRewardsCelebrations(address: string | null | undefined, rewards: RewardsSnapshot | null | undefined): CelebrationState & {
|
|
22
|
+
dismiss: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export { useRewardsCelebrations };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
interface RewardsSnapshot {
|
|
2
|
+
currentLevel: number;
|
|
3
|
+
badges: {
|
|
4
|
+
key: string;
|
|
5
|
+
}[];
|
|
6
|
+
}
|
|
7
|
+
interface CelebrationState {
|
|
8
|
+
leveledUpTo: number | null;
|
|
9
|
+
newBadgeKeys: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Diffs the current rewards snapshot against the last-seen state cached in
|
|
13
|
+
* localStorage (per address) and reports what's new since last time. A
|
|
14
|
+
* first-ever snapshot for an address seeds the cache silently — no
|
|
15
|
+
* celebration on first sight, only on a detected increase/addition.
|
|
16
|
+
* Call `dismiss()` to clear the returned state once the caller has shown it
|
|
17
|
+
* (the storage baseline is written the moment new state is detected, not on
|
|
18
|
+
* dismiss, so a level-up and a badge unlock arriving together can each
|
|
19
|
+
* dismiss on their own timeline without racing the other's write).
|
|
20
|
+
*/
|
|
21
|
+
declare function useRewardsCelebrations(address: string | null | undefined, rewards: RewardsSnapshot | null | undefined): CelebrationState & {
|
|
22
|
+
dismiss: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export { useRewardsCelebrations };
|