Skip to main content

Phone Numbers - Developer Guide

Complete developer guide for implementing the multiple phone numbers feature with location-based display.

Overview

The Site Settings document supports multiple phone numbers that can be displayed in different locations (contact page, footer, or both). This guide covers frontend implementation, GraphQL queries, TypeScript types, and utility functions.

GraphQL Query

Basic Query

query GetSiteSettings {
SiteSettings(id: "siteSettings") {
contactPhones {
number
label {
en
es
ro
}
displayLocation
}
contactPhone
}
}

Complete Query with All Fields

query GetSiteSettings {
SiteSettings(id: "siteSettings") {
logo {
asset {
url
}
}
socialLinks {
facebook
twitter
linkedin
instagram
github
}
contactPhones {
number
label {
en
es
ro
}
displayLocation
}
contactPhone
contactEmail
address {
street
city
state
zipCode
country
}
businessHours {
monday
tuesday
wednesday
thursday
friday
saturday
sunday
}
}
}

TypeScript Types

Contact Phone Number Type

interface ContactPhoneNumber {
number: string
label?: {
en?: string
es?: string
ro?: string
}
displayLocation: ('contact' | 'footer')[]
}

Site Settings Type

interface SiteSettings {
contactPhones?: ContactPhoneNumber[]
contactPhone?: string // Legacy field
// ... other fields
}

Filtering by Location

Get Phones for Contact Page

// Get phones for contact page
const contactPhones = siteSettings.contactPhones?.filter(
phone => phone.displayLocation.includes('contact')
) || []
// Get phones for footer
const footerPhones = siteSettings.contactPhones?.filter(
phone => phone.displayLocation.includes('footer')
) || []

Utility Function

function getPhonesByLocation(
phones: ContactPhoneNumber[] | undefined,
location: 'contact' | 'footer'
): ContactPhoneNumber[] {
if (!phones) return []
return phones.filter(phone => phone.displayLocation.includes(location))
}

Backward Compatibility

Fallback to Legacy Field

// Fallback to legacy field if array is empty
const getPhonesWithFallback = (
siteSettings: SiteSettings,
location: 'contact' | 'footer'
): ContactPhoneNumber[] => {
const phones = siteSettings.contactPhones?.filter(
phone => phone.displayLocation.includes(location)
) || []

// Fallback to legacy field if no phones found
if (phones.length === 0 && siteSettings.contactPhone) {
return [{
number: siteSettings.contactPhone,
displayLocation: [location]
}]
}

return phones
}

Complete Utility Function

import type { SiteSettings, ContactPhoneNumber } from '@/types'

export function getPhonesByLocation(
phones: ContactPhoneNumber[] | undefined,
location: 'contact' | 'footer'
): ContactPhoneNumber[] {
if (!phones) return []
return phones.filter(phone => phone.displayLocation.includes(location))
}

export function getPhonesWithFallback(
siteSettings: SiteSettings,
location: 'contact' | 'footer'
): ContactPhoneNumber[] {
const phones = getPhonesByLocation(siteSettings.contactPhones, location)

if (phones.length === 0 && siteSettings.contactPhone) {
return [{
number: siteSettings.contactPhone,
displayLocation: [location]
}]
}

return phones
}

export function formatPhoneForLink(phoneNumber: string): string {
// Remove all non-digit characters except +
return phoneNumber.replace(/[^\d+]/g, '')
}

export function createPhoneLink(phoneNumber: string): string {
return `tel:${formatPhoneForLink(phoneNumber)}`
}

export function getFallbackPhone(siteSettings: SiteSettings): string | undefined {
return siteSettings.contactPhone
}

Display Examples

React Component - Contact Page

import { getPhonesWithFallback, createPhoneLink } from '@/lib/utils/phoneNumbers'
import type { SiteSettings } from '@/types'

interface ContactPageProps {
siteSettings: SiteSettings
locale: 'en' | 'es' | 'ro'
}

export function ContactPhones({ siteSettings, locale }: ContactPageProps) {
const phones = getPhonesWithFallback(siteSettings, 'contact')

if (phones.length === 0) {
return null
}

return (
<div className="space-y-2">
{phones.map((phone, index) => (
<a
key={index}
href={createPhoneLink(phone.number)}
className="flex items-center gap-2 hover:text-teal-600"
>
{phone.label?.[locale] && (
<span className="font-medium">{phone.label[locale]}:</span>
)}
<span>{phone.number}</span>
</a>
))}
</div>
)
}
import { getPhonesWithFallback, createPhoneLink } from '@/lib/utils/phoneNumbers'
import type { SiteSettings } from '@/types'

interface FooterProps {
siteSettings: SiteSettings
locale: 'en' | 'es' | 'ro'
}

export function FooterPhones({ siteSettings, locale }: FooterProps) {
const phones = getPhonesWithFallback(siteSettings, 'footer')

if (phones.length === 0) {
return null
}

return (
<div className="space-y-1">
{phones.map((phone, index) => (
<a
key={index}
href={createPhoneLink(phone.number)}
className="block hover:text-teal-600"
>
{phone.label?.[locale]
? `${phone.label[locale]}: ${phone.number}`
: phone.number
}
</a>
))}
</div>
)
}

Next.js Server Component

import { getSiteSettings } from '@/lib/sanity'
import { getPhonesWithFallback, createPhoneLink } from '@/lib/utils/phoneNumbers'

export default async function ContactPage() {
const siteSettings = await getSiteSettings()
const phones = getPhonesWithFallback(siteSettings, 'contact')

return (
<section>
<h2>Contact Us</h2>
<div className="space-y-2">
{phones.map((phone, index) => (
<a
key={index}
href={createPhoneLink(phone.number)}
className="flex items-center gap-2"
>
{phone.label?.en && (
<span className="font-medium">{phone.label.en}:</span>
)}
<span>{phone.number}</span>
</a>
))}
</div>
</section>
)
}

Phone Number Formatting

Format for Display

function formatPhoneForDisplay(phoneNumber: string): string {
// Keep original formatting for display
return phoneNumber
}
function formatPhoneForLink(phoneNumber: string): string {
// Remove spaces and special characters except +
return phoneNumber.replace(/[^\d+]/g, '')
}

International Format

function formatInternational(phoneNumber: string): string {
// Ensure international format
if (!phoneNumber.startsWith('+')) {
return `+1${phoneNumber.replace(/\D/g, '')}`
}
return phoneNumber
}

Localization

Get Label for Current Locale

function getPhoneLabel(
phone: ContactPhoneNumber,
locale: 'en' | 'es' | 'ro'
): string | undefined {
return phone.label?.[locale]
}

Display with Label

function formatPhoneWithLabel(
phone: ContactPhoneNumber,
locale: 'en' | 'es' | 'ro'
): string {
const label = getPhoneLabel(phone, locale)
return label ? `${label}: ${phone.number}` : phone.number
}

Testing

Unit Tests

import { describe, it, expect } from 'vitest'
import { getPhonesByLocation, getPhonesWithFallback } from '@/lib/utils/phoneNumbers'

describe('Phone Numbers Utils', () => {
const mockPhones = [
{
number: '+1 234 567 8900',
label: { en: 'Main' },
displayLocation: ['contact', 'footer']
},
{
number: '+1 234 567 8901',
label: { en: 'Sales' },
displayLocation: ['contact']
}
]

it('filters phones by location', () => {
const contactPhones = getPhonesByLocation(mockPhones, 'contact')
expect(contactPhones).toHaveLength(2)

const footerPhones = getPhonesByLocation(mockPhones, 'footer')
expect(footerPhones).toHaveLength(1)
})

it('falls back to legacy field', () => {
const siteSettings = {
contactPhone: '+1 234 567 8900'
}
const phones = getPhonesWithFallback(siteSettings, 'contact')
expect(phones).toHaveLength(1)
expect(phones[0].number).toBe('+1 234 567 8900')
})
})

Best Practices

1. Always Use Utility Functions

Use the provided utility functions instead of filtering manually:

// ✅ Good
const phones = getPhonesWithFallback(siteSettings, 'contact')

// ❌ Bad
const phones = siteSettings.contactPhones?.filter(...)

2. Handle Empty States

Always check if phones array is empty:

const phones = getPhonesWithFallback(siteSettings, 'contact')
if (phones.length === 0) {
return null // or show a message
}

Always format phone numbers for tel: links:

// ✅ Good
<a href={createPhoneLink(phone.number)}>

// ❌ Bad
<a href={`tel:${phone.number}`}>

4. Support Localization

Always use locale-aware labels:

const label = phone.label?.[locale] || phone.label?.en

5. Maintain Backward Compatibility

Always support the legacy contactPhone field:

const phones = getPhonesWithFallback(siteSettings, location)