Source

src/components/LandmarkDetails.tsx

import { FontAwesome } from "@expo/vector-icons";
import React, { memo, useCallback, useEffect, useRef, useState } from "react";
import { ActivityIndicator, Dimensions, Image, Keyboard, ListRenderItem, StyleSheet, Text, TextInput, View } from "react-native";
import { ScrollView, TouchableWithoutFeedback } from "react-native-gesture-handler";
import Picker from "react-native-picker-select";
import { colors, Icons, IconStrings } from "../globals";
import { Landmark, LMComment, useLandmarks } from "../hooks/useLandmarks";
import { authStore } from "../stores/AuthStore";
import { PrimaryButton } from "./Auth/Buttons";

/**
 * Props for the {@link Comment} component.
 */
export interface CommentProps {
    /**
     * The [comment]{@link LMComment} object being displayed by the {@link Comment} component.
     */
    comment: LMComment
}

/**
 * Component that displays a {@link LMComment} object in a clean format.
 * @component
 */
export const Comment: React.FC<CommentProps> = ({comment}) => (
    <View>
        <Text testID="comment-text">{comment.text}</Text>
    </View>
)

/**
 * Props for the {@link LandmarkDetails} component.
 */
export interface LandmarkDetailsProps {
    /**
     * The {@link Landmark} object being displayed/edited in the {@link LandmarkDetails} modal. 
     */
    landmark: Landmark | undefined
    /**
     * A callback passed from the parent {@link Map} that toggles the visibility of the {@link LandmarkDetails} modal.
     */
    setVisible: (state: boolean) => void;
    /**
     * A callback passed from the parent {@link Map} that toggles the ability to edit the {@link Landmark} in {@link LandmarkDetails} modal.
     */
    setEditing: (state: boolean) => void;
    /**
     * A flag that determines whether the properties of the {@link Landmark} displayed in the {@link LandmarkDetails} modal can be edited
     */
    editingEnabled: boolean
}

/**
 * Component that renders the details of a selected {@link Landmark} and allows the user to edit those details. Contained within a [react-native-modal]{@link https://github.com/react-native-modal/react-native-modal}.
 * @component
 * @category Map
 */
const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({landmark, setVisible, setEditing, editingEnabled}) => {
    /**
     * Holds the state of the {@link Landmark} being displayed.
     */
    const selectedLandmarkState = undefined;
    const [selectedLandmark, setLandmark] = useState<Landmark | undefined>(selectedLandmarkState);
    /**
     * Holds state of a {@link Landmark} object parallel to {@linkcode selectedLandmarkState} that is manipulated when {@linkcode editingEnabled} is true.
     */
    const updatedLandmarkState = undefined;
    const [updatedLandmark, setUpdatedLandmark] = useState<Landmark | undefined>(updatedLandmarkState);
    const { 
        updateLandmark, updateLandmarkStatus, resetUpdateLm,
        deleteLandmark, deleteLandmarkStatus, resetDeleteLm,
    } = useLandmarks(undefined);


    useEffect(() => {
        /**
         * Updates this component's {@linkcode selectedLandmarkState} when the {@linkcode landmark} prop passed down from the parent {@link Map}'s {@linkcode selectedLandmark} is changed. 
         * Embedded in a useEffect that listens to the {@linkcode landmark} prop.
         * @memberOf LandmarkDetails
         */
        const updateLandmarkStateFromParent = () => {
            setLandmark(landmark)
        };
        updateLandmarkStateFromParent();
    }, [landmark])

    useEffect(() => {
        /**
         * Resets the {@linkcode updateLandmark} mutation on successful add.
         * Embedded in a useEffect that listens to the {@linkcode updateLandmarkStatus} value from the {@link useLandmarks} hook.
         * @memberOf LandmarkDetails
         */
         const resetUpdateMutationOnSuccess = () => {
            if (updateLandmarkStatus == 'success') {
                resetUpdateLm();
                setLandmark(updatedLandmark)
            }
        }
        resetUpdateMutationOnSuccess();
    }, [updateLandmarkStatus]);

    useEffect(() => {
        /**
         * Resets the {@linkcode deleteLandmark} mutation on successful delete.
         * Embedded in a useEffect that listens to the {@linkcode deleteLandmarkStatus} value from the {@link useLandmarks} hook.
         * @memberOf LandmarkDetails
         */
        const resetDeleteMutationOnSuccess = () => {
            if (deleteLandmarkStatus == 'success') {
                resetDeleteLm();
            }
        }
        resetDeleteMutationOnSuccess();
    }, [deleteLandmarkStatus]);

    /**
     * Toggles whether or not the {@linkcode selectedLandmark} can be edited through the {@linkcode editingEnabled} prop.
     */
    const toggleEditing = (editingState: boolean) => {
        if (editingState) { setUpdatedLandmark(selectedLandmark); }
        else { setUpdatedLandmark(undefined); }
        setEditing(editingState);   
    }

    /**
     * Calls the {@linkcode updateLandmark} mutation from the {@link useLandmarks} hook and closes the modal once finished.
     */
    const editLandmark = async () => {
        if (updatedLandmark) {
            await updateLandmark(updatedLandmark);       
        }
        
        setEditing(false);
    }

    /**
     * Calls the {@linkcode deleteLandmark} mutation from the {@link useLandmarks} hook and closes the modal once finished.
     */
    const removeLandmark = async () => {
        await deleteLandmark(selectedLandmark?.id);   
        setVisible(false);
    }

    /**
     * Renders all [comments]{@link LMComment} associated with the {@linkcode selectedLandmark} as items for the [FlatList]{@link https://reactnative.dev/docs/flatlist} in this component.
     */
    const renderComment: ListRenderItem<LMComment> = useCallback(({item}) => (
        <Comment comment={item} />
    ), []);

    return (
        <View style={[styles.container, editingEnabled ? {height: Dimensions.get("window").height * .4,} : {height: Dimensions.get("window").height * .4,}]}>
            {updateLandmarkStatus == "idle" && deleteLandmarkStatus == "idle" ?
            <>
            <View style={styles.detailsHeader}>
                {authStore.userId !== selectedLandmark?.user ? null : !editingEnabled ? 
                <View style={{flexDirection: 'row', alignItems: "center"}}>
                    <FontAwesome style={{marginRight: 20, marginTop: 2}} size={25} color="white" name="edit" onPress={() => toggleEditing(true)}/>
                    <FontAwesome color="white" size={25} name="trash" onPress={async () => removeLandmark()}/>
                </View> : 
                <View style={{flexDirection: 'row', alignItems: "center",}}>
                    <FontAwesome color="white" size={25} name="close" onPress={() => toggleEditing(false)}/>
                </View>}
                {!editingEnabled ?
                <FontAwesome color="white" size={25} name="close" onPress={() => setVisible(false)}/>: 
                updatedLandmark?.description && updatedLandmark.landmark_type ?
                <FontAwesome style={{marginTop: 2}} size={25} color="white" name="check" onPress={async () => editLandmark()}/> : null}
            </View>
            <View style={styles.detailsContainer}>
                {editingEnabled ?
                <View>
                    <View style={{flexDirection: 'row', marginBottom: 20, justifyContent: "space-between"}}> 
                    {updatedLandmark?.landmark_type ? 
                    <>
                    <Picker
                        style={{
                            inputIOS: {color: 'white'}, 
                            inputAndroid: {color: 'white'},
                            viewContainer: {marginVertical: 10, flex: 1}, placeholder: {color: 'white'}}}
                        textInputProps={{placeholderTextColor: 'white', selectionColor: 'white'}}
                        Icon={() => <FontAwesome name="chevron-down" color='white' size={20} />}
                        placeholder={
                            selectedLandmark?.landmark_type ? 
                                {label: IconStrings[selectedLandmark.landmark_type].toUpperCase(), value: selectedLandmark.landmark_type} : 
                                {label: "Select landmark type", value: 0} }
                        value={updatedLandmark.landmark_type}
                        onValueChange={(value) => {
                            setUpdatedLandmark({...updatedLandmark, landmark_type: value, title: IconStrings[value]})
                        }}
                        useNativeAndroidPickerStyle={true}
                        items={Object.keys(Icons)?.filter(icon => parseInt(icon) != selectedLandmark?.landmark_type).map(icon => {
                            return (
                                {label: IconStrings[parseInt(icon)].toUpperCase(), value: icon, key: icon}
                            )})}   
                    />
                    <Image style={{marginLeft: 20}} source={Icons[updatedLandmark.landmark_type]}/>
                    </>
                    : null}
                    </View>
                    <TouchableWithoutFeedback style={{marginBottom: 20}} onPress={() => Keyboard.dismiss()}>
                        <ScrollView style={{backgroundColor: 'white'}}>
                            <TextInput 
                                multiline={true} 
                                style={[styles.input, {fontSize: 13, marginBottom: 10}]}
                                onChangeText={(value) => setUpdatedLandmark({...updatedLandmark, description: value})} >
                                {updatedLandmark?.description}
                            </TextInput>
                        </ScrollView>
                    </TouchableWithoutFeedback> 
                </View>:
                <View style={{flexDirection: 'row'}}>
                    <View style={{flex: 8, flexDirection: 'column', marginBottom: 20}}>
                        <Text style={{color: 'white', marginBottom: 10, fontSize: 15}}>{selectedLandmark?.title?.toUpperCase()}</Text>
                        <ScrollView>
                            <Text style={{color: 'white', fontSize: 13}}>{selectedLandmark?.description}</Text>
                        </ScrollView>
                    </View>
                    {selectedLandmark?.landmark_type ? <Image source={Icons[selectedLandmark.landmark_type]} /> : null}
                </View>}
                <TouchableWithoutFeedback style={{height: '100%'}} onPress={() => Keyboard.dismiss()}><Text></Text></TouchableWithoutFeedback>
                {/* {!editingEnabled ?
                <View style={styles.commentContainer}>
                    <Text style={{color: 'white', marginBottom: 20}}>Comments: </Text>
                    <FlatList<LMComment>
                        keyExtractor={i => i.id}
                        data={[
                            {id: '1', text: 'Hello', poster: 'chase'}, {id: '3', text: 'Hello', poster: 'chase'}, {id: '2', text: 'Hello', poster: 'chase'},
                            {id: '4', text: 'Hello', poster: 'chase'}, {id: '5', text: 'Hello', poster: 'chase'}, {id: '6', text: 'Hello', poster: 'chase'},
                            {id: '9', text: 'Hello', poster: 'chase'}, {id: '8', text: 'Hello', poster: 'chase'}, {id: '7', text: 'Hello', poster: 'chase'},
                            {id: '12', text: 'Hello', poster: 'chase'}, {id: '10', text: 'Hello', poster: 'chase'}, {id: '11', text: 'Hello', poster: 'chase'},
                        ]} 
                        renderItem={renderComment}
                        style={{backgroundColor: 'white'}}/>
                        
                </View> : null} */}
            </View>
            </> :
            <View style={{height: '100%', justifyContent: "space-evenly", alignItems: "center", marginHorizontal: 20}}>
                <Text style={{color: 'white', fontSize: 20}}>{
                    deleteLandmarkStatus == 'loading' ? "Deleting landmark..." : 
                    deleteLandmarkStatus == 'error' ? "Something went wrong trying to delete the landmark" :
                    updateLandmarkStatus == 'loading' ? "Updating landmark..." : 
                    updateLandmarkStatus == 'error' ? "Something went wrong trying to update the landmark" : null}
                </Text>
                {
                    deleteLandmarkStatus == 'loading' || updateLandmarkStatus == 'loading' ? <ActivityIndicator color='white' size="large"/> :
                    deleteLandmarkStatus == 'error' || updateLandmarkStatus == 'error' ? <PrimaryButton text="Okay" style={{borderColor: 'white', borderWidth: 1}} onPress={() => setVisible(false)} /> : null
                }
            </View> }
        </View>
    )
}

const styles = StyleSheet.create({
    container: {
        backgroundColor: colors.red,
    },
    detailsHeader: {
        borderBottomWidth: 1,
        borderColor: 'white',
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center',
        backgroundColor: '#df3f3f',
        marginBottom: 10,
        paddingVertical: 10,
        paddingHorizontal: 20
    },
    detailsContainer: {
        flex: 1,
        marginHorizontal: 20
    },

    commentContainer: {
        flex: 5,
        marginBottom: 40
    },
    title: {

    },
    input: {
        padding: 5,
        color: 'black'
    }
})

export default memo(LandmarkDetails);