I am making a simple messaging app, when i try to send messages the same div gets updated with new message but a new div does not get added. i was using index first while mapping the messages to display but it did not work so i added a msgId to the messages and tried to use it as the key but it's still not working
this is OpenConversation.js
import React, { useState } from 'react'
import {Button, Form, InputGroup, ListGroup} from 'react-bootstrap'
import { useConversations } from '../contexts/ConversationsProvider'
export default function OpenConversation() {
const[text,setText]=useState()
const {sendMessage,selectedConversation}=useConversations()
function handleSubmit(e) {
e.preventDefault()
console.log("index",selectedConversation)
sendMessage(
selectedConversation.recipients.map(r => r.id),
text
)
setText('')
}
return (
<div className="d-flex flex-column flex-grow-1">
<div className="flex-grow-1 overflow-auto">
<div className="d-flex flex-column align-items-start justify-content-end px-3">
{selectedConversation.messages.map((message) => {
return (
<div
key={message.msgId}
className={`my-1 d-flex flex-column ${message.fromMe ? 'align-self-end align-items-end' : 'align-items-start'}`}
>
<div
className={`rounded px-2 py-1 ${message.fromMe ? 'bg-primary text-white' : 'border'}`}>
{message.text} {message.msgId}
</div>
<div className={`text-muted small ${message.fromMe ? 'text-right' : ''}`}>
{message.fromMe ? 'You' : message.senderName}
</div>
</div>
)
})}
</div>
</div>
<Form style={{position:'absolute',bottom:'0rem'}}
onSubmit={handleSubmit}
>
<Form.Group className='m-2'>
<InputGroup >
<Form.Control as='textarea' required
value={text}
onChange={e=>setText(e.target.value)}
style={{height:'75PX',resize:'none',}}
/>
<Button type="submit" style={{background:'#7c73e6',border:'none'}}>Send</Button>
</InputGroup>
</Form.Group>
</Form>
</div>
)
}
ConversationsProvider.js
import React, { useContext, useState, useEffect, useCallback } from 'react'
import useLocalStorage from '../hooks/useLocalStorage';
import { useContacts } from './ContactsProvider';
import {v4 as uuidv4} from 'uuid'
const ConversationsContext = React.createContext()
export function useConversations() {
return useContext(ConversationsContext)
}
export default function ConversationsProvider({ id, children }) {
const [conversations, setConversations] = useLocalStorage('conversations', [])
const [selectedConversationIndex,setSelectedConversationIndex]=useState(0)
const { contacts } = useContacts()
function createConversation(recipients) {
setConversations(prevConversations => {
return [...prevConversations, { recipients, messages: [] }]
})
}
function selectConversationIndex(i)
{
setSelectedConversationIndex(i)
}
function addMessageToConversation({recipients,text,sender})
{
setConversations(prevConversations=>{
let madeChange=false
let msgId=uuidv4()
const newMessage={sender,text,msgId}
const newConversations=prevConversations.map(conversation=>{
if(arrayEquality(conversation.recipients,recipients))
{
madeChange=true
return{
...conversation,
messages:[conversation.messages,newMessage]
}
}
return conversation
})
if(madeChange){
return newConversations
}
else{
return [...prevConversations,{recipients,messages:[newMessage]}]
}
})
console.log(conversations)
}
function sendMessage(recipients,text)
{ console.log("working")
addMessageToConversation({recipients,text,sender:id})
}
const formattedConversations = conversations.map((conversation, index) => {
const recipients = conversation.recipients.map(recipient => {
const contact = contacts.find(contact => {
return contact.id === recipient
})
const name = (contact && contact.name) || recipient
return { id: recipient, name }
})
const messages = conversation.messages.map(message => {
const contact = contacts.find(contact => {
return contact.id === message.sender
})
const name = (contact && contact.name) || message.sender
const fromMe = id === message.sender
return { ...message, senderName: name, fromMe }
})
const selected = index === selectedConversationIndex
return { ...conversation, messages, recipients, selected }
})
const value = {
conversations: formattedConversations,
selectedConversation: formattedConversations[selectedConversationIndex],
sendMessage,
selectConversationIndex: setSelectedConversationIndex,
createConversation
}
return (
<ConversationsContext.Provider value={value}>
{children}
</ConversationsContext.Provider>
)
}
function arrayEquality(a, b) {
console.log("working")
if (a.length !== b.length) return false
a.sort()
b.sort()
return a.every((element, index) => {
return element === b[index]
})
}
i did not understand why index did not work, is there another way to map the messages?
Your error comes from this line
messages:[conversation.messages,newMessage]
in the addMessageToConversation function
I think you need to spread conversation.messages.
Change [conversation.messages, newMessage] to [...conversation.messages, newMessage].
With what you're doing currently, you're making the previous messages object, the first item in the array and then the new one as the second item.
Related
I am developing an app with react and redux toolkit. I am using an API. the part that I would like help with is this:
I have a favorite Icon. this icon takes a copy of the movie and displays it in the favorite section. There is another icon which is for watched movies. so the idea is that if I click on watched it should disable only the favorite icon of the movie card I clicked. however, it disables all favorite icons for all movie cards. The watched icon is referred to as Eye the favorite icon is referred to as star
This is the eye component (Watched)
import { useState } from "react";
import { BsEyeSlash, BsEyeFill } from "react-icons/bs";
import { useDispatch, useSelector } from "react-redux";
import {
add_watched,
remove_watched,
add_occupied,
remove_occupied,
} from "../features/animeSlice";
const Eye = ({ anime, type }) => {
const [active_eye, setActive_eye] = useState(false);
const dispatch = useDispatch();
const toggle_eye = () => {
if (!active_eye) {
setActive_eye((prev) => {
return !prev;
});
dispatch(add_watched(anime));
dispatch(add_occupied(type));
} else {
setActive_eye((prev) => {
return !prev;
});
dispatch(remove_watched(anime?.mal_id));
dispatch(remove_occupied());
}
};
return (
<div>
{!active_eye ? (
<BsEyeSlash
className="text-xl text-green-500 icons_1"
onClick={toggle_eye}
/>
) : (
<BsEyeFill
className={"text-xl text-red-500 icons_1"}
onClick={toggle_eye}
/>
)}
</div>
);
};
export default Eye;
This is the Star Component (Favorite)
import { useState } from "react";
import { AiOutlineStar, AiFillStar } from "react-icons/ai";
import { add_favourite, remove_favourite } from "../features/animeSlice";
import { useDispatch, useSelector } from "react-redux";
const Star = ({ anime }) => {
const { value} = useSelector((state) => state.anime);
const [active_star, setActive_star] = useState(false);
const dispatch = useDispatch();
const toggle_star = () => {
if (!active_star) {
setActive_star((prev) => {
return !prev;
});
dispatch(add_favourite(anime));
} else {
setActive_star((prev) => {
return !prev;
});
dispatch(remove_favourite(anime?.mal_id));
}
};
return (
<div>
{!active_star ? (
<AiOutlineStar
className={
value === "occupied"
? "text-xl text-gray-300 pointer-events-none"
: "text-xl text-yellow-500 icon_1"
}
onClick={toggle_star}
/>
) : (
<AiFillStar
className={
value === "occupied"
? "text-xl text-gray-300 pointer-events-none"
: "text-xl text-yellow-500 icon_1"
}
onClick={toggle_star}
/>
)}
</div>
);
};
export default Star;
this is the redux toolkit slice
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
favourite: [],
watched: [],
value: "",
page:""
};
const animeSlice = createSlice({
name: "anime",
initialState,
reducers: {
add_favourite(state, { payload }) {
state.favourite.push(payload);
},
remove_favourite(state, { payload }) {
state.favourite = state.favourite.filter(
(anime) => anime?.mal_id !== payload
);
},
add_watched(state, { payload }) {
state.watched.push(payload);
},
remove_watched(state, { payload }) {
state.watched = state.watched.filter((anime) => anime?.mal_id !== payload);
},
add_occupied(state, { payload }) {
state.value = payload;
},
remove_occupied(state) {
state.value = "";
},
pageNumber(state, { payload }) {
state.page = payload
}
},
});
export const {
add_favourite,
remove_favourite,
add_watched,
remove_watched,
add_occupied,
remove_occupied,
pageNumber
} = animeSlice.actions;
export default animeSlice.reducer;
This is the component that holds the component of eye and star
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import Eye from "./Eye";
import Star from "./Star";
const TopAnime = ({ anime }) => {
//pagination
//
//
let colorYear = (year) => {
if (year === "N/A") {
return "text-violet-500";
} else {
return "";
}
};
return (
<section className="grand px-4 pt-2 pb-5 w-64 bg-white/5 bg-opacity-80 backdrop-blur-sm rounded-lg cursor-pointer font-poppins animate-slideup">
<div className="pb-1 wrapper_icons">
<div className="wrapper_hover">
<Eye anime={anime} type="occupied" />
<Star anime={anime} />
</div>
</div>
<Link to={`/anime/${anime?.mal_id}`}>
<div className="wrapper_1 flex flex-col items-center justify-center">
<div className="h-[313px] w-[219px]">
<img
src={anime?.images?.jpg?.large_image_url}
alt={anime?.title}
className="h-full w-full"
/>
</div>
</div>
<div className="flex flex-col mt-3">
<p className="text-sm text-white truncate mx-1">
{anime?.title_english ? anime?.title_english : anime?.title}
</p>
<div className="flex justify-between items-center text-sm text-yellow-500 mx-1">
<p className={colorYear(anime?.year ? anime?.year : "N/A")}>
{anime?.year ? anime?.year : "N/A"}
</p>
<p
className={
anime?.score <= 7
? "text-cyan-500"
: anime?.score <= 5
? "text-red-600"
: "text-green-500"
}
>
{anime?.score}
</p>
</div>
</div>
</Link>
</section>
);
};
export default TopAnime;
This is where TopAnime is rendered
import { useGetAnimeQuery } from "../features/API";
import { useDispatch, useSelector } from "react-redux";
import { useState, useEffect } from "react";
import TopAnime from "../components/TopAnime";
import Spinner from "../components/Spinner";
import { NavLink, Link } from "react-router-dom";
import Paginator from "../components/Paginator";
//import ReactPaginate from "react-paginate";
import Pagination from "#mui/material/Pagination";
const Home = () => {
const [page, setPage] = useState(1);
const {
data: manga = [],
isLoading,
isFetching,
error,
} = useGetAnimeQuery(page);
const { data, pagination } = manga;
//destructuring objects
//const { data, pagination } = manga;
//const top_anime = data;
const total = Math.ceil(pagination?.items?.total / 24)
//const current_page = pagination?.current_page;
//const per_page = pagination?.items?.per_page;
//const { items: pages } = total;
/* let fetchData = async (page = 1) => {
let res = await fetch(
`https://api.jikan.moe/v4/top/anime?page=${page}&limit=24`
);
let query = await res.json();
const { data, pagination } = query;
let totalPages = Math.ceil(pagination?.items.total / 24);
setPageCount(totalPages);
setData(data);
};
useEffect(() => {
fetchData();
}, []);
*/
import { useGetAnimeQuery } from "../features/API";
import { useDispatch, useSelector } from "react-redux";
import { useState, useEffect } from "react";
import TopAnime from "../components/TopAnime";
import Spinner from "../components/Spinner";
import { NavLink, Link } from "react-router-dom";
import Paginator from "../components/Paginator";
//import ReactPaginate from "react-paginate";
import Pagination from "#mui/material/Pagination";
const Home = () => {
const [page, setPage] = useState(1);
const {
data: manga = [],
isLoading,
isFetching,
error,
} = useGetAnimeQuery(page);
const { data, pagination } = manga;
//destructuring objects
//const { data, pagination } = manga;
//const top_anime = data;
const total = Math.ceil(pagination?.items?.total / 24)
//const current_page = pagination?.current_page;
//const per_page = pagination?.items?.per_page;
//const { items: pages } = total;
/* let fetchData = async (page = 1) => {
let res = await fetch(
`https://api.jikan.moe/v4/top/anime?page=${page}&limit=24`
);
let query = await res.json();
const { data, pagination } = query;
let totalPages = Math.ceil(pagination?.items.total / 24);
setPageCount(totalPages);
setData(data);
};
useEffect(() => {
fetchData();
}, []);
*/
const handleChange = (event, value) => {
setPage(value);
};
const display = data?.map((anime) => (
<TopAnime anime={anime} key={anime.mal_id} />
));
//const pageCount = Math.ceil(pagination?.items?.total / 24);
if (isLoading) {
return <Spinner color_1={"#141e30"} color_2={"#243b55"} />;
} else if (isFetching) {
return <Spinner color_1={"#141e30"} color_2={"#243b55"} />;
} else if (error) {
console.log("ERROR =>", error.message);
}
return (
<section className="bg-gradient-to-r from-[#141e30] to-[#243b55]">
<div className="container font-poppins">
<div className="grid grid-cols-4 gap-3 place-items-center px-20">
{/* {top_anime &&
top_anime?.map((anime) => (
<TopAnime anime={anime} key={anime.mal_id} />
))} */}
{display}
</div>
<div className="button text-yellow-500 flex items-center justify-center mt-2 pb-2 cursor-pointer">
{/* <Paginator paginated={paginated} NumP={pagination?.last_visible_page} /> */}
{/* <ReactPaginate
previousLabel={"Previous"}
nextLabel={"Next"}
onPageChange={(page) => fetchData(page.selected + 1)}
pageCount={pageCount}
className="flex space-x-2"
activeClassName="active"
/> */}
<Pagination count={total} page={page} onChange={handleChange} defaultPage={1} boundaryCount={3} color="secondary" sx={{button:{color:'#ffffff'}}} />
</div>
</div>
</section>
);
};
export default Home;
Issue
The issue with the code/current implementation is that there is only one state.anime.value and one "occupied" value. Each time a movie's "watched" status is toggled the Redux state sets/unsets the single state.anime.value state. In other words, you can toggle N movies "watched" and the state.anime.value value is "occupied", but then toggling just 1 movie back to "unwatched" and state.anime.value is reset back to "". All the Star components read this single state value and this is why the stars all toggle together.
Solution
If you want a specific movie to toggle the Star component when the Eye icon is toggled then I think the solution is a bit simpler than you are making it out to be in your code.
The "eye" and "star" components effectively duplicate your Redux state and don't synchronize well with it. It's often considered a React anti-pattern to duplicate state. The state.anime.favourite and state.anime.watched arrays are all the app needs to know what has been marked "watched" and what has been favourited.
Update the animeSlice to store the anime.mal_id properties in objects instead of arrays to provide O(1) lookup in the UI. Remove the anime.value state as it's unnecessary.
animeSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
favourite: {},
watched: {},
page: ""
};
const animeSlice = createSlice({
name: "anime",
initialState,
reducers: {
add_favourite(state, { payload }) {
state.favourite[payload] = true;
},
remove_favourite(state, { payload }) {
delete state.favourite[payload];
},
add_watched(state, { payload }) {
state.watched[payload] = true;
delete state.favourite[payload]; // un-star when adding as watched
},
remove_watched(state, { payload }) {
delete state.watched[payload];
},
pageNumber(state, { payload }) {
state.page = payload;
}
}
});
export const {
add_favourite,
remove_favourite,
add_watched,
remove_watched,
pageNumber
} = animeSlice.actions;
export default animeSlice.reducer;
Update the Eye and Star components to read the true state from the store. These components should pass anime.mal_id to the dispatched actions and use anime.mal_id to check the watched/favourites map objects.
Eye
import { BsEyeSlash, BsEyeFill } from "react-icons/bs";
import { useDispatch, useSelector } from "react-redux";
import { add_watched, remove_watched } from "../features/animeSlice";
const Eye = ({ anime }) => {
const dispatch = useDispatch();
const { watched } = useSelector((state) => state.anime);
const isWatched = watched[anime.mal_id];
const toggle_eye = () => {
dispatch((isWatched ? remove_watched : add_watched)(anime.mal_id));
};
const Eye = isWatched ? BsEyeFill : BsEyeSlash;
const className = [
"text-xl icons_1",
isWatched ? "text-green-500" : "text-red-500"
].join(" ");
return (
<div>
<Eye className={className} onClick={toggle_eye} />
</div>
);
};
export default Eye;
Star
import { AiOutlineStar, AiFillStar } from "react-icons/ai";
import { add_favourite, remove_favourite } from "../features/animeSlice";
import { useDispatch, useSelector } from "react-redux";
const Star = ({ anime }) => {
const { favourite, watched } = useSelector((state) => state.anime);
const dispatch = useDispatch();
const isFavorited = favourite[anime.mal_id];
const isWatched = watched[anime.mal_id];
const toggle_star = () => {
dispatch((isFavorited ? remove_favourite : add_favourite)(anime.mal_id));
};
const Star = isFavorited ? AiFillStar : AiOutlineStar;
const className = [
"text-xl",
isWatched ? "text-gray-300 pointer-events-none" : "text-yellow-500 icon_1"
].join(" ");
return (
<div>
<Star className={className} onClick={toggle_star} />
</div>
);
};
export default Star;
I am working with a video application website with a fake json server. Everything is going well but the problem appears while I want to like a video it becomes disappears and throwing an error: Objects are not valid as react child. Why this is happening?
The whole state slice:
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit"
import { getVideo, updateReaction } from "./videoApi"
//initializing the state
const initialState = {
video: {}, //while we are going to push a single video into the state we should put it an object instance...
loading: false,
isError: false,
error: ''
}
// async thunk for getting single video
export const fetchVideo = createAsyncThunk('vidoes/fetchVideo', async (id) => {
const videos = await getVideo(id);
return videos;
});
// another async thunk to react on a single video (like or dislike)
export const fetchReact = createAsyncThunk('video/reaction', async ({ id, reaction }) => {
const videoReaction = await updateReaction({ id, reaction })
return videoReaction;
})
//creating slice
const videoSlice = createSlice({
name: 'video',
initialState,
extraReducers: (builder) => {
builder.addCase(fetchVideo.pending, (state) => {
state.loading = true;
state.isError = false;
})
builder.addCase(fetchVideo.fulfilled, (state, action) => {
state.loading = false;
state.video= action.payload;
})
builder.addCase(fetchVideo.rejected, (state, action) => {
state.loading = false;
state.isError = true;
state.error = action.error?.message;
state.video = [];
})
//writing codes for emplementing like funcitonality
builder.addCase(fetchReact.pending, (state) => {
return state;
})
builder.addCase(fetchReact.fulfilled, (state, action) => {
state.video.likes = action.payload;
state.video.unlikes = action.payload;
})
builder.addCase(fetchReact.rejected, (state) => {
return state;
})
}
});
export default videoSlice.reducer;
This one is video api
import axios from '../../utils/axios'
export const getVideo = async (id) => {
const response = await axios.get(`/videos/${id}`)
return response.data;
}
export const updateReaction = async ({ id, reaction }) => {
console.log(id)
const { data } = await axios.get(`/videos/${id}`)
if (data) {
let updatedReaction =
reaction === 'like'
? {
likes: data.likes + 1,
}
: {
unlikes: data.unlikes + 1,
};
const response = await axios.patch(`/videos/${id}`, updatedReaction);
return response.data;
}
}
Video description component
import LIke from '../Like/LIke';
const VidoeDescription = ({ video }) => {
const { title, date, description, likes, unlikes, id } = video;
return (
<div>
<h1 className="text-lg font-semibold tracking-tight text-slate-800">
{title}
</h1>
<div className="pb-4 flex items-center space-between border-b">
<h2 className="text-sm leading-[1.7142857] text-slate-600 w-full">
Uploaded on {date}
</h2>
<LIke likes={likes} unlikes={unlikes} id={id} />
</div>
<div className="mt-4 text-sm text-[#334155] dark:text-slate-400">
{description}
</div>
</div>
);
};
export default VidoeDescription;
Like component where I put my like and unlike component
import React from 'react';
import { useDispatch } from 'react-redux';
import LikeImg from '../../assets/like.svg';
import UnLikeImg from '../../assets/unlike.svg'
// import { updateReaction } from '../../features/video/videoApi';
import { fetchReact } from '../../features/video/video_slice';
const LIke = ({ likes, unlikes, id }) => {
console.log(id)
const dispatch = useDispatch();
//handle like features
const reactionHandler = ({ id, reaction }) => {
dispatch(fetchReact({ id, reaction }))
}
return (
<div className="flex gap-10 w-48">
<div className="flex gap-1">
<div
className="shrink-0 cursor-pointer"
onClick={() => reactionHandler({ id, reaction: 'like' })}
>
<img className="w-5 block" src={LikeImg} alt="Like" />
</div>
<div className="text-sm leading-[1.7142857] text-slate-600">
{likes >= 1000 ? `${ likes }K` : likes}
</div>
</div>
<div className="flex gap-1">
<div
onClick={() => reactionHandler({ id, reaction: 'unlike' })}
className="shrink-0 cursor-pointer"
>
<img className="w-5 block" src={UnLikeImg} alt="Unlike" />
</div>
<div className="text-sm leading-[1.7142857] text-slate-600">
{unlikes >= 1000 ? `${ unlikes }K` : unlikes}
</div>
</div>
</div>
);
};
export default LIke;
This one is root video page
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import Player from '../component/Player/Player';
import VidoeDescription from '../component/VideoDescription/VidoeDescription';
import { fetchVideo } from '../features/video/video_slice';
import Loading from '../component/Loading/Loading';
import RelatedVideo from '../component/VideoList/RelatedVideo';
const Videos = () => {
const { loading, isError, video, error } = useSelector(state => state.video)
const { id, link, title , tags } = video || {};
const dispatch = useDispatch()
const { videoId } = useParams();
useEffect(() => {
dispatch(fetchVideo(videoId))
}, [dispatch, videoId])
//decide what to render in the ui
let content = null;
if (loading) {
content = <Loading />
}
if (loading && isError) {
content = <div className="col-span-12"><srong>{error}</srong></div>
}
if (!loading && !isError && video?.id) {
content = <div className="col-span-12"><srong>{error}</srong></div>
}
if (!loading && !isError && video?.id) {
content = <div className="mx-auto max-w-7xl px-2 pb-20 min-h-[400px]">
<div className="grid grid-cols-3 gap-2 lg:gap-8">
<div className="col-span-full w-full space-y-8 lg:col-span-2">
<Player link={link} title={title} />
<VidoeDescription video={video} />
</div>
<RelatedVideo currentId={id} tags={tags} />
</div>
</div>
}
return (
<div>
<section className="pt-6 pb-20">
{content}
</section>
</div>
);
};
export default Videos;
Problem
Here:
builder.addCase(fetchReact.fulfilled, (state, action) => {
state.video.likes = action.payload;
state.video.unlikes = action.payload;
})
action.payload looks like this:
{
likes: 1,
}
because it's how you created it in updateReaction
Solution
You wanted to do this instead:
builder.addCase(fetchReact.fulfilled, (state, action) => {
state.video.likes = action.payload.likes;
state.video.unlikes = action.payload.unlikes;
})
The affected code is (Typescript errors in local) (componet is correct, the name is as that):
{template.elements.map((element: TemplateElementModel, i) => {
const stand = roomStands?.find(
(stand: ExhibitorModel) => stand.standNumber === `${i + 1}`
);
const componet = stand ? (
<img
src={stand?.logo}
alt={`${stand?.name} logo`}
className="w-24 object-contain"
/>
) : undefined;
const handleClick = stand
? () => {
dispatch(setExhibitor(stand?.id));
dispatch(pushScreen(EventDetailScreen.stand));
}
: undefined;
That's when the code has an error but is just the new registers. When all is ok it shows the view but if has that error the view was a blank page.
I've tried a lot of solutions but i cannot detect what is the problem.
The complete code is:
import React, { useEffect, useState } from "react";
import {
TemplateElementModel,
TemplateModel,
} from "../../template/template.model";
import {
currentEventIdSelector,
roomIdSelector as eventRoomSelector,
popScreen,
pushScreen,
setExhibitor,
setRoom,
} from "../event.slice";
import { useDispatch, useSelector } from "react-redux";
import { Collections } from "../../../config/app.collections";
import DeimosModal from "../../../lib/deimos-ui/components/deimos-modal/DeimosModal";
import { EventDetailScreen } from "../enums/event-detail-screen";
import { ExhibitorModel } from "../../exhibitor/exhibitor.model";
import { RoomModel } from "../../room/room.model";
import { RootState } from "../../../app.store";
import TemplateMarker from "../../template/components/TemplateMarker";
import { TemplateTypes } from "../../template/template-types.enum";
import { useFirestoreConnect } from "react-redux-firebase";
interface EventExhibitionsPageProps {}
const EventExhibitionsPage: React.FC<EventExhibitionsPageProps> = () => {
const dispatch = useDispatch();
const [template, setTemplate] = useState<TemplateModel>();
const [roomStands, setRoomStands] = useState<ExhibitorModel[]>();
const [showRoomsModal, setShowRoomsModal] = useState(true);
const currentRoom = useSelector(eventRoomSelector);
const eventId = useSelector(currentEventIdSelector);
const rooms = useSelector(
(state: RootState) => state.firestore.data[Collections.ROOM]
);
const templates = useSelector(
(state: RootState) => state.firestore.data[Collections.TEMPLATE]
);
const stands = useSelector(
(state: RootState) => state.firestore.data[Collections.EXHIBITOR]
);
useFirestoreConnect([
{
collection: Collections.ROOM,
where: ["event", "==", eventId],
},
{
collection: Collections.TEMPLATE,
where: ["type", "==", TemplateTypes.Room],
},
{
collection: Collections.EXHIBITOR,
where: ["eventId", "==", eventId],
},
]);
useEffect(() => {
if (!currentRoom) return;
const template = templates[currentRoom?.template];
if (template) setTemplate(template);
}, [currentRoom, templates]);
useEffect(() => {
if (!currentRoom) return;
const filteredStands = Object.values<ExhibitorModel>(stands).filter(
(stand) => stand.room === currentRoom.id
);
setRoomStands(filteredStands);
}, [currentRoom, stands]);
return (
<div className="h-full w-full relative bg-no-repeat bg-center text-center">
<DeimosModal
isOpen={showRoomsModal}
hideCancelButton={!currentRoom}
closeButtonText="Salir"
cancelButtonText={`Seguir en ${currentRoom?.name}`}
handleCancel={() => {
setShowRoomsModal(false);
}}
handleClose={() => {
dispatch(popScreen());
}}
>
{!!rooms && (
<div>
<p className="font-bold">Selecciona una sala a la que entrar</p>
{Object.values<RoomModel>(rooms).map((room) => {
return (
<div
className="flex flex-row w-full my-2 p-3 bg-gray-200 rounded cursor-pointer"
onClick={() => {
dispatch(setRoom(room));
setShowRoomsModal(false);
}}
>
{room.name}
</div>
);
})}
</div>
)}
</DeimosModal>
{currentRoom && template && (
<div
className="w-full h-full flex flex-row justify-center align-center"
style={{
backgroundImage: `url(${template.background})`,
backgroundSize: "100% 100%",
}}
>
{template.elements.map((element: TemplateElementModel, i) => {
const stand = roomStands?.find(
(stand: ExhibitorModel) => stand.standNumber === `${i + 1}`
);
const componet = stand ? (
<img
src={stand?.logo}
alt={`${stand?.name} logo`}
className="w-24 object-contain"
/>
) : undefined;
const handleClick = stand
? () => {
dispatch(setExhibitor(stand?.id));
dispatch(pushScreen(EventDetailScreen.stand));
}
: undefined;
return (
<TemplateMarker
// backgroundColor="#327ba8"
width={element.w}
height={element.h}
x={element.x}
y={element.y}
key={element.id}
skewX={element.skewX ?? template.skewX}
skewY={element.skewY ?? template.skewY}
labelComponent={componet}
onClick={handleClick}
/>
);
})}
</div>
)}
</div>
);
};
export default EventExhibitionsPage;
TemplateMarker might not accept labelComponent to be undefined or null. You can add an extra check before sending TemplateMarker.
return componet && (
<TemplateMarker
// backgroundColor="#327ba8"
width={element.w}
height={element.h}
x={element.x}
y={element.y}
key={element.id}
skewX={element.skewX ?? template.skewX}
skewY={element.skewY ?? template.skewY}
labelComponent={componet}
onClick={handleClick}
/>
);
I have created a component to function as a "Like/Unlike" button. When the state is true, the "Unlike" button successfully displays, but when I click "Unlike", and it DOES unlike successfully, the state should be set to false as (liked: false). However, I don't see the button.
One thing I noticed is, when I click "Unlike", the "Unlike" button disappears and the "Like" button does appear, for a millisecond, and then it vanishes in thin air. I cannot figure it out why.
Here are all the codes for my like button component:
import React from "react";
import { API, graphqlOperation } from "aws-amplify";
import { Button } from "element-react";
import { createLike, deleteLike } from "../graphql/mutations";
import { UserContext } from "../App";
class Like extends React.Component {
state = {
liked: "",
};
componentDidMount() {
this.setLiked();
}
setLiked() {
console.log(this.props);
const { user } = this.props;
const { post } = this.props;
if (post.likes.items.find((items) => items.liker === user.username)) {
this.setState({ liked: true });
console.log("liked: true");
} else {
this.setState({ liked: false });
console.log("liked: false");
}
}
handleLike = async (user) => {
try {
const input = {
liker: user.username,
likePostId: this.props.postId,
};
await API.graphql(graphqlOperation(createLike, { input }));
this.setState({
liked: true,
});
console.log("Liked!");
} catch (err) {
console.log("Failed to like", err);
}
};
handleUnlike = async (likeId) => {
try {
const input = {
id: likeId,
};
await API.graphql(graphqlOperation(deleteLike, { input }));
this.setState({
liked: false,
});
console.log("Unliked!");
} catch (err) {
console.log("Failed to unlike", err);
}
};
render() {
const { like } = this.props;
const { liked } = this.state;
return (
<UserContext.Consumer>
{({ user }) => (
<React.Fragment>
{liked ? (
<Button type="primary" onClick={() => this.handleUnlike(like.id)}>
Unlike
</Button>
) : (
<Button
type="primary"
onClick={() => this.handleLike(user, like.id)}
>
Like
</Button>
)}
</React.Fragment>
)}
</UserContext.Consumer>
);
}
}
export default Like;
The code of the parent component:
import React from "react";
import { API, graphqlOperation } from "aws-amplify";
import {
onCreateComment,
onCreateLike,
onDeleteLike,
} from "../graphql/subscriptions";
import { getPost } from "../graphql/queries";
import Comment from "../components/Comment";
import Like from "../components/Like";
import LikeButton from "../components/LikeButton";
import { Loading, Tabs, Icon } from "element-react";
import { Link } from "react-router-dom";
import { S3Image } from "aws-amplify-react";
import NewComment from "../components/NewComment";
class PostDetailPage extends React.Component {
state = {
post: null,
isLoading: true,
isAuthor: false,
};
componentDidMount() {
this.handleGetPost();
this.createCommentListener = API.graphql(
graphqlOperation(onCreateComment)
).subscribe({
next: (commentData) => {
const createdComment = commentData.value.data.onCreateComment;
const prevComments = this.state.post.comments.items.filter(
(item) => item.id !== createdComment.id
);
const updatedComments = [createdComment, ...prevComments];
const post = { ...this.state.post };
post.comments.items = updatedComments;
this.setState({ post });
},
});
this.createLikeListener = API.graphql(
graphqlOperation(onCreateLike)
).subscribe({
next: (likeData) => {
const createdLike = likeData.value.data.onCreateLike;
const prevLikes = this.state.post.likes.items.filter(
(item) => item.id !== createdLike.id
);
const updatedLikes = [createdLike, ...prevLikes];
const post = { ...this.state.post };
post.likes.items = updatedLikes;
this.setState({ post });
},
});
this.deleteLikeListener = API.graphql(
graphqlOperation(onDeleteLike)
).subscribe({
next: (likeData) => {
const deletedLike = likeData.value.data.onDeleteLike;
const updatedLikes = this.state.post.likes.items.filter(
(item) => item.id !== deletedLike.id
);
const post = { ...this.state.post };
post.likes.items = updatedLikes;
this.setState({ post });
},
});
}
componentWillUnmount() {
this.createCommentListener.unsubscribe();
}
handleGetPost = async () => {
const input = {
id: this.props.postId,
};
const result = await API.graphql(graphqlOperation(getPost, input));
console.log({ result });
this.setState({ post: result.data.getPost, isLoading: false }, () => {});
};
checkPostAuthor = () => {
const { user } = this.props;
const { post } = this.state;
if (user) {
this.setState({ isAuthor: user.username === post.author });
}
};
render() {
const { post, isLoading } = this.state;
return isLoading ? (
<Loading fullscreen={true} />
) : (
<React.Fragment>
{/*Back Button */}
<Link className="link" to="/">
Back to Home Page
</Link>
{/*Post MetaData*/}
<span className="items-center pt-2">
<h2 className="mb-mr">{post.title}</h2>
</span>
<span className="items-center pt-2">{post.content}</span>
<S3Image imgKey={post.file.key} />
<div className="items-center pt-2">
<span style={{ color: "var(--lightSquidInk)", paddingBottom: "1em" }}>
<Icon name="date" className="icon" />
{post.createdAt}
</span>
</div>
<div className="items-center pt-2">
{post.likes.items.map((like) => (
<Like
user={this.props.user}
like={like}
post={post}
postId={this.props.postId}
/>
))}
</div>
<div className="items-center pt-2">
{post.likes.items.length}people liked this.
</div>
<div>
Add Comment
<NewComment postId={this.props.postId} />
</div>
{/* Comments */}
Comments: ({post.comments.items.length})
<div className="comment-list">
{post.comments.items.map((comment) => (
<Comment comment={comment} />
))}
</div>
</React.Fragment>
);
}
}
export default PostDetailPage;
I think I know why it doesn't show up. It's because at first when the user hasn't liked it, there is no "like" object, so there is nothing to be shown, as it is only shown when there is a "like" mapped to it. I don't know how to fix it though.
today I offer you a new challenge
my problem and the next
in the following code
I map objects in my database
I also map a list of items
and so basically I would like to pass my database and not the Items table
basically I want to do exactly the same thing as in the following code except that instead of using the items array, I would like to be able to use the data array which contains my database
do you have any idea how to do this?
I hope I was precise thanks to you for the help Neff
ps: Sorry for the length of the code i try to do my best to clean a little
import React, { Component } from 'react';
import { CardText, Card,Row, Col, Button } from 'reactstrap';
import axios from 'axios'
import GridLayout from 'react-grid-layout';
import SmsForm from './Sms/SMSForm'
import FruitList from './FruitList'
import './AdminPage.scss'
const UP = -1;
const DOWN = 1;
const entrypoint = process.env.REACT_APP_API_ENTRYPOINT+'/api';
class AdminPage extends Component {
constructor(props) {
super(props);
this.state = {
items: [
{ id: 1, name: "orange", bgColor: "#f9cb9c" },
{ id: 2, name: "lemon", bgColor: "#fee599" },
{ id: 3, name: "strawberry", bgColor: "#e06666" }
],
data: [],
}
handleMove = (id, direction) => {
const { items } = this.state;
const position = items.findIndex(i => i.id === id);
if (position < 0) {
throw new Error("Given item not found.");
} else if (
(direction === UP && position === 0) ||
(direction === DOWN && position === items.length - 1)
) {
return; // canot move outside of array
}
const item = items[position]; // save item for later
const newItems = items.filter(i => i.id !== id); // remove item from array
newItems.splice(position + direction, 0, item);
this.setState({ items: newItems });
};
// rest of the component
onHandleChange(event) {
const name = event.target.getAttribute('name');
this.setState({
message: { ...this.state.message, [name]: event.target.value }
});
}
getRandom = async () => {
const res = await axios.get(
entrypoint + "/alluserpls"
)
this.setState({ data: res.data })
}
componentDidMount() {
this.getRandom()
}
render() {
let datas = this.state.data.map(datass => {
const status = JSON.parse(localStorage.getItem("validated-order") || "{}")[datass.id];
return (
<div>
< Col sm="12" key={datass.id} className="wtfFuHereIsForOnlyBackGroundColorForCol12Nice">
<FruitList fruitList={this.state.items} onMove={this.handleMove} />
<GridLayout className="GridlayoutTextOnlyForGridOuiAndHeigthbecauseHeigthWasBug" layout={layout} cols={12} rowHeight={30} width={1200}>
<div key="a">
<Card body className="yoloCardBodyForbackGroundAndRaduisBorderForAdminPageWtf">
<CardText className="cardTextForAdminPageForDataName"> Commande de {datass.name}</CardText>
</Card>
</div>
</ Col>
</div>
)
})
return (
<div> <div>
<Row className="wtfHereIsAgainOnlyForRowAndMarginForAdminPageJustWtf">
<div className="isJustForOnlyPaddingOnRowForAdminPage" >
<div className="navBarGridMenuAdminPage">
<div className="thatIsOnlyForSpaceArroundOnDivAdminPage">
<CardText className="maybeColForAdminPageOuiOui"> Nouvelles commandes </CardText>
</div>
</div>
<div>
{datas}
</div>
</div>
</Row>
</div>
<div className="box">
</div>
</div>
)
}
}
export default AdminPage
here my second components
import React from "react";
const LEFT = -1;
const RIGHT = 1;
class FruitList extends React.Component {
render() {
const { fruitList, onMove } = this.props;
return (
<div style={{ display: "flex" }}>
{fruitList.map(item => (
<div
key={item.id}
style={{
backgroundColor: item.bgColor,
display: "flex"
}}
>
<div className="fruitsArrows">
<a onClick={() => onMove(item.id, LEFT)}>←</a>
</div>
<div className="fruitsId">{item.id}</div>
<div className="fruitsName">{item.name}</div>
<div className="fruitsArrows">
<a onClick={() => onMove(item.id, RIGHT)}>→</a>
</div>
</div>
))}
</div>
);
}
}
export default FruitList;
To delete particular list do like this-
pass your item(object).
<a onClick={() => onMove(item)}>→</a>
handleMove function
handleMove = (row) => {
let filtered = this.state.items.filter(item=>item.id!==row.id);
this.setState({ items: filtered});
};