From aaf224a2783a859bb9a98207d81b18cc6ef912c7 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 23 Jan 2026 18:35:17 +0100 Subject: [PATCH] refactor: migrate UI components to @ciphera-net/ui v0.0.11 --- app/auth/callback/page.tsx | 6 +- app/layout-content.tsx | 1 + app/onboarding/page.tsx | 7 +- app/page.tsx | 2 +- app/share/[id]/page.tsx | 2 +- app/signup/page.tsx | 2 +- app/sites/[id]/page.tsx | 2 +- app/sites/[id]/realtime/page.tsx | 2 +- app/sites/[id]/settings/page.tsx | 4 +- app/sites/new/page.tsx | 3 +- components/LoadingOverlay.tsx | 48 ----------- components/PasswordInput.tsx | 109 ------------------------ components/WorkspaceSwitcher.tsx | 109 ------------------------ components/dashboard/Chart.tsx | 2 +- components/settings/ProfileSettings.tsx | 2 +- components/sites/SiteList.tsx | 2 +- components/ui/Button.tsx | 44 ---------- components/ui/Input.tsx | 37 -------- lib/auth/context.tsx | 2 +- package.json | 2 +- 20 files changed, 20 insertions(+), 368 deletions(-) delete mode 100644 components/LoadingOverlay.tsx delete mode 100644 components/PasswordInput.tsx delete mode 100644 components/WorkspaceSwitcher.tsx delete mode 100644 components/ui/Button.tsx delete mode 100644 components/ui/Input.tsx diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx index 58e186a..205a469 100644 --- a/app/auth/callback/page.tsx +++ b/app/auth/callback/page.tsx @@ -5,7 +5,7 @@ import { useRouter, useSearchParams } from 'next/navigation' import { useAuth } from '@/lib/auth/context' import { AUTH_URL } from '@/lib/api/client' import { exchangeAuthCode, setSessionAction } from '@/app/actions/auth' -import LoadingOverlay from '@/components/LoadingOverlay' +import { LoadingOverlay } from '@ciphera-net/ui' function AuthCallbackContent() { const router = useRouter() @@ -111,12 +111,12 @@ function AuthCallbackContent() { } // * Use standard Pulse loading screen to make transition to Home seamless - return + return } export default function AuthCallback() { return ( - }> + }> ) diff --git a/app/layout-content.tsx b/app/layout-content.tsx index 5bec93d..9415038 100644 --- a/app/layout-content.tsx +++ b/app/layout-content.tsx @@ -47,6 +47,7 @@ export default function LayoutContent({ children }: { children: React.ReactNode activeOrgId={auth.user?.org_id} onSwitchWorkspace={handleSwitchWorkspace} onCreateOrganization={handleCreateOrganization} + allowPersonalWorkspace={false} />
{children} diff --git a/app/onboarding/page.tsx b/app/onboarding/page.tsx index f4d24d6..f8d8ecd 100644 --- a/app/onboarding/page.tsx +++ b/app/onboarding/page.tsx @@ -4,9 +4,8 @@ import { useState } from 'react' import { useRouter } from 'next/navigation' import { createOrganization } from '@/lib/api/organization' import { useAuth } from '@/lib/auth/context' -import LoadingOverlay from '@/components/LoadingOverlay' -import { Button } from '@/components/ui/Button' -import { Input } from '@/components/ui/Input' +import { LoadingOverlay } from '@ciphera-net/ui' +import { Button, Input } from '@ciphera-net/ui' export default function OnboardingPage() { const [name, setName] = useState('') @@ -41,7 +40,7 @@ export default function OnboardingPage() { } } - if (loading) return + if (loading) return return (
diff --git a/app/page.tsx b/app/page.tsx index 8d5f596..b7d5892 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import { useAuth } from '@/lib/auth/context' import { initiateOAuthFlow, initiateSignupFlow } from '@/lib/api/oauth' -import LoadingOverlay from '@/components/LoadingOverlay' +import { LoadingOverlay } from '@ciphera-net/ui' import SiteList from '@/components/sites/SiteList' import { Button } from '@ciphera-net/ui' import { BarChartIcon, LockClosedIcon, LightningBoltIcon } from '@radix-ui/react-icons' diff --git a/app/share/[id]/page.tsx b/app/share/[id]/page.tsx index f004a77..393af46 100644 --- a/app/share/[id]/page.tsx +++ b/app/share/[id]/page.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react' import { useParams, useSearchParams, useRouter } from 'next/navigation' import { getPublicDashboard, type DashboardData } from '@/lib/api/stats' import { toast } from 'sonner' -import LoadingOverlay from '@/components/LoadingOverlay' +import { LoadingOverlay } from '@ciphera-net/ui' import Chart from '@/components/dashboard/Chart' import TopPages from '@/components/dashboard/ContentStats' import TopReferrers from '@/components/dashboard/TopReferrers' diff --git a/app/signup/page.tsx b/app/signup/page.tsx index 53646ea..db7fb0e 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { initiateSignupFlow } from '@/lib/api/oauth' -import LoadingOverlay from '@/components/LoadingOverlay' +import { LoadingOverlay } from '@ciphera-net/ui' export default function SignupPage() { useEffect(() => { diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index edf6506..1732a0d 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -7,7 +7,7 @@ import { getSite, type Site } from '@/lib/api/sites' import { getStats, getRealtime, getDailyStats, getTopPages, getTopReferrers, getCountries, getCities, getRegions, getBrowsers, getOS, getDevices, getScreenResolutions, getEntryPages, getExitPages, getDashboard, getPerformanceByPage, type Stats, type DailyStat, type PerformanceByPageStat } from '@/lib/api/stats' import { formatNumber, formatDuration, getDateRange } from '@/lib/utils/format' import { toast } from 'sonner' -import LoadingOverlay from '@/components/LoadingOverlay' +import { LoadingOverlay } from '@ciphera-net/ui' import Select from '@/components/ui/Select' import DatePicker from '@/components/ui/DatePicker' import ContentStats from '@/components/dashboard/ContentStats' diff --git a/app/sites/[id]/realtime/page.tsx b/app/sites/[id]/realtime/page.tsx index 4a51523..e9aa655 100644 --- a/app/sites/[id]/realtime/page.tsx +++ b/app/sites/[id]/realtime/page.tsx @@ -5,7 +5,7 @@ import { useParams, useRouter } from 'next/navigation' import { getSite, type Site } from '@/lib/api/sites' import { getRealtimeVisitors, getSessionDetails, type Visitor, type SessionEvent } from '@/lib/api/realtime' import { toast } from 'sonner' -import LoadingOverlay from '@/components/LoadingOverlay' +import { LoadingOverlay } from '@ciphera-net/ui' function formatTimeAgo(dateString: string) { const date = new Date(dateString) diff --git a/app/sites/[id]/settings/page.tsx b/app/sites/[id]/settings/page.tsx index 366983c..c2565d9 100644 --- a/app/sites/[id]/settings/page.tsx +++ b/app/sites/[id]/settings/page.tsx @@ -4,9 +4,9 @@ import { useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import { getSite, updateSite, resetSiteData, deleteSite, type Site, type GeoDataLevel } from '@/lib/api/sites' import { toast } from 'sonner' -import LoadingOverlay from '@/components/LoadingOverlay' +import { LoadingOverlay } from '@ciphera-net/ui' import VerificationModal from '@/components/sites/VerificationModal' -import PasswordInput from '@/components/PasswordInput' +import { PasswordInput } from '@ciphera-net/ui' import Select from '@/components/ui/Select' import { APP_URL, API_URL } from '@/lib/api/client' import { generatePrivacySnippet } from '@/lib/utils/privacySnippet' diff --git a/app/sites/new/page.tsx b/app/sites/new/page.tsx index 441f21c..2bc58ce 100644 --- a/app/sites/new/page.tsx +++ b/app/sites/new/page.tsx @@ -4,8 +4,7 @@ import { useState } from 'react' import { useRouter } from 'next/navigation' import { createSite } from '@/lib/api/sites' import { toast } from 'sonner' -import { Button } from '@/components/ui/Button' -import { Input } from '@/components/ui/Input' +import { Button, Input } from '@ciphera-net/ui' export default function NewSitePage() { const router = useRouter() diff --git a/components/LoadingOverlay.tsx b/components/LoadingOverlay.tsx deleted file mode 100644 index 0ca364c..0000000 --- a/components/LoadingOverlay.tsx +++ /dev/null @@ -1,48 +0,0 @@ -'use client' - -import React, { useEffect, useState } from 'react' -import { createPortal } from 'react-dom' - -interface LoadingOverlayProps { - logoSrc?: string - title?: string - portal?: boolean -} - -export default function LoadingOverlay({ - logoSrc = "/pulse_icon_no_margins.png", - title = "Pulse", - portal = true -}: LoadingOverlayProps) { - const [mounted, setMounted] = useState(false) - - useEffect(() => { - setMounted(true) - return () => setMounted(false) - }, []) - - const content = ( -
-
-
- {typeof - - {title} - -
-
-
-
- ) - - if (portal) { - if (!mounted) return null - return createPortal(content, document.body) - } - - return content -} diff --git a/components/PasswordInput.tsx b/components/PasswordInput.tsx deleted file mode 100644 index db9b651..0000000 --- a/components/PasswordInput.tsx +++ /dev/null @@ -1,109 +0,0 @@ -'use client' - -import { useState } from 'react' - -interface PasswordInputProps { - value: string - onChange: (value: string) => void - label?: string - placeholder?: string - error?: string | null - disabled?: boolean - required?: boolean - className?: string - id?: string - autoComplete?: string - minLength?: number - onFocus?: () => void - onBlur?: () => void -} - -export default function PasswordInput({ - value, - onChange, - label = 'Password', - placeholder = 'Enter password', - error, - disabled = false, - required = false, - className = '', - id, - autoComplete, - minLength, - onFocus, - onBlur -}: PasswordInputProps) { - const [showPassword, setShowPassword] = useState(false) - const inputId = id || 'password-input' - const errorId = `${inputId}-error` - - return ( -
- {label && ( - - )} -
- onChange(e.target.value)} - placeholder={placeholder} - disabled={disabled} - autoComplete={autoComplete} - minLength={minLength} - onFocus={onFocus} - onBlur={onBlur} - aria-invalid={!!error} - aria-describedby={error ? errorId : undefined} - className={`w-full pl-11 pr-12 py-3 border rounded-xl bg-neutral-50/50 dark:bg-neutral-900/50 focus:bg-white dark:focus:bg-neutral-900 - transition-all duration-200 outline-none disabled:opacity-50 disabled:cursor-not-allowed dark:text-white - ${error - ? 'border-red-300 dark:border-red-800 focus:border-red-500 focus:ring-4 focus:ring-red-500/10' - : 'border-neutral-200 dark:border-neutral-800 hover:border-brand-orange/50 focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10' - }`} - /> - - {/* Lock Icon (Left) */} -
- -
- - {/* Toggle Visibility Button (Right) */} - -
- {error && ( - - )} -
- ) -} diff --git a/components/WorkspaceSwitcher.tsx b/components/WorkspaceSwitcher.tsx deleted file mode 100644 index 93fe0aa..0000000 --- a/components/WorkspaceSwitcher.tsx +++ /dev/null @@ -1,109 +0,0 @@ -'use client' - -import { useState } from 'react' -import { useRouter } from 'next/navigation' -import { PlusIcon, PersonIcon, CubeIcon, CheckIcon } from '@radix-ui/react-icons' -import { switchContext, OrganizationMember } from '@/lib/api/organization' -import { setSessionAction } from '@/app/actions/auth' -import Link from 'next/link' - -export default function WorkspaceSwitcher({ orgs, activeOrgId }: { orgs: OrganizationMember[], activeOrgId: string | null }) { - const router = useRouter() - const [switching, setSwitching] = useState(null) - - const handleSwitch = async (orgId: string | null) => { - console.log('Switching to workspace:', orgId) - setSwitching(orgId || 'personal') - try { - // * If orgId is null, we can't switch context via API in the same way if strict mode is on - // * BUT, Pulse doesn't support personal workspace. - // * So we should probably NOT show the "Personal" option in Pulse if strict mode is enforced. - // * However, to match Drop exactly, we might want to show it but have it fail or redirect? - // * Let's assume for now we want to match Drop's UI structure. - - if (!orgId) { - // * Pulse doesn't support personal context. - // * We could redirect to onboarding or show an error. - // * For now, let's just return to avoid breaking. - return - } - - const { access_token } = await switchContext(orgId) - - // * Update session cookie via server action - // * Note: switchContext only returns access_token, we keep existing refresh token - await setSessionAction(access_token) - - // Force reload to pick up new permissions - window.location.reload() - - } catch (err) { - console.error('Failed to switch workspace', err) - setSwitching(null) - } - } - - return ( -
-
- Workspaces -
- - {/* Personal Workspace - HIDDEN IN PULSE (Strict Mode) */} - {/* - - */} - - {/* Organization Workspaces */} - {orgs.map((org) => ( - - ))} - - {/* Create New */} - -
- -
- Create Organization - -
- ) -} diff --git a/components/dashboard/Chart.tsx b/components/dashboard/Chart.tsx index 17adfc2..7020802 100644 --- a/components/dashboard/Chart.tsx +++ b/components/dashboard/Chart.tsx @@ -15,7 +15,7 @@ import { import type { TooltipProps } from 'recharts' import { formatNumber, formatDuration } from '@/lib/utils/format' import { ArrowTopRightIcon, ArrowBottomRightIcon, DownloadIcon, BarChartIcon } from '@radix-ui/react-icons' -import { Button } from '@/components/ui/Button' +import { Button } from '@ciphera-net/ui' import { Checkbox } from '@/components/ui/Checkbox' const COLORS = { diff --git a/components/settings/ProfileSettings.tsx b/components/settings/ProfileSettings.tsx index 51edfe7..e7868b9 100644 --- a/components/settings/ProfileSettings.tsx +++ b/components/settings/ProfileSettings.tsx @@ -6,7 +6,7 @@ import { motion, AnimatePresence } from 'framer-motion' import { PersonIcon, LockClosedIcon, EnvelopeClosedIcon, CheckIcon, ExclamationTriangleIcon, Cross2Icon, GearIcon, MobileIcon, FileTextIcon, CopyIcon } from '@radix-ui/react-icons' // @ts-ignore import { Button, Input } from '@ciphera-net/ui' -import PasswordInput from '../PasswordInput' +import { PasswordInput } from '@ciphera-net/ui' import { toast } from 'sonner' import api from '@/lib/api/client' import { deriveAuthKey } from '@/lib/crypto/password' diff --git a/components/sites/SiteList.tsx b/components/sites/SiteList.tsx index ef5d24e..77cb2a0 100644 --- a/components/sites/SiteList.tsx +++ b/components/sites/SiteList.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react' import Link from 'next/link' import { listSites, deleteSite, type Site } from '@/lib/api/sites' import { toast } from 'sonner' -import LoadingOverlay from '../LoadingOverlay' +import { LoadingOverlay } from '@ciphera-net/ui' import { useAuth } from '@/lib/auth/context' import { BarChartIcon } from '@radix-ui/react-icons' diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx deleted file mode 100644 index 766e1cc..0000000 --- a/components/ui/Button.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; - -export interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: 'primary' | 'secondary' | 'ghost'; - isLoading?: boolean; -} - -export const Button = React.forwardRef( - ({ className = '', variant = 'primary', isLoading, children, disabled, ...props }, ref) => { - // * Base styles common to all buttons - // * Removed font-medium from here to allow variants to control weight - const baseStyles = 'inline-flex items-center justify-center rounded-xl text-sm px-5 py-2.5 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-neutral-900 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:translate-y-0 disabled:hover:shadow-none'; - - const variants = { - // * Primary: Brand orange, semibold, lift effect, colored shadow - primary: 'bg-brand-orange text-white font-semibold shadow-sm shadow-orange-200 dark:shadow-none hover:shadow-orange-300 dark:hover:shadow-brand-orange/20 hover:-translate-y-0.5 focus:ring-brand-orange', - - // * Secondary: White/Dark bg, medium weight, subtle border, hover shadow - secondary: 'bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 text-neutral-900 dark:text-white font-medium hover:bg-neutral-50 dark:hover:bg-neutral-800 shadow-sm hover:shadow-md dark:shadow-none focus:ring-neutral-200 dark:focus:ring-neutral-700', - - // * Ghost: Transparent, medium weight, hover background - ghost: 'text-neutral-600 dark:text-neutral-400 font-medium hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 focus:ring-neutral-200', - }; - - return ( - - ); - } -); - -Button.displayName = 'Button'; diff --git a/components/ui/Input.tsx b/components/ui/Input.tsx deleted file mode 100644 index 37d2535..0000000 --- a/components/ui/Input.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; - -export interface InputProps extends React.InputHTMLAttributes { - error?: string; - icon?: React.ReactNode; -} - -export const Input = React.forwardRef( - ({ className = '', error, icon, ...props }, ref) => { - return ( -
- - {icon && ( -
- {icon} -
- )} -
- ); - } -); - -Input.displayName = 'Input'; diff --git a/lib/auth/context.tsx b/lib/auth/context.tsx index ddad4f7..daa50b5 100644 --- a/lib/auth/context.tsx +++ b/lib/auth/context.tsx @@ -3,7 +3,7 @@ import React, { createContext, useContext, useEffect, useState, useCallback } from 'react' import { useRouter, usePathname } from 'next/navigation' import apiRequest from '@/lib/api/client' -import LoadingOverlay from '@/components/LoadingOverlay' +import { LoadingOverlay } from '@ciphera-net/ui' import { logoutAction, getSessionAction, setSessionAction } from '@/app/actions/auth' import { getUserOrganizations, switchContext } from '@/lib/api/organization' diff --git a/package.json b/package.json index 3e5b4ba..683c1fc 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@ciphera-net/ui": "^0.0.10", + "@ciphera-net/ui": "^0.0.11", "@radix-ui/react-icons": "^1.3.2", "axios": "^1.13.2", "country-flag-icons": "^1.6.4",