Add cookies page + refactor relay tree
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
|||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
Button,
|
Button,
|
||||||
IconImage,
|
IconImage,
|
||||||
|
IconListStack,
|
||||||
IconPageTextLine,
|
IconPageTextLine,
|
||||||
IconSettingsGear2,
|
IconSettingsGear2,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
@@ -188,6 +189,10 @@ export default function CookieBannerConfigLayout({ queryRef }: CookieBannerConfi
|
|||||||
<IconSettingsGear2 size={20} />
|
<IconSettingsGear2 size={20} />
|
||||||
{__("Settings")}
|
{__("Settings")}
|
||||||
</TabLink>
|
</TabLink>
|
||||||
|
<TabLink to={`/organizations/${organizationId}/cookie-banners/${cookieBannerId}/cookies`}>
|
||||||
|
<IconListStack size={20} />
|
||||||
|
{__("Cookies")}
|
||||||
|
</TabLink>
|
||||||
<TabLink to={`/organizations/${organizationId}/cookie-banners/${cookieBannerId}/snippet`}>
|
<TabLink to={`/organizations/${organizationId}/cookie-banners/${cookieBannerId}/snippet`}>
|
||||||
<IconPageTextLine size={20} />
|
<IconPageTextLine size={20} />
|
||||||
{__("Snippet")}
|
{__("Snippet")}
|
||||||
|
|||||||
@@ -16,11 +16,23 @@ import { formatError, type GraphQLError } from "@probo/helpers";
|
|||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { Button, Card, Field, Input, Label, Option, Select, useToast } from "@probo/ui";
|
import { Button, Card, Field, Input, Label, Option, Select, useToast } from "@probo/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation } from "react-relay";
|
import { useFragment, useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
|
import type { BannerSettingsForm_cookieBanner$key } from "#/__generated__/core/BannerSettingsForm_cookieBanner.graphql";
|
||||||
import type { BannerSettingsFormMutation } from "#/__generated__/core/BannerSettingsFormMutation.graphql";
|
import type { BannerSettingsFormMutation } from "#/__generated__/core/BannerSettingsFormMutation.graphql";
|
||||||
|
|
||||||
|
const bannerSettingsFormFragment = graphql`
|
||||||
|
fragment BannerSettingsForm_cookieBanner on CookieBanner {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
origin
|
||||||
|
privacyPolicyUrl
|
||||||
|
consentExpiryDays
|
||||||
|
consentMode
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const updateBannerMutation = graphql`
|
const updateBannerMutation = graphql`
|
||||||
mutation BannerSettingsFormMutation($input: UpdateCookieBannerInput!) {
|
mutation BannerSettingsFormMutation($input: UpdateCookieBannerInput!) {
|
||||||
updateCookieBanner(input: $input) {
|
updateCookieBanner(input: $input) {
|
||||||
@@ -37,20 +49,15 @@ const updateBannerMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
interface BannerSettingsFormProps {
|
interface BannerSettingsFormProps {
|
||||||
banner: {
|
cookieBannerKey: BannerSettingsForm_cookieBanner$key;
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
origin: string;
|
|
||||||
privacyPolicyUrl: string;
|
|
||||||
consentExpiryDays: number;
|
|
||||||
consentMode: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BannerSettingsForm({ banner }: BannerSettingsFormProps) {
|
export function BannerSettingsForm({ cookieBannerKey }: BannerSettingsFormProps) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const banner = useFragment(bannerSettingsFormFragment, cookieBannerKey);
|
||||||
|
|
||||||
const [commitMutation, isInFlight] = useMutation<BannerSettingsFormMutation>(updateBannerMutation);
|
const [commitMutation, isInFlight] = useMutation<BannerSettingsFormMutation>(updateBannerMutation);
|
||||||
|
|
||||||
const [name, setName] = useState(banner.name);
|
const [name, setName] = useState(banner.name);
|
||||||
@@ -70,7 +77,7 @@ export function BannerSettingsForm({ banner }: BannerSettingsFormProps) {
|
|||||||
origin,
|
origin,
|
||||||
privacyPolicyUrl,
|
privacyPolicyUrl,
|
||||||
consentExpiryDays: parseInt(consentExpiryDays, 10),
|
consentExpiryDays: parseInt(consentExpiryDays, 10),
|
||||||
consentMode: consentMode as "OPT_IN" | "OPT_OUT",
|
consentMode: consentMode,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onCompleted() {
|
onCompleted() {
|
||||||
@@ -112,7 +119,7 @@ export function BannerSettingsForm({ banner }: BannerSettingsFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>{__("Consent Mode")}</Label>
|
<Label>{__("Consent Mode")}</Label>
|
||||||
<Select value={consentMode} onValueChange={setConsentMode}>
|
<Select value={consentMode} onValueChange={v => setConsentMode(v as "OPT_IN" | "OPT_OUT")}>
|
||||||
<Option value="OPT_IN">{__("Opt-in")}</Option>
|
<Option value="OPT_IN">{__("Opt-in")}</Option>
|
||||||
<Option value="OPT_OUT">{__("Opt-out")}</Option>
|
<Option value="OPT_OUT">{__("Opt-out")}</Option>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -16,13 +16,11 @@ import { formatError, type GraphQLError } from "@probo/helpers";
|
|||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
Field,
|
Field,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
|
||||||
Textarea,
|
Textarea,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
@@ -31,7 +29,6 @@ import { useMutation } from "react-relay";
|
|||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
import type { CategoryDialogCreateMutation } from "#/__generated__/core/CategoryDialogCreateMutation.graphql";
|
import type { CategoryDialogCreateMutation } from "#/__generated__/core/CategoryDialogCreateMutation.graphql";
|
||||||
import type { CategoryDialogUpdateMutation } from "#/__generated__/core/CategoryDialogUpdateMutation.graphql";
|
|
||||||
|
|
||||||
const createMutation = graphql`
|
const createMutation = graphql`
|
||||||
mutation CategoryDialogCreateMutation(
|
mutation CategoryDialogCreateMutation(
|
||||||
@@ -59,196 +56,71 @@ const createMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const updateMutation = graphql`
|
|
||||||
mutation CategoryDialogUpdateMutation($input: UpdateCookieCategoryInput!) {
|
|
||||||
updateCookieCategory(input: $input) {
|
|
||||||
cookieCategory {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
rank
|
|
||||||
cookies {
|
|
||||||
name
|
|
||||||
duration
|
|
||||||
description
|
|
||||||
}
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface CookieEntry {
|
|
||||||
name: string;
|
|
||||||
duration: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CategoryDialogProps {
|
interface CategoryDialogProps {
|
||||||
cookieBannerId: string;
|
cookieBannerId: string;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
category?: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
required: boolean;
|
|
||||||
rank: number;
|
|
||||||
cookies: ReadonlyArray<CookieEntry>;
|
|
||||||
};
|
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CategoryDialog({
|
export function CategoryDialog({
|
||||||
cookieBannerId,
|
cookieBannerId,
|
||||||
connectionId,
|
connectionId,
|
||||||
category,
|
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: CategoryDialogProps) {
|
}: CategoryDialogProps) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const isEditing = !!category;
|
|
||||||
|
|
||||||
const [commitCreate, isCreating] = useMutation<CategoryDialogCreateMutation>(createMutation);
|
const [commitCreate, isCreating] = useMutation<CategoryDialogCreateMutation>(createMutation);
|
||||||
const [commitUpdate, isUpdating] = useMutation<CategoryDialogUpdateMutation>(updateMutation);
|
|
||||||
|
|
||||||
const [name, setName] = useState(category?.name ?? "");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState(category?.description ?? "");
|
const [description, setDescription] = useState("");
|
||||||
const [required, setRequired] = useState(category?.required ?? false);
|
|
||||||
const [cookies, setCookies] = useState<CookieEntry[]>(
|
|
||||||
category?.cookies ? [...category.cookies.map(c => ({ ...c }))] : [],
|
|
||||||
);
|
|
||||||
|
|
||||||
const addCookie = () => {
|
|
||||||
setCookies([...cookies, { name: "", duration: "", description: "" }]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeCookie = (index: number) => {
|
|
||||||
setCookies(cookies.filter((_, i) => i !== index));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateCookie = (index: number, field: keyof CookieEntry, value: string) => {
|
|
||||||
setCookies(cookies.map((c, i) => (i === index ? { ...c, [field]: value } : c)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const cookieItems = cookies.filter(c => c.name.trim() !== "");
|
commitCreate({
|
||||||
|
variables: {
|
||||||
if (isEditing) {
|
input: {
|
||||||
commitUpdate({
|
cookieBannerId,
|
||||||
variables: {
|
name,
|
||||||
input: {
|
description,
|
||||||
cookieCategoryId: category.id,
|
required: false,
|
||||||
name,
|
rank: 0,
|
||||||
description,
|
|
||||||
cookies: cookieItems,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
onCompleted() {
|
connections: [connectionId],
|
||||||
toast({ title: __("Success"), description: __("Category updated"), variant: "success" });
|
},
|
||||||
onOpenChange(false);
|
onCompleted() {
|
||||||
},
|
toast({ title: __("Success"), description: __("Category created"), variant: "success" });
|
||||||
onError(error) {
|
onOpenChange(false);
|
||||||
toast({ title: __("Error"), description: formatError(__("Failed to update category"), error as GraphQLError), variant: "error" });
|
},
|
||||||
},
|
onError(error) {
|
||||||
});
|
toast({ title: __("Error"), description: formatError(__("Failed to create category"), error as GraphQLError), variant: "error" });
|
||||||
} else {
|
},
|
||||||
commitCreate({
|
});
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
cookieBannerId,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
required,
|
|
||||||
rank: 0,
|
|
||||||
cookies: cookieItems.length > 0 ? cookieItems : null,
|
|
||||||
},
|
|
||||||
connections: [connectionId],
|
|
||||||
},
|
|
||||||
onCompleted() {
|
|
||||||
toast({ title: __("Success"), description: __("Category created"), variant: "success" });
|
|
||||||
onOpenChange(false);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
toast({ title: __("Error"), description: formatError(__("Failed to create category"), error as GraphQLError), variant: "error" });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
defaultOpen
|
defaultOpen
|
||||||
onClose={() => onOpenChange(false)}
|
onClose={() => onOpenChange(false)}
|
||||||
title={isEditing ? __("Edit Category") : __("Add Category")}
|
title={__("Add Category")}
|
||||||
className="max-w-lg"
|
className="max-w-lg"
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<DialogContent padded className="space-y-4">
|
<DialogContent padded className="space-y-4">
|
||||||
<Field label={__("Name")}>
|
<Field label={__("Name")}>
|
||||||
<Input value={name} onChange={(e) => setName(e.target.value)} required />
|
<Input value={name} onChange={e => setName(e.target.value)} required />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label={__("Description")}>
|
<Field label={__("Description")}>
|
||||||
<Textarea value={description} onChange={(e) => setDescription(e.target.value)} required rows={2} />
|
<Textarea value={description} onChange={e => setDescription(e.target.value)} required rows={2} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{!isEditing && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox
|
|
||||||
checked={required}
|
|
||||||
onCheckedChange={(v) => setRequired(v === true)}
|
|
||||||
id="required"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="required">{__("Required")}</Label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label>{__("Cookies")}</Label>
|
|
||||||
<Button type="button" variant="secondary" onClick={addCookie}>
|
|
||||||
{__("Add Cookie")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{cookies.map((cookie, i) => (
|
|
||||||
<div key={i} className="rounded border p-3 space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-muted-foreground">{__("Cookie")} {i + 1}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeCookie(i)}
|
|
||||||
className="text-xs text-red-500 hover:text-red-700"
|
|
||||||
>
|
|
||||||
{__("Remove")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<Input
|
|
||||||
placeholder={__("Cookie name")}
|
|
||||||
value={cookie.name}
|
|
||||||
onChange={(e) => updateCookie(i, "name", e.target.value)}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={__("Duration (e.g. 1 year)")}
|
|
||||||
value={cookie.duration}
|
|
||||||
onChange={(e) => updateCookie(i, "duration", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
placeholder={__("Description")}
|
|
||||||
value={cookie.description}
|
|
||||||
onChange={(e) => updateCookie(i, "description", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="submit" disabled={isCreating || isUpdating}>
|
<Button type="submit" disabled={isCreating}>
|
||||||
{isCreating || isUpdating ? __("Saving...") : isEditing ? __("Update") : __("Create")}
|
{isCreating ? __("Saving...") : __("Create")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -16,14 +16,32 @@ import { formatError, type GraphQLError } from "@probo/helpers";
|
|||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { Badge, Button, Card, IconArrowDown, IconArrowUp, useToast } from "@probo/ui";
|
import { Badge, Button, Card, IconArrowDown, IconArrowUp, useToast } from "@probo/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation } from "react-relay";
|
import { useFragment, useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
|
import type { CategoryList_cookieBanner$key } from "#/__generated__/core/CategoryList_cookieBanner.graphql";
|
||||||
import type { CategoryListDeleteMutation } from "#/__generated__/core/CategoryListDeleteMutation.graphql";
|
import type { CategoryListDeleteMutation } from "#/__generated__/core/CategoryListDeleteMutation.graphql";
|
||||||
import type { CategoryListUpdateMutation } from "#/__generated__/core/CategoryListUpdateMutation.graphql";
|
import type { CategoryListUpdateMutation } from "#/__generated__/core/CategoryListUpdateMutation.graphql";
|
||||||
|
|
||||||
import { CategoryDialog } from "./CategoryDialog";
|
import { CategoryDialog } from "./CategoryDialog";
|
||||||
import { CookieDialog } from "./CookieDialog";
|
|
||||||
|
const categoryListFragment = graphql`
|
||||||
|
fragment CategoryList_cookieBanner on CookieBanner {
|
||||||
|
id
|
||||||
|
categories(first: 50, orderBy: { field: RANK, direction: ASC }) {
|
||||||
|
__id
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
required
|
||||||
|
rank
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const deleteCategoryMutation = graphql`
|
const deleteCategoryMutation = graphql`
|
||||||
mutation CategoryListDeleteMutation(
|
mutation CategoryListDeleteMutation(
|
||||||
@@ -55,31 +73,18 @@ const updateCategoryMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Category {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
required: boolean;
|
|
||||||
rank: number;
|
|
||||||
cookies: ReadonlyArray<{
|
|
||||||
name: string;
|
|
||||||
duration: string;
|
|
||||||
description: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CategoryListProps {
|
interface CategoryListProps {
|
||||||
cookieBannerId: string;
|
cookieBannerKey: CategoryList_cookieBanner$key;
|
||||||
categories: ReadonlyArray<Category>;
|
|
||||||
connectionId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CategoryList({ cookieBannerId, categories, connectionId }: CategoryListProps) {
|
export function CategoryList({ cookieBannerKey }: CategoryListProps) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||||
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
|
|
||||||
const [showCookieDialog, setShowCookieDialog] = useState(false);
|
const banner = useFragment(categoryListFragment, cookieBannerKey);
|
||||||
|
const connectionId = banner.categories.__id;
|
||||||
|
const categories = banner.categories.edges.map(e => e.node);
|
||||||
|
|
||||||
const [commitDelete] = useMutation<CategoryListDeleteMutation>(deleteCategoryMutation);
|
const [commitDelete] = useMutation<CategoryListDeleteMutation>(deleteCategoryMutation);
|
||||||
const [commitUpdate] = useMutation<CategoryListUpdateMutation>(updateCategoryMutation);
|
const [commitUpdate] = useMutation<CategoryListUpdateMutation>(updateCategoryMutation);
|
||||||
@@ -140,17 +145,14 @@ export function CategoryList({ cookieBannerId, categories, connectionId }: Categ
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="font-medium">{__("Categories")}</h3>
|
<h3 className="font-medium">{__("Categories Sorting")}</h3>
|
||||||
<div className="flex items-center gap-2">
|
<Button variant="secondary" onClick={() => setShowCreateDialog(true)}>
|
||||||
<Button variant="secondary" onClick={() => setShowCookieDialog(true)}>
|
{__("Add Category")}
|
||||||
{__("Add Cookie")}
|
</Button>
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" onClick={() => setShowCreateDialog(true)}>
|
|
||||||
{__("Add Category")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-sm text-txt-secondary">
|
||||||
|
{__("Categories will be displayed in your cookie banner in the same order as below.")}
|
||||||
|
</p>
|
||||||
<Card className="divide-y divide-border-low rounded-lg border">
|
<Card className="divide-y divide-border-low rounded-lg border">
|
||||||
{sorted.map((category, index) => (
|
{sorted.map((category, index) => (
|
||||||
<div key={category.id} className="p-4">
|
<div key={category.id} className="p-4">
|
||||||
@@ -180,13 +182,6 @@ export function CategoryList({ cookieBannerId, categories, connectionId }: Categ
|
|||||||
<IconArrowDown size={14} />
|
<IconArrowDown size={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
className="h-6 px-2 text-xs"
|
|
||||||
onClick={() => setEditingCategory(category)}
|
|
||||||
>
|
|
||||||
{__("Edit")}
|
|
||||||
</Button>
|
|
||||||
{!category.required && (
|
{!category.required && (
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
@@ -199,53 +194,17 @@ export function CategoryList({ cookieBannerId, categories, connectionId }: Categ
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mb-2">{category.description}</p>
|
<p className="text-sm text-muted-foreground mb-2">{category.description}</p>
|
||||||
|
|
||||||
{category.cookies.length > 0 && (
|
|
||||||
<div className="mt-3 rounded bg-muted/50 p-3">
|
|
||||||
<div className="text-xs font-medium text-muted-foreground mb-2">
|
|
||||||
{__("Cookies")}
|
|
||||||
{" "}
|
|
||||||
(
|
|
||||||
{category.cookies.length}
|
|
||||||
)
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{category.cookies.map((cookie, i) => (
|
|
||||||
<div key={i} className="flex items-baseline justify-between text-xs">
|
|
||||||
<code className="font-mono">{cookie.name}</code>
|
|
||||||
<span className="text-muted-foreground">{cookie.duration}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{showCreateDialog && (
|
{showCreateDialog && (
|
||||||
<CategoryDialog
|
<CategoryDialog
|
||||||
cookieBannerId={cookieBannerId}
|
cookieBannerId={banner.id}
|
||||||
connectionId={connectionId}
|
connectionId={connectionId}
|
||||||
onOpenChange={setShowCreateDialog}
|
onOpenChange={setShowCreateDialog}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{editingCategory && (
|
|
||||||
<CategoryDialog
|
|
||||||
cookieBannerId={cookieBannerId}
|
|
||||||
connectionId={connectionId}
|
|
||||||
category={editingCategory}
|
|
||||||
onOpenChange={(open) => { if (!open) setEditingCategory(null); }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showCookieDialog && (
|
|
||||||
<CookieDialog
|
|
||||||
categories={sorted}
|
|
||||||
onOpenChange={setShowCookieDialog}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// 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 { useTranslate } from "@probo/i18n";
|
||||||
|
import { Button, Card, IconPlusSmall } from "@probo/ui";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
|
import type { CookieBannerCookiesPageQuery } from "#/__generated__/core/CookieBannerCookiesPageQuery.graphql";
|
||||||
|
|
||||||
|
import { CategoryDialog } from "../_components/CategoryDialog";
|
||||||
|
|
||||||
|
import { CategorySection } from "./_components/CategorySection";
|
||||||
|
|
||||||
|
export const cookieBannerCookiesPageQuery = graphql`
|
||||||
|
query CookieBannerCookiesPageQuery($cookieBannerId: ID!) {
|
||||||
|
node(id: $cookieBannerId) {
|
||||||
|
__typename
|
||||||
|
... on CookieBanner {
|
||||||
|
id
|
||||||
|
categories(first: 50, orderBy: { field: RANK, direction: ASC }) {
|
||||||
|
__id
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
rank
|
||||||
|
...CategorySectionFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface CookieBannerCookiesPageProps {
|
||||||
|
queryRef: PreloadedQuery<CookieBannerCookiesPageQuery>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CookieBannerCookiesPage({
|
||||||
|
queryRef,
|
||||||
|
}: CookieBannerCookiesPageProps) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const data = usePreloadedQuery(cookieBannerCookiesPageQuery, queryRef);
|
||||||
|
|
||||||
|
if (data.node.__typename !== "CookieBanner") {
|
||||||
|
throw new Error("invalid type for node");
|
||||||
|
}
|
||||||
|
|
||||||
|
const banner = data.node;
|
||||||
|
const connectionId = banner.categories.__id;
|
||||||
|
const categories = banner.categories.edges.map(e => e.node);
|
||||||
|
const sorted = [...categories].sort((a, b) => a.rank - b.rank);
|
||||||
|
|
||||||
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-medium">{__("Cookies")}</h3>
|
||||||
|
<Button variant="secondary" onClick={() => setShowCreateDialog(true)}>
|
||||||
|
<IconPlusSmall size={16} />
|
||||||
|
{__("Add Category")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sorted.length === 0 && (
|
||||||
|
<Card className="border p-8 text-center text-muted-foreground">
|
||||||
|
{__("No categories yet. Add a category to start managing cookies.")}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sorted.map(category => (
|
||||||
|
<CategorySection key={category.id} categoryKey={category} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{showCreateDialog && (
|
||||||
|
<CategoryDialog
|
||||||
|
cookieBannerId={banner.id}
|
||||||
|
connectionId={connectionId}
|
||||||
|
onOpenChange={setShowCreateDialog}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 { CookieBannerCookiesPageQuery } from "#/__generated__/core/CookieBannerCookiesPageQuery.graphql";
|
||||||
|
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
|
||||||
|
|
||||||
|
import CookieBannerCookiesPage, { cookieBannerCookiesPageQuery } from "./CookieBannerCookiesPage";
|
||||||
|
|
||||||
|
export default function CookieBannerCookiesPageLoader() {
|
||||||
|
const { cookieBannerId } = useParams<{ cookieBannerId: string }>();
|
||||||
|
const [queryRef, loadQuery] = useQueryLoader<CookieBannerCookiesPageQuery>(cookieBannerCookiesPageQuery);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cookieBannerId) {
|
||||||
|
loadQuery({ cookieBannerId });
|
||||||
|
}
|
||||||
|
}, [loadQuery, cookieBannerId]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <PageSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<PageSkeleton />}>
|
||||||
|
<CookieBannerCookiesPage queryRef={queryRef} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
// 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 { formatError, type GraphQLError } from "@probo/helpers";
|
||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
IconPencil,
|
||||||
|
IconPlusSmall,
|
||||||
|
IconTrashCan,
|
||||||
|
Input,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Textarea,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
useToast,
|
||||||
|
} from "@probo/ui";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useFragment, useMutation } from "react-relay";
|
||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
|
import type { CategorySectionFragment$key } from "#/__generated__/core/CategorySectionFragment.graphql";
|
||||||
|
import type { CategorySectionUpdateMutation } from "#/__generated__/core/CategorySectionUpdateMutation.graphql";
|
||||||
|
|
||||||
|
export const categorySectionFragment = graphql`
|
||||||
|
fragment CategorySectionFragment on CookieCategory {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
required
|
||||||
|
rank
|
||||||
|
cookies {
|
||||||
|
name
|
||||||
|
duration
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const updateCategoryMutation = graphql`
|
||||||
|
mutation CategorySectionUpdateMutation(
|
||||||
|
$input: UpdateCookieCategoryInput!
|
||||||
|
) {
|
||||||
|
updateCookieCategory(input: $input) {
|
||||||
|
cookieCategory {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
rank
|
||||||
|
cookies {
|
||||||
|
name
|
||||||
|
duration
|
||||||
|
description
|
||||||
|
}
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface CookieEntry {
|
||||||
|
name: string;
|
||||||
|
duration: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategorySectionProps {
|
||||||
|
categoryKey: CategorySectionFragment$key;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CategorySection({ categoryKey }: CategorySectionProps) {
|
||||||
|
const category = useFragment(categorySectionFragment, categoryKey);
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [commitUpdate, isUpdating]
|
||||||
|
= useMutation<CategorySectionUpdateMutation>(updateCategoryMutation);
|
||||||
|
|
||||||
|
const [isEditingCategory, setIsEditingCategory] = useState(false);
|
||||||
|
const [editName, setEditName] = useState(category.name);
|
||||||
|
const [editDescription, setEditDescription] = useState(category.description);
|
||||||
|
|
||||||
|
const [editingCookieIndex, setEditingCookieIndex] = useState<number | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [isAddingCookie, setIsAddingCookie] = useState(false);
|
||||||
|
const [cookieForm, setCookieForm] = useState<CookieEntry>({
|
||||||
|
name: "",
|
||||||
|
duration: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const doUpdate = (
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
onSuccess?: () => void,
|
||||||
|
) => {
|
||||||
|
commitUpdate({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
cookieCategoryId: category.id,
|
||||||
|
...input,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted() {
|
||||||
|
toast({
|
||||||
|
title: __("Success"),
|
||||||
|
description: __("Category updated"),
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast({
|
||||||
|
title: __("Error"),
|
||||||
|
description: formatError(
|
||||||
|
__("Failed to update category"),
|
||||||
|
error as GraphQLError,
|
||||||
|
),
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveCategory = () => {
|
||||||
|
doUpdate({ name: editName, description: editDescription }, () => {
|
||||||
|
setIsEditingCategory(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelCategoryEdit = () => {
|
||||||
|
setEditName(category.name);
|
||||||
|
setEditDescription(category.description);
|
||||||
|
setIsEditingCategory(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartEditCookie = (index: number) => {
|
||||||
|
const c = category.cookies[index];
|
||||||
|
setCookieForm({
|
||||||
|
name: c.name,
|
||||||
|
duration: c.duration,
|
||||||
|
description: c.description,
|
||||||
|
});
|
||||||
|
setEditingCookieIndex(index);
|
||||||
|
setIsAddingCookie(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveEditCookie = () => {
|
||||||
|
if (editingCookieIndex === null) return;
|
||||||
|
const newCookies = category.cookies.map((c, i) =>
|
||||||
|
i === editingCookieIndex
|
||||||
|
? { ...cookieForm }
|
||||||
|
: { name: c.name, duration: c.duration, description: c.description },
|
||||||
|
);
|
||||||
|
doUpdate({ cookies: newCookies }, () => {
|
||||||
|
setEditingCookieIndex(null);
|
||||||
|
setCookieForm({ name: "", duration: "", description: "" });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelEditCookie = () => {
|
||||||
|
setEditingCookieIndex(null);
|
||||||
|
setCookieForm({ name: "", duration: "", description: "" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteCookie = (index: number) => {
|
||||||
|
const newCookies = category.cookies
|
||||||
|
.filter((_, i) => i !== index)
|
||||||
|
.map(c => ({
|
||||||
|
name: c.name,
|
||||||
|
duration: c.duration,
|
||||||
|
description: c.description,
|
||||||
|
}));
|
||||||
|
doUpdate({ cookies: newCookies });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartAddCookie = () => {
|
||||||
|
setCookieForm({ name: "", duration: "", description: "" });
|
||||||
|
setIsAddingCookie(true);
|
||||||
|
setEditingCookieIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveNewCookie = () => {
|
||||||
|
if (!cookieForm.name.trim()) return;
|
||||||
|
const newCookies = [
|
||||||
|
...category.cookies.map(c => ({
|
||||||
|
name: c.name,
|
||||||
|
duration: c.duration,
|
||||||
|
description: c.description,
|
||||||
|
})),
|
||||||
|
{ ...cookieForm },
|
||||||
|
];
|
||||||
|
doUpdate({ cookies: newCookies }, () => {
|
||||||
|
setIsAddingCookie(false);
|
||||||
|
setCookieForm({ name: "", duration: "", description: "" });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelAddCookie = () => {
|
||||||
|
setIsAddingCookie(false);
|
||||||
|
setCookieForm({ name: "", duration: "", description: "" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="border overflow-hidden">
|
||||||
|
<div className="p-4">
|
||||||
|
{isEditingCategory
|
||||||
|
? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Input
|
||||||
|
value={editName}
|
||||||
|
onChange={e => setEditName(e.target.value)}
|
||||||
|
placeholder={__("Category name")}
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
value={editDescription}
|
||||||
|
onChange={e => setEditDescription(e.target.value)}
|
||||||
|
placeholder={__("Category description")}
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveCategory}
|
||||||
|
disabled={isUpdating}
|
||||||
|
>
|
||||||
|
{isUpdating ? __("Saving...") : __("Save")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleCancelCategoryEdit}
|
||||||
|
>
|
||||||
|
{__("Cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium">{category.name}</span>
|
||||||
|
{category.required && (
|
||||||
|
<Badge variant="neutral">{__("Required")}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setIsEditingCategory(true)}
|
||||||
|
>
|
||||||
|
<IconPencil size={14} />
|
||||||
|
{__("Edit")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isEditingCategory && (
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
{category.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table className="w-full text-left">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>{__("Name")}</Th>
|
||||||
|
<Th>{__("Duration")}</Th>
|
||||||
|
<Th>{__("Description")}</Th>
|
||||||
|
<Th className="w-20" />
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{category.cookies.map((cookie, index) =>
|
||||||
|
editingCookieIndex === index
|
||||||
|
? (
|
||||||
|
<Tr key={index}>
|
||||||
|
<Td>
|
||||||
|
<Input
|
||||||
|
value={cookieForm.name}
|
||||||
|
onChange={e =>
|
||||||
|
setCookieForm({ ...cookieForm, name: e.target.value })}
|
||||||
|
placeholder={__("Cookie name")}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Input
|
||||||
|
value={cookieForm.duration}
|
||||||
|
onChange={e =>
|
||||||
|
setCookieForm({
|
||||||
|
...cookieForm,
|
||||||
|
duration: e.target.value,
|
||||||
|
})}
|
||||||
|
placeholder={__("e.g. 1 year")}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Input
|
||||||
|
value={cookieForm.description}
|
||||||
|
onChange={e =>
|
||||||
|
setCookieForm({
|
||||||
|
...cookieForm,
|
||||||
|
description: e.target.value,
|
||||||
|
})}
|
||||||
|
placeholder={__("Description")}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveEditCookie}
|
||||||
|
disabled={isUpdating}
|
||||||
|
>
|
||||||
|
{__("Save")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleCancelEditCookie}
|
||||||
|
>
|
||||||
|
{__("Cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<Tr key={index}>
|
||||||
|
<Td>
|
||||||
|
<code className="text-sm font-mono">{cookie.name}</code>
|
||||||
|
</Td>
|
||||||
|
<Td className="text-sm text-muted-foreground">
|
||||||
|
{cookie.duration}
|
||||||
|
</Td>
|
||||||
|
<Td className="text-sm text-muted-foreground">
|
||||||
|
{cookie.description}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleStartEditCookie(index)}
|
||||||
|
className="p-1 rounded cursor-pointer text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<IconPencil size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleDeleteCookie(index)}
|
||||||
|
className="p-1 rounded cursor-pointer text-muted-foreground hover:text-red-500"
|
||||||
|
>
|
||||||
|
<IconTrashCan size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
{isAddingCookie && (
|
||||||
|
<Tr>
|
||||||
|
<Td className="pr-3">
|
||||||
|
<Input
|
||||||
|
value={cookieForm.name}
|
||||||
|
onChange={e =>
|
||||||
|
setCookieForm({ ...cookieForm, name: e.target.value })}
|
||||||
|
placeholder={__("Cookie name")}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td className="pr-3">
|
||||||
|
<Input
|
||||||
|
value={cookieForm.duration}
|
||||||
|
onChange={e =>
|
||||||
|
setCookieForm({ ...cookieForm, duration: e.target.value })}
|
||||||
|
placeholder={__("e.g. 1 year")}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td className="pr-3">
|
||||||
|
<Input
|
||||||
|
value={cookieForm.description}
|
||||||
|
onChange={e =>
|
||||||
|
setCookieForm({
|
||||||
|
...cookieForm,
|
||||||
|
description: e.target.value,
|
||||||
|
})}
|
||||||
|
placeholder={__("Description")}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveNewCookie}
|
||||||
|
disabled={isUpdating}
|
||||||
|
>
|
||||||
|
{__("Save")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleCancelAddCookie}
|
||||||
|
>
|
||||||
|
{__("Cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</Tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{!isAddingCookie && (
|
||||||
|
<div className="p-3 border-t border-border-low">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleStartAddCookie}
|
||||||
|
>
|
||||||
|
<IconPlusSmall size={14} />
|
||||||
|
{__("Add Cookie")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,8 +12,7 @@
|
|||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import { useLazyLoadQuery } from "react-relay";
|
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||||
import { useParams } from "react-router";
|
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
import type { CookieBannerSettingsPageQuery } from "#/__generated__/core/CookieBannerSettingsPageQuery.graphql";
|
import type { CookieBannerSettingsPageQuery } from "#/__generated__/core/CookieBannerSettingsPageQuery.graphql";
|
||||||
@@ -21,68 +20,35 @@ import type { CookieBannerSettingsPageQuery } from "#/__generated__/core/CookieB
|
|||||||
import { BannerSettingsForm } from "../_components/BannerSettingsForm";
|
import { BannerSettingsForm } from "../_components/BannerSettingsForm";
|
||||||
import { CategoryList } from "../_components/CategoryList";
|
import { CategoryList } from "../_components/CategoryList";
|
||||||
|
|
||||||
const settingsPageQuery = graphql`
|
export const cookieBannerSettingsPageQuery = graphql`
|
||||||
query CookieBannerSettingsPageQuery($cookieBannerId: ID!) {
|
query CookieBannerSettingsPageQuery($cookieBannerId: ID!) {
|
||||||
node(id: $cookieBannerId) {
|
node(id: $cookieBannerId) {
|
||||||
__typename
|
__typename
|
||||||
... on CookieBanner {
|
... on CookieBanner {
|
||||||
id
|
...BannerSettingsForm_cookieBanner
|
||||||
name
|
...CategoryList_cookieBanner
|
||||||
origin
|
|
||||||
privacyPolicyUrl
|
|
||||||
consentExpiryDays
|
|
||||||
consentMode
|
|
||||||
categories(first: 50, orderBy: { field: RANK, direction: ASC }) {
|
|
||||||
__id
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
description
|
|
||||||
required
|
|
||||||
rank
|
|
||||||
cookies {
|
|
||||||
name
|
|
||||||
duration
|
|
||||||
description
|
|
||||||
}
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function CookieBannerSettingsPage() {
|
interface CookieBannerSettingsPageProps {
|
||||||
const { cookieBannerId } = useParams<{ cookieBannerId: string }>();
|
queryRef: PreloadedQuery<CookieBannerSettingsPageQuery>;
|
||||||
if (!cookieBannerId) {
|
}
|
||||||
throw new Error("Missing :cookieBannerId param in route");
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = useLazyLoadQuery<CookieBannerSettingsPageQuery>(
|
export default function CookieBannerSettingsPage({
|
||||||
settingsPageQuery,
|
queryRef,
|
||||||
{ cookieBannerId },
|
}: CookieBannerSettingsPageProps) {
|
||||||
);
|
const data = usePreloadedQuery(cookieBannerSettingsPageQuery, queryRef);
|
||||||
|
|
||||||
if (data.node.__typename !== "CookieBanner") {
|
if (data.node.__typename !== "CookieBanner") {
|
||||||
throw new Error("invalid type for node");
|
throw new Error("invalid type for node");
|
||||||
}
|
}
|
||||||
|
|
||||||
const banner = data.node;
|
|
||||||
const connectionId = banner.categories.__id;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<BannerSettingsForm banner={banner} />
|
<BannerSettingsForm cookieBannerKey={data.node} />
|
||||||
|
<CategoryList cookieBannerKey={data.node} />
|
||||||
<CategoryList
|
|
||||||
cookieBannerId={banner.id}
|
|
||||||
categories={banner.categories.edges.map(e => e.node)}
|
|
||||||
connectionId={connectionId}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { CookieBannerSettingsPageQuery } from "#/__generated__/core/CookieBannerSettingsPageQuery.graphql";
|
||||||
|
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
|
||||||
|
|
||||||
|
import CookieBannerSettingsPage, { cookieBannerSettingsPageQuery } from "./CookieBannerSettingsPage";
|
||||||
|
|
||||||
|
export default function CookieBannerSettingsPageLoader() {
|
||||||
|
const { cookieBannerId } = useParams<{ cookieBannerId: string }>();
|
||||||
|
const [queryRef, loadQuery] = useQueryLoader<CookieBannerSettingsPageQuery>(cookieBannerSettingsPageQuery);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cookieBannerId) {
|
||||||
|
loadQuery({ cookieBannerId });
|
||||||
|
}
|
||||||
|
}, [loadQuery, cookieBannerId]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <PageSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<PageSkeleton />}>
|
||||||
|
<CookieBannerSettingsPage queryRef={queryRef} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,44 +14,12 @@
|
|||||||
|
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { Card } from "@probo/ui";
|
import { Card } from "@probo/ui";
|
||||||
import { useLazyLoadQuery } from "react-relay";
|
import type { ReactNode } from "react";
|
||||||
import { useParams } from "react-router";
|
|
||||||
import { graphql } from "relay-runtime";
|
|
||||||
|
|
||||||
import type { CookieBannerSnippetPageQuery } from "#/__generated__/core/CookieBannerSnippetPageQuery.graphql";
|
|
||||||
|
|
||||||
import { CodeSnippets } from "./_components/CodeSnippets";
|
import { CodeSnippets } from "./_components/CodeSnippets";
|
||||||
|
|
||||||
const snippetPageQuery = graphql`
|
|
||||||
query CookieBannerSnippetPageQuery($cookieBannerId: ID!) {
|
|
||||||
node(id: $cookieBannerId) {
|
|
||||||
__typename
|
|
||||||
... on CookieBanner {
|
|
||||||
id
|
|
||||||
origin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function CookieBannerSnippetPage() {
|
export default function CookieBannerSnippetPage() {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { cookieBannerId } = useParams<{ cookieBannerId: string }>();
|
|
||||||
if (!cookieBannerId) {
|
|
||||||
throw new Error("Missing :cookieBannerId param in route");
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = useLazyLoadQuery<CookieBannerSnippetPageQuery>(
|
|
||||||
snippetPageQuery,
|
|
||||||
{ cookieBannerId },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.node.__typename !== "CookieBanner") {
|
|
||||||
throw new Error("invalid type for node");
|
|
||||||
}
|
|
||||||
|
|
||||||
const banner = data.node;
|
|
||||||
const baseUrl = `${window.location.origin}/api/cookie-banner/v1`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-10">
|
||||||
@@ -60,7 +28,7 @@ export default function CookieBannerSnippetPage() {
|
|||||||
title={__("Add the cookie banner snippet")}
|
title={__("Add the cookie banner snippet")}
|
||||||
description={__("Include the Probo cookie banner on your website by adding one of the following snippets. The banner will automatically appear and collect visitor consent.")}
|
description={__("Include the Probo cookie banner on your website by adding one of the following snippets. The banner will automatically appear and collect visitor consent.")}
|
||||||
>
|
>
|
||||||
<CodeSnippets bannerId={banner.id} baseUrl={baseUrl} />
|
<CodeSnippets />
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step
|
<Step
|
||||||
@@ -72,7 +40,8 @@ export default function CookieBannerSnippetPage() {
|
|||||||
<h4 className="text-sm font-medium">{__("Scripts")}</h4>
|
<h4 className="text-sm font-medium">{__("Scripts")}</h4>
|
||||||
<Card className="border">
|
<Card className="border">
|
||||||
<pre className="overflow-x-auto p-4 text-sm font-mono text-invert bg-accent rounded-lg">
|
<pre className="overflow-x-auto p-4 text-sm font-mono text-invert bg-accent rounded-lg">
|
||||||
<code>{`<!-- Before: loads immediately -->
|
<code>
|
||||||
|
{`<!-- Before: loads immediately -->
|
||||||
<script src="https://analytics.example.com/tracker.js"></script>
|
<script src="https://analytics.example.com/tracker.js"></script>
|
||||||
|
|
||||||
<!-- After: loads only when "analytics" consent is granted -->
|
<!-- After: loads only when "analytics" consent is granted -->
|
||||||
@@ -80,56 +49,65 @@ export default function CookieBannerSnippetPage() {
|
|||||||
type="text/plain"
|
type="text/plain"
|
||||||
data-cookie-consent="analytics"
|
data-cookie-consent="analytics"
|
||||||
data-src="https://analytics.example.com/tracker.js"
|
data-src="https://analytics.example.com/tracker.js"
|
||||||
></script>`}</code>
|
></script>`}
|
||||||
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<h4 className="text-sm font-medium">{__("Inline scripts")}</h4>
|
<h4 className="text-sm font-medium">{__("Inline scripts")}</h4>
|
||||||
<Card className="border">
|
<Card className="border">
|
||||||
<pre className="overflow-x-auto p-4 text-sm font-mono text-invert bg-accent rounded-lg">
|
<pre className="overflow-x-auto p-4 text-sm font-mono text-invert bg-accent rounded-lg">
|
||||||
<code>{`<script
|
<code>
|
||||||
|
{`<script
|
||||||
type="text/plain"
|
type="text/plain"
|
||||||
data-cookie-consent="analytics"
|
data-cookie-consent="analytics"
|
||||||
data-type="text/javascript"
|
data-type="text/javascript"
|
||||||
>
|
>
|
||||||
// This code runs only after consent
|
// This code runs only after consent
|
||||||
gtag('config', 'G-XXXXXXX');
|
gtag('config', 'G-XXXXXXX');
|
||||||
</script>`}</code>
|
</script>`}
|
||||||
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<h4 className="text-sm font-medium">{__("Iframes & embeds")}</h4>
|
<h4 className="text-sm font-medium">{__("Iframes & embeds")}</h4>
|
||||||
<Card className="border">
|
<Card className="border">
|
||||||
<pre className="overflow-x-auto p-4 text-sm font-mono text-invert bg-accent rounded-lg">
|
<pre className="overflow-x-auto p-4 text-sm font-mono text-invert bg-accent rounded-lg">
|
||||||
<code>{`<iframe
|
<code>
|
||||||
|
{`<iframe
|
||||||
data-cookie-consent="marketing"
|
data-cookie-consent="marketing"
|
||||||
data-src="https://www.youtube.com/embed/VIDEO_ID"
|
data-src="https://www.youtube.com/embed/VIDEO_ID"
|
||||||
width="560"
|
width="560"
|
||||||
height="315"
|
height="315"
|
||||||
></iframe>`}</code>
|
></iframe>`}
|
||||||
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<h4 className="text-sm font-medium">{__("Images, video & audio")}</h4>
|
<h4 className="text-sm font-medium">{__("Images, video & audio")}</h4>
|
||||||
<Card className="border">
|
<Card className="border">
|
||||||
<pre className="overflow-x-auto p-4 text-sm font-mono text-invert bg-accent rounded-lg">
|
<pre className="overflow-x-auto p-4 text-sm font-mono text-invert bg-accent rounded-lg">
|
||||||
<code>{`<img
|
<code>
|
||||||
|
{`<img
|
||||||
data-cookie-consent="analytics"
|
data-cookie-consent="analytics"
|
||||||
data-src="https://tracker.example.com/pixel.gif"
|
data-src="https://tracker.example.com/pixel.gif"
|
||||||
width="1"
|
width="1"
|
||||||
height="1"
|
height="1"
|
||||||
/>`}</code>
|
/>`}
|
||||||
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<h4 className="text-sm font-medium">{__("Stylesheets")}</h4>
|
<h4 className="text-sm font-medium">{__("Stylesheets")}</h4>
|
||||||
<Card className="border">
|
<Card className="border">
|
||||||
<pre className="overflow-x-auto p-4 text-sm font-mono text-invert bg-accent rounded-lg">
|
<pre className="overflow-x-auto p-4 text-sm font-mono text-invert bg-accent rounded-lg">
|
||||||
<code>{`<link
|
<code>
|
||||||
|
{`<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
data-cookie-consent="marketing"
|
data-cookie-consent="marketing"
|
||||||
data-href="https://widgets.example.com/styles.css"
|
data-href="https://widgets.example.com/styles.css"
|
||||||
/>`}</code>
|
/>`}
|
||||||
|
</code>
|
||||||
</pre>
|
</pre>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -142,17 +120,14 @@ export default function CookieBannerSnippetPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Step({
|
interface StepProps {
|
||||||
number,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
number: number;
|
number: number;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
children: React.ReactNode;
|
children: ReactNode;
|
||||||
}) {
|
}
|
||||||
|
|
||||||
|
function Step({ number, title, description, children }: StepProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-accent text-sm font-semibold text-invert">
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-accent text-sm font-semibold text-invert">
|
||||||
|
|||||||
@@ -15,22 +15,21 @@
|
|||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { Button, Card, useToast } from "@probo/ui";
|
import { Button, Card, useToast } from "@probo/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
|
||||||
interface CodeSnippetsProps {
|
export function CodeSnippets() {
|
||||||
bannerId: string;
|
|
||||||
baseUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CodeSnippets({ bannerId, baseUrl }: CodeSnippetsProps) {
|
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { cookieBannerId } = useParams<{ cookieBannerId: string }>();
|
||||||
|
|
||||||
|
const baseUrl = `${window.location.origin}/api/cookie-banner/v1`;
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: __("Script Tag"),
|
label: __("Script Tag"),
|
||||||
code: `<script
|
code: `<script
|
||||||
src="https://cdn.jsdelivr.net/npm/@probo/cookie-banner/dist/cookie-banner.iife.js"
|
src="https://cdn.jsdelivr.net/npm/@probo/cookie-banner/dist/cookie-banner.iife.js"
|
||||||
data-banner-id="${bannerId}"
|
data-banner-id="${cookieBannerId}"
|
||||||
data-base-url="${baseUrl}"
|
data-base-url="${baseUrl}"
|
||||||
data-position="bottom-left"
|
data-position="bottom-left"
|
||||||
></script>`,
|
></script>`,
|
||||||
@@ -43,7 +42,7 @@ registerThemedBanner();
|
|||||||
|
|
||||||
// In your HTML or template:
|
// In your HTML or template:
|
||||||
// <probo-cookie-banner
|
// <probo-cookie-banner
|
||||||
// banner-id="${bannerId}"
|
// banner-id="${cookieBannerId}"
|
||||||
// base-url="${baseUrl}"
|
// base-url="${baseUrl}"
|
||||||
// position="bottom-left"
|
// position="bottom-left"
|
||||||
// ></probo-cookie-banner>`,
|
// ></probo-cookie-banner>`,
|
||||||
@@ -55,7 +54,7 @@ registerThemedBanner();
|
|||||||
registerComponents();
|
registerComponents();
|
||||||
|
|
||||||
// Build your own UI with headless components:
|
// Build your own UI with headless components:
|
||||||
// <probo-cookie-banner-root banner-id="${bannerId}" base-url="${baseUrl}">
|
// <probo-cookie-banner-root banner-id="${cookieBannerId}" base-url="${baseUrl}">
|
||||||
// <probo-banner>
|
// <probo-banner>
|
||||||
// <probo-accept-button><button>Accept all</button></probo-accept-button>
|
// <probo-accept-button><button>Accept all</button></probo-accept-button>
|
||||||
// <probo-reject-button><button>Reject all</button></probo-reject-button>
|
// <probo-reject-button><button>Reject all</button></probo-reject-button>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const cookieBannerRoutes = [
|
|||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
loader: () => {
|
loader: () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
||||||
throw redirect("settings");
|
throw redirect("settings");
|
||||||
},
|
},
|
||||||
Component: Fragment,
|
Component: Fragment,
|
||||||
@@ -52,7 +53,12 @@ export const cookieBannerRoutes = [
|
|||||||
{
|
{
|
||||||
path: "settings",
|
path: "settings",
|
||||||
Fallback: LinkCardSkeleton,
|
Fallback: LinkCardSkeleton,
|
||||||
Component: lazy(() => import("#/pages/organizations/cookie-banners/configuration/settings/CookieBannerSettingsPage")),
|
Component: lazy(() => import("#/pages/organizations/cookie-banners/configuration/settings/CookieBannerSettingsPageLoader")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "cookies",
|
||||||
|
Fallback: LinkCardSkeleton,
|
||||||
|
Component: lazy(() => import("#/pages/organizations/cookie-banners/configuration/cookies/CookieBannerCookiesPageLoader")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "snippet",
|
path: "snippet",
|
||||||
|
|||||||
Reference in New Issue
Block a user