diff --git a/thisable/package.json b/thisable/package.json index ec6b9f5..427cd6f 100644 --- a/thisable/package.json +++ b/thisable/package.json @@ -8,7 +8,9 @@ "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^13.5.0", + "antd": "^3.26.5", "axios": "^0.26.1", + "prop-types": "^15.7.2", "bootstrap": "^5.1.3", "dotenv": "^16.0.0", "geolocation": "^0.2.0", @@ -16,11 +18,16 @@ "react": "^17.0.2", "react-bootstrap": "^2.1.2", "react-dom": "^17.0.2", + "react-redux": "^7.1.3", "react-geolocated": "^3.2.0", "react-router-dom": "^6.2.2", "react-scripts": "5.0.0", "react-simple-star-rating": "^4.0.5", "react-star-ratings": "^2.3.0", + "redux": "^4.0.5", + "redux-promise": "^0.6.0", + "redux-thunk": "^2.3.0", + "uuid": "^3.3.2", "web-vitals": "^2.1.4" }, "scripts": { @@ -46,5 +53,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "http-proxy-middleware": "^0.19.1" } } diff --git a/thisable/src/_actions/message_actions.js b/thisable/src/_actions/message_actions.js new file mode 100644 index 0000000..79aa12b --- /dev/null +++ b/thisable/src/_actions/message_actions.js @@ -0,0 +1,8 @@ +import { SAVE_MESSAGE } from "./types"; + +export function saveMessage(dataToSubmit) { + return { + type: SAVE_MESSAGE, + payload: dataToSubmit, + }; +} diff --git a/thisable/src/_actions/types.js b/thisable/src/_actions/types.js new file mode 100644 index 0000000..96c94b5 --- /dev/null +++ b/thisable/src/_actions/types.js @@ -0,0 +1 @@ +export const SAVE_MESSAGE = "save_message"; diff --git a/thisable/src/_reducers/index.js b/thisable/src/_reducers/index.js new file mode 100644 index 0000000..0ea56ac --- /dev/null +++ b/thisable/src/_reducers/index.js @@ -0,0 +1,8 @@ +import { combineReducers } from "redux"; +import message from "./message_reducer"; + +const rootReducer = combineReducers({ + message, +}); + +export default rootReducer; diff --git a/thisable/src/_reducers/message_reducer.js b/thisable/src/_reducers/message_reducer.js new file mode 100644 index 0000000..c7421e6 --- /dev/null +++ b/thisable/src/_reducers/message_reducer.js @@ -0,0 +1,13 @@ +import { SAVE_MESSAGE } from "../_actions/types"; + +export default function (state = { messages: [] }, action) { + switch (action.type) { + case SAVE_MESSAGE: + return { + ...state, + messages: state.messages.concat(action.payload), + }; + default: + return state; + } +} diff --git a/thisable/src/assets/images/thumbs_down.svg b/thisable/src/assets/images/thumbs_down.svg new file mode 100644 index 0000000..e1eed21 --- /dev/null +++ b/thisable/src/assets/images/thumbs_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/thisable/src/assets/images/thumbs_up.svg b/thisable/src/assets/images/thumbs_up.svg new file mode 100644 index 0000000..860b520 --- /dev/null +++ b/thisable/src/assets/images/thumbs_up.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/thisable/src/components/Chatbot/Chatbot.css b/thisable/src/components/Chatbot/Chatbot.css new file mode 100644 index 0000000..e9a3e5c --- /dev/null +++ b/thisable/src/components/Chatbot/Chatbot.css @@ -0,0 +1,39 @@ +* { + word-break: keep-all; + overflow-wrap: break-word; + word-wrap: break-word; +} + +.chatbotModal { + background-color: #e599b3; + width: 5rem; + height: 5rem; + border-radius: 10px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + position: fixed; + right: 50px; + bottom: 50px; + box-shadow: 0 10px 10px rgb(0 0 0 / 30%); +} + +.robotBtn > svg { + width: 4rem; + height: 4rem; +} + +.ant-list-item-meta-content { + max-width: 500px; + overflow-x: none; +} + +.ant-list-item-meta-title { + font-size: 20px !important; +} + +.ant-list-item-meta-description { + display: flex; + color: black !important; +} diff --git a/thisable/src/components/Chatbot/Chatbot.js b/thisable/src/components/Chatbot/Chatbot.js new file mode 100644 index 0000000..e5631ab --- /dev/null +++ b/thisable/src/components/Chatbot/Chatbot.js @@ -0,0 +1,180 @@ +import React, { useEffect } from "react"; +import Axios from "axios"; +import { useDispatch, useSelector } from "react-redux"; +import { saveMessage } from "../../_actions/message_actions"; +import Message from "./Sections/Message"; +import { List, Icon, Avatar } from "antd"; +import Card from "./Sections/Card"; +import "./Chatbot.css"; + +function Chatbot() { + const dispatch = useDispatch(); + const messagesFromRedux = useSelector((state) => state.message.messages); + + useEffect(() => { + eventQuery("welcomeToMyWebsite"); + }, []); + + const textQuery = async (text) => { + // First Need to take care of the message I sent + let conversation = { + who: "user", + content: { + text: { + text: text, + }, + }, + }; + + dispatch(saveMessage(conversation)); + // console.log('text I sent', conversation) + + // We need to take care of the message Chatbot sent + const textQueryVariables = { + text, //== text: text + }; + try { + //I will send request to the textQuery ROUTE + const response = await Axios.post( + "/api/dialogflow/textQuery", + textQueryVariables + ); + + for (let content of response.data.fulfillmentMessages) { + conversation = { + who: "bot", + content: content, + }; + + dispatch(saveMessage(conversation)); + } + } catch (error) { + conversation = { + who: "bot", + content: { + text: { + text: " Error just occured, please check the problem", + }, + }, + }; + dispatch(saveMessage(conversation)); + } + }; + + const eventQuery = async (event) => { + // We need to take care of the message Chatbot sent + const eventQueryVariables = { + event, + }; + try { + //I will send request to the textQuery ROUTE + const response = await Axios.post( + "/api/dialogflow/eventQuery", + eventQueryVariables + ); + for (let content of response.data.fulfillmentMessages) { + let conversation = { + who: "bot", + content: content, + }; + dispatch(saveMessage(conversation)); + } + } catch (error) { + let conversation = { + who: "bot", + content: { + text: { + text: " Error just occured, please check the problem", + }, + }, + }; + dispatch(saveMessage(conversation)); + } + }; + + const keyPressHanlder = (e) => { + if (e.key === "Enter") { + if (!e.target.value) { + return alert("you need to type somthing first"); + } + + //we will send request to text query route + textQuery(e.target.value); + e.target.value = ""; + } + }; + + const renderCards = (cards) => { + return cards.map((card, i) => ); + }; + + const renderOneMessage = (message, i) => { + console.log("message", message); + // we need to give some condition here to separate message kinds + + // template for normal text + if (message.content && message.content.text && message.content.text.text) { + return ( + + ); + } else if (message.content && message.content.payload.fields.card) { + const AvatarSrc = + message.who === "bot" ? : ; + + return ( +
+ + } + title={message.who} + description={renderCards( + message.content.payload.fields.card.listValue.values + )} + /> + +
+ ); + } + // template for card message + }; + + const renderMessage = (returnedMessages) => { + if (returnedMessages) { + return returnedMessages.map((message, i) => { + return renderOneMessage(message, i); + }); + } else { + return null; + } + }; + + return ( +
+
+ {renderMessage(messagesFromRedux)} +
+ +
+ ); +} + +export default Chatbot; diff --git a/thisable/src/components/Chatbot/ChatbotModal.js b/thisable/src/components/Chatbot/ChatbotModal.js new file mode 100644 index 0000000..41dc3c7 --- /dev/null +++ b/thisable/src/components/Chatbot/ChatbotModal.js @@ -0,0 +1,32 @@ +import React, { useState } from "react"; +import { Modal } from "react-bootstrap"; +import { Icon, Avatar } from "antd"; +import Chatbot from "./Chatbot"; + +function ChatbotModal() { + const [modalShow, setModalShow] = useState(false); + + return ( + <> +
setModalShow(true)} className="chatbotModal"> + +
+ setModalShow(false)}> + thisABLE Chatbot + +
+
+ +
+
+
+ + ); +} + +export default ChatbotModal; diff --git a/thisable/src/components/Chatbot/Sections/Card.js b/thisable/src/components/Chatbot/Sections/Card.js new file mode 100644 index 0000000..bf4b33a --- /dev/null +++ b/thisable/src/components/Chatbot/Sections/Card.js @@ -0,0 +1,36 @@ +import React from "react"; +import { Card, Icon } from "antd"; + +const { Meta } = Card; + +function CardComponent(props) { + const cardwidth = props.cardInfo.fields.cardWidth.stringValue; + return ( + + {props.cardInfo.fields.description.stringValue} + + } + // actions={[ + // + // ]} + > + + + ); +} + +export default CardComponent; diff --git a/thisable/src/components/Chatbot/Sections/Message.js b/thisable/src/components/Chatbot/Sections/Message.js new file mode 100644 index 0000000..5f0a2f2 --- /dev/null +++ b/thisable/src/components/Chatbot/Sections/Message.js @@ -0,0 +1,19 @@ +import React from "react"; +import { List, Icon, Avatar } from "antd"; + +function Message(props) { + const AvatarSrc = + props.who === "bot" ? : ; + + return ( + + } + title={props.who} + description={props.text} + /> + + ); +} + +export default Message; diff --git a/thisable/src/components/DetailPage/DetailPage.css b/thisable/src/components/DetailPage/DetailPage.css index d9b6e96..abd6683 100644 --- a/thisable/src/components/DetailPage/DetailPage.css +++ b/thisable/src/components/DetailPage/DetailPage.css @@ -13,6 +13,8 @@ .placenametype { display: flex; flex-direction: row; + flex-wrap: wrap; + justify-content: center; align-items: flex-end; margin-top: 1rem; } @@ -54,7 +56,10 @@ display: flex; flex-direction: column; align-content: center; - width: 70px; +} + +.placeicon > img { + width: 70px } .placeiconname { @@ -112,12 +117,14 @@ .reviewinputtop { display: flex; flex-direction: row; - justify-content: space-evenly; - align-items: flex-end; + justify-content: space-around; + align-items: center; + margin-bottom: 1rem; } .reviewtitle { - font-size: 1rem; + font-size: 1.2rem; + font-weight: bold; } .reviewinputstar { @@ -168,15 +175,29 @@ .reviewlistsort { font-size: 1rem; + display: flex; +} + +.reviewlistsort > div { + cursor: pointer; +} + +.reviewlistsort > div:last-child { + margin-left: 5px; } .review { margin-top: 1rem; + padding-bottom: 1rem; + border-bottom: #9d9d9d; + border-bottom-width: 0.08rem; + border-bottom-style: solid; } .reviewtop { display: flex; justify-content: space-between; + align-items: center; } .reviewtopleft { @@ -190,9 +211,17 @@ margin-right: 1rem; } +.userinfo { + display: flex; + align-items: center; + justify-content: space-around; +} + .reviewuser { position: relative; font-size: 0.9rem; + margin-left: 0.5rem; + color: #f0a044; } .reviewdate { @@ -203,6 +232,8 @@ .reviewcontent { font-size: 1rem; align-items: center; + margin-left: 0.5rem; + margin-top: 0.5rem; } .helpbuttoncontainer { @@ -213,9 +244,9 @@ .helpbutton { display: flex; font-weight: bold; - font-size: 0.5rem; + font-size: 0.8rem; margin-top: 1rem; - height: 1.3rem; + height: 1.7rem; align-items: center; border-radius: 10px; border-color: #f0a044; @@ -224,9 +255,42 @@ padding-right: 1rem; } +.buttondisplay { + display: flex; + align-items: center; + flex-direction: row; +} + .helpbuttonnum { color: #cb3267; - font-size: 0.8rem; + font-size: 01rem; font-weight: bold; margin-left: 0.6rem; } + +.backweb { + margin-top: 1rem; + margin-left: 1rem; + color: black; + text-decoration: none; +} + +@media screen and (max-width: 768px) { + .backweb { + display: none; + } + + .backmobile { + margin-top: 1rem; + margin-left: 1rem; + color: black; + text-decoration: none; + } +} + +@media screen and (min-width: 769px) { + + .backmobile { + display: none; + } +} \ No newline at end of file diff --git a/thisable/src/components/DetailPage/DetailPage.js b/thisable/src/components/DetailPage/DetailPage.js index b87d276..ef9bcf5 100644 --- a/thisable/src/components/DetailPage/DetailPage.js +++ b/thisable/src/components/DetailPage/DetailPage.js @@ -1,6 +1,5 @@ import React, { useState, useEffect } from "react"; import { useParams } from "react-router-dom"; -import MapPage from "../MapPage/MapPage"; import ReviewPage from "./ReviewPage"; import ToggleView from "./ToggleView"; import { getPlaceDetail, getReviewAverage } from "../../services/user.service"; @@ -9,6 +8,8 @@ import chargerImg from "../../assets/images/charger.svg"; import toiletImg from "../../assets/images/toilet.svg"; import elevatorImg from "../../assets/images/elevator.svg"; import "./DetailPage.css"; +import { Rating } from "react-simple-star-rating"; +import {Link} from 'react-router-dom'; function DetailPage() { const [place, setPlace] = useState(""); @@ -24,6 +25,8 @@ function DetailPage() { return (
+ ◀ 뒤로가기 + ◀ 리스트로 가기
{place.name}
@@ -31,7 +34,8 @@ function DetailPage() {
{place.address}
- {reviewCount.average} ({reviewCount.count}) + + ({reviewCount.count})
{place.isToiletExists && ( diff --git a/thisable/src/components/DetailPage/ReviewPage.js b/thisable/src/components/DetailPage/ReviewPage.js index aa985bb..7214e01 100644 --- a/thisable/src/components/DetailPage/ReviewPage.js +++ b/thisable/src/components/DetailPage/ReviewPage.js @@ -8,11 +8,15 @@ import { postReviewRecommend, postReviewDiscourage, } from "../../services/user.service"; +import thumbsup from "../../assets/images/thumbs_up.svg" +import thumbsdown from "../../assets/images/thumbs_down.svg" function ReviewPage({ locationId }) { const [reviews, setReviews] = useState(""); const [rating, setRating] = useState(0); const [reviewNum, setReviewNum] = useState(""); + const [sort, setSort] = useState("recommended"); + const [userType, setUserType] = useState("anonymous"); const handleRating = (rate) => { setRating(rate / 20); @@ -20,11 +24,11 @@ function ReviewPage({ locationId }) { }; useEffect(async () => { - const reviewList = await getReview(locationId); + const reviewList = await getReview(locationId, sort); const averageNum = await getReviewAverage(locationId); setReviews(reviewList); setReviewNum(averageNum.count); - }, [locationId]); + }, [locationId, sort]); const [inputValue, setInputValue] = useState(""); console.log("input: ", inputValue); @@ -40,28 +44,38 @@ function ReviewPage({ locationId }) { const renderReviews = reviews && reviews.response.map((review) => { - var good = review.good - var bad = review.bad - + var good = review.good; + var bad = review.bad; + return (
- +
{review.userType}
-
{review.createdAt}
+
+ {review.createdAt.replace("T", " ").substring(0, 10)} +
{review.detail}