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)