feat: ship creator journey v2 and profile updates

This commit is contained in:
2026-04-12 21:42:07 +02:00
parent a2457f4e49
commit d5cff21ea2
335 changed files with 20147 additions and 1545 deletions

View File

@@ -1,39 +1,18 @@
import React, { useEffect, useRef } from 'react'
import React, { useEffect, useRef, useCallback } from 'react'
let emojiMartRegistrationPromise = null
let emojiMartPromise = null
function ensureEmojiMartRegistered() {
if (!emojiMartRegistrationPromise) {
emojiMartRegistrationPromise = import('emoji-mart')
function ensureEmojiMart() {
if (!emojiMartPromise) {
emojiMartPromise = import('emoji-mart')
}
return emojiMartRegistrationPromise
}
function applyPickerProps(element, props) {
if (!element) {
return
}
element.data = props.data
element.onEmojiSelect = props.onEmojiSelect
element.theme = props.theme
element.previewPosition = props.previewPosition
element.skinTonePosition = props.skinTonePosition
element.maxFrequentRows = props.maxFrequentRows
element.perLine = props.perLine
element.navPosition = props.navPosition
element.set = props.set
element.locale = props.locale
element.autoFocus = props.autoFocus
element.searchPosition = props.searchPosition
element.dynamicWidth = props.dynamicWidth
element.noCountryFlags = props.noCountryFlags
return emojiMartPromise
}
export default function EmojiMartPicker({
data,
onEmojiSelect,
onClickOutside,
theme = 'auto',
previewPosition = 'bottom',
skinTonePosition = 'preview',
@@ -51,56 +30,66 @@ export default function EmojiMartPicker({
const hostRef = useRef(null)
const pickerRef = useRef(null)
// Keep refs pointing at the latest callback props so stable wrappers
// never capture a stale closure.
const onEmojiSelectRef = useRef(onEmojiSelect)
const onClickOutsideRef = useRef(onClickOutside)
onEmojiSelectRef.current = onEmojiSelect
onClickOutsideRef.current = onClickOutside
// Stable wrappers with fixed identity — safe to pass once to the Picker
// constructor without needing to re-initialise the element on every render.
const stableOnEmojiSelect = useCallback((emoji) => {
onEmojiSelectRef.current?.(emoji)
}, [])
const stableOnClickOutside = useCallback((e) => {
onClickOutsideRef.current?.(e)
}, [])
useEffect(() => {
let cancelled = false
ensureEmojiMartRegistered().then(() => {
if (cancelled || !hostRef.current) {
return
}
// emoji-mart's Picker stores callbacks in `this.props` during construction.
// connectedCallback reads from `this.props`, NOT from plain element
// properties set after construction, so we MUST use `new Picker(props)`
// rather than `document.createElement('em-emoji-picker')` + property
// assignment, which would leave onEmojiSelect as null internally.
ensureEmojiMart().then(({ Picker }) => {
if (cancelled || !hostRef.current) return
if (!pickerRef.current) {
pickerRef.current = document.createElement('em-emoji-picker')
hostRef.current.replaceChildren(pickerRef.current)
}
const pickerProps = {
data,
onEmojiSelect: stableOnEmojiSelect,
onClickOutside: stableOnClickOutside,
theme,
previewPosition,
skinTonePosition,
maxFrequentRows,
perLine,
navPosition,
set,
locale,
autoFocus,
}
if (searchPosition !== undefined) pickerProps.searchPosition = searchPosition
if (dynamicWidth !== undefined) pickerProps.dynamicWidth = dynamicWidth
if (noCountryFlags !== undefined) pickerProps.noCountryFlags = noCountryFlags
applyPickerProps(pickerRef.current, {
data,
onEmojiSelect,
theme,
previewPosition,
skinTonePosition,
maxFrequentRows,
perLine,
navPosition,
set,
locale,
autoFocus,
searchPosition,
dynamicWidth,
noCountryFlags,
})
const el = new Picker(pickerProps)
pickerRef.current = el
hostRef.current.replaceChildren(el)
}
})
return () => {
cancelled = true
}
}, [
data,
onEmojiSelect,
theme,
previewPosition,
skinTonePosition,
maxFrequentRows,
perLine,
navPosition,
set,
locale,
autoFocus,
searchPosition,
dynamicWidth,
noCountryFlags,
])
// Run once on mount. Callbacks stay fresh via refs; static display options
// (theme, perLine, etc.) don't change during a single picker session.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
return () => {

View File

@@ -0,0 +1,19 @@
export default function extractNativeEmoji(selection) {
if (typeof selection === 'string') {
return selection
}
const detail = selection?.detail ?? null
return (
selection?.native
?? selection?.emoji
?? selection?.unicode
?? selection?.skins?.[0]?.native
?? detail?.native
?? detail?.emoji
?? detail?.unicode
?? detail?.skins?.[0]?.native
?? ''
)
}

View File

@@ -0,0 +1,11 @@
export default function isEventWithinNode(event, node) {
if (!event || !node) {
return false
}
if (typeof event.composedPath === 'function') {
return event.composedPath().includes(node)
}
return node.contains(event.target)
}