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.
|
||||
- **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
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ export default function HomePage() {
|
||||
setSitesLoading(true)
|
||||
const data = await listSites()
|
||||
setSites(Array.isArray(data) ? data : [])
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || 'Failed to load your sites')
|
||||
setSites([])
|
||||
} finally {
|
||||
@@ -198,7 +198,7 @@ export default function HomePage() {
|
||||
await deleteSite(id)
|
||||
toast.success('Site deleted successfully')
|
||||
loadSites()
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
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 { toast } from '@ciphera-net/ui'
|
||||
import { getAuthErrorMessage } from '@ciphera-net/ui'
|
||||
import { ApiError } from '@/lib/api/client'
|
||||
import { LoadingOverlay, Button } from '@ciphera-net/ui'
|
||||
import Chart from '@/components/dashboard/Chart'
|
||||
import TopPages from '@/components/dashboard/ContentStats'
|
||||
@@ -154,8 +155,9 @@ export default function PublicDashboardPage() {
|
||||
setCaptchaId('')
|
||||
setCaptchaSolution('')
|
||||
setCaptchaToken('')
|
||||
} catch (error: any) {
|
||||
if ((error.status === 401 || error.response?.status === 401) && (error.data?.is_protected || error.response?.data?.is_protected)) {
|
||||
} catch (error: unknown) {
|
||||
const apiErr = error instanceof ApiError ? error : null
|
||||
if (apiErr?.status === 401 && (apiErr.data as Record<string, unknown>)?.is_protected) {
|
||||
setIsPasswordProtected(true)
|
||||
if (password) {
|
||||
toast.error('Invalid password or captcha')
|
||||
@@ -164,7 +166,7 @@ export default function PublicDashboardPage() {
|
||||
setCaptchaSolution('')
|
||||
setCaptchaToken('')
|
||||
}
|
||||
} else if (error.status === 404 || error.response?.status === 404) {
|
||||
} else if (apiErr?.status === 404) {
|
||||
toast.error('Site not found')
|
||||
} else if (!silent) {
|
||||
toast.error(getAuthErrorMessage(error) || 'Failed to load public dashboard')
|
||||
|
||||
@@ -167,7 +167,7 @@ export default function SiteSettingsPage() {
|
||||
} else {
|
||||
setIsPasswordEnabled(false)
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || 'Failed to load site settings')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -295,7 +295,7 @@ export default function SiteSettingsPage() {
|
||||
data_retention_months: formData.data_retention_months
|
||||
})
|
||||
loadSite()
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || 'Failed to save site settings')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
@@ -310,7 +310,7 @@ export default function SiteSettingsPage() {
|
||||
try {
|
||||
await resetSiteData(siteId)
|
||||
toast.success('All site data has been reset')
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || 'Failed to reset site data')
|
||||
}
|
||||
}
|
||||
@@ -326,7 +326,7 @@ export default function SiteSettingsPage() {
|
||||
await deleteSite(siteId)
|
||||
toast.success('Site deleted successfully')
|
||||
router.push('/')
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
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'
|
||||
|
||||
interface FooterProps {
|
||||
LinkComponent?: any
|
||||
LinkComponent?: React.ElementType
|
||||
appName?: string
|
||||
isAuthenticated?: boolean
|
||||
}
|
||||
|
||||
@@ -150,8 +150,7 @@ export default function PricingSection() {
|
||||
|
||||
// Helper to get all price details
|
||||
const getPriceDetails = (planId: string) => {
|
||||
// @ts-ignore
|
||||
const basePrice = currentTraffic.prices[planId]
|
||||
const basePrice = currentTraffic.prices[planId as keyof typeof currentTraffic.prices]
|
||||
|
||||
// Handle "Custom"
|
||||
if (basePrice === null || basePrice === undefined) return null
|
||||
@@ -203,7 +202,7 @@ export default function PricingSection() {
|
||||
throw new Error('No checkout URL returned')
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error('Checkout error:', error)
|
||||
toast.error('Failed to start checkout — please try again')
|
||||
} finally {
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function Locations({ countries, cities }: LocationProps) {
|
||||
if (!countryCode || countryCode === 'Unknown') return null
|
||||
// * The API returns 2-letter country codes (e.g. US, DE)
|
||||
// * 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
|
||||
}
|
||||
|
||||
|
||||
@@ -67,9 +67,9 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
||||
|
||||
// Prepare data
|
||||
const exportData = data.map((item) => {
|
||||
const filteredItem: Partial<DailyStat> = {}
|
||||
const filteredItem: Record<string, string | number> = {}
|
||||
fields.forEach((field) => {
|
||||
(filteredItem as any)[field] = item[field]
|
||||
filteredItem[field] = item[field]
|
||||
})
|
||||
return filteredItem
|
||||
})
|
||||
@@ -212,7 +212,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
||||
autoTable(doc, {
|
||||
startY: startY,
|
||||
head: [fields.map(f => f.charAt(0).toUpperCase() + f.slice(1).replace('_', ' '))],
|
||||
body: tableData as any[][],
|
||||
body: tableData as (string | number)[][],
|
||||
styles: {
|
||||
font: 'helvetica',
|
||||
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
|
||||
if (topPages && topPages.length > 0) {
|
||||
@@ -276,7 +276,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
||||
alternateRowStyles: { fillColor: [255, 250, 245] },
|
||||
})
|
||||
|
||||
finalY = (doc as any).lastAutoTable.finalY + 10
|
||||
finalY = doc.lastAutoTable.finalY + 10
|
||||
}
|
||||
|
||||
// Top Referrers Table
|
||||
@@ -305,7 +305,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
||||
alternateRowStyles: { fillColor: [255, 250, 245] },
|
||||
})
|
||||
|
||||
finalY = (doc as any).lastAutoTable.finalY + 10
|
||||
finalY = doc.lastAutoTable.finalY + 10
|
||||
}
|
||||
|
||||
// Campaigns Table
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { formatNumber } from '@ciphera-net/ui'
|
||||
import * as Flags from 'country-flag-icons/react/3x2'
|
||||
// @ts-ignore
|
||||
import iso3166 from 'iso-3166-2'
|
||||
import WorldMap from './WorldMap'
|
||||
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) {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('map')
|
||||
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)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -36,7 +36,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
const fetchData = async () => {
|
||||
setIsLoadingFull(true)
|
||||
try {
|
||||
let data: any[] = []
|
||||
let data: LocationItem[] = []
|
||||
if (activeTab === 'countries') {
|
||||
data = await getCountries(siteId, dateRange.start, dateRange.end, 250)
|
||||
} 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" />
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
|
||||
}
|
||||
|
||||
// Filter out "Unknown" entries that result from disabled collection
|
||||
const filterUnknown = (data: any[]) => {
|
||||
const filterUnknown = (data: LocationItem[]) => {
|
||||
return data.filter(item => {
|
||||
if (activeTab === 'countries') return item.country && item.country !== 'Unknown' && item.country !== ''
|
||||
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'
|
||||
? (countries && filterUnknown(countries).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 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>
|
||||
</div>
|
||||
) : 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="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
|
||||
<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) => (
|
||||
<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">
|
||||
{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">
|
||||
{activeTab === 'countries' ? getCountryName(item.country) :
|
||||
activeTab === 'regions' ? getRegionName(item.region, item.country) :
|
||||
getCityName(item.city)}
|
||||
{activeTab === 'countries' ? getCountryName(item.country ?? '') :
|
||||
activeTab === 'regions' ? getRegionName(item.region ?? '', item.country ?? '') :
|
||||
getCityName(item.city ?? '')}
|
||||
</span>
|
||||
</div>
|
||||
<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} />
|
||||
</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 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">
|
||||
{activeTab === 'countries' ? getCountryName(item.country) :
|
||||
activeTab === 'regions' ? getRegionName(item.region, item.country) :
|
||||
getCityName(item.city)}
|
||||
{activeTab === 'countries' ? getCountryName(item.country ?? '') :
|
||||
activeTab === 'regions' ? getRegionName(item.region ?? '', item.country ?? '') :
|
||||
getCityName(item.city ?? '')}
|
||||
</span>
|
||||
</div>
|
||||
<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) {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('browsers')
|
||||
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)
|
||||
|
||||
// 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 () => {
|
||||
setIsLoadingFull(true)
|
||||
try {
|
||||
let data: any[] = []
|
||||
let data: TechItem[] = []
|
||||
if (activeTab === 'browsers') {
|
||||
const res = await getBrowsers(siteId, dateRange.start, dateRange.end, 100)
|
||||
data = res.map(b => ({ name: b.browser, pageviews: b.pageviews, icon: getBrowserIcon(b.browser) }))
|
||||
|
||||
@@ -49,7 +49,6 @@ function BellIcon({ className }: { className?: string }) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
// @ts-ignore
|
||||
import { Button, Input } from '@ciphera-net/ui'
|
||||
|
||||
export default function OrganizationSettings() {
|
||||
@@ -333,8 +332,8 @@ export default function OrganizationSettings() {
|
||||
try {
|
||||
const { url } = await createPortalSession()
|
||||
window.location.href = url
|
||||
} catch (error: any) {
|
||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to open billing portal')
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to open billing portal')
|
||||
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.')
|
||||
setShowCancelPrompt(false)
|
||||
loadSubscription()
|
||||
} catch (error: any) {
|
||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to cancel subscription')
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to cancel subscription')
|
||||
} finally {
|
||||
setCancelLoadingAction(null)
|
||||
}
|
||||
@@ -359,8 +358,8 @@ export default function OrganizationSettings() {
|
||||
await resumeSubscription()
|
||||
toast.success('Subscription will continue. Cancellation has been undone.')
|
||||
loadSubscription()
|
||||
} catch (error: any) {
|
||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to resume subscription')
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to resume subscription')
|
||||
} finally {
|
||||
setIsResuming(false)
|
||||
}
|
||||
@@ -398,8 +397,8 @@ export default function OrganizationSettings() {
|
||||
if (url) window.location.href = url
|
||||
else throw new Error('No checkout URL')
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to update member role')
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to update plan')
|
||||
} finally {
|
||||
setIsChangingPlan(false)
|
||||
}
|
||||
@@ -427,9 +426,9 @@ export default function OrganizationSettings() {
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -457,8 +456,8 @@ export default function OrganizationSettings() {
|
||||
setCaptchaSolution('')
|
||||
setCaptchaToken('')
|
||||
loadMembers() // Refresh list
|
||||
} catch (error: any) {
|
||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to send invitation')
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to send invitation')
|
||||
} finally {
|
||||
setIsInviting(false)
|
||||
}
|
||||
@@ -469,8 +468,8 @@ export default function OrganizationSettings() {
|
||||
await revokeInvitation(currentOrgId, inviteId)
|
||||
toast.success('Invitation revoked')
|
||||
loadMembers() // Refresh list
|
||||
} catch (error: any) {
|
||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to revoke invitation')
|
||||
} catch (error: unknown) {
|
||||
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')
|
||||
setIsEditing(false)
|
||||
loadMembers()
|
||||
} catch (error: any) {
|
||||
toast.error(getAuthErrorMessage(error) || error.message || 'Failed to save organization settings')
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || (error instanceof Error ? error.message : '') || 'Failed to save organization settings')
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
@@ -603,7 +602,7 @@ export default function OrganizationSettings() {
|
||||
<Input
|
||||
type="text"
|
||||
value={orgName}
|
||||
onChange={(e: any) => setOrgName(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setOrgName(e.target.value)}
|
||||
required
|
||||
minLength={2}
|
||||
maxLength={50}
|
||||
@@ -623,7 +622,7 @@ export default function OrganizationSettings() {
|
||||
<Input
|
||||
type="text"
|
||||
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
|
||||
minLength={3}
|
||||
maxLength={30}
|
||||
@@ -703,7 +702,7 @@ export default function OrganizationSettings() {
|
||||
type="email"
|
||||
placeholder="colleague@company.com"
|
||||
value={inviteEmail}
|
||||
onChange={(e: any) => setInviteEmail(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInviteEmail(e.target.value)}
|
||||
required
|
||||
className="bg-white dark:bg-neutral-900"
|
||||
/>
|
||||
|
||||
@@ -24,9 +24,9 @@ export function getSignupUrl(redirectPath = '/auth/callback') {
|
||||
|
||||
export class ApiError extends Error {
|
||||
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)
|
||||
this.status = status
|
||||
this.data = data
|
||||
|
||||
@@ -86,10 +86,7 @@ export async function sendInvitation(
|
||||
role: string = 'member',
|
||||
captcha?: { captcha_id?: string, captcha_solution?: string, captcha_token?: string }
|
||||
): Promise<OrganizationInvitation> {
|
||||
const body: any = {
|
||||
email,
|
||||
role
|
||||
}
|
||||
const body: Record<string, string> = { email, role }
|
||||
|
||||
if (captcha?.captcha_id) body.captcha_id = captcha.captcha_id
|
||||
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