From af9dc66211b7fd9911e186fb1da90df9698e72af Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:16:09 -0500 Subject: [PATCH 01/17] adjusted heights of popovers --- .../components/capture/editor-popover.tsx | 218 +++++++++--------- 1 file changed, 104 insertions(+), 114 deletions(-) diff --git a/frontend/components/capture/editor-popover.tsx b/frontend/components/capture/editor-popover.tsx index 864ab83..41297cc 100644 --- a/frontend/components/capture/editor-popover.tsx +++ b/frontend/components/capture/editor-popover.tsx @@ -1,44 +1,41 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { View, - Text, TouchableOpacity, StyleSheet, Dimensions, - TextInput, - FlatList, - Image, - KeyboardAvoidingView, } from "react-native"; import Animated, { SlideInDown, SlideOutDown, useAnimatedStyle, useSharedValue, + withTiming, } from "react-native-reanimated"; -import { Gesture, GestureDetector } from "react-native-gesture-handler"; -import { Music, MusicIcon, StickerIcon, TextIcon, X, MapPin } from "lucide-react-native"; import { verticalScale } from "react-native-size-matters"; import { useDebounce } from "@/hooks/use-debounce"; import { useMusicTag } from "@/hooks/use-music-tag"; import { MediaCanvasItemType, MusicTag } from "@/types/capture"; -import { MusicListItem } from "./music/music-list-item"; -import ColorSlider from "./editor/color-slider"; -import FontStyleSelector from "./editor/font-style-selector"; import TextTab from "./editor/text-tab"; import StickerTab from "./editor/sticker-tab"; import MusicTab from "./editor/music-tab"; import LocationTab from "./editor/location-tab"; const { height } = Dimensions.get("window"); +const TEXT_TAB_HEIGHT = height * 0.72; +const DEFAULT_TAB_HEIGHT = height * 0.95; interface EditorPopoverProps { isVisible: boolean; - onClose: () => void; - addText: (text: string, style: { color: string; fontFamily?: string }) => void; + onClose: (currentText?: string) => void; + addText: (text: string, style: { color: string; fontFamily?: string; backgroundColor?: string }) => void; addSticker: (uri: string) => void; addMusic: (music: MusicTag) => void; addLocation: (location: string) => void; + defaultTab?: MediaCanvasItemType; + onTextChange?: (text: string) => void; + onStyleChange?: (styleUpdates: { color?: string; fontFamily?: string; backgroundColor?: string }) => void; + initialText?: string; } export default function EditorPopover({ @@ -48,19 +45,54 @@ export default function EditorPopover({ addSticker, addMusic, addLocation, + defaultTab, + onTextChange, + onStyleChange, + initialText = "", }: EditorPopoverProps) { - const [activeTab, setActiveTab] = useState("text"); - const [textInput, setTextInput] = useState(""); + const [activeTab, setActiveTab] = useState(defaultTab || "text"); + const [textInput, setTextInput] = useState(initialText); const [musicTag, setMusicTag] = useState(""); const musicQuery = useDebounce(musicTag, 600); + + // Update activeTab when defaultTab changes + useEffect(() => { + if (defaultTab) { + setActiveTab(defaultTab); + } + }, [defaultTab]); + + // Update textInput when initialText changes + useEffect(() => { + if (initialText !== undefined) { + setTextInput(initialText); + } + }, [initialText]); + + // Handle text input changes + const handleTextChange = (text: string) => { + setTextInput(text); + if (onTextChange) { + onTextChange(text); + } + }; const { musicTags, isLoading } = useMusicTag(musicQuery); const [selectedStyle, setSelectedStyle] = useState({ - color: "#000", + color: "#FFFFFF", fontFamily: "Arial", + backgroundColor: "#000000", }); - const popoverHeight = useSharedValue(height * 0.6); + const popoverHeight = useSharedValue( + activeTab === "text" ? TEXT_TAB_HEIGHT : DEFAULT_TAB_HEIGHT + ); + + // Update height when activeTab changes + useEffect(() => { + const targetHeight = activeTab === "text" ? TEXT_TAB_HEIGHT : DEFAULT_TAB_HEIGHT; + popoverHeight.value = withTiming(targetHeight, { duration: 300 }); + }, [activeTab]); const animatedPopoverStyle = useAnimatedStyle(() => { return { @@ -68,19 +100,8 @@ export default function EditorPopover({ }; }); - const swipeDownGesture = Gesture.Pan().onEnd((event) => { - if (event.translationY > 100 && event.velocityY > 500) { - onClose(); - } - }); - - const confirmTextSelection = () => { - if (textInput.trim()) { - addText(textInput.trim(), selectedStyle); - setTextInput(""); - onClose(); - } - } + // Text is now updated in real-time, so we don't need a confirm function + // This is kept for potential future use but not currently called const confirmStickerSelection = (uri: string) => { addSticker(uri); @@ -102,102 +123,70 @@ export default function EditorPopover({ return ( - - - - - {/* Handle */} - + onClose(textInput)} /> - {/* Header */} - - - setActiveTab("text")} - > - - - setActiveTab("sticker")} - > - - - setActiveTab("music")} - > - - - setActiveTab("location")} - > - - - + + {/* Handle */} + - - - - - - {/* Content */} - + {/* Content */} + {activeTab === "text" ? ( setSelectedStyle({ ...selectedStyle, color })} + onColorChange={(color) => { + setSelectedStyle({ ...selectedStyle, color }); + if (onStyleChange) { + onStyleChange({ color }); + } + }} selectedFont={selectedStyle.fontFamily} - onFontChange={(font) => setSelectedStyle({ ...selectedStyle, fontFamily: font })} - onConfirm={confirmTextSelection} - /> - ) : activeTab === "sticker" ? ( - { + setSelectedStyle({ ...selectedStyle, fontFamily: font }); + if (onStyleChange) { + onStyleChange({ fontFamily: font }); + } + }} + selectedBackgroundColor={selectedStyle.backgroundColor} + onBackgroundColorChange={(color) => { + setSelectedStyle({ ...selectedStyle, backgroundColor: color }); + if (onStyleChange) { + onStyleChange({ backgroundColor: color }); + } + }} /> - ) : activeTab === "music" ? ( - - ) : ( - - )} - - - + ) : activeTab === "sticker" ? ( + + ) : activeTab === "music" ? ( + + ) : ( + + )} + + ); } const styles = StyleSheet.create({ popoverContent: { - minHeight: verticalScale(670) + //minHeight: verticalScale(470) }, overlay: { position: "absolute", @@ -262,8 +251,9 @@ const styles = StyleSheet.create({ marginTop: 20, gap: 16, }, - - + tabContent: { + marginVertical: verticalScale(0), + }, styleButton: { padding: 8, borderWidth: 1, From 4effbfb38d79457bf4a0e3235044dd8af5198e6b Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:16:32 -0500 Subject: [PATCH 02/17] fixed layout and styling for indivual tabs in text tab --- .../components/capture/editor/text-tab.tsx | 383 +++++++++++++++--- 1 file changed, 328 insertions(+), 55 deletions(-) diff --git a/frontend/components/capture/editor/text-tab.tsx b/frontend/components/capture/editor/text-tab.tsx index 5dd6da2..f8fa6ee 100644 --- a/frontend/components/capture/editor/text-tab.tsx +++ b/frontend/components/capture/editor/text-tab.tsx @@ -1,7 +1,11 @@ -import { View, StyleSheet, TextInput, Text, StyleProp, ViewStyle, TextStyle, TouchableOpacity } from "react-native"; +import { View, StyleSheet, TextInput, Text, TouchableOpacity, ScrollView, Platform, Keyboard, Dimensions } from "react-native"; import ColorSlider from "./color-slider"; import FontStyleSelector from "./font-style-selector"; -import { Input } from "@/components/ui/input"; +import { scale, verticalScale } from "react-native-size-matters"; +import { useState, useRef, useEffect } from "react"; +import { Plus, Type, Palette, Square, AlignLeft } from "lucide-react-native"; + +type InternalTab = "text" | "textColor" | "backgroundColor" | "font"; interface TextTabProps { textInput: string; @@ -10,9 +14,20 @@ interface TextTabProps { onColorChange: (color: string) => void; selectedFont: string; onFontChange: (font: string) => void; - onConfirm: () => void; + selectedBackgroundColor?: string; + onBackgroundColorChange?: (color: string) => void; } +const { height } = Dimensions.get("window"); + +// Popular colors palette +const POPULAR_COLORS = [ + "#FFFFFF", "#000000", "#FF0000", "#00FF00", "#0000FF", + "#FFFF00", "#FF00FF", "#00FFFF", "#FFA500", "#800080", + "#FFC0CB", "#A52A2A", "#808080", "#FFD700", "#4B0082", + "#FF1493", "#00CED1", "#32CD32", "#FF4500", "#DA70D6", +]; + export default function TextTab({ textInput, onTextChange, @@ -20,80 +35,338 @@ export default function TextTab({ onColorChange, selectedFont, onFontChange, - onConfirm, + selectedBackgroundColor = "#000000", + onBackgroundColorChange }: TextTabProps) { + const [activeInternalTab, setActiveInternalTab] = useState("text"); + const [showCustomColor, setShowCustomColor] = useState(false); + const textInputRef = useRef(null); - const selectedStyle: StyleProp = { - color: selectedColor, - fontFamily: selectedFont, - } + // Auto-focus when text tab becomes active, but keep keyboard persistent when switching tabs + useEffect(() => { + if (activeInternalTab === "text") { + // Small delay to ensure the component is rendered + setTimeout(() => { + textInputRef.current?.focus(); + }, 100); + } + // Don't blur when switching tabs - keep keyboard persistent + }, [activeInternalTab]); - return ( - - - Text - ( + + + - - Color - + + ); + + const renderTextColorTab = () => ( + + + Custom Color + setShowCustomColor(!showCustomColor)} + > + + - - Font - + + + + {POPULAR_COLORS.map((color) => ( + { + onColorChange(color); + setShowCustomColor(false); + }} + > + {selectedColor === color && ( + + )} + + ))} + + + + {showCustomColor && ( + + + {selectedColor.toUpperCase()} + + )} + + + ); + + const renderBackgroundColorTab = () => ( + + + + {POPULAR_COLORS.map((color) => ( + { + onBackgroundColorChange?.(color); + setShowCustomColor(false); + }} + > + {selectedBackgroundColor === color && ( + + )} + + ))} + + + setShowCustomColor(!showCustomColor)} + > + + {showCustomColor ? "Hide Custom Color" : "Custom Color"} + + + + {showCustomColor && ( + + {})} + /> + {selectedBackgroundColor.toUpperCase()} + + )} + + ); - ( + + + + ); + + + + return ( + + {/* Internal Tabs */} + - Add Text - + setActiveInternalTab("text")} + > + + + { + setActiveInternalTab("textColor"); + setShowCustomColor(false); + }} + > + + + { + setActiveInternalTab("backgroundColor"); + setShowCustomColor(false); + }} + > + + + setActiveInternalTab("font")} + > + + + + + {/* Tab Content */} + {activeInternalTab === "text" && renderTextTab()} + {activeInternalTab === "textColor" && renderTextColorTab()} + {activeInternalTab === "backgroundColor" && renderBackgroundColorTab()} + {activeInternalTab === "font" && renderFontTab()} ); } const styles = StyleSheet.create({ - textTab: { - marginTop: 20, - gap: 16, - }, - styleRow: { - flexDirection: "column", - flexWrap: "wrap", - width: "100%", - marginVertical: 12, - //borderWidth: 1, - gap: 6, - }, - input: { - borderWidth: 1, - borderColor: "#E2E8F0", + container: { + height: verticalScale(350) + }, + tabBarScrollView: { + flexGrow: 0, + marginBottom: 16, + }, + tabBar: { + flexDirection: 'row', + alignItems: 'center', + }, + tab: { + paddingVertical: 10, + paddingHorizontal: 16, borderRadius: 12, + marginRight: 8, + backgroundColor: 'transparent', + }, + activeTab: { + backgroundColor: '#EEF2FF', + }, + tabText: { + fontSize: 12, + fontWeight: '600', + color: '#64748B', + }, + activeTabText: { + color: '#8B5CF6', + }, + tabContent: { + flex: 1, + minHeight: 200, + }, + scrollContentContainer: { + paddingBottom: 20, + }, + sectionTitle: { + fontSize: 14, + fontWeight: '600', + color: '#1E293B', + marginBottom: 12, + }, + textInputSection: { + gap: 12, + paddingVertical: 8, + width: '100%', + }, + textInput: { + width: '100%', + minHeight: 100, padding: 12, + borderRadius: 8, + borderWidth: 1, + borderColor: '#E2E8F0', fontSize: 16, - width: "100%", }, - addButton: { - backgroundColor: "#8B5CF6", - paddingVertical: 14, + labelCentered: { + fontSize: 12, + fontWeight: "600", + color: "#1E293B", + marginBottom: 4, + textAlign: 'center', + }, + colorSection: { + marginBottom: 24, + }, + colorGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 10, + marginBottom: 8, + }, + colorGridItem: { + width: '17.5%', + height: 40, + aspectRatio: 1, borderRadius: 12, - alignItems: "center", + borderWidth: 2, + borderColor: '#E2E8F0', + alignItems: 'center', + justifyContent: 'center', }, - addButtonText: { - color: "white", - fontWeight: "600", - fontSize: 16, + selectedColorItem: { + borderColor: '#8B5CF6', + borderWidth: 3, + }, + checkmark: { + width: 16, + height: 16, + borderRadius: 8, + backgroundColor: 'rgba(139, 92, 246, 0.8)', + borderWidth: 2, + borderColor: '#FFFFFF', + }, + customColorButton: { + marginTop: 16, + paddingVertical: 12, + paddingHorizontal: 16, + backgroundColor: '#F8FAFC', + borderWidth: 1, + borderColor: '#E2E8F0', + borderRadius: 12, + alignItems: 'center', + }, + customColorButtonText: { + fontSize: 14, + fontWeight: '600', + color: '#8B5CF6', + }, + customColorSection: { + marginTop: 16, + gap: 12, + }, + colorCode: { + fontSize: 14, + fontWeight: '600', + color: '#1E293B', + fontFamily: 'monospace', + textAlign: 'center', }, }) From 0499f0fae5442ca9b50dd61733c354c04160d296 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:16:49 -0500 Subject: [PATCH 03/17] fixed music playr components styling --- .../components/capture/music/audio-preview-popover.tsx | 9 ++------- frontend/components/capture/music/music-list-item.tsx | 7 ++++--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/frontend/components/capture/music/audio-preview-popover.tsx b/frontend/components/capture/music/audio-preview-popover.tsx index 987e0c2..30f30f4 100644 --- a/frontend/components/capture/music/audio-preview-popover.tsx +++ b/frontend/components/capture/music/audio-preview-popover.tsx @@ -23,11 +23,6 @@ interface MusicPopoverProps { } export default function AudioPreviewPopover({ isVisible, onClose, music }: MusicPopoverProps) { - const popoverHeight = useSharedValue(height * 0.5); - - const animatedPopoverStyle = useAnimatedStyle(() => ({ - maxHeight: popoverHeight.value, - })); // Don't return null - let the exit animation play if (!isVisible && !music) return null; @@ -45,7 +40,7 @@ export default function AudioPreviewPopover({ isVisible, onClose, music }: Music {isVisible && ( @@ -130,7 +125,7 @@ const styles = StyleSheet.create({ borderRadius: scale(24), padding: scale(12), width: '90%', - marginBottom: verticalScale(16), + marginBottom: verticalScale(42), }, closeButtonText: { fontSize: scale(16), diff --git a/frontend/components/capture/music/music-list-item.tsx b/frontend/components/capture/music/music-list-item.tsx index db80cd1..0c45f3c 100644 --- a/frontend/components/capture/music/music-list-item.tsx +++ b/frontend/components/capture/music/music-list-item.tsx @@ -34,7 +34,7 @@ export function MusicListItem({ music, onPress }: MusicListItemProps) { @@ -43,8 +43,8 @@ export function MusicListItem({ music, onPress }: MusicListItemProps) { const styles = StyleSheet.create({ image: { - width: scale(35), - height: verticalScale(33), + width: scale(40), + height: verticalScale(37), borderRadius: 5 }, listItem: { @@ -53,6 +53,7 @@ const styles = StyleSheet.create({ alignItems: "center", justifyContent: "space-between", marginBottom: 8, + paddingRight: scale(7), }, listItemInner: { display: "flex", From caf33b747d5e7c56dcd922ee28999d5a55030e50 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:17:13 -0500 Subject: [PATCH 04/17] added attachments to capture details by including mode switch --- frontend/app/capture/details.tsx | 260 ++++++++++++------ .../capture/entry-attachment-list.tsx | 89 ++++++ 2 files changed, 272 insertions(+), 77 deletions(-) create mode 100644 frontend/components/capture/entry-attachment-list.tsx diff --git a/frontend/app/capture/details.tsx b/frontend/app/capture/details.tsx index 88edd7a..9744779 100644 --- a/frontend/app/capture/details.tsx +++ b/frontend/app/capture/details.tsx @@ -1,7 +1,7 @@ -import React, { useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { View, Text, TouchableOpacity, StyleSheet, ScrollView, KeyboardAvoidingView, Platform, Pressable } from 'react-native'; import { router, useLocalSearchParams } from 'expo-router'; -import { X, Sticker } from 'lucide-react-native'; +import { X, Sticker, UserPlus, UserPlus2 } from 'lucide-react-native'; import { useEntryOperations } from '@/hooks/use-entry-operations'; import { useDeviceLocation } from '@/hooks/use-device-location'; import { useAuthContext } from '@/providers/auth-provider'; @@ -27,6 +27,8 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { Colors } from '@/lib/constants'; import AudioEntry from '@/components/audio/audio-entry'; import EntryShareList from '@/components/friends/entry-share-list'; +import EntryAttachmentList from '@/components/capture/entry-attachment-list'; +import { MediaCanvasItemType } from '@/types/capture'; interface Friend { id: string; @@ -91,6 +93,11 @@ export default function DetailsScreen() { const { toast } = useToast(); const [showEditorPopover, setShowEditorPopover] = useState(false); + const [showAttachmentList, setShowAttachmentList] = useState(false); + const [editorDefaultTab, setEditorDefaultTab] = useState(undefined); + const [pendingTextItemId, setPendingTextItemId] = useState(null); + const [pendingTextValue, setPendingTextValue] = useState(""); + const [attachmentListStateBeforeEditor, setAttachmentListStateBeforeEditor] = useState(false); @@ -139,7 +146,95 @@ export default function DetailsScreen() { - const { viewShotRef, items, addText, addSticker, saveImage, addMusic, addLocation, removeElement } = useMediaCanvas(); + const { viewShotRef, items, addText, addSticker, addMusic, addLocation, removeElement, updateTextItem } = useMediaCanvas(); + + // Custom addText handler that handles pending text items + const handleAddText = (text: string, style: { color: string; fontFamily?: string; backgroundColor?: string }) => { + // If there's a pending text item, remove it first + if (pendingTextItemId !== null) { + removeElement(pendingTextItemId); + setPendingTextItemId(null); + setPendingTextValue(""); + } + // Add the new text item + addText(text, style); + }; + + // Handle attachment type selection + const handleAttachmentSelect = (type: MediaCanvasItemType) => { + // Save the current attachment list state before opening editor + setAttachmentListStateBeforeEditor(showAttachmentList); + + if (type === "text") { + // Auto-add text with default value + const defaultText = "Enter text"; + const defaultStyle = { + color: "#FFFFFF", + fontFamily: "Arial", + backgroundColor: "#000000", + }; + const tempId = addText(defaultText, defaultStyle); // Returns the ID + setPendingTextItemId(tempId); + setPendingTextValue(defaultText); + // Open editor with text tab + setEditorDefaultTab("text"); + setShowEditorPopover(true); + } else { + // For other types, just open the editor with the selected tab + setEditorDefaultTab(type); + setShowEditorPopover(true); + } + setShowAttachmentList(false); + }; + + // Handle editor popover close + const handleEditorClose = (currentText?: string) => { + // If there's a pending text item and it hasn't been changed or is empty, remove it + const textValue = currentText !== undefined ? currentText : pendingTextValue; + if (pendingTextItemId !== null && (textValue === "Enter text" || !textValue.trim())) { + removeElement(pendingTextItemId); + setPendingTextItemId(null); + setPendingTextValue(""); + } + setShowEditorPopover(false); + setEditorDefaultTab(undefined); + // Restore the attachment list state to what it was before opening the editor + setShowAttachmentList(attachmentListStateBeforeEditor); + }; + + // Handle text changes in editor - update in real-time + const handleTextChange = (text: string) => { + if (pendingTextItemId !== null) { + setPendingTextValue(text); + // Find the current style from the item + const currentItem = items.find(item => item.id === pendingTextItemId); + if (currentItem && currentItem.type === "text") { + updateTextItem(pendingTextItemId, text, currentItem.style || { + color: "#FFFFFF", + fontFamily: "Arial", + backgroundColor: "#000000", + }); + } + } + }; + + // Handle style changes in real-time + const handleStyleChange = (styleUpdates: { color?: string; fontFamily?: string; backgroundColor?: string }) => { + if (pendingTextItemId !== null) { + const currentItem = items.find(item => item.id === pendingTextItemId); + if (currentItem && currentItem.type === "text") { + const updatedStyle = { + ...currentItem.style, + ...styleUpdates, + }; + updateTextItem( + pendingTextItemId, + currentItem.text || pendingTextValue, + updatedStyle as { color: string; fontFamily?: string; backgroundColor?: string } + ); + } + } + }; @@ -255,58 +350,65 @@ export default function DetailsScreen() { }; return ( - - - - router.back()} - > - - - - Add Details - - setShowEditorPopover(true)} - > - - - - - - - {capture?.type === 'photo' && capture.uri ? ( - + + router.back()} + > + + + + Add Details + + setShowAttachmentList(!showAttachmentList)} + > + { + showAttachmentList ? ( + + ) : ( + + ) + } + + + + + + {capture?.type === 'photo' && capture.uri ? ( + + ) : + capture?.type === 'video' ? ( + videPlaying ? player.pause() : player.play()}> + - ) : - capture?.type === 'video' ? ( - videPlaying ? player.pause() : player.play()}> - - - ) : - capture?.type === 'audio' ? ( - - ) : null} - - - + + ) : + capture?.type === 'audio' ? ( + + ) : null} + + + + {showAttachmentList ? ( + + ) : ( + )} - - {getSaveButtonText()} - - - - - setShowEditorPopover(false)} - addText={addText} - addSticker={addSticker} - addMusic={addMusic} - addLocation={addLocation} - /> - - + + {getSaveButtonText()} + + + + + + ); } diff --git a/frontend/components/capture/entry-attachment-list.tsx b/frontend/components/capture/entry-attachment-list.tsx new file mode 100644 index 0000000..dae5007 --- /dev/null +++ b/frontend/components/capture/entry-attachment-list.tsx @@ -0,0 +1,89 @@ +import { View, ScrollView, StyleSheet, Text, TouchableOpacity } from "react-native"; +import { scale, verticalScale } from "react-native-size-matters"; +import { TextIcon, MusicIcon, MapPin } from "lucide-react-native"; +import { MediaCanvasItemType } from "@/types/capture"; + +interface EntryAttachmentListProps { + onSelectAttachment: (type: MediaCanvasItemType) => void; +} + +const attachmentTypes: { type: MediaCanvasItemType; icon: React.ComponentType; label: string }[] = [ + { type: "text", icon: TextIcon, label: "Text" }, + { type: "music", icon: MusicIcon, label: "Music" }, + { type: "location", icon: MapPin, label: "Location" }, +]; + +export default function EntryAttachmentList({ onSelectAttachment }: EntryAttachmentListProps) { + return ( + + + Add Attachment + + + + + {attachmentTypes.map((attachment) => { + const IconComponent = attachment.icon; + return ( + onSelectAttachment(attachment.type)} + > + + + + {attachment.label} + + ); + })} + + + + ); +} + +const styles = StyleSheet.create({ + attachmentText: { + textAlign: "center", + fontSize: scale(16), + fontWeight: '500', + marginVertical: verticalScale(8) + }, + attachmentSection: { + marginBottom: 32, + }, + attachmentsScroll: { + marginBottom: 8, + }, + attachmentsScrollContent: { + paddingRight: 20, + }, + attachmentOption: { + alignItems: 'center', + marginRight: 16, + padding: 8, + borderRadius: 16, + }, + attachmentAvatar: { + width: 48, + height: 48, + borderRadius: 24, + marginBottom: 8, + backgroundColor: '#F1F5F9', + justifyContent: 'center', + alignItems: 'center', + }, + attachmentName: { + fontSize: 12, + color: '#64748B', + fontWeight: '500', + textAlign: 'center', + }, +}); + From 7f01023cccfcbe5a4ca47208c2add9983b0f6818 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:19:03 -0500 Subject: [PATCH 05/17] included places search in location tab --- .../capture/editor/location-tab.tsx | 320 ++++++++---------- frontend/hooks/use-device-location.ts | 115 ++++++- frontend/hooks/use-places-search.ts | 31 ++ frontend/services/places-search-service.ts | 110 ++++++ 4 files changed, 394 insertions(+), 182 deletions(-) create mode 100644 frontend/hooks/use-places-search.ts create mode 100644 frontend/services/places-search-service.ts diff --git a/frontend/components/capture/editor/location-tab.tsx b/frontend/components/capture/editor/location-tab.tsx index d8c574e..ea5b926 100644 --- a/frontend/components/capture/editor/location-tab.tsx +++ b/frontend/components/capture/editor/location-tab.tsx @@ -2,11 +2,11 @@ import { View, StyleSheet, Text, TouchableOpacity, FlatList, ActivityIndicator, import { Input } from "@/components/ui/input"; import { useDeviceLocation } from "@/hooks/use-device-location"; import { useDebounce } from "@/hooks/use-debounce"; -import { useState, useEffect } from "react"; -import * as Location from 'expo-location'; -import { MapPin } from "lucide-react-native"; +import { usePlacesSearch } from "@/hooks/use-places-search"; +import { useState, useEffect, useMemo } from "react"; +import { MapPin, Navigation } from "lucide-react-native"; import { Colors } from "@/lib/constants"; -import { verticalScale } from "react-native-size-matters"; +import { scale, verticalScale } from "react-native-size-matters"; interface LocationTabProps { onSelectLocation: (location: string) => void; @@ -14,6 +14,7 @@ interface LocationTabProps { interface LocationSearchResult { id: string; + name: string; formattedAddress: string; isCurrentLocation?: boolean; } @@ -21,98 +22,30 @@ interface LocationSearchResult { export default function LocationTab({ onSelectLocation }: LocationTabProps) { const [searchQuery, setSearchQuery] = useState(""); const debouncedQuery = useDebounce(searchQuery, 500); - const [searchResults, setSearchResults] = useState([]); - const [isSearching, setIsSearching] = useState(false); + const [requestedCurrentLocation, setRequestedCurrentLocation] = useState(false); - const { location: currentLocation, getCurrentLocation, isLoading: isLoadingCurrent } = useDeviceLocation(); - - // Search for locations when query changes - useEffect(() => { - const searchLocations = async () => { - if (!debouncedQuery || debouncedQuery.trim().length === 0) { - setSearchResults([]); - setIsSearching(false); - return; - } + const { location: currentLocation, isLoading: isLoadingCurrent, placesInState, isLoadingPlaces } = useDeviceLocation(); - setIsSearching(true); - try { - // Use expo-location geocoding to search for addresses - // geocodeAsync returns LocationGeocodedLocation objects (lat/long only) - const geocodedLocations = await Location.geocodeAsync(debouncedQuery); - - if (geocodedLocations.length === 0) { - setSearchResults([]); - return; - } - - // Reverse geocode each result to get address details - const formattedResultsPromises = geocodedLocations.map(async (location, index) => { - try { - // Reverse geocode to get address from coordinates - const reverseGeocode = await Location.reverseGeocodeAsync({ - latitude: location.latitude, - longitude: location.longitude, - }); + console.log('currentLocation', currentLocation); - if (reverseGeocode.length > 0) { - const address = reverseGeocode[0]; - const parts: string[] = []; - - if (address.streetNumber && address.street) { - parts.push(`${address.streetNumber} ${address.street}`); - } else if (address.street) { - parts.push(address.street); - } - - if (address.city) { - parts.push(address.city); - } else if (address.subregion) { - parts.push(address.subregion); - } - - if (address.region) { - parts.push(address.region); - } - - const formattedAddress = parts.join(', ') || debouncedQuery; - - return { - id: `search-${index}-${location.latitude}-${location.longitude}`, - formattedAddress, - }; - } else { - // Fallback to query if reverse geocoding fails - return { - id: `search-${index}-${location.latitude}-${location.longitude}`, - formattedAddress: debouncedQuery, - }; - } - } catch (error) { - console.error('Reverse geocoding error:', error); - // Fallback to query if reverse geocoding fails - return { - id: `search-${index}-${location.latitude}-${location.longitude}`, - formattedAddress: debouncedQuery, - }; - } - }); + const coordinates = + currentLocation?.latitude != null && currentLocation?.longitude != null + ? { latitude: currentLocation.latitude, longitude: currentLocation.longitude } + : undefined; - const formattedResults = await Promise.all(formattedResultsPromises); - setSearchResults(formattedResults); - } catch (error) { - console.error('Location search error:', error); - setSearchResults([]); - } finally { - setIsSearching(false); - } - }; + const { places: mapboxPlaces, isLoading: isLoadingMapboxPlaces } = usePlacesSearch(debouncedQuery, { + coordinates, + }); - searchLocations(); - }, [debouncedQuery]); + // Convert Mapbox Places results to LocationSearchResult format + const mapboxPlacesResults: LocationSearchResult[] = useMemo(() => { + return mapboxPlaces.map(place => ({ + id: place.id, + name: place.name || '', + formattedAddress: place.formattedAddress, + })); + }, [mapboxPlaces]); - // Track if user requested current location - const [requestedCurrentLocation, setRequestedCurrentLocation] = useState(false); // Auto-select location when it becomes available after request useEffect(() => { @@ -122,16 +55,12 @@ export default function LocationTab({ onSelectLocation }: LocationTabProps) { } }, [currentLocation, requestedCurrentLocation, onSelectLocation]); - const handleSelectCurrentLocation = async () => { + const handleSelectCurrentLocation = () => { if (!currentLocation) { - // Mark that user requested location setRequestedCurrentLocation(true); - await getCurrentLocation(); - // The effect will handle selecting the location once it's available return; } - // If location is already available, select it immediately if (currentLocation.formattedAddress) { onSelectLocation(currentLocation.address); } @@ -141,22 +70,41 @@ export default function LocationTab({ onSelectLocation }: LocationTabProps) { onSelectLocation(location); }; - const currentLocationItem: LocationSearchResult | null = currentLocation - ? { - id: 'current-location', - formattedAddress: currentLocation.address, - isCurrentLocation: true, + // Build results list: current location + (Mapbox search results OR places in state) + const allResults = useMemo(() => { + const results: LocationSearchResult[] = []; + + // Add current location if available + if (currentLocation) { + results.push({ + id: 'current-location', + name: currentLocation.address, + formattedAddress: `${currentLocation.city}, ${currentLocation.region}, ${currentLocation.postalCode}`, + isCurrentLocation: true, + }); } - : null; + + // Add Mapbox Places search results if query exists, otherwise add places in state + results.push(...mapboxPlacesResults); + + return results; + }, [currentLocation, mapboxPlacesResults, debouncedQuery]); + + const hasQuery = debouncedQuery && debouncedQuery.trim().length > 0; + const showLoading = (isLoadingCurrent && !currentLocation) || (isLoadingMapboxPlaces && hasQuery) || (isLoadingPlaces && !hasQuery); + const showResults = !isLoadingMapboxPlaces && !isLoadingCurrent; + + const getSectionTitle = () => { + if (hasQuery) return "Search Results"; + if (currentLocation) return "Places Nearby"; + return "Places Nearby"; + }; - const allResults = currentLocationItem - ? [currentLocationItem, ...searchResults] - : searchResults; + console.log('allResults', allResults); return ( - Search Location - - {currentLocationItem ? "Use Current Location" : "Search Results"} - + {getSectionTitle()} - {isLoadingCurrent && !currentLocation && ( - - - Getting your location... - - )} - - {isSearching && searchQuery && ( + {showLoading ? ( - Searching... + + {isLoadingCurrent && !currentLocation + ? "Getting your location..." + : isLoadingPlaces && !hasQuery + ? "Loading places..." + : "Searching..."} + - )} - - {!isSearching && !isLoadingCurrent && ( - <> - - - - Use Current Location - - - {allResults.length > 0 && ( - item.id} - renderItem={({ item }) => ( - { - if (item.isCurrentLocation && !currentLocation) { - handleSelectCurrentLocation(); - } else { - handleSelectLocation(item.formattedAddress); - } - }} - > - item.id} + keyboardShouldPersistTaps="handled" + showsVerticalScrollIndicator={false} + style={styles.list} + contentContainerStyle={styles.listContent} + ListEmptyComponent={ + + {hasQuery + ? "No results found" + : "Enter a location to search or use your current location"} + + } + renderItem={({ item }) => ( + handleSelectLocation(item.name)} + > + { + item.isCurrentLocation ? ( + + ) : ( + - + {item.name} + - {item.formattedAddress} - - {item.isCurrentLocation && ( - Current - )} - + item.isCurrentLocation && styles.currentLocationText, + ]} + numberOfLines={2} + > + {item.formattedAddress} + + + {item.isCurrentLocation && ( + Current )} - scrollEnabled={false} - /> - )} - - {!searchQuery && !currentLocation && allResults.length === 0 && ( - - Enter a location to search or use your current location - + )} + /> - {searchQuery && !isSearching && allResults.length === 0 && ( - No results found - )} - )} + + + ); @@ -248,12 +191,8 @@ export default function LocationTab({ onSelectLocation }: LocationTabProps) { const styles = StyleSheet.create({ container: { - //flex: 1, marginTop: 20, }, - searchContainer: { - marginBottom: 20, - }, label: { fontSize: 14, fontWeight: "500", @@ -261,32 +200,50 @@ const styles = StyleSheet.create({ marginBottom: 8, }, resultsContainer: { - flex: 1, + //flex: 1, + }, + list: { + // Give the VirtualizedList a measurable viewport inside the popover + maxHeight: verticalScale(320), + }, + listContent: { + paddingBottom: verticalScale(8), }, sectionTitle: { fontSize: 14, fontWeight: "600", color: "#64748B", - marginBottom: 12, + marginVertical: 12, }, locationItem: { flexDirection: "row", alignItems: "center", paddingVertical: verticalScale(16), paddingHorizontal: 16, - backgroundColor: Colors.primary, + backgroundColor: Colors.card, borderRadius: 12, marginBottom: 8, gap: 12, + borderWidth: 1, + borderColor: Colors.border, + width: '100%', }, locationText: { - fontSize: 14, + fontSize: scale(12), + fontWeight: "400", + color: Colors.text, + }, + textColumn: { + flex: 1, + minWidth: 0, + }, + locationName: { + fontSize: scale(14), fontWeight: "600", - color: Colors.white, + color: Colors.text, }, currentLocationText: { - fontWeight: "600", - color: "#8B5CF6", + color: Colors.primary, }, badge: { fontSize: 11, @@ -296,6 +253,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8, + marginLeft: "auto", }, loadingContainer: { flexDirection: "row", diff --git a/frontend/hooks/use-device-location.ts b/frontend/hooks/use-device-location.ts index 37e0c43..c2b5d32 100644 --- a/frontend/hooks/use-device-location.ts +++ b/frontend/hooks/use-device-location.ts @@ -4,15 +4,27 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; interface LocationData { address: string; + postalCode: string; city: string; region: string; country: string; formattedAddress: string; + latitude: number; + longitude: number; +} + +interface PlaceResult { + id: string; + formattedAddress: string; + latitude: number; + longitude: number; } interface UseDeviceLocationResult { location: LocationData | null; + placesInState: PlaceResult[]; isLoading: boolean; + isLoadingPlaces: boolean; error: string | null; clearLocation: () => void; } @@ -87,8 +99,13 @@ export function useDeviceLocation(): UseDeviceLocationResult { region: address.region || '', country: address.country || '', formattedAddress: formatAddress(address), + latitude: position.coords.latitude, + longitude: position.coords.longitude, + postalCode: address.postalCode || '', }; + console.log({ address }); + return locationData; } catch (err: any) { console.error('Location error:', err); @@ -120,13 +137,109 @@ export function useDeviceLocation(): UseDeviceLocationResult { }, }); + // Query for places in state - automatically fetches when location is available + const { + data: placesData, + isLoading: isLoadingPlaces, + } = useQuery({ + queryKey: ['places-in-state', data?.region, data?.city], + queryFn: async () => { + if (!data || !data.region) { + return []; + } + + try { + // Search for places in the state using the region + const searchQuery = data.region; + const geocodedLocations = await Location.geocodeAsync(searchQuery); + + if (geocodedLocations.length === 0) { + return []; + } + + // Reverse geocode each result to get formatted addresses + const placesPromises = geocodedLocations.slice(0, 20).map(async (location, index) => { + try { + const reverseGeocode = await Location.reverseGeocodeAsync({ + latitude: location.latitude, + longitude: location.longitude, + }); + + if (reverseGeocode.length > 0) { + const address = reverseGeocode[0]; + const parts: string[] = []; + + // Build formatted address + if (address.name) { + parts.push(address.name); + } + + if (address.streetNumber && address.street) { + parts.push(`${address.streetNumber} ${address.street}`); + } else if (address.street) { + parts.push(address.street); + } + + if (address.city) { + parts.push(address.city); + } else if (address.subregion) { + parts.push(address.subregion); + } + + if (address.region) { + parts.push(address.region); + } + + const formattedAddress = parts.join(', ') || searchQuery; + + return { + id: `place-${index}-${location.latitude}-${location.longitude}`, + formattedAddress, + latitude: location.latitude, + longitude: location.longitude, + }; + } else { + // Fallback if reverse geocoding fails + return { + id: `place-${index}-${location.latitude}-${location.longitude}`, + formattedAddress: searchQuery, + latitude: location.latitude, + longitude: location.longitude, + }; + } + } catch (error) { + console.error('Reverse geocoding error for place:', error); + // Fallback + return { + id: `place-${index}-${location.latitude}-${location.longitude}`, + formattedAddress: searchQuery, + latitude: location.latitude, + longitude: location.longitude, + }; + } + }); + + const places = await Promise.all(placesPromises); + return places; + } catch (err: any) { + console.error('Error getting places in state:', err); + return []; + } + }, + enabled: !!data && !!data.region, // Only fetch when location and region are available + staleTime: 10 * 60 * 1000, // 10 minutes - places don't change as often + }); + const clearLocation = useCallback(() => { queryClient.setQueryData(['device-location'], null); - }, [queryClient]); + queryClient.setQueryData(['places-in-state', data?.region, data?.city], []); + }, [queryClient, data?.region, data?.city]); return { location: data ?? null, + placesInState: placesData ?? [], isLoading: isLoading || isFetching, + isLoadingPlaces, error: error ? error.message : null, clearLocation, }; diff --git a/frontend/hooks/use-places-search.ts b/frontend/hooks/use-places-search.ts new file mode 100644 index 0000000..c6d6fd1 --- /dev/null +++ b/frontend/hooks/use-places-search.ts @@ -0,0 +1,31 @@ +import { useQuery } from '@tanstack/react-query'; +import { PlacesSearchService } from '@/services/places-search-service'; + +export type PlacesSearchCoordinates = { + latitude: number; + longitude: number; +}; + +export function usePlacesSearch( + query: string, + options: { coordinates?: PlacesSearchCoordinates } = {} +) { + const coordinatesKey = options.coordinates ? `${options.coordinates.latitude},${options.coordinates.longitude}` : 'N/A'; + const { + data: places, + isLoading, + error, + } = useQuery({ + queryKey: ['placesSearch', query, coordinatesKey], + queryFn: () => PlacesSearchService.searchPlaces(query, options), + enabled: !!query && query.trim().length > 0, // Only run the query if there is a query + staleTime: 30000, // Cache results for 30 seconds + }); + + return { + places: places ?? [], + isLoading, + error, + }; +} + diff --git a/frontend/services/places-search-service.ts b/frontend/services/places-search-service.ts new file mode 100644 index 0000000..95919d4 --- /dev/null +++ b/frontend/services/places-search-service.ts @@ -0,0 +1,110 @@ +import { logger } from "@/lib/logger"; +import axios from "axios"; + +export interface MapboxFeature { + id: string; + type: string; + place_type: string[]; + relevance: number; + properties: { + accuracy?: string; + name?: string; + address?: string; + full_address?: string; + place_formatted?: string; + name_preferred?: string; + context?: { + country?: { name: string }; + region?: { name: string }; + postcode?: { name: string }; + place?: { name: string }; + }; + }; + text: string; + place_name: string; + center: [number, number]; // [longitude, latitude] + geometry: { + type: string; + coordinates: [number, number]; + }; + context?: Array<{ + id: string; + text: string; + [key: string]: any; + }>; +} + +export interface MapboxSearchResponse { + type: string; + query: string[]; + features: MapboxFeature[]; + attribution: string; +} + +export interface LocationSearchResult { + id: string; + formattedAddress: string; + placeId: string; + name?: string; +} + +export interface PlacesSearchCoordinates { + latitude: number; + longitude: number; +} + +export class PlacesSearchService { + private static readonly ACCESS_TOKEN = process.env.EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN; + private static readonly BASE_URL = "https://api.mapbox.com/search/searchbox/v1/forward"; + + static async searchPlaces( + query: string, + options: { coordinates?: PlacesSearchCoordinates } = {} + ): Promise { + if (!this.ACCESS_TOKEN) { + console.warn('Mapbox access token not configured'); + return []; + } + + if (!query || query.trim().length === 0) { + return []; + } + + try { + logger.info('PlacesSearchService: starting search', { query }); + const response = await axios.get( + `${this.BASE_URL}`, + { + params: { + access_token: this.ACCESS_TOKEN, + //types: 'place,address,poi', // Search for places, addresses, and points of interest + limit: 10, // Limit results to 10 + q: query.trim(), + ...(options.coordinates + ? { proximity: `${options.coordinates.longitude},${options.coordinates.latitude}` } + : {}), + }, + } + ); + + logger.info('PlacesSearchService: response', { response: response.data.features }); + + if (!response.data.features || response.data.features.length === 0) { + return []; + } + + const results: LocationSearchResult[] = response.data.features.map((feature, index) => ({ + id: feature.id || `place-${index}`, + formattedAddress: feature.properties.place_formatted || feature.properties.full_address || '', + placeId: feature.id, + name: feature.properties.name, + })); + + return results; + } catch (error) { + console.error('Error fetching places from Mapbox Search API:', error); + return []; + } + } +} + From db1708f83b8c01e4b96879429f2ecbaa966ee127 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:19:37 -0500 Subject: [PATCH 06/17] changed logo for empty state --- frontend/components/capture/editor/music-tab.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/frontend/components/capture/editor/music-tab.tsx b/frontend/components/capture/editor/music-tab.tsx index 1a74f0a..c82f4ae 100644 --- a/frontend/components/capture/editor/music-tab.tsx +++ b/frontend/components/capture/editor/music-tab.tsx @@ -1,11 +1,9 @@ import { MusicTag } from "@/types/capture"; -import { FlashList } from "@shopify/flash-list"; import { KeyboardAvoidingView, Text, TextInput, StyleSheet, View, FlatList, ActivityIndicator } from "react-native"; import { MusicListItem } from "../music/music-list-item"; -import { Album } from "lucide-react-native"; +import { Music } from "lucide-react-native"; import { Input } from "@/components/ui/input"; import { scale, verticalScale } from "react-native-size-matters"; -import { Colors } from "@/lib/constants"; import Skeleton, { SkeletonText } from "@/components/ui/skeleton"; interface MusicTabProps { @@ -55,7 +53,7 @@ export default function MusicTab({ isLoading, musicQuery, onMusicQueryChange, mu function EmptyComponent() { return ( - + Start searching for music ) @@ -87,7 +85,7 @@ const styles = StyleSheet.create({ justifyContent: "center", alignItems: "center", rowGap: verticalScale(8), - marginVertical: verticalScale(36), + marginVertical: verticalScale(120), }, emptyText: { fontSize: 16, From 802a8c1eeed026c5d2ce3eee9ac686a68bb874e2 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:22:13 -0500 Subject: [PATCH 07/17] added realtime text and text style functionality --- .../capture/canvas/text-canvas-item.tsx | 8 +++----- .../capture/editor/font-style-selector.tsx | 20 +++++++++++++------ frontend/hooks/use-media-canvas.ts | 18 +++++++++++++---- frontend/types/capture.ts | 2 +- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/frontend/components/capture/canvas/text-canvas-item.tsx b/frontend/components/capture/canvas/text-canvas-item.tsx index b2afde4..4a25b31 100644 --- a/frontend/components/capture/canvas/text-canvas-item.tsx +++ b/frontend/components/capture/canvas/text-canvas-item.tsx @@ -2,7 +2,7 @@ import { View, Text, StyleSheet } from "react-native"; interface TextCanvasItemProps { text: string, - textStyle?: { color: string; fontFamily?: string } + textStyle?: { color: string; fontFamily?: string; backgroundColor?: string } } export function TextCanvasItem({ text, textStyle }: TextCanvasItemProps) { @@ -10,8 +10,8 @@ export function TextCanvasItem({ text, textStyle }: TextCanvasItemProps) { console.log({ textStyle }) return ( - - + + {text} @@ -22,12 +22,10 @@ const styles = StyleSheet.create({ textContainer: { paddingVertical: 6, paddingHorizontal: 12, - backgroundColor: "black", borderRadius: 45 }, textStyle: { fontSize: 12, - color: "white", fontWeight: "500" }, }) \ No newline at end of file diff --git a/frontend/components/capture/editor/font-style-selector.tsx b/frontend/components/capture/editor/font-style-selector.tsx index 2b74cf4..3d5d348 100644 --- a/frontend/components/capture/editor/font-style-selector.tsx +++ b/frontend/components/capture/editor/font-style-selector.tsx @@ -8,6 +8,7 @@ import { StyleSheet, ViewStyle, } from "react-native"; +import { scale, verticalScale } from "react-native-size-matters"; type FontStyleSelectorProps = { fonts: string[]; // List of font family names @@ -21,7 +22,7 @@ const FontStyleSelector: React.FC = ({ fonts, onSelect, style, - previewChar = "A", + previewChar, initialFont, }) => { const [selected, setSelected] = useState( @@ -37,7 +38,7 @@ const FontStyleSelector: React.FC = ({ item} renderItem={({ item }) => { const isSelected = selected === item; @@ -55,8 +56,11 @@ const FontStyleSelector: React.FC = ({ { fontFamily: item }, isSelected && { color: "#007AFF" }, ]} + numberOfLines={2} + adjustsFontSizeToFit + minimumFontScale={0.8} > - {previewChar} + {previewChar || item} ); @@ -64,6 +68,7 @@ const FontStyleSelector: React.FC = ({ contentContainerStyle={styles.grid} showsVerticalScrollIndicator={false} style={styles.flatList} + keyboardShouldPersistTaps="handled" /> ); @@ -71,7 +76,7 @@ const FontStyleSelector: React.FC = ({ const styles = StyleSheet.create({ container: { - height: 400, // 👈 Fixed height instead of flex: 1 + height: verticalScale(280), // 👈 Fixed height instead of flex: 1 width: "100%", }, flatList: { @@ -84,8 +89,9 @@ const styles = StyleSheet.create({ option: { flex: 1, margin: 8, - aspectRatio: 1, + minHeight: 60, borderRadius: 12, + padding: scale(10), borderWidth: 1, borderColor: "#ccc", justifyContent: "center", @@ -97,8 +103,10 @@ const styles = StyleSheet.create({ backgroundColor: "#e6f0ff", }, preview: { - fontSize: 28, + fontSize: scale(16), color: "#000", + textAlign: "center", + flexWrap: "wrap", }, }); diff --git a/frontend/hooks/use-media-canvas.ts b/frontend/hooks/use-media-canvas.ts index 415ae2f..e653802 100644 --- a/frontend/hooks/use-media-canvas.ts +++ b/frontend/hooks/use-media-canvas.ts @@ -7,7 +7,9 @@ export function useMediaCanvas() { const [items, setItems] = useState>([]); const addText = (text: string, style: { color: string; fontFamily?: string }) => { - setItems([...items, { id: Date.now(), type: "text", text: text, style }]); + const id = Date.now(); + setItems(prevItems => [...prevItems, { id, type: "text", text: text, style }]); + return id; }; const addSticker = (sticker: any) => { @@ -23,8 +25,15 @@ export function useMediaCanvas() { } const removeElement = (id: number) => { - const remainingElements = items.filter(item => item.id != id); - setItems(remainingElements); + setItems(prevItems => prevItems.filter(item => item.id != id)); + } + + const updateTextItem = (id: number, text: string, style: { color: string; fontFamily?: string; backgroundColor?: string }) => { + setItems(prevItems => prevItems.map(item => + item.id === id && item.type === "text" + ? { ...item, text, style } + : item + )); } const viewShotRef = useRef(null); @@ -79,6 +88,7 @@ export function useMediaCanvas() { saveImage, removeElement, addMusic, - addLocation + addLocation, + updateTextItem } } \ No newline at end of file diff --git a/frontend/types/capture.ts b/frontend/types/capture.ts index 173752a..3911232 100644 --- a/frontend/types/capture.ts +++ b/frontend/types/capture.ts @@ -7,7 +7,7 @@ export interface MediaCanvasItem { sticker?: any; music_tag?: MusicTag; location?: string; // formatted address string - style?: { color: string; fontFamily?: string }; + style?: { color: string; fontFamily?: string; backgroundColor?: string }; } export interface RenderedMediaCanvasItem extends MediaCanvasItem { From 06eb107d79cbfcb4490374db21af0405c0b092b1 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:25:53 -0500 Subject: [PATCH 08/17] added types to query --- frontend/services/device-storage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/services/device-storage.ts b/frontend/services/device-storage.ts index 22d4f34..cae389d 100644 --- a/frontend/services/device-storage.ts +++ b/frontend/services/device-storage.ts @@ -1,4 +1,4 @@ -import { SuggestedFriend } from '@/types/friends'; +import { FriendWithProfile, SuggestedFriend } from '@/types/friends'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { Platform } from 'react-native'; @@ -113,7 +113,7 @@ class DeviceStorage { await this.setItem(`friends_${userId}`, friends, 60); // Cache for 1 hour } - async getFriends(userId: string): Promise { + async getFriends(userId: string): Promise { return await this.getItem(`friends_${userId}`); } From f0c481b7bce87ca9376709eb9baf3a9e2ab36bc0 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:51:30 -0500 Subject: [PATCH 09/17] cleared set timeout to avoid memory leaks --- frontend/components/capture/editor/text-tab.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/components/capture/editor/text-tab.tsx b/frontend/components/capture/editor/text-tab.tsx index f8fa6ee..0d74e04 100644 --- a/frontend/components/capture/editor/text-tab.tsx +++ b/frontend/components/capture/editor/text-tab.tsx @@ -44,13 +44,23 @@ export default function TextTab({ // Auto-focus when text tab becomes active, but keep keyboard persistent when switching tabs useEffect(() => { + let timer: ReturnType | null = null; + if (activeInternalTab === "text") { // Small delay to ensure the component is rendered - setTimeout(() => { - textInputRef.current?.focus(); + timer = setTimeout(() => { + if (textInputRef.current) { + textInputRef.current.focus(); + } }, 100); } - // Don't blur when switching tabs - keep keyboard persistent + + // Cleanup function to clear timeout if activeInternalTab changes or component unmounts + return () => { + if (timer !== null) { + clearTimeout(timer); + } + }; }, [activeInternalTab]); From c0363c5c458a5f95bca782d1d8c9933ff7e1498e Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:51:38 -0500 Subject: [PATCH 10/17] added timeout to request --- frontend/services/places-search-service.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/services/places-search-service.ts b/frontend/services/places-search-service.ts index 95919d4..dce942a 100644 --- a/frontend/services/places-search-service.ts +++ b/frontend/services/places-search-service.ts @@ -84,6 +84,7 @@ export class PlacesSearchService { ? { proximity: `${options.coordinates.longitude},${options.coordinates.latitude}` } : {}), }, + timeout: 5000, // 5 second timeout to fail-fast } ); @@ -102,7 +103,18 @@ export class PlacesSearchService { return results; } catch (error) { - console.error('Error fetching places from Mapbox Search API:', error); + if (axios.isAxiosError(error)) { + if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) { + logger.error('PlacesSearchService: request timeout', { query, error: error.message }); + console.error('Mapbox Search API request timed out after 5 seconds'); + } else { + logger.error('PlacesSearchService: request error', { query, error: error.message, status: error.response?.status }); + console.error('Error fetching places from Mapbox Search API:', error.message); + } + } else { + logger.error('PlacesSearchService: unexpected error', { query, error }); + console.error('Unexpected error fetching places from Mapbox Search API:', error); + } return []; } } From 5fcc36ec279ca8ddbec6af8e4e22aeff958a7585 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:54:48 -0500 Subject: [PATCH 11/17] chnaged logger level --- frontend/services/places-search-service.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/services/places-search-service.ts b/frontend/services/places-search-service.ts index dce942a..fbbd8ba 100644 --- a/frontend/services/places-search-service.ts +++ b/frontend/services/places-search-service.ts @@ -71,7 +71,7 @@ export class PlacesSearchService { } try { - logger.info('PlacesSearchService: starting search', { query }); + logger.debug('PlacesSearchService: starting search', { query }); const response = await axios.get( `${this.BASE_URL}`, { @@ -88,7 +88,7 @@ export class PlacesSearchService { } ); - logger.info('PlacesSearchService: response', { response: response.data.features }); + logger.debug('PlacesSearchService: response', { response: response.data.features }); if (!response.data.features || response.data.features.length === 0) { return []; @@ -105,14 +105,14 @@ export class PlacesSearchService { } catch (error) { if (axios.isAxiosError(error)) { if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) { - logger.error('PlacesSearchService: request timeout', { query, error: error.message }); + logger.error('PlacesSearchService: request timeout', { error: error.message }); console.error('Mapbox Search API request timed out after 5 seconds'); } else { - logger.error('PlacesSearchService: request error', { query, error: error.message, status: error.response?.status }); + logger.error('PlacesSearchService: request error', { error: error.message, status: error.response?.status }); console.error('Error fetching places from Mapbox Search API:', error.message); } } else { - logger.error('PlacesSearchService: unexpected error', { query, error }); + logger.error('PlacesSearchService: unexpected error', { error }); console.error('Unexpected error fetching places from Mapbox Search API:', error); } return []; From d410d6e30dee4b54e29d20b1d5fa6fa34f1c328f Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:55:05 -0500 Subject: [PATCH 12/17] used type from service file instead --- frontend/hooks/use-places-search.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/frontend/hooks/use-places-search.ts b/frontend/hooks/use-places-search.ts index c6d6fd1..3a6cdab 100644 --- a/frontend/hooks/use-places-search.ts +++ b/frontend/hooks/use-places-search.ts @@ -1,10 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import { PlacesSearchService } from '@/services/places-search-service'; - -export type PlacesSearchCoordinates = { - latitude: number; - longitude: number; -}; +import { PlacesSearchService, PlacesSearchCoordinates } from '@/services/places-search-service'; export function usePlacesSearch( query: string, From fba2307c6d9edceb9a360a5c6d074c9c6b7a13c4 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:55:58 -0500 Subject: [PATCH 13/17] changed type from any[] --- frontend/services/device-storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/services/device-storage.ts b/frontend/services/device-storage.ts index cae389d..9599bd6 100644 --- a/frontend/services/device-storage.ts +++ b/frontend/services/device-storage.ts @@ -114,7 +114,7 @@ class DeviceStorage { } async getFriends(userId: string): Promise { - return await this.getItem(`friends_${userId}`); + return await this.getItem(`friends_${userId}`); } async setEntries(userId: string, entries: any[]): Promise { From f66677a60e0634c6bca3ed385826cfa21fbd8d48 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:57:09 -0500 Subject: [PATCH 14/17] added batching and caching to bypass rate limits for reverse geocoding --- frontend/hooks/use-device-location.ts | 69 ++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/frontend/hooks/use-device-location.ts b/frontend/hooks/use-device-location.ts index c2b5d32..dec4d36 100644 --- a/frontend/hooks/use-device-location.ts +++ b/frontend/hooks/use-device-location.ts @@ -2,6 +2,12 @@ import { useCallback } from 'react'; import * as Location from 'expo-location'; import { useQuery, useQueryClient } from '@tanstack/react-query'; +// In-memory cache for reverse geocoding results to avoid duplicate requests +const reverseGeocodeCache = new Map(); + +// Utility function to sleep/delay +const sleep = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); + interface LocationData { address: string; postalCode: string; @@ -157,8 +163,24 @@ export function useDeviceLocation(): UseDeviceLocationResult { return []; } - // Reverse geocode each result to get formatted addresses - const placesPromises = geocodedLocations.slice(0, 20).map(async (location, index) => { + // Helper function to reverse geocode a single location with caching + const reverseGeocodeLocation = async ( + location: Location.LocationGeocodedLocation, + index: number + ): Promise => { + const cacheKey = `${location.latitude},${location.longitude}`; + + // Check cache first + const cachedAddress = reverseGeocodeCache.get(cacheKey); + if (cachedAddress) { + return { + id: `place-${index}-${location.latitude}-${location.longitude}`, + formattedAddress: cachedAddress, + latitude: location.latitude, + longitude: location.longitude, + }; + } + try { const reverseGeocode = await Location.reverseGeocodeAsync({ latitude: location.latitude, @@ -192,6 +214,9 @@ export function useDeviceLocation(): UseDeviceLocationResult { const formattedAddress = parts.join(', ') || searchQuery; + // Cache the result + reverseGeocodeCache.set(cacheKey, formattedAddress); + return { id: `place-${index}-${location.latitude}-${location.longitude}`, formattedAddress, @@ -200,9 +225,11 @@ export function useDeviceLocation(): UseDeviceLocationResult { }; } else { // Fallback if reverse geocoding fails + const fallbackAddress = searchQuery; + reverseGeocodeCache.set(cacheKey, fallbackAddress); return { id: `place-${index}-${location.latitude}-${location.longitude}`, - formattedAddress: searchQuery, + formattedAddress: fallbackAddress, latitude: location.latitude, longitude: location.longitude, }; @@ -210,16 +237,46 @@ export function useDeviceLocation(): UseDeviceLocationResult { } catch (error) { console.error('Reverse geocoding error for place:', error); // Fallback + const fallbackAddress = searchQuery; + reverseGeocodeCache.set(cacheKey, fallbackAddress); return { id: `place-${index}-${location.latitude}-${location.longitude}`, - formattedAddress: searchQuery, + formattedAddress: fallbackAddress, latitude: location.latitude, longitude: location.longitude, }; } - }); + }; + + // Process locations in small batches with throttling + const BATCH_SIZE = 5; + const DELAY_BETWEEN_BATCHES = 200; // 200ms delay between batches + const locationsToProcess = geocodedLocations.slice(0, 20); + const places: PlaceResult[] = []; + + for (let i = 0; i < locationsToProcess.length; i += BATCH_SIZE) { + const batch = locationsToProcess.slice(i, i + BATCH_SIZE); + + // Process batch with Promise.allSettled to handle individual failures + const batchResults = await Promise.allSettled( + batch.map((location, batchIndex) => + reverseGeocodeLocation(location, i + batchIndex) + ) + ); + + // Extract successful results + batchResults.forEach((result) => { + if (result.status === 'fulfilled') { + places.push(result.value); + } + }); + + // Add delay between batches (except for the last batch) + if (i + BATCH_SIZE < locationsToProcess.length) { + await sleep(DELAY_BETWEEN_BATCHES); + } + } - const places = await Promise.all(placesPromises); return places; } catch (err: any) { console.error('Error getting places in state:', err); From 5adef5ff7d03303190f714adfbac5505db9ca9e8 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:58:12 -0500 Subject: [PATCH 15/17] switched to strict equality --- frontend/hooks/use-media-canvas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/hooks/use-media-canvas.ts b/frontend/hooks/use-media-canvas.ts index e653802..829d9f1 100644 --- a/frontend/hooks/use-media-canvas.ts +++ b/frontend/hooks/use-media-canvas.ts @@ -25,7 +25,7 @@ export function useMediaCanvas() { } const removeElement = (id: number) => { - setItems(prevItems => prevItems.filter(item => item.id != id)); + setItems(prevItems => prevItems.filter(item => item.id !== id)); } const updateTextItem = (id: number, text: string, style: { color: string; fontFamily?: string; backgroundColor?: string }) => { From d7cc28c7ea35d29da630983ae51f03e7782374c8 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 21:59:11 -0500 Subject: [PATCH 16/17] removed log --- frontend/hooks/use-device-location.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/hooks/use-device-location.ts b/frontend/hooks/use-device-location.ts index dec4d36..3115df4 100644 --- a/frontend/hooks/use-device-location.ts +++ b/frontend/hooks/use-device-location.ts @@ -110,8 +110,6 @@ export function useDeviceLocation(): UseDeviceLocationResult { postalCode: address.postalCode || '', }; - console.log({ address }); - return locationData; } catch (err: any) { console.error('Location error:', err); From 36c8303796f9c879e3e7cfe9e3dceafd98518c68 Mon Sep 17 00:00:00 2001 From: Fortune Oluwasemilore Alebiosu Date: Tue, 6 Jan 2026 22:03:10 -0500 Subject: [PATCH 17/17] added default text basck incase text clears --- frontend/app/capture/details.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/capture/details.tsx b/frontend/app/capture/details.tsx index 9744779..3f0c807 100644 --- a/frontend/app/capture/details.tsx +++ b/frontend/app/capture/details.tsx @@ -229,7 +229,7 @@ export default function DetailsScreen() { }; updateTextItem( pendingTextItemId, - currentItem.text || pendingTextValue, + currentItem.text ?? pendingTextValue, updatedStyle as { color: string; fontFamily?: string; backgroundColor?: string } ); }