refactor: enhance type safety by replacing any types with stricter types across the codebase, improving error handling and reducing potential bugs
This commit is contained in:
@@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
|
|
||||||
- **Organization context switch.** Switching away from a deleted organization now stores the session correctly instead of using an insecure fallback.
|
- **Organization context switch.** Switching away from a deleted organization now stores the session correctly instead of using an insecure fallback.
|
||||||
- **Removed debug logs.** Auth and organization-switching details no longer leak into the browser console in production.
|
- **Removed debug logs.** Auth and organization-switching details no longer leak into the browser console in production.
|
||||||
|
- **Stricter type safety.** Eliminated all `any` types and `@ts-ignore` suppressions across the codebase, so the TypeScript compiler catches more bugs at build time.
|
||||||
|
|
||||||
## [0.10.0-alpha] - 2026-02-21
|
## [0.10.0-alpha] - 2026-02-21
|
||||||
|
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export default function HomePage() {
|
|||||||
setSitesLoading(true)
|
setSitesLoading(true)
|
||||||
const data = await listSites()
|
const data = await listSites()
|
||||||
setSites(Array.isArray(data) ? data : [])
|
setSites(Array.isArray(data) ? data : [])
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(getAuthErrorMessage(error) || 'Failed to load your sites')
|
toast.error(getAuthErrorMessage(error) || 'Failed to load your sites')
|
||||||
setSites([])
|
setSites([])
|
||||||
} finally {
|
} finally {
|
||||||
@@ -198,7 +198,7 @@ export default function HomePage() {
|
|||||||
await deleteSite(id)
|
await deleteSite(id)
|
||||||
toast.success('Site deleted successfully')
|
toast.success('Site deleted successfully')
|
||||||
loadSites()
|
loadSites()
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(getAuthErrorMessage(error) || 'Failed to delete site')
|
toast.error(getAuthErrorMessage(error) || 'Failed to delete site')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useParams, useSearchParams, useRouter } from 'next/navigation'
|
|||||||
import { getPublicDashboard, getPublicStats, getPublicDailyStats, getPublicRealtime, getPublicPerformanceByPage, type DashboardData, type Stats, type DailyStat, type PerformanceByPageStat } from '@/lib/api/stats'
|
import { getPublicDashboard, getPublicStats, getPublicDailyStats, getPublicRealtime, getPublicPerformanceByPage, type DashboardData, type Stats, type DailyStat, type PerformanceByPageStat } from '@/lib/api/stats'
|
||||||
import { toast } from '@ciphera-net/ui'
|
import { toast } from '@ciphera-net/ui'
|
||||||
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
||||||
|
import { ApiError } from '@/lib/api/client'
|
||||||
import { LoadingOverlay, Button } from '@ciphera-net/ui'
|
import { LoadingOverlay, Button } from '@ciphera-net/ui'
|
||||||
import Chart from '@/components/dashboard/Chart'
|
import Chart from '@/components/dashboard/Chart'
|
||||||
import TopPages from '@/components/dashboard/ContentStats'
|
import TopPages from '@/components/dashboard/ContentStats'
|
||||||
@@ -154,8 +155,9 @@ export default function PublicDashboardPage() {
|
|||||||
setCaptchaId('')
|
setCaptchaId('')
|
||||||
setCaptchaSolution('')
|
setCaptchaSolution('')
|
||||||
setCaptchaToken('')
|
setCaptchaToken('')
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
if ((error.status === 401 || error.response?.status === 401) && (error.data?.is_protected || error.response?.data?.is_protected)) {
|
const apiErr = error instanceof ApiError ? error : null
|
||||||
|
if (apiErr?.status === 401 && (apiErr.data as Record<string, unknown>)?.is_protected) {
|
||||||
setIsPasswordProtected(true)
|
setIsPasswordProtected(true)
|
||||||
if (password) {
|
if (password) {
|
||||||
toast.error('Invalid password or captcha')
|
toast.error('Invalid password or captcha')
|
||||||
@@ -164,7 +166,7 @@ export default function PublicDashboardPage() {
|
|||||||
setCaptchaSolution('')
|
setCaptchaSolution('')
|
||||||
setCaptchaToken('')
|
setCaptchaToken('')
|
||||||
}
|
}
|
||||||
} else if (error.status === 404 || error.response?.status === 404) {
|
} else if (apiErr?.status === 404) {
|
||||||
toast.error('Site not found')
|
toast.error('Site not found')
|
||||||
} else if (!silent) {
|
} else if (!silent) {
|
||||||
toast.error(getAuthErrorMessage(error) || 'Failed to load public dashboard')
|
toast.error(getAuthErrorMessage(error) || 'Failed to load public dashboard')
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export default function SiteSettingsPage() {
|
|||||||
} else {
|
} else {
|
||||||
setIsPasswordEnabled(false)
|
setIsPasswordEnabled(false)
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(getAuthErrorMessage(error) || 'Failed to load site settings')
|
toast.error(getAuthErrorMessage(error) || 'Failed to load site settings')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -295,7 +295,7 @@ export default function SiteSettingsPage() {
|
|||||||
data_retention_months: formData.data_retention_months
|
data_retention_months: formData.data_retention_months
|
||||||
})
|
})
|
||||||
loadSite()
|
loadSite()
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(getAuthErrorMessage(error) || 'Failed to save site settings')
|
toast.error(getAuthErrorMessage(error) || 'Failed to save site settings')
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
@@ -310,7 +310,7 @@ export default function SiteSettingsPage() {
|
|||||||
try {
|
try {
|
||||||
await resetSiteData(siteId)
|
await resetSiteData(siteId)
|
||||||
toast.success('All site data has been reset')
|
toast.success('All site data has been reset')
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(getAuthErrorMessage(error) || 'Failed to reset site data')
|
toast.error(getAuthErrorMessage(error) || 'Failed to reset site data')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,7 +326,7 @@ export default function SiteSettingsPage() {
|
|||||||
await deleteSite(siteId)
|
await deleteSite(siteId)
|
||||||
toast.success('Site deleted successfully')
|
toast.success('Site deleted successfully')
|
||||||
router.push('/')
|
router.push('/')
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(getAuthErrorMessage(error) || 'Failed to delete site')
|
toast.error(getAuthErrorMessage(error) || 'Failed to delete site')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Image from 'next/image'
|
|||||||
import { GithubIcon, TwitterIcon, SwissFlagIcon } from '@ciphera-net/ui'
|
import { GithubIcon, TwitterIcon, SwissFlagIcon } from '@ciphera-net/ui'
|
||||||
|
|
||||||
interface FooterProps {
|
interface FooterProps {
|
||||||
LinkComponent?: any
|
LinkComponent?: React.ElementType
|
||||||
appName?: string
|
appName?: string
|
||||||
isAuthenticated?: boolean
|
isAuthenticated?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,8 +150,7 @@ export default function PricingSection() {
|
|||||||
|
|
||||||
// Helper to get all price details
|
// Helper to get all price details
|
||||||
const getPriceDetails = (planId: string) => {
|
const getPriceDetails = (planId: string) => {
|
||||||
// @ts-ignore
|
const basePrice = currentTraffic.prices[planId as keyof typeof currentTraffic.prices]
|
||||||
const basePrice = currentTraffic.prices[planId]
|
|
||||||
|
|
||||||
// Handle "Custom"
|
// Handle "Custom"
|
||||||
if (basePrice === null || basePrice === undefined) return null
|
if (basePrice === null || basePrice === undefined) return null
|
||||||
@@ -203,7 +202,7 @@ export default function PricingSection() {
|
|||||||
throw new Error('No checkout URL returned')
|
throw new Error('No checkout URL returned')
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error('Checkout error:', error)
|
console.error('Checkout error:', error)
|
||||||
toast.error('Failed to start checkout — please try again')
|
toast.error('Failed to start checkout — please try again')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function Locations({ countries, cities }: LocationProps) {
|
|||||||
if (!countryCode || countryCode === 'Unknown') return null
|
if (!countryCode || countryCode === 'Unknown') return null
|
||||||
// * The API returns 2-letter country codes (e.g. US, DE)
|
// * The API returns 2-letter country codes (e.g. US, DE)
|
||||||
// * We cast it to the flag component name
|
// * We cast it to the flag component name
|
||||||
const FlagComponent = (Flags as any)[countryCode]
|
const FlagComponent = (Flags as Record<string, React.ComponentType<{ className?: string }>>)[countryCode]
|
||||||
return FlagComponent ? <FlagComponent className="w-5 h-5 rounded-sm shadow-sm" /> : null
|
return FlagComponent ? <FlagComponent className="w-5 h-5 rounded-sm shadow-sm" /> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
|||||||
|
|
||||||
// Prepare data
|
// Prepare data
|
||||||
const exportData = data.map((item) => {
|
const exportData = data.map((item) => {
|
||||||
const filteredItem: Partial<DailyStat> = {}
|
const filteredItem: Record<string, string | number> = {}
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
(filteredItem as any)[field] = item[field]
|
filteredItem[field] = item[field]
|
||||||
})
|
})
|
||||||
return filteredItem
|
return filteredItem
|
||||||
})
|
})
|
||||||
@@ -212,7 +212,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
|||||||
autoTable(doc, {
|
autoTable(doc, {
|
||||||
startY: startY,
|
startY: startY,
|
||||||
head: [fields.map(f => f.charAt(0).toUpperCase() + f.slice(1).replace('_', ' '))],
|
head: [fields.map(f => f.charAt(0).toUpperCase() + f.slice(1).replace('_', ' '))],
|
||||||
body: tableData as any[][],
|
body: tableData as (string | number)[][],
|
||||||
styles: {
|
styles: {
|
||||||
font: 'helvetica',
|
font: 'helvetica',
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
@@ -249,7 +249,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let finalY = (doc as any).lastAutoTable.finalY + 10
|
let finalY = doc.lastAutoTable.finalY + 10
|
||||||
|
|
||||||
// Top Pages Table
|
// Top Pages Table
|
||||||
if (topPages && topPages.length > 0) {
|
if (topPages && topPages.length > 0) {
|
||||||
@@ -276,7 +276,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
|||||||
alternateRowStyles: { fillColor: [255, 250, 245] },
|
alternateRowStyles: { fillColor: [255, 250, 245] },
|
||||||
})
|
})
|
||||||
|
|
||||||
finalY = (doc as any).lastAutoTable.finalY + 10
|
finalY = doc.lastAutoTable.finalY + 10
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top Referrers Table
|
// Top Referrers Table
|
||||||
@@ -305,7 +305,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
|||||||
alternateRowStyles: { fillColor: [255, 250, 245] },
|
alternateRowStyles: { fillColor: [255, 250, 245] },
|
||||||
})
|
})
|
||||||
|
|
||||||
finalY = (doc as any).lastAutoTable.finalY + 10
|
finalY = doc.lastAutoTable.finalY + 10
|
||||||
}
|
}
|
||||||
|
|
||||||
// Campaigns Table
|
// Campaigns Table
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { formatNumber } from '@ciphera-net/ui'
|
import { formatNumber } from '@ciphera-net/ui'
|
||||||
import * as Flags from 'country-flag-icons/react/3x2'
|
import * as Flags from 'country-flag-icons/react/3x2'
|
||||||
// @ts-ignore
|
|
||||||
import iso3166 from 'iso-3166-2'
|
import iso3166 from 'iso-3166-2'
|
||||||
import WorldMap from './WorldMap'
|
import WorldMap from './WorldMap'
|
||||||
import { Modal, GlobeIcon } from '@ciphera-net/ui'
|
import { Modal, GlobeIcon } from '@ciphera-net/ui'
|
||||||
@@ -28,7 +27,8 @@ const LIMIT = 7
|
|||||||
export default function Locations({ countries, cities, regions, geoDataLevel = 'full', siteId, dateRange }: LocationProps) {
|
export default function Locations({ countries, cities, regions, geoDataLevel = 'full', siteId, dateRange }: LocationProps) {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('map')
|
const [activeTab, setActiveTab] = useState<Tab>('map')
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [fullData, setFullData] = useState<any[]>([])
|
type LocationItem = { country?: string; city?: string; region?: string; pageviews: number }
|
||||||
|
const [fullData, setFullData] = useState<LocationItem[]>([])
|
||||||
const [isLoadingFull, setIsLoadingFull] = useState(false)
|
const [isLoadingFull, setIsLoadingFull] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -36,7 +36,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setIsLoadingFull(true)
|
setIsLoadingFull(true)
|
||||||
try {
|
try {
|
||||||
let data: any[] = []
|
let data: LocationItem[] = []
|
||||||
if (activeTab === 'countries') {
|
if (activeTab === 'countries') {
|
||||||
data = await getCountries(siteId, dateRange.start, dateRange.end, 250)
|
data = await getCountries(siteId, dateRange.start, dateRange.end, 250)
|
||||||
} else if (activeTab === 'regions') {
|
} else if (activeTab === 'regions') {
|
||||||
@@ -73,7 +73,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
|||||||
return <GlobeIcon className="w-5 h-5 text-neutral-500 dark:text-neutral-400" />
|
return <GlobeIcon className="w-5 h-5 text-neutral-500 dark:text-neutral-400" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const FlagComponent = (Flags as any)[countryCode]
|
const FlagComponent = (Flags as Record<string, React.ComponentType<{ className?: string }>>)[countryCode]
|
||||||
return FlagComponent ? <FlagComponent className="w-5 h-5 rounded-sm shadow-sm" /> : null
|
return FlagComponent ? <FlagComponent className="w-5 h-5 rounded-sm shadow-sm" /> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter out "Unknown" entries that result from disabled collection
|
// Filter out "Unknown" entries that result from disabled collection
|
||||||
const filterUnknown = (data: any[]) => {
|
const filterUnknown = (data: LocationItem[]) => {
|
||||||
return data.filter(item => {
|
return data.filter(item => {
|
||||||
if (activeTab === 'countries') return item.country && item.country !== 'Unknown' && item.country !== ''
|
if (activeTab === 'countries') return item.country && item.country !== 'Unknown' && item.country !== ''
|
||||||
if (activeTab === 'regions') return item.region && item.region !== 'Unknown' && item.region !== ''
|
if (activeTab === 'regions') return item.region && item.region !== 'Unknown' && item.region !== ''
|
||||||
@@ -172,7 +172,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
|||||||
const hasData = activeTab === 'map'
|
const hasData = activeTab === 'map'
|
||||||
? (countries && filterUnknown(countries).length > 0)
|
? (countries && filterUnknown(countries).length > 0)
|
||||||
: (data && data.length > 0)
|
: (data && data.length > 0)
|
||||||
const displayedData = (activeTab !== 'map' && hasData) ? (data as any[]).slice(0, LIMIT) : []
|
const displayedData = (activeTab !== 'map' && hasData) ? data.slice(0, LIMIT) : []
|
||||||
const emptySlots = Math.max(0, LIMIT - displayedData.length)
|
const emptySlots = Math.max(0, LIMIT - displayedData.length)
|
||||||
const showViewAll = activeTab !== 'map' && hasData && data.length > LIMIT
|
const showViewAll = activeTab !== 'map' && hasData && data.length > LIMIT
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
|||||||
<p className="text-neutral-500 dark:text-neutral-400 text-sm">{getDisabledMessage()}</p>
|
<p className="text-neutral-500 dark:text-neutral-400 text-sm">{getDisabledMessage()}</p>
|
||||||
</div>
|
</div>
|
||||||
) : activeTab === 'map' ? (
|
) : activeTab === 'map' ? (
|
||||||
hasData ? <WorldMap data={filterUnknown(countries)} /> : (
|
hasData ? <WorldMap data={filterUnknown(countries) as { country: string; pageviews: number }[]} /> : (
|
||||||
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
|
||||||
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||||
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
<GlobeIcon className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
|
||||||
@@ -247,13 +247,13 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
|||||||
{displayedData.map((item, index) => (
|
{displayedData.map((item, index) => (
|
||||||
<div key={index} className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
|
<div key={index} className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
|
||||||
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
|
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
|
||||||
{activeTab === 'countries' && <span className="shrink-0">{getFlagComponent(item.country)}</span>}
|
{activeTab === 'countries' && <span className="shrink-0">{getFlagComponent(item.country ?? '')}</span>}
|
||||||
{activeTab !== 'countries' && <span className="shrink-0">{getFlagComponent(item.country)}</span>}
|
{activeTab !== 'countries' && <span className="shrink-0">{getFlagComponent(item.country ?? '')}</span>}
|
||||||
|
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{activeTab === 'countries' ? getCountryName(item.country) :
|
{activeTab === 'countries' ? getCountryName(item.country ?? '') :
|
||||||
activeTab === 'regions' ? getRegionName(item.region, item.country) :
|
activeTab === 'regions' ? getRegionName(item.region ?? '', item.country ?? '') :
|
||||||
getCityName(item.city)}
|
getCityName(item.city ?? '')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">
|
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">
|
||||||
@@ -293,14 +293,14 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
|||||||
<ListSkeleton rows={10} />
|
<ListSkeleton rows={10} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
(fullData.length > 0 ? fullData : data as any[]).map((item, index) => (
|
(fullData.length > 0 ? fullData : data).map((item, index) => (
|
||||||
<div key={index} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
|
<div key={index} className="flex items-center justify-between py-2 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
|
||||||
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
|
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
|
||||||
<span className="shrink-0">{getFlagComponent(item.country)}</span>
|
<span className="shrink-0">{getFlagComponent(item.country ?? '')}</span>
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{activeTab === 'countries' ? getCountryName(item.country) :
|
{activeTab === 'countries' ? getCountryName(item.country ?? '') :
|
||||||
activeTab === 'regions' ? getRegionName(item.region, item.country) :
|
activeTab === 'regions' ? getRegionName(item.region ?? '', item.country ?? '') :
|
||||||
getCityName(item.city)}
|
getCityName(item.city ?? '')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">
|
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ const LIMIT = 7
|
|||||||
export default function TechSpecs({ browsers, os, devices, screenResolutions, collectDeviceInfo = true, collectScreenResolution = true, siteId, dateRange }: TechSpecsProps) {
|
export default function TechSpecs({ browsers, os, devices, screenResolutions, collectDeviceInfo = true, collectScreenResolution = true, siteId, dateRange }: TechSpecsProps) {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('browsers')
|
const [activeTab, setActiveTab] = useState<Tab>('browsers')
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [fullData, setFullData] = useState<any[]>([])
|
type TechItem = { name: string; pageviews: number; icon: React.ReactNode }
|
||||||
|
const [fullData, setFullData] = useState<TechItem[]>([])
|
||||||
const [isLoadingFull, setIsLoadingFull] = useState(false)
|
const [isLoadingFull, setIsLoadingFull] = useState(false)
|
||||||
|
|
||||||
// Filter out "Unknown" entries that result from disabled collection
|
// Filter out "Unknown" entries that result from disabled collection
|
||||||
@@ -39,7 +40,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setIsLoadingFull(true)
|
setIsLoadingFull(true)
|
||||||
try {
|
try {
|
||||||
let data: any[] = []
|
let data: TechItem[] = []
|
||||||
if (activeTab === 'browsers') {
|
if (activeTab === 'browsers') {
|
||||||
const res = await getBrowsers(siteId, dateRange.start, dateRange.end, 100)
|
const res = await getBrowsers(siteId, dateRange.start, dateRange.end, 100)
|
||||||
data = res.map(b => ({ name: b.browser, pageviews: b.pageviews, icon: getBrowserIcon(b.browser) }))
|
data = res.map(b => ({ name: b.browser, pageviews: b.pageviews, icon: getBrowserIcon(b.browser) }))
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ function BellIcon({ className }: { className?: string }) {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
|
||||||
import { Button, Input } from '@ciphera-net/ui'
|
import { Button, Input } from '@ciphera-net/ui'
|
||||||
|
|
||||||
export default function OrganizationSettings() {
|
export default function OrganizationSettings() {
|
||||||
@@ -333,8 +332,8 @@ export default function OrganizationSettings() {
|
|||||||
try {
|
try {
|
||||||
const { url } = await createPortalSession()
|
const { url } = await createPortalSession()
|
||||||
window.location.href = url
|
window.location.href = url
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to open billing portal')
|
toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to open billing portal')
|
||||||
setIsRedirectingToPortal(false)
|
setIsRedirectingToPortal(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,8 +345,8 @@ export default function OrganizationSettings() {
|
|||||||
toast.success(atPeriodEnd ? 'Subscription will cancel at the end of the billing period.' : 'Subscription canceled.')
|
toast.success(atPeriodEnd ? 'Subscription will cancel at the end of the billing period.' : 'Subscription canceled.')
|
||||||
setShowCancelPrompt(false)
|
setShowCancelPrompt(false)
|
||||||
loadSubscription()
|
loadSubscription()
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to cancel subscription')
|
toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to cancel subscription')
|
||||||
} finally {
|
} finally {
|
||||||
setCancelLoadingAction(null)
|
setCancelLoadingAction(null)
|
||||||
}
|
}
|
||||||
@@ -359,8 +358,8 @@ export default function OrganizationSettings() {
|
|||||||
await resumeSubscription()
|
await resumeSubscription()
|
||||||
toast.success('Subscription will continue. Cancellation has been undone.')
|
toast.success('Subscription will continue. Cancellation has been undone.')
|
||||||
loadSubscription()
|
loadSubscription()
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to resume subscription')
|
toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to resume subscription')
|
||||||
} finally {
|
} finally {
|
||||||
setIsResuming(false)
|
setIsResuming(false)
|
||||||
}
|
}
|
||||||
@@ -398,8 +397,8 @@ export default function OrganizationSettings() {
|
|||||||
if (url) window.location.href = url
|
if (url) window.location.href = url
|
||||||
else throw new Error('No checkout URL')
|
else throw new Error('No checkout URL')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to update member role')
|
toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to update plan')
|
||||||
} finally {
|
} finally {
|
||||||
setIsChangingPlan(false)
|
setIsChangingPlan(false)
|
||||||
}
|
}
|
||||||
@@ -427,9 +426,9 @@ export default function OrganizationSettings() {
|
|||||||
window.location.href = '/'
|
window.location.href = '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
toast.error(getAuthErrorMessage(err) || err.message || 'Failed to delete organization')
|
toast.error(getAuthErrorMessage(err) || (err instanceof Error ? err.message : '') || 'Failed to delete organization')
|
||||||
setIsDeleting(false)
|
setIsDeleting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -457,8 +456,8 @@ export default function OrganizationSettings() {
|
|||||||
setCaptchaSolution('')
|
setCaptchaSolution('')
|
||||||
setCaptchaToken('')
|
setCaptchaToken('')
|
||||||
loadMembers() // Refresh list
|
loadMembers() // Refresh list
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to send invitation')
|
toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to send invitation')
|
||||||
} finally {
|
} finally {
|
||||||
setIsInviting(false)
|
setIsInviting(false)
|
||||||
}
|
}
|
||||||
@@ -469,8 +468,8 @@ export default function OrganizationSettings() {
|
|||||||
await revokeInvitation(currentOrgId, inviteId)
|
await revokeInvitation(currentOrgId, inviteId)
|
||||||
toast.success('Invitation revoked')
|
toast.success('Invitation revoked')
|
||||||
loadMembers() // Refresh list
|
loadMembers() // Refresh list
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to revoke invitation')
|
toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to revoke invitation')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,8 +483,8 @@ export default function OrganizationSettings() {
|
|||||||
toast.success('Organization updated successfully')
|
toast.success('Organization updated successfully')
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
loadMembers()
|
loadMembers()
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to save organization settings')
|
toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to save organization settings')
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false)
|
setIsSaving(false)
|
||||||
}
|
}
|
||||||
@@ -603,7 +602,7 @@ export default function OrganizationSettings() {
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={orgName}
|
value={orgName}
|
||||||
onChange={(e: any) => setOrgName(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setOrgName(e.target.value)}
|
||||||
required
|
required
|
||||||
minLength={2}
|
minLength={2}
|
||||||
maxLength={50}
|
maxLength={50}
|
||||||
@@ -623,7 +622,7 @@ export default function OrganizationSettings() {
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={orgSlug}
|
value={orgSlug}
|
||||||
onChange={(e: any) => setOrgSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setOrgSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
|
||||||
required
|
required
|
||||||
minLength={3}
|
minLength={3}
|
||||||
maxLength={30}
|
maxLength={30}
|
||||||
@@ -703,7 +702,7 @@ export default function OrganizationSettings() {
|
|||||||
type="email"
|
type="email"
|
||||||
placeholder="colleague@company.com"
|
placeholder="colleague@company.com"
|
||||||
value={inviteEmail}
|
value={inviteEmail}
|
||||||
onChange={(e: any) => setInviteEmail(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInviteEmail(e.target.value)}
|
||||||
required
|
required
|
||||||
className="bg-white dark:bg-neutral-900"
|
className="bg-white dark:bg-neutral-900"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ export function getSignupUrl(redirectPath = '/auth/callback') {
|
|||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
status: number
|
status: number
|
||||||
data?: any
|
data?: Record<string, unknown>
|
||||||
|
|
||||||
constructor(message: string, status: number, data?: any) {
|
constructor(message: string, status: number, data?: Record<string, unknown>) {
|
||||||
super(message)
|
super(message)
|
||||||
this.status = status
|
this.status = status
|
||||||
this.data = data
|
this.data = data
|
||||||
|
|||||||
@@ -86,10 +86,7 @@ export async function sendInvitation(
|
|||||||
role: string = 'member',
|
role: string = 'member',
|
||||||
captcha?: { captcha_id?: string, captcha_solution?: string, captcha_token?: string }
|
captcha?: { captcha_id?: string, captcha_solution?: string, captcha_token?: string }
|
||||||
): Promise<OrganizationInvitation> {
|
): Promise<OrganizationInvitation> {
|
||||||
const body: any = {
|
const body: Record<string, string> = { email, role }
|
||||||
email,
|
|
||||||
role
|
|
||||||
}
|
|
||||||
|
|
||||||
if (captcha?.captcha_id) body.captcha_id = captcha.captcha_id
|
if (captcha?.captcha_id) body.captcha_id = captcha.captcha_id
|
||||||
if (captcha?.captcha_solution) body.captcha_solution = captcha.captcha_solution
|
if (captcha?.captcha_solution) body.captcha_solution = captcha.captcha_solution
|
||||||
|
|||||||
21
types/iso-3166-2.d.ts
vendored
Normal file
21
types/iso-3166-2.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
declare module 'iso-3166-2' {
|
||||||
|
interface SubdivisionInfo {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
parent?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CountryInfo {
|
||||||
|
name: string
|
||||||
|
sub: Record<string, SubdivisionInfo>
|
||||||
|
}
|
||||||
|
|
||||||
|
const iso3166: {
|
||||||
|
data: Record<string, CountryInfo>
|
||||||
|
country(code: string): CountryInfo | undefined
|
||||||
|
subdivision(code: string): SubdivisionInfo | undefined
|
||||||
|
codes: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default iso3166
|
||||||
|
}
|
||||||
9
types/jspdf-autotable.d.ts
vendored
Normal file
9
types/jspdf-autotable.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import 'jspdf'
|
||||||
|
|
||||||
|
declare module 'jspdf' {
|
||||||
|
interface jsPDF {
|
||||||
|
lastAutoTable: {
|
||||||
|
finalY: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user