From f2927098ac942c186fa1bb51942a1277623dea83 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Wed, 4 Feb 2026 20:18:58 +0100 Subject: [PATCH] feat: update version to 0.1.1, upgrade @ciphera-net/ui to 0.0.43, and enhance Campaigns component with URL builder modal --- app/layout-content.tsx | 11 +- app/tools/page.tsx | 15 +++ components/dashboard/Campaigns.tsx | 32 +++++- components/tools/UtmBuilder.tsx | 170 +++++++++++++++++++++++++++++ package-lock.json | 12 +- package.json | 4 +- 6 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 app/tools/page.tsx create mode 100644 components/tools/UtmBuilder.tsx diff --git a/app/layout-content.tsx b/app/layout-content.tsx index f279464..625d159 100644 --- a/app/layout-content.tsx +++ b/app/layout-content.tsx @@ -1,7 +1,7 @@ 'use client' import { OfflineBanner } from '@/components/OfflineBanner' -import { Header, Footer } from '@ciphera-net/ui' +import { Header, Footer, GridIcon } from '@ciphera-net/ui' import { useAuth } from '@/lib/auth/context' import { useOnlineStatus } from '@/lib/hooks/useOnlineStatus' import Link from 'next/link' @@ -62,6 +62,15 @@ export default function LayoutContent({ children }: { children: React.ReactNode showSecurity={false} showPricing={true} topOffset={showOfflineBar ? `${barHeightRem}rem` : undefined} + userMenuCustomItems={ + + + Tools + + } />
+

Tools

+
+

UTM Campaign Builder

+ +
+ + ) +} diff --git a/components/dashboard/Campaigns.tsx b/components/dashboard/Campaigns.tsx index 580466f..97dd753 100644 --- a/components/dashboard/Campaigns.tsx +++ b/components/dashboard/Campaigns.tsx @@ -6,6 +6,8 @@ import { formatNumber } from '@/lib/utils/format' import { Modal, ArrowRightIcon } from '@ciphera-net/ui' import { getCampaigns, CampaignStat } from '@/lib/api/stats' import { FaBullhorn } from 'react-icons/fa' +import { PlusIcon } from '@radix-ui/react-icons' +import UtmBuilder from '@/components/tools/UtmBuilder' interface CampaignsProps { siteId: string @@ -18,6 +20,7 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) { const [data, setData] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isModalOpen, setIsModalOpen] = useState(false) + const [isBuilderOpen, setIsBuilderOpen] = useState(false) const [fullData, setFullData] = useState([]) const [isLoadingFull, setIsLoadingFull] = useState(false) @@ -67,14 +70,23 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) {

Campaigns

- {showViewAll && ( +
- )} + {showViewAll && ( + + )} +
{isLoading ? ( @@ -167,6 +179,16 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) { )} + + setIsBuilderOpen(false)} + title="Campaign URL Builder" + > +
+ +
+
) } diff --git a/components/tools/UtmBuilder.tsx b/components/tools/UtmBuilder.tsx new file mode 100644 index 0000000..ea13381 --- /dev/null +++ b/components/tools/UtmBuilder.tsx @@ -0,0 +1,170 @@ +'use client' + +import { useState, useEffect } from 'react' +import { CopyIcon, CheckIcon } from '@radix-ui/react-icons' +import { listSites, Site } from '@/lib/api/sites' +import { Select } from '@ciphera-net/ui' + +interface UtmBuilderProps { + initialSiteId?: string +} + +export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) { + const [sites, setSites] = useState([]) + const [selectedSiteId, setSelectedSiteId] = useState(initialSiteId || '') + + const [values, setValues] = useState({ + url: '', + source: '', + medium: '', + campaign: '', + term: '', + content: '' + }) + const [copied, setCopied] = useState(false) + + // 1. Fetch sites on mount + useEffect(() => { + async function fetchSites() { + try { + const data = await listSites() + setSites(data) + + // If we have an initialSiteId, try to find it and set the URL + if (initialSiteId) { + const site = data.find(s => s.id === initialSiteId) + if (site) { + setValues(v => ({ ...v, url: `https://${site.domain}` })) + } + } else if (data.length > 0 && !values.url) { + // Optional: Default to first site if no initial ID provided + setSelectedSiteId(data[0].id) + setValues(v => ({ ...v, url: `https://${data[0].domain}` })) + } + } catch (e) { + console.error('Failed to load sites for UTM builder', e) + } + } + fetchSites() + }, [initialSiteId]) + + // 2. Handle Site Selection + const handleSiteChange = (siteId: string) => { + setSelectedSiteId(siteId) + const site = sites.find(s => s.id === siteId) + if (site) { + setValues(prev => ({ + ...prev, + url: `https://${site.domain}` // Reset URL to base domain of selected site + })) + } + } + + const generatedUrl = (() => { + if (!values.url || !values.source || !values.medium || !values.campaign) return '' + try { + const url = new URL(values.url) + url.searchParams.set('utm_source', values.source.toLowerCase()) + url.searchParams.set('utm_medium', values.medium.toLowerCase()) + url.searchParams.set('utm_campaign', values.campaign.toLowerCase()) + if (values.term) url.searchParams.set('utm_term', values.term) + if (values.content) url.searchParams.set('utm_content', values.content) + return url.toString() + } catch (e) { + return '' + } + })() + + const copyToClipboard = () => { + if (!generatedUrl) return + navigator.clipboard.writeText(generatedUrl) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + const handleChange = (e: React.ChangeEvent) => { + setValues({ ...values, [e.target.name]: e.target.value }) + } + + return ( +
+
+ + {/* Site Selector */} + {sites.length > 0 && ( +
+ + +

+ You can add specific paths (e.g., /blog/post-1) to the URL above. +

+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + {generatedUrl && ( +
+ {generatedUrl} + +
+ )} +
+ ) +} diff --git a/package-lock.json b/package-lock.json index 1574eb3..7989bb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,14 @@ { "name": "pulse-frontend", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-frontend", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { - "@ciphera-net/ui": "^0.0.42", + "@ciphera-net/ui": "^0.0.43", "@ducanh2912/next-pwa": "^10.2.9", "axios": "^1.13.2", "country-flag-icons": "^1.6.4", @@ -1467,9 +1467,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.0.42", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.42/f3476f7f1e6e2210b4c7b1ae84964777e1b2b718", - "integrity": "sha512-PuzwXKR2DrtTWXELDFH5GhQxnz0qPHTNGtMbTdhslWEp/taEy+n3UsoBp+NFw0uQEvz7mHQ9PgDpDVztQncMfw==", + "version": "0.0.43", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.43/d3b37aacb407bf0343d66a4ad5a84de1b5edd712", + "integrity": "sha512-q1giUPWB5+/CIVAAf1H+3bROCBowNy43bCzq1pycIUsm30lF9NkIqnikCAmvfVJLbtt5qJw7GdIgD39gj6awog==", "dependencies": { "@radix-ui/react-icons": "^1.3.0", "clsx": "^2.1.0", diff --git a/package.json b/package.json index 02bf60a..743f43f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-frontend", - "version": "0.1.0", + "version": "0.1.1", "private": true, "scripts": { "dev": "next dev", @@ -10,7 +10,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@ciphera-net/ui": "^0.0.42", + "@ciphera-net/ui": "^0.0.43", "@ducanh2912/next-pwa": "^10.2.9", "axios": "^1.13.2", "country-flag-icons": "^1.6.4",