From f2927098ac942c186fa1bb51942a1277623dea83 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Wed, 4 Feb 2026 20:18:58 +0100 Subject: [PATCH 1/5] 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", From 87fdde18e53fa2078026ea56a47c9346a904e7ac Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Wed, 4 Feb 2026 20:25:54 +0100 Subject: [PATCH 2/5] feat: enhance UtmBuilder component with path handling for selected sites and improve URL input experience --- components/tools/UtmBuilder.tsx | 55 ++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/components/tools/UtmBuilder.tsx b/components/tools/UtmBuilder.tsx index ea13381..37c3710 100644 --- a/components/tools/UtmBuilder.tsx +++ b/components/tools/UtmBuilder.tsx @@ -86,6 +86,32 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) { setValues({ ...values, [e.target.name]: e.target.value }) } + // Helper to handle path changes when a site is selected + const handlePathChange = (e: React.ChangeEvent) => { + const site = sites.find(s => s.id === selectedSiteId) + if (!site) return + + const path = e.target.value + // Ensure path starts with / if not empty + const safePath = path.startsWith('/') || path === '' ? path : `/${path}` + setValues(v => ({ ...v, url: `https://${site.domain}${safePath}` })) + } + + // Extract path from current URL if site is selected + const getCurrentPath = () => { + const site = sites.find(s => s.id === selectedSiteId) + if (!site || !values.url) return '' + + try { + const urlObj = new URL(values.url) + return urlObj.pathname === '/' ? '' : urlObj.pathname + } catch { + return '' + } + } + + const selectedSite = sites.find(s => s.id === selectedSiteId) + return (
@@ -107,13 +133,28 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
- + {selectedSite ? ( +
+ + https://{selectedSite.domain} + + +
+ ) : ( + + )}

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

From 210ea7b754f7be7f9f8a93d66a9744c1022afc1a Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Wed, 4 Feb 2026 20:34:30 +0100 Subject: [PATCH 3/5] feat: update version to 0.1.2 and upgrade @ciphera-net/ui to 0.0.44; refactor layout-content for improved navigation handling --- app/layout-content.tsx | 17 +++++++++-------- package-lock.json | 12 ++++++------ package.json | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/app/layout-content.tsx b/app/layout-content.tsx index 625d159..b037f67 100644 --- a/app/layout-content.tsx +++ b/app/layout-content.tsx @@ -62,14 +62,15 @@ export default function LayoutContent({ children }: { children: React.ReactNode showSecurity={false} showPricing={true} topOffset={showOfflineBar ? `${barHeightRem}rem` : undefined} - userMenuCustomItems={ - - - Tools - + customNavItems={ + auth.user ? ( + + Tools + + ) : null } />
Date: Wed, 4 Feb 2026 20:40:16 +0100 Subject: [PATCH 4/5] feat: enhance UI components by updating button styles and improving layout in ToolsPage and Campaigns; refactor UtmBuilder for better input handling and user experience --- app/tools/page.tsx | 2 +- components/dashboard/Campaigns.tsx | 18 ++++++++------ components/tools/UtmBuilder.tsx | 39 ++++++++++++++---------------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/app/tools/page.tsx b/app/tools/page.tsx index ddb51fa..5904741 100644 --- a/app/tools/page.tsx +++ b/app/tools/page.tsx @@ -6,7 +6,7 @@ export default function ToolsPage() { return (

Tools

-
+

UTM Campaign Builder

diff --git a/components/dashboard/Campaigns.tsx b/components/dashboard/Campaigns.tsx index 97dd753..5bee4ee 100644 --- a/components/dashboard/Campaigns.tsx +++ b/components/dashboard/Campaigns.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react' import Link from 'next/link' import { formatNumber } from '@/lib/utils/format' -import { Modal, ArrowRightIcon } from '@ciphera-net/ui' +import { Modal, ArrowRightIcon, Button } from '@ciphera-net/ui' import { getCampaigns, CampaignStat } from '@/lib/api/stats' import { FaBullhorn } from 'react-icons/fa' import { PlusIcon } from '@radix-ui/react-icons' @@ -70,21 +70,23 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) {

Campaigns

-
- + {showViewAll && ( - + )}
diff --git a/components/tools/UtmBuilder.tsx b/components/tools/UtmBuilder.tsx index 37c3710..f4b1593 100644 --- a/components/tools/UtmBuilder.tsx +++ b/components/tools/UtmBuilder.tsx @@ -3,7 +3,7 @@ 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' +import { Select, Input, Button } from '@ciphera-net/ui' interface UtmBuilderProps { initialSiteId?: string @@ -132,62 +132,58 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) { )}
- + {selectedSite ? ( -
- +
+ https://{selectedSite.domain}
) : ( - )} -

+

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

- - Source * +
- - Medium * +
- - Campaign Name * + @@ -197,13 +193,14 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) { {generatedUrl && (
{generatedUrl} - + {copied ? : } +
)}
From e824b270afaebb4b40b363f161c2228071ba2b8d Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Wed, 4 Feb 2026 20:56:08 +0100 Subject: [PATCH 5/5] feat: refactor UtmBuilder to improve site selection logic and URL initialization based on initialSiteId; enhance user experience by ensuring proper state updates --- components/tools/UtmBuilder.tsx | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/components/tools/UtmBuilder.tsx b/components/tools/UtmBuilder.tsx index f4b1593..2c7a67c 100644 --- a/components/tools/UtmBuilder.tsx +++ b/components/tools/UtmBuilder.tsx @@ -29,24 +29,29 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) { 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. Initialize default selection + useEffect(() => { + if (sites.length === 0) return + + if (initialSiteId) { + const site = sites.find(s => s.id === initialSiteId) + if (site && selectedSiteId !== site.id) { + setSelectedSiteId(site.id) + setValues(v => ({ ...v, url: `https://${site.domain}` })) + } + } else if (!selectedSiteId && !values.url) { + const firstSite = sites[0] + setSelectedSiteId(firstSite.id) + setValues(v => ({ ...v, url: `https://${firstSite.domain}` })) + } + }, [sites, initialSiteId, selectedSiteId, values.url]) // 2. Handle Site Selection const handleSiteChange = (siteId: string) => {