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,6 +291,7 @@ function BannerPreview() {
</button>
</span>
</div>
{showBranding && (
<div
style={{
textAlign: "center",
@@ -305,6 +322,7 @@ function BannerPreview() {
{" "}
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,
}