Expose showBranding as read-only GraphQL field and reflect it in theme preview

Uses the Loader + Page + Fragment pattern so the snippet page fetches
its own data instead of relying on Outlet context.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-22 14:47:36 +04:00
parent df698b5b21
commit b1607d76c5
6 changed files with 123 additions and 36 deletions

View File

@@ -15,12 +15,36 @@
import { useTranslate } from "@probo/i18n";
import { Card } from "@probo/ui";
import type { ReactNode } from "react";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { graphql } from "relay-runtime";
import type { CookieBannerSnippetPageQuery } from "#/__generated__/core/CookieBannerSnippetPageQuery.graphql";
import { CodeSnippets } from "./_components/CodeSnippets";
import { ThemePreview } from "./_components/ThemePreview";
export default function CookieBannerSnippetPage() {
export const cookieBannerSnippetPageQuery = graphql`
query CookieBannerSnippetPageQuery($cookieBannerId: ID!) {
node(id: $cookieBannerId) {
__typename
... on CookieBanner {
...ThemePreview_cookieBanner
}
}
}
`;
interface CookieBannerSnippetPageProps {
queryRef: PreloadedQuery<CookieBannerSnippetPageQuery>;
}
export default function CookieBannerSnippetPage({ queryRef }: CookieBannerSnippetPageProps) {
const { __ } = useTranslate();
const data = usePreloadedQuery(cookieBannerSnippetPageQuery, queryRef);
if (data.node.__typename !== "CookieBanner") {
throw new Error("invalid type for node");
}
return (
<div className="space-y-10">
@@ -37,7 +61,7 @@ export default function CookieBannerSnippetPage() {
title={__("Customize the theme")}
description={__("Adjust colors, fonts, and spacing to match your brand. The generated CSS snippet can be added to your website to override the default banner styles.")}
>
<ThemePreview />
<ThemePreview cookieBannerKey={data.node} />
</Step>
<Step

View File

@@ -0,0 +1,43 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { Suspense, useEffect } from "react";
import { useQueryLoader } from "react-relay";
import { useParams } from "react-router";
import type { CookieBannerSnippetPageQuery } from "#/__generated__/core/CookieBannerSnippetPageQuery.graphql";
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
import CookieBannerSnippetPage, { cookieBannerSnippetPageQuery } from "./CookieBannerSnippetPage";
export default function CookieBannerSnippetPageLoader() {
const { cookieBannerId } = useParams<{ cookieBannerId: string }>();
const [queryRef, loadQuery] = useQueryLoader<CookieBannerSnippetPageQuery>(cookieBannerSnippetPageQuery);
useEffect(() => {
if (cookieBannerId) {
loadQuery({ cookieBannerId });
}
}, [loadQuery, cookieBannerId]);
if (!queryRef) {
return <PageSkeleton />;
}
return (
<Suspense fallback={<PageSkeleton />}>
<CookieBannerSnippetPage queryRef={queryRef} />
</Suspense>
);
}

View File

@@ -15,6 +15,10 @@
import { useTranslate } from "@probo/i18n";
import { Button, Card, Field, Input, useToast } from "@probo/ui";
import { useCallback, useMemo, useState } from "react";
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
import type { ThemePreview_cookieBanner$key } from "#/__generated__/core/ThemePreview_cookieBanner.graphql";
type CSSVariable = {
key: string;
@@ -49,7 +53,19 @@ function buildCSSSnippet(values: Record<string, string>): string {
return `probo-cookie-banner {\n${overrides.join("\n")}\n}`;
}
export function ThemePreview() {
export const themePreviewFragment = graphql`
fragment ThemePreview_cookieBanner on CookieBanner {
showBranding
}
`;
interface ThemePreviewProps {
cookieBannerKey: ThemePreview_cookieBanner$key;
}
export function ThemePreview({ cookieBannerKey }: ThemePreviewProps) {
const cookieBanner = useFragment(themePreviewFragment, cookieBannerKey);
const { showBranding } = cookieBanner;
const { __ } = useTranslate();
const { toast } = useToast();
@@ -148,7 +164,7 @@ export function ThemePreview() {
className="relative flex items-end justify-center bg-[repeating-conic-gradient(#e5e7eb_0%_25%,transparent_0%_50%)] bg-size-[20px_20px] p-8"
style={{ minHeight: 280, ...previewStyle }}
>
<BannerPreview />
<BannerPreview showBranding={showBranding} />
</div>
</Card>
@@ -168,7 +184,7 @@ export function ThemePreview() {
);
}
function BannerPreview() {
function BannerPreview({ showBranding }: { showBranding: boolean }) {
return (
<div
style={{
@@ -275,36 +291,38 @@ function BannerPreview() {
</button>
</span>
</div>
<div
style={{
textAlign: "center",
fontSize: "calc(var(--probo-font-size, 14px) - 2px)",
fontWeight: 400,
color: "var(--probo-text-secondary, #555555)",
}}
>
Privacy by
{" "}
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 25 24" fill="none" aria-hidden="true" style={{ verticalAlign: "-2px", display: "inline" }}>
<g clipPath="url(#pc-preview)">
<rect width="24" height="24" x=".85" fill="currentColor" rx="5.14" />
<path stroke="#fff" strokeWidth="3.53" d="M20.57 6.17c2.96 4.47-6.43 13.97-20.32 5.66" />
<path stroke="url(#pg-preview)" strokeWidth="3.53" d="M20.57 6.17c2.96 4.47-6.43 13.97-20.32 5.66" />
<path fill="#fff" d="M19.07 7.1a1.77 1.77 0 0 0 3-1.86l-1.5.93-1.5.94ZM8.82 25.18l1.71-.41c-2.3-9.67.01-14.66 2.54-16.83A6.25 6.25 0 0 1 17 6.33c1.22-.01 1.87.44 2.08.78l1.5-.94 1.5-.93c-1.1-1.74-3.16-2.46-5.13-2.44-2.03.03-4.26.81-6.17 2.45-3.9 3.35-6.16 9.92-3.67 20.33l1.72-.41Z" />
</g>
<defs>
<linearGradient id="pg-preview" x1="18.77" x2="1.2" y1="13.56" y2="13.56" gradientUnits="userSpaceOnUse">
<stop stopColor="#101E1C" stopOpacity="0" />
<stop offset="1" stopColor="#fff" />
</linearGradient>
<clipPath id="pc-preview">
<rect width="24" height="24" x=".85" fill="#fff" rx="5.14" />
</clipPath>
</defs>
</svg>
{" "}
Probo
</div>
{showBranding && (
<div
style={{
textAlign: "center",
fontSize: "calc(var(--probo-font-size, 14px) - 2px)",
fontWeight: 400,
color: "var(--probo-text-secondary, #555555)",
}}
>
Privacy by
{" "}
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 25 24" fill="none" aria-hidden="true" style={{ verticalAlign: "-2px", display: "inline" }}>
<g clipPath="url(#pc-preview)">
<rect width="24" height="24" x=".85" fill="currentColor" rx="5.14" />
<path stroke="#fff" strokeWidth="3.53" d="M20.57 6.17c2.96 4.47-6.43 13.97-20.32 5.66" />
<path stroke="url(#pg-preview)" strokeWidth="3.53" d="M20.57 6.17c2.96 4.47-6.43 13.97-20.32 5.66" />
<path fill="#fff" d="M19.07 7.1a1.77 1.77 0 0 0 3-1.86l-1.5.93-1.5.94ZM8.82 25.18l1.71-.41c-2.3-9.67.01-14.66 2.54-16.83A6.25 6.25 0 0 1 17 6.33c1.22-.01 1.87.44 2.08.78l1.5-.94 1.5-.93c-1.1-1.74-3.16-2.46-5.13-2.44-2.03.03-4.26.81-6.17 2.45-3.9 3.35-6.16 9.92-3.67 20.33l1.72-.41Z" />
</g>
<defs>
<linearGradient id="pg-preview" x1="18.77" x2="1.2" y1="13.56" y2="13.56" gradientUnits="userSpaceOnUse">
<stop stopColor="#101E1C" stopOpacity="0" />
<stop offset="1" stopColor="#fff" />
</linearGradient>
<clipPath id="pc-preview">
<rect width="24" height="24" x=".85" fill="#fff" rx="5.14" />
</clipPath>
</defs>
</svg>
{" "}
Probo
</div>
)}
</div>
);
}

View File

@@ -57,7 +57,7 @@ export const cookieBannerRoutes = [
{
path: "snippet",
Fallback: LinkCardSkeleton,
Component: lazy(() => import("#/pages/organizations/cookie-banners/configuration/snippet/CookieBannerSnippetPage")),
Component: lazy(() => import("#/pages/organizations/cookie-banners/configuration/snippet/CookieBannerSnippetPageLoader")),
},
],
},

View File

@@ -84,6 +84,7 @@ type CookieBanner implements Node {
privacyPolicyUrl: String!
consentExpiryDays: Int!
consentMode: CookieConsentMode!
showBranding: Boolean!
organization: Organization @goField(forceResolver: true)

View File

@@ -72,6 +72,7 @@ func NewCookieBanner(b *coredata.CookieBanner) *CookieBanner {
PrivacyPolicyURL: b.PrivacyPolicyURL,
ConsentExpiryDays: b.ConsentExpiryDays,
ConsentMode: b.ConsentMode,
ShowBranding: b.ShowBranding,
CreatedAt: b.CreatedAt,
UpdatedAt: b.UpdatedAt,
}