import React, { useMemo, useState } from 'react'; import { FlatList, Image, Pressable, StyleSheet, Text, View, useWindowDimensions, } from 'react-native'; import { LinearGradient } from 'expo-linear-gradient'; import type { SduiBlock } from '@proulr/sdui-contract'; import { TextField as UiTextField } from '../ui/TextField'; import { Chip } from '../ui/Chip'; import { ChipRow } from '../ui/ChipRow'; import { AppIcon } from '../ui/app-icon'; import { colors, fonts, gradients, m3Colors, m3Elevation, m3Shape, spacing } from '../theme/proulr-theme'; import { resolveDataRef, resolveDataRefString } from '../lib/resolve-data-ref'; import { useSduiHybridRenderer } from '../sdui-hybrid-renderer-context'; import { formatDistanceLabel, getPresenceStatus, profileAgeFromBirthDate, readDiscoverGridItems, type DiscoverGridItemWire, type DiscoverNearbyProfileWire, } from '../lib/discover-profile-formatters'; import { SduiNativeAdBlock } from './SduiNativeAdBlock'; import type { SduiBlockRenderProps } from './SduiPrimitiveBlocks'; const GRID_COLUMNS = 3; const GRID_GAP = spacing.sm; const GRID_INITIAL_VISIBLE = 24; const GRID_VISIBLE_BATCH = 24; function readStringProp(block: SduiBlock, key: string): string | undefined { const value = block.props[key]; return typeof value === 'string' ? value : undefined; } function readBooleanProp(block: SduiBlock, key: string): boolean { return block.props[key] === true; } function readStringRecordProp(block: SduiBlock, key: string): Record | undefined { const value = block.props[key]; if (!value || typeof value !== 'object') return undefined; const entries = Object.entries(value as Record).filter( (entry): entry is [string, string] => typeof entry[1] === 'string', ); return entries.length > 0 ? Object.fromEntries(entries) : undefined; } function readNumberProp(block: SduiBlock, key: string, fallback: number): number { const value = block.props[key]; return typeof value === 'number' ? value : fallback; } function readProfile(data: Record, ref?: string): DiscoverNearbyProfileWire | null { if (!ref) return null; const value = ref.split('.').reduce((acc, key) => { if (acc && typeof acc === 'object' && key in (acc as Record)) { return (acc as Record)[key]; } return undefined; }, data); if (!value || typeof value !== 'object') return null; const profile = value as DiscoverNearbyProfileWire; return typeof profile.id === 'string' ? profile : null; } function PresenceDot({ status }: { status: ReturnType }) { const dotColor = status === 'online' ? m3Colors.online : status === 'recent' ? m3Colors.presenceRecent : m3Colors.outline; return ; } export function SduiDiscoverProfileCardBlock({ block, data, onAction }: SduiBlockRenderProps) { const hybrid = useSduiHybridRenderer(); const profileRef = readStringProp(block, 'profile_ref'); const actionId = readStringProp(block, 'action_id'); const width = readNumberProp(block, 'width', 120); const person = readProfile(data, profileRef); if (!person) return null; const handlePress = () => { if (!actionId) return; onAction(actionId, { profile_id: person.id }); }; if (hybrid.renderDiscoverProfileCard) { return ( <> {hybrid.renderDiscoverProfileCard({ person, width, onPress: handlePress, })} ); } const age = profileAgeFromBirthDate(person.birth_date); const photoUri = person.photo_url ?? person.primary_photo_path ?? null; const presence = getPresenceStatus(person.last_seen); const online = presence === 'online'; const borderColor = person.has_new_media ? m3Colors.primary : online ? m3Colors.online : m3Colors.outline; return ( {photoUri ? ( ) : ( )} {person.has_new_media ? ( Novo ) : null} {person.traveling ? ( Viajando ) : null} {person.is_right_now ? ( ) : null} {person.display_name ?? 'Alguém'} {age ? `, ${age}` : ''} {formatDistanceLabel(person.distance_m)} ); } function DiscoverGridCell({ item, cardWidth, profileActionId, adPlacement, data, onAction, }: { item: DiscoverGridItemWire; cardWidth: number; profileActionId?: string; adPlacement: string; data: Record; onAction: SduiBlockRenderProps['onAction']; }) { if (item.kind === 'ad') { return ( ); } return ( ); } function SduiDiscoverGridBlockFallback({ block, data, onAction }: SduiBlockRenderProps) { const itemsRef = readStringProp(block, 'items_ref') ?? 'grid_items'; const columns = readNumberProp(block, 'columns', GRID_COLUMNS); const profileActionId = readStringProp(block, 'profile_action_id'); const adPlacement = readStringProp(block, 'ad_placement') ?? 'discover_grid'; const { width: screenWidth } = useWindowDimensions(); const horizontalPadding = spacing.sm * 2; const cardWidth = (screenWidth - horizontalPadding - GRID_GAP * (columns - 1)) / columns; const items = useMemo(() => readDiscoverGridItems(data, itemsRef), [data, itemsRef]); const [visibleCount, setVisibleCount] = useState(GRID_INITIAL_VISIBLE); const visibleItems = useMemo(() => items.slice(0, visibleCount), [items, visibleCount]); const hasMore = visibleCount < items.length; return ( item.id} numColumns={columns} columnWrapperStyle={{ gap: GRID_GAP }} contentContainerStyle={styles.gridContent} scrollEnabled={false} removeClippedSubviews windowSize={5} maxToRenderPerBatch={9} initialNumToRender={GRID_INITIAL_VISIBLE} renderItem={({ item }) => ( )} /> {hasMore ? ( setVisibleCount((count) => Math.min(count + GRID_VISIBLE_BATCH, items.length))} > Carregar mais perfis ) : null} ); } export function SduiDiscoverGridBlock({ block, data, onAction }: SduiBlockRenderProps) { const hybrid = useSduiHybridRenderer(); if (hybrid.renderDiscoverGrid) { return ( <> {hybrid.renderDiscoverGrid({ block, data, onAction, })} ); } return ; } export function SduiChipBlock({ block, onAction }: SduiBlockRenderProps) { const label = readStringProp(block, 'label') ?? ''; const glyph = readStringProp(block, 'glyph'); const actionId = readStringProp(block, 'action_id'); const actionParams = readStringRecordProp(block, 'action_params'); const active = readBooleanProp(block, 'active'); const variant = readStringProp(block, 'variant') ?? 'default'; const isHeader = variant === 'on_header'; return ( actionId && onAction(actionId, actionParams)} /> ); } export function SduiChipRowBlock({ block, children }: SduiBlockRenderProps) { const variant = readStringProp(block, 'variant') ?? 'default'; const isHeader = variant === 'on_header'; return {children}; } export function SduiTextFieldBlock({ block, data, onAction }: SduiBlockRenderProps) { const hybrid = useSduiHybridRenderer(); const placeholder = readStringProp(block, 'placeholder') ?? ''; const dataRef = readStringProp(block, 'data_ref'); const submitActionId = readStringProp(block, 'submit_action_id'); const value = dataRef ? resolveDataRefString(data, dataRef) : ''; if (hybrid.renderTextField && dataRef) { return ( <> {hybrid.renderTextField({ dataRef, placeholder, value, submitActionId, onSubmit: () => submitActionId && onAction(submitActionId), })} ); } return ( { // Hybrid: app-side state for text input until api_call wiring lands. }} onSubmitEditing={() => submitActionId && onAction(submitActionId)} returnKeyType={submitActionId ? 'search' : 'done'} autoCapitalize="words" autoCorrect={false} containerStyle={styles.textField} /> ); } function readMapPins(data: Record, ref: string) { const raw = resolveDataRef(data, ref); if (!Array.isArray(raw)) return []; return raw .map((item) => { if (!item || typeof item !== 'object') return null; const row = item as Record; const id = typeof row.id === 'string' ? row.id : null; const lat = typeof row.lat === 'number' ? row.lat : Number(row.lat); const lng = typeof row.lng === 'number' ? row.lng : Number(row.lng); if (!id || !Number.isFinite(lat) || !Number.isFinite(lng)) return null; const distanceRaw = row.distance_m; const distance_m = typeof distanceRaw === 'number' && Number.isFinite(distanceRaw) ? distanceRaw : typeof distanceRaw === 'string' && Number.isFinite(Number(distanceRaw)) ? Number(distanceRaw) : null; return { id, lat, lng, display_name: typeof row.display_name === 'string' ? row.display_name : null, primary_photo_path: typeof row.primary_photo_path === 'string' ? row.primary_photo_path : null, is_online: row.is_online === true || row.is_online === false ? row.is_online : null, is_hosting: row.is_hosting === true || row.is_hosting === false ? row.is_hosting : null, mood_text: typeof row.mood_text === 'string' ? row.mood_text : null, distance_m, status_label: typeof row.status_label === 'string' ? row.status_label : null, }; }) .filter((pin): pin is NonNullable => Boolean(pin)); } export function SduiDiscoverMapBlock({ block, data, onAction }: SduiBlockRenderProps) { const hybrid = useSduiHybridRenderer(); const centerRefProp = readStringProp(block, 'center_ref') ?? 'browse_center'; const labelRef = readStringProp(block, 'label_ref') ?? 'map_label'; const pinsRef = readStringProp(block, 'pins_ref'); const height = readNumberProp(block, 'height', 340); const regionLabel = resolveDataRefString(data, labelRef) || 'Região selecionada'; const centerValue = resolveDataRef(data, centerRefProp); const center = centerValue && typeof centerValue === 'object' && typeof (centerValue as { lat?: unknown }).lat === 'number' && typeof (centerValue as { lng?: unknown }).lng === 'number' ? (centerValue as { lat: number; lng: number }) : { lat: -23.5505, lng: -46.6333 }; const centerMoveToken = typeof data.map_center_move_token === 'number' ? data.map_center_move_token : 0; const pins = pinsRef ? readMapPins(data, pinsRef) : []; const interactiveOverride = block.props.interactive; const interactive = typeof interactiveOverride === 'boolean' ? interactiveOverride : pins.length === 0; if (hybrid.renderDiscoverMap) { return ( <> {hybrid.renderDiscoverMap({ center, label: regionLabel, height, pins, interactive, centerMoveToken, onCenterChange: hybrid.onDiscoverMapCenterChange ?? (() => {}), onPinPress: (profileId) => onAction('open_profile', { profile_id: profileId }), })} ); } return ( {regionLabel} {`${center.lat.toFixed(4)}, ${center.lng.toFixed(4)}`} Mapa interativo — render nativo no app (híbrido). ); } const styles = StyleSheet.create({ profileCard: { aspectRatio: 0.82, borderRadius: m3Shape.cornerMedium, overflow: 'hidden', backgroundColor: m3Colors.surfaceContainerHigh, borderWidth: 1, ...m3Elevation.level1, }, profileShade: { position: 'absolute', left: 0, right: 0, bottom: 0, height: 80 }, newBadge: { position: 'absolute', top: spacing.sm, left: spacing.sm, backgroundColor: m3Colors.primary, paddingHorizontal: 8, paddingVertical: 3, borderRadius: m3Shape.cornerSmall, }, newBadgeText: { color: m3Colors.onPrimary, fontSize: 11, fontWeight: '700' }, travelBadge: { position: 'absolute', top: spacing.sm, right: spacing.sm, flexDirection: 'row', alignItems: 'center', gap: 4, backgroundColor: m3Colors.secondaryContainer, paddingHorizontal: 8, paddingVertical: 3, borderRadius: m3Shape.cornerSmall, }, travelBadgeOffset: { top: spacing.sm + 24, }, travelBadgeText: { color: m3Colors.onSecondaryContainer, fontSize: 11, fontWeight: '700' }, rightNowBadge: { position: 'absolute', top: spacing.sm, left: spacing.sm, width: 26, height: 26, borderRadius: 13, alignItems: 'center', justifyContent: 'center', backgroundColor: m3Colors.surfaceContainerHighest, borderWidth: 1, borderColor: m3Colors.primary, }, rightNowBadgeOffset: { left: spacing.sm + 52, }, profileInfo: { position: 'absolute', left: spacing.sm, bottom: spacing.sm, right: spacing.sm }, profileName: { color: colors.white, fontSize: 13, fontWeight: '700' }, distRow: { flexDirection: 'row', alignItems: 'center', gap: 6, marginTop: 2 }, distText: { color: colors.textSecondary, fontSize: 11, fontWeight: '500' }, presenceDot: { width: 8, height: 8, borderRadius: 4 }, gridContent: { gap: GRID_GAP, paddingHorizontal: spacing.sm, paddingBottom: spacing.sm, }, loadMore: { alignItems: 'center', paddingVertical: spacing.md, marginHorizontal: spacing.sm, }, loadMoreText: { color: m3Colors.primary, fontSize: 14, fontWeight: '600', fontFamily: fonts.semibold, }, textField: { flex: 1, marginBottom: 0, }, mapShell: { borderRadius: m3Shape.cornerLarge, overflow: 'hidden', borderWidth: 1, borderColor: m3Colors.outline, }, mapOverlay: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: spacing.lg, backgroundColor: 'rgba(0,0,0,0.45)', gap: spacing.sm, }, mapTitle: { color: colors.white, fontSize: 16, fontFamily: fonts.semibold, textAlign: 'center', }, mapCoords: { color: colors.textSecondary, fontSize: 14, fontFamily: fonts.regular, textAlign: 'center', }, mapHint: { color: colors.textSecondary, fontSize: 12, fontFamily: fonts.regular, textAlign: 'center', }, });