diff --git a/PluginBuilder/Controllers/HomeController.cs b/PluginBuilder/Controllers/HomeController.cs index 994a0f27..380ce5ab 100644 --- a/PluginBuilder/Controllers/HomeController.cs +++ b/PluginBuilder/Controllers/HomeController.cs @@ -509,7 +509,8 @@ ORDER BY " + orderBy + @" Contributors = pluginContributors, RatingFilter = model.RatingFilter, OwnerGithubUrl = ownerGithubUrl, - OwnerNostrUrl = ownerNostrUrl + OwnerNostrUrl = ownerNostrUrl, + EmbedMode = string.Equals(Request.Query["embed"], "1", StringComparison.Ordinal) }; return View(vm); } @@ -518,7 +519,7 @@ ORDER BY " + orderBy + @" [ValidateAntiForgeryToken] public async Task UpsertReview( [ModelBinder(typeof(PluginSlugModelBinder))] - PluginSlug pluginSlug, int rating, string? body, string? pluginVersion) + PluginSlug pluginSlug, int rating, string? body, string? pluginVersion, string? embed = null) { if (rating is < 1 or > 5) return BadRequest("Invalid rating"); @@ -533,7 +534,7 @@ public async Task UpsertReview( if (isOwner) { TempData[TempDataConstant.WarningMessage] = "You cannot review your own plugin."; - return RedirectToAction(nameof(GetPluginDetails), "Home", new { pluginSlug = pluginSlug.ToString() }); + return RedirectToAction(nameof(GetPluginDetails), "Home", new { pluginSlug = pluginSlug.ToString(), embed = string.IsNullOrEmpty(embed) ? null : embed }); } var reviewerAccountDetails = await conn.GetAccountDetailSettings(userId) ?? new AccountSettings(); @@ -558,7 +559,12 @@ public async Task UpsertReview( await conn.UpsertPluginReview(reviewViewModel); var sort = Request.Query["sort"].ToString(); - var url = Url.Action(nameof(GetPluginDetails), "Home", new { pluginSlug = pluginSlug.ToString(), sort = string.IsNullOrEmpty(sort) ? null : sort }); + var url = Url.Action(nameof(GetPluginDetails), "Home", new + { + pluginSlug = pluginSlug.ToString(), + sort = string.IsNullOrEmpty(sort) ? null : sort, + embed = string.IsNullOrEmpty(embed) ? null : embed + }); return Redirect((url ?? "/") + "#reviews"); } @@ -618,7 +624,8 @@ public async Task VoteReview( [ModelBinder(typeof(PluginSlugModelBinder))] PluginSlug pluginSlug, long id, - bool isHelpful) + bool isHelpful, + string? embed = null) { var userId = userManager.GetUserId(User); if (string.IsNullOrEmpty(userId)) @@ -635,7 +642,11 @@ public async Task VoteReview( if (!ok) TempData[TempDataConstant.WarningMessage] = "Error while updating review helpful vote"; - var url = Url.Action(nameof(GetPluginDetails), new { pluginSlug = pluginSlug.ToString() }); + var url = Url.Action(nameof(GetPluginDetails), new + { + pluginSlug = pluginSlug.ToString(), + embed = string.IsNullOrEmpty(embed) ? null : embed + }); return Redirect((url ?? "/") + "#reviews"); } @@ -644,7 +655,8 @@ public async Task VoteReview( public async Task DeleteReview( [ModelBinder(typeof(PluginSlugModelBinder))] PluginSlug pluginSlug, - long id) + long id, + string? embed = null) { var userId = userManager.GetUserId(User); if (string.IsNullOrEmpty(userId)) @@ -659,7 +671,11 @@ public async Task DeleteReview( if (!ok) TempData[TempDataConstant.WarningMessage] = "Error while deleting review"; - var url = Url.Action(nameof(GetPluginDetails), new { pluginSlug = pluginSlug.ToString() }); + var url = Url.Action(nameof(GetPluginDetails), new + { + pluginSlug = pluginSlug.ToString(), + embed = string.IsNullOrEmpty(embed) ? null : embed + }); return Redirect((url ?? "/") + "#reviews"); } diff --git a/PluginBuilder/ViewModels/Home/PluginDetailsViewModel.cs b/PluginBuilder/ViewModels/Home/PluginDetailsViewModel.cs index 654c47c9..a0b1e0f9 100644 --- a/PluginBuilder/ViewModels/Home/PluginDetailsViewModel.cs +++ b/PluginBuilder/ViewModels/Home/PluginDetailsViewModel.cs @@ -24,6 +24,7 @@ public PluginDetailsViewModel() public string? OwnerGithubUrl { get; set; } public string? OwnerNostrUrl { get; set; } + public bool EmbedMode { get; set; } } public class Review diff --git a/PluginBuilder/Views/Home/AllPlugins.cshtml b/PluginBuilder/Views/Home/AllPlugins.cshtml index dffb8ed6..abe41e96 100644 --- a/PluginBuilder/Views/Home/AllPlugins.cshtml +++ b/PluginBuilder/Views/Home/AllPlugins.cshtml @@ -6,6 +6,8 @@ const string title = "BTCPay Server Plugin Directory"; const string desc = "Extend and customize your BTCPay Server with community and official plugins."; ViewData["Title"] = title; + var embedMode = Context.Request.Query["embed"] == "1"; + var embedRoute = embedMode ? "1" : null; var currentSearch = Context.Request.Query["searchPluginName"].ToString(); var currentSort = (string?)Context.Request.Query["sort"] ?? "smart"; @@ -35,17 +37,24 @@ -
-
-
-

BTCPay Server Plugin Directory

-

Extend and customize your BTCPay Server with community and official plugins.

+
+ @if (!embedMode) + { +
+
+

BTCPay Server Plugin Directory

+

Extend and customize your BTCPay Server with community and official plugins.

+
-
-
-
+ } +
+
+ @if (embedMode) + { + + } Relevance Highest rated Most recent Alphabetical @@ -92,14 +105,16 @@
-
+
+
@if (Model != null && Model.Any()) { @foreach (var plugin in Model) { var owner = plugin.GetOwnerName(GitHostingProviderFactory); var ownerProfileUrl = plugin.GetOwnerProfileUrl(GitHostingProviderFactory); -
+ var pluginIdentifier = plugin.ManifestInfo?["Identifier"]?.ToString(); +
@if (plugin.IsUnlisted) @@ -116,6 +131,9 @@

@plugin.PluginTitle

@@ -159,10 +177,21 @@ {

No plugins available yet

-

Check back later, or - login to upload one -

+ @if (!embedMode) + { +

Check back later, or + login to upload one +

+ } +
+ } + + @if (embedMode && Model != null && Model.Any()) + { + } +
diff --git a/PluginBuilder/Views/Home/GetPluginDetails.cshtml b/PluginBuilder/Views/Home/GetPluginDetails.cshtml index 1da07abc..9f8945fe 100644 --- a/PluginBuilder/Views/Home/GetPluginDetails.cshtml +++ b/PluginBuilder/Views/Home/GetPluginDetails.cshtml @@ -7,11 +7,17 @@ Layout = "_LayoutPublicModal"; var desc = string.IsNullOrWhiteSpace(Model.Plugin.Description) ? "Plugin for BTCPay Server" : Model.Plugin.Description; ViewData["Title"] = Model.Plugin.PluginTitle + " - " + desc; + var embedRoute = Model.EmbedMode ? "1" : null; + var pluginIdentifier = Model.Plugin.ManifestInfo?["Identifier"]?.ToString(); var owner = Model.Plugin.GetGithubRepository()?.Owner; var dependencies = Model.Plugin.ManifestInfo?["Dependencies"] as JArray; var sourceUrl = Model.Plugin.GetGithubRepository()?.GetSourceUrl(Model.Plugin.BuildInfo?["gitCommit"]?.ToString(), Model.Plugin.BuildInfo?["pluginDir"]?.ToString()); var pluginUrl = Url.Action(nameof(HomeController.GetPluginDetails), "Home", new { pluginSlug = Model.Plugin.ProjectSlug }, Context.Request.Scheme, Context.Request.Host.ToString()); + var writeReviewUrl = Model.EmbedMode ? $"{pluginUrl}#write-review" : "#write-review"; + var writeReviewTarget = Model.EmbedMode ? "_blank" : null; + var writeReviewRel = Model.EmbedMode ? "noopener noreferrer" : null; var currentRating = Model.RatingFilter; + var containerClass = Model.EmbedMode ? "container-fluid px-0" : "container"; DateTimeOffset.TryParse(Model.Plugin.BuildInfo?["buildDate"]?.ToString(), out var buildDate); var videoEmbedUrl = Model.Plugin.VideoUrl.GetVideoEmbedUrl(); @@ -52,7 +58,10 @@
} -
+
@@ -168,7 +177,7 @@
@@ -180,6 +189,7 @@ @@ -223,7 +235,7 @@ @if (!Model.Reviews.Any()) {
- No reviews yet. Be the first to write one. + No reviews yet. Be the first to write one.
} else @@ -278,13 +290,13 @@ } else { -
+ @Html.AntiForgeryToken()
-
+ @Html.AntiForgeryToken()
@@ -440,11 +455,14 @@
- Download + @if (!Model.EmbedMode) + { + Download + }
diff --git a/PluginBuilder/Views/Shared/_LayoutPublicModal.cshtml b/PluginBuilder/Views/Shared/_LayoutPublicModal.cshtml index c92341b0..fdaca09a 100644 --- a/PluginBuilder/Views/Shared/_LayoutPublicModal.cshtml +++ b/PluginBuilder/Views/Shared/_LayoutPublicModal.cshtml @@ -1,5 +1,6 @@ @{ Layout = null; + var embedMode = string.Equals(Context.Request.Query["embed"], "1", StringComparison.Ordinal); } @@ -24,14 +25,23 @@ .account-form h4 { margin-bottom: 1.5rem; } + + body.embed-mode { + background: transparent; + overflow-x: hidden; + } - +
@RenderBody()
+@if (embedMode) +{ + +} diff --git a/PluginBuilder/Views/Shared/_PublicHeader.cshtml b/PluginBuilder/Views/Shared/_PublicHeader.cshtml index edf8b1d2..8ba5f732 100644 --- a/PluginBuilder/Views/Shared/_PublicHeader.cshtml +++ b/PluginBuilder/Views/Shared/_PublicHeader.cshtml @@ -1,39 +1,43 @@ @{ var isAuth = User.Identity?.IsAuthenticated == true; + var embedMode = string.Equals(Context.Request.Query["embed"], "1", StringComparison.Ordinal); } -
-
- +@if (!embedMode) +{ +
+
+ -
-
-
+} diff --git a/PluginBuilder/wwwroot/js/public-embed.js b/PluginBuilder/wwwroot/js/public-embed.js new file mode 100644 index 00000000..d6eaf1e4 --- /dev/null +++ b/PluginBuilder/wwwroot/js/public-embed.js @@ -0,0 +1,185 @@ +(function () { + const embedPage = document.querySelector("[data-embed-page]"); + if (!embedPage || window.parent === window) { + return; + } + + let lastHeight = 0; + let heightPostQueued = false; + let hiddenPluginIdentifiers = new Set(); + + function applyHostColorMode(colorMode) { + if (colorMode !== "light" && colorMode !== "dark") { + return; + } + + document.documentElement.setAttribute("data-btcpay-theme", colorMode); + + const darkThemeLink = document.getElementById("DarkThemeLinkTag"); + if (darkThemeLink) { + darkThemeLink.disabled = colorMode !== "dark"; + } + + scheduleHeightPost(); + } + + function postReady() { + window.parent.postMessage({ type: "pb:ready" }, "*"); + } + + function getContentHeight() { + const rectHeight = embedPage.getBoundingClientRect ? embedPage.getBoundingClientRect().height : 0; + return Math.max( + embedPage.scrollHeight || 0, + embedPage.offsetHeight || 0, + Math.ceil(rectHeight) + ); + } + + function postHeight() { + heightPostQueued = false; + const height = Math.ceil(getContentHeight()) + 4; + if (!height || Math.abs(height - lastHeight) < 2) { + return; + } + + lastHeight = height; + window.parent.postMessage({ + type: "pb:content-height", + height: height + }, "*"); + } + + function scheduleHeightPost() { + if (heightPostQueued) { + return; + } + + heightPostQueued = true; + window.requestAnimationFrame(postHeight); + } + + function normalizeIdentifier(identifier) { + return typeof identifier === "string" ? identifier.trim().toLowerCase() : ""; + } + + function normalizeHiddenPluginIdentifiers(value) { + const identifiers = new Set(); + if (!Array.isArray(value)) { + return identifiers; + } + + value.forEach(function (identifier) { + const normalizedIdentifier = normalizeIdentifier(identifier); + if (normalizedIdentifier) { + identifiers.add(normalizedIdentifier); + } + }); + + return identifiers; + } + + function applyHiddenPluginFilter() { + if (embedPage.dataset.embedPage !== "list") { + return; + } + + let visibleCount = 0; + document.querySelectorAll("[data-plugin-card]").forEach(function (card) { + const identifier = normalizeIdentifier(card.dataset.pluginIdentifier); + const shouldHide = identifier && hiddenPluginIdentifiers.has(identifier); + card.hidden = shouldHide; + if (!shouldHide) { + visibleCount += 1; + } + }); + + const emptyState = document.querySelector("[data-plugin-directory-empty-state]"); + if (emptyState) { + emptyState.hidden = visibleCount !== 0; + } + + scheduleHeightPost(); + } + + function postSelection(slug, identifier) { + if (!slug) { + return; + } + + window.parent.postMessage({ + type: "pb:plugin-selected", + slug: slug, + identifier: identifier || null + }, "*"); + } + + function buildDetailsUrl(slug) { + const url = new URL("/public/plugins/" + encodeURIComponent(slug), window.location.origin); + url.searchParams.set("embed", "1"); + return url.toString(); + } + + function handleHostContext(event) { + if (event.source !== window.parent) { + return; + } + + const data = event.data; + if (!data || typeof data !== "object" || data.type !== "btcpay:host-context") { + return; + } + + hiddenPluginIdentifiers = normalizeHiddenPluginIdentifiers(data.hiddenPluginIdentifiers); + applyHostColorMode(data.colorMode); + applyHiddenPluginFilter(); + + const currentSlug = embedPage.dataset.pluginSlug || ""; + const selectedSlug = typeof data.selectedSlug === "string" ? data.selectedSlug : ""; + + if (embedPage.dataset.embedPage === "list") { + return; + } + + if (!selectedSlug || selectedSlug === currentSlug) { + return; + } + + window.location.replace(buildDetailsUrl(selectedSlug)); + } + + postReady(); + scheduleHeightPost(); + + if (embedPage.dataset.embedPage === "details") { + postSelection(embedPage.dataset.pluginSlug || "", embedPage.dataset.pluginIdentifier || ""); + } + + document.querySelectorAll("img").forEach(function (image) { + image.addEventListener("load", scheduleHeightPost); + image.addEventListener("error", scheduleHeightPost); + }); + + document.querySelectorAll("a[data-plugin-slug]").forEach(function (link) { + link.addEventListener("click", function (event) { + if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) { + return; + } + + event.preventDefault(); + postSelection(link.dataset.pluginSlug || "", link.dataset.pluginIdentifier || ""); + }); + }); + + window.addEventListener("message", handleHostContext); + window.addEventListener("resize", scheduleHeightPost); + + if (window.ResizeObserver) { + const resizeObserver = new window.ResizeObserver(scheduleHeightPost); + resizeObserver.observe(embedPage); + } + + if (document.fonts && document.fonts.ready) { + document.fonts.ready.then(scheduleHeightPost); + } +})();