Overview
Members can record how they shoot, name a daily-driver body and lens, and keep a gear inventory. That shows up as a short summary on the public profile and a full list at /@nickname/gear. Names come from a catalog built from Lensfun, Wikidata, a small phone list, and camera/lens strings already stored on photos.
Account and public pages
/account/gear is the editor. Shooting formats are a set of toggles (digital, analog, phone, and so on), with an optional label when "Other" is selected. Daily driver fields search the catalog as you type. The inventory can be sorted by type, name, recently added, or a custom drag order.
When the list is grouped by type, each heading has an add button (Add lens, Add camera body, …). That opens the same add form with the type already selected. The public gear page uses the same list component in read-only mode, so those buttons stay off.
The profile page renders ProfileGearSummary only when the member has shared formats, a daily driver, or at least one item. "My gear" is in the account menu.
Saving goes through save_user_gear_profile, which replaces the profile row and the item list in one call.
Catalog
gear_catalog stores a source (lensfun, wikidata, curated, exif), a category, and a search string. gear_catalog_aliases holds extra names so a search can hit a model that is filed under another spelling.
Refresh pulls Lensfun XML from GitHub and camera bodies from Wikidata, then upserts them. Curated phones are a short in-repo list. Photo upload calls /api/gear/ingest with the new photo id so Make/Model and lens EXIF can be added as catalog rows.
Vercel cron (and the Coolify scheduled-task table) runs /api/cron/refresh-gear-catalog weekly (0 4 * * 1). /api/cron/backfill-gear-from-exif walks existing photos and is meant to be run once, not on a schedule.
Search (/api/gear/search) uses the alias table plus buildPostgrestIlikeOr, which also fixes admin member search so a query with commas or other PostgREST-special characters does not break the .or() filter.
Form and carousel fixes
The add form reads category, name, catalog id, and note with useWatch instead of useForm().watch(), so React Compiler can memoize the component. The daily-driver inputs are controlled by the form value directly. Inventory actions passed up to the sticky bar are stored on a ref inside an effect, not during render.
The achievements carousel no longer leaves a disabled previous or next button on screen. The button is omitted when that direction cannot scroll.
Autocomplete suggestions render in a portal so the gear modal does not clip the list. The account gear save bar uses the terminal sticky gap so it sits flush with the mobile tab bar.
All Modified Files
New:
changelog/2026-10-14/sha.txtchangelog/2026-10-14/commit-message.txtchangelog/2026-10-14/files-changed.mdpublic/icons/drag-handle.svgpublic/icons/gear/format-analog.svgpublic/icons/gear/format-digital.svgpublic/icons/gear/format-drone.svgpublic/icons/gear/format-instant.svgpublic/icons/gear/format-lighting.svgpublic/icons/gear/format-lofi.svgpublic/icons/gear/format-other.svgpublic/icons/gear/format-phone.svgpublic/icons/gear/format-support.svgpublic/icons/gear/format-video.svgpublic/icons/gear/micro/category-accessories.svgpublic/icons/gear/micro/category-lens.svgpublic/icons/gear/micro/category-lighting.svgpublic/icons/gear/micro/category-support.svgpublic/icons/gear/micro/format-analog.svgpublic/icons/gear/micro/format-digital.svgpublic/icons/gear/micro/format-drone.svgpublic/icons/gear/micro/format-instant.svgpublic/icons/gear/micro/format-lofi.svgpublic/icons/gear/micro/format-other.svgpublic/icons/gear/micro/format-phone.svgpublic/icons/gear/micro/format-video.svgscripts/count-lensfun-parse.tsscripts/gear-catalog-stats.tsscripts/refresh-gear-catalog.tssrc/app/[nickname]/gear/page.tsxsrc/app/account/gear/AccountGearClient.tsxsrc/app/account/gear/layout.tsxsrc/app/account/gear/page.tsxsrc/app/api/account/gear/route.tssrc/app/api/cron/backfill-gear-from-exif/route.tssrc/app/api/cron/refresh-gear-catalog/route.tssrc/app/api/gear/ingest/route.tssrc/app/api/gear/search/route.tssrc/app/api/profiles/[nickname]/gear/route.tssrc/components/gear/GearAccountPanel.tsxsrc/components/gear/GearAddEditForm.tsxsrc/components/gear/GearItemRow.tsxsrc/components/gear/GearPageClient.tsxsrc/components/gear/GearShootingPreferences.tsxsrc/components/gear/GearSortableList.tsxsrc/components/gear/sortGearItems.test.tssrc/components/gear/sortGearItems.tssrc/components/profile/ProfileGearSummary.tsxsrc/hooks/useGearAccountForm.tssrc/hooks/useGearCatalogSearch.tssrc/hooks/useGearProfile.tssrc/lib/data/gear.tssrc/lib/focusTrapPortals.tssrc/lib/gear/__tests__/validateUserGearProfile.test.tssrc/lib/gear/buildSearchText.tssrc/lib/gear/catalogDb.tssrc/lib/gear/curatedPhones.tssrc/lib/gear/fetchUserGearProfile.tssrc/lib/gear/gearAliasKey.test.tssrc/lib/gear/gearAliasKey.tssrc/lib/gear/gearCategoryIcons.tsxsrc/lib/gear/gearProfileEqual.tssrc/lib/gear/gearProfileSchema.tssrc/lib/gear/ingestFromExif.tssrc/lib/gear/mapGearProfile.tssrc/lib/gear/normalizeExifBrand.tssrc/lib/gear/parseLensfunXml.tssrc/lib/gear/refreshGearCatalog.tssrc/lib/gear/sampleKit.tssrc/lib/gear/saveUserGearProfile.tssrc/lib/gear/searchCatalog.tssrc/lib/gear/shootingFormatIcons.tssrc/lib/gear/storage.tssrc/lib/gear/triggerIngest.tssrc/lib/gear/types.tssrc/lib/gear/validateUserGearProfile.tssrc/lib/gear/wikidataCatalog.test.tssrc/lib/gear/wikidataCatalog.tssrc/lib/search/fuzzyQuery.test.tssrc/lib/search/fuzzyQuery.ts
Modified:
README.mdinfra/coolify/scheduled-tasks.mdpublic/noise-dark.svgsrc/app/[nickname]/(profile)/page.tsxsrc/app/api/admin/members/route.tssrc/app/api/admin/search-members/route.tssrc/app/globals.csssrc/app/layout.tsxsrc/app/members/page.tsxsrc/components/admin/AddRsvpModal.tsxsrc/components/layout/AccountMenuPanel.tsxsrc/components/layout/mobileChrome.tssrc/components/shared/AutocompleteInput.tsxsrc/components/shared/Modal.tsxsrc/components/shared/ProfileActionsMenu.tsxsrc/components/shared/ProfileStatsBadges.tsxsrc/components/shared/Select.tsxsrc/components/shared/StickyActionBar.tsxsrc/components/shared/SuccessMessage.tsxsrc/config/routes.tssrc/database.types.tssrc/hooks/usePhotoUpload.tssrc/utils/mobileTerminalStickySettle.tssrc/utils/uploadPhoto.tssupabase/migrations/00000000000000_baseline.sqltailwind.config.tsvercel.json