Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/app/(default)/area/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getAreaRSC } from '@/js/graphql/getAreaRSC'
import { StickyHeaderContainer } from '@/app/(default)/components/ui/StickyHeaderContainer'
import { AreaCrumbs } from '@/components/breadcrumbs/AreaCrumbs'
import { ArticleLastUpdate } from '@/components/edit/ArticleLastUpdate'
import { getMapHref, getFriendlySlug, getAreaPageFriendlyUrl, sanitizeName, parseUuidAsFirstParam } from '@/js/utils'
import { getMapHref, getFriendlySlug, getAreaPageFriendlyUrl, sanitizeName, parseUuidAsFirstParam, safeDecode } from '@/js/utils'
import { LazyAreaMap } from '@/components/maps/AreaMap'
import { DefaultPageContainer } from '@/app/(default)/components/ui/DefaultPageContainer'
import { AreaAndClimbPageActions } from '../../components/AreaAndClimb/AreaAndClimbPageActions'
Expand Down Expand Up @@ -56,7 +56,7 @@ export default async function Page ({ params }: PageWithCatchAllUuidProps): Prom
? <UploadPhotoCTA />
: <PhotoMontage photoList={photoList} />
}
pageActions={<AreaAndClimbPageActions name={areaName} uuid={uuid} targetType={TagTargetType.area} parentUuid={uuid} area={area} />}
pageActions={<AreaAndClimbPageActions name={safeDecode(areaName)} uuid={uuid} targetType={TagTargetType.area} parentUuid={uuid} area={area} />}
breadcrumbs={
<StickyHeaderContainer>
<AreaCrumbs pathTokens={pathTokens} ancestors={ancestors} />
Expand All @@ -73,7 +73,7 @@ export default async function Page ({ params }: PageWithCatchAllUuidProps): Prom
summary={{
left: (
<AreaData
areaName={areaName}
areaName={safeDecode(areaName)}
lat={lat}
lng={lng}
authorMetadata={authorMetadata}
Expand Down Expand Up @@ -231,7 +231,7 @@ export async function generateMetadata ({ params }: PageWithCatchAllUuidProps):
wall = sanitizeName(pathTokens[pathTokens.length - 2]) + ' • '
}

const name = sanitizeName(areaName)
const name = sanitizeName(safeDecode(areaName))

const previewImage = media.length > 0 ? `${CLIENT_CONFIG.CDN_BASE_URL}${media[0].mediaUrl}?w=1200&q=75` : null

Expand Down
4 changes: 2 additions & 2 deletions src/app/(default)/climb/[[...slug]]/components/ClimbData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import RouteTypeChips from '@/components/ui/RouteTypeChips'
import { ArticleLastUpdate } from '@/components/edit/ArticleLastUpdate'
import { ClimbType, AreaType } from '@/js/types'
import Grade from '@/js/grades/Grade'
import { removeTypenameFromDisciplines, getDisciplineList } from '@/js/utils'
import { removeTypenameFromDisciplines, getDisciplineList, safeDecode } from '@/js/utils'

export const ClimbData: React.FC<ClimbType & Pick<AreaType, 'gradeContext'> & { isBoulder: boolean }> = (props) => {
const { id, name, type, safety, length, grades, fa: legacyFA, authorMetadata, gradeContext, isBoulder } = props
Expand All @@ -21,7 +21,7 @@ export const ClimbData: React.FC<ClimbType & Pick<AreaType, 'gradeContext'> & {
return (
<>
<h1 className='text-4xl md:text-5xl mr-10'>
{name}
{safeDecode(name)}
</h1>
<div className='mt-6'>
<div className='flex items-center space-x-2 w-full'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import clx from 'classnames'
import { AreaMetadataType, ClimbDisciplineRecord, ClimbType } from '@/js/types'
import { disciplineTypeToDisplay } from '@/js/grades/util'
import { removeTypenameFromDisciplines, climbLeftRightIndexComparator, getClimbPageFriendlyUrl } from '@/js/utils'
import { removeTypenameFromDisciplines, climbLeftRightIndexComparator, getClimbPageFriendlyUrl, safeDecode } from '@/js/utils'
import Grade, { GradeContexts } from '@/js/grades/Grade'
import { ClimbListMiniToolbar } from '../../../manageClimbs/components/ClimbListMiniToolbar'

Expand Down Expand Up @@ -53,7 +53,7 @@ export const ClimbRow: React.FC<ClimbType & { index: number, gradeContext: Grade
<ListBullet index={index} disciplines={disciplines} />
<div className='w-full'>
<div className='flex justify-between'>
<div className={clx('text-base font-semibold uppercase tracking-tight', isThisRoute ? '' : 'hover:underline')}>{name}</div>
<div className={clx('text-base font-semibold uppercase tracking-tight', isThisRoute ? '' : 'hover:underline')}>{safeDecode(name)}</div>
<div>{gradeStr}</div>
</div>
<div><DisciplinesInfo disciplines={disciplines} /></div>
Expand All @@ -62,7 +62,7 @@ export const ClimbRow: React.FC<ClimbType & { index: number, gradeContext: Grade
</LinkWrapper>
</div>
{editMode &&
<ClimbListMiniToolbar climbId={id} parentAreaId={areaMetadata.areaId} climbName={name} />}
<ClimbListMiniToolbar climbId={id} parentAreaId={areaMetadata.areaId} climbName={safeDecode(name)} />}
</li>
)
}
Expand Down
21 changes: 21 additions & 0 deletions src/js/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,24 @@ export const parseUuidAsFirstParam = ({ params }: PageWithCatchAllUuidProps): st
}
return uuid
}

export const decodeAmpersand = (s: string): string => {
if (s == null) return ''
return s.replace(/&amp;/g, '&')
}
Comment on lines +361 to +364
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decodeAmpersand function is exported but never used in the codebase. Since safeDecode provides the same functionality plus additional entity decoding, consider removing this unused export to reduce maintenance burden.

Suggested change
export const decodeAmpersand = (s: string): string => {
if (s == null) return ''
return s.replace(/&amp;/g, '&')
}

Copilot uses AI. Check for mistakes.

export const safeDecode = (s: string): string => {
if (s == null) return ''
Comment on lines +362 to +367
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use strict equality (===) instead of loose equality (==) for null checks. The current check will match both null and undefined, which may be unintended. Replace with if (s == null || s === undefined) or if (s == null) depending on the intended behavior, but prefer === for consistency with TypeScript best practices.

Suggested change
if (s == null) return ''
return s.replace(/&amp;/g, '&')
}
export const safeDecode = (s: string): string => {
if (s == null) return ''
if (s === null || s === undefined) return ''
return s.replace(/&amp;/g, '&')
}
export const safeDecode = (s: string): string => {
if (s === null || s === undefined) return ''

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use strict equality (===) instead of loose equality (==) for null checks. Replace with explicit null/undefined checks using strict equality for consistency with TypeScript best practices.

Suggested change
if (s == null) return ''
if (s === null || s === undefined) return ''

Copilot uses AI. Check for mistakes.
if (typeof window !== 'undefined') {
const txt = document.createElement('textarea')
txt.innerHTML = s
return txt.value
}
// Basic SSR-safe decoding for common entities
return s
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
}
Loading