Add files via upload
This commit is contained in:
35
ui/src/App.tsx
Normal file
35
ui/src/App.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import { FormType } from './components/Start/Form';
|
||||
import Start from './components/Start/Start';
|
||||
import ChatHistory from './components/Chat/ChatHistory';
|
||||
import ChatBody from './components/Chat/ChatBody';
|
||||
import { FilterProvider } from './contexts/FilterContext';
|
||||
import { LoggedUserProvider } from './contexts/LoggedUserContext';
|
||||
import { MatchedUserProvider } from './contexts/MatchedUserContext';
|
||||
import Landing from "./components/Landing/Landing";
|
||||
import { SocketProvider } from './contexts/SocketContext';
|
||||
|
||||
function App() {
|
||||
|
||||
return <BrowserRouter>
|
||||
<LoggedUserProvider>
|
||||
<FilterProvider>
|
||||
<MatchedUserProvider>
|
||||
<SocketProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Start formType={FormType.Registration}></Start>} />
|
||||
<Route path="/register" element={<Start formType={FormType.Registration}></Start>} />
|
||||
<Route path="/login" element={<Start formType={FormType.Login}></Start>} />
|
||||
<Route path="/landing" element={<Landing></Landing>}/>
|
||||
<Route path="/history" element={<ChatHistory />} />
|
||||
<Route path="/chat" element={<ChatBody />} />
|
||||
</Routes>
|
||||
</SocketProvider>
|
||||
</MatchedUserProvider>
|
||||
</FilterProvider>
|
||||
</LoggedUserProvider>
|
||||
</BrowserRouter>;
|
||||
}
|
||||
|
||||
export default App;
|
||||
415
ui/src/components/Chat/ChatBody.tsx
Normal file
415
ui/src/components/Chat/ChatBody.tsx
Normal file
@@ -0,0 +1,415 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import ProfileCard from "./ProfileCard";
|
||||
import MessageContainer from "./MessageContainer";
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { SocketContext } from "../../contexts/SocketContext";
|
||||
import { LoggedUserContext } from "../../contexts/LoggedUserContext";
|
||||
import { Link } from "react-router-dom";
|
||||
import { MatchedUserContext } from "../../contexts/MatchedUserContext";
|
||||
import { ILoadMsgDTO, IMessage, IReceiveMsgDTO } from "../../models/ChatModels";
|
||||
import { Gender } from "../../models/FiltersModels";
|
||||
import { EnvConfig } from "../../util/EnvConfig";
|
||||
import { ChatService, IChatResponse } from "../../services/ChatService";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export default function ChatBody() {
|
||||
// Contexts
|
||||
const loggedUserContext = useContext(LoggedUserContext);
|
||||
const matchedUser = useContext(MatchedUserContext);
|
||||
const socketContext = useContext(SocketContext);
|
||||
|
||||
// State
|
||||
const [messages, setMessages] = useState<IMessage[]>([]);
|
||||
const [typedMessage, setTypedMessage] = useState("");
|
||||
|
||||
|
||||
// Use State
|
||||
useEffect(() => {
|
||||
|
||||
const fetchMessages = async () : Promise<void> => {
|
||||
|
||||
const res : IChatResponse = await ChatService.retrieve({senderId : loggedUserContext?.loggedUser?._id, receiverId : matchedUser?.matchedUser?._id});
|
||||
|
||||
if(res.statusCode !== 200){
|
||||
toast.error("Could not retrieve chat for match.")
|
||||
return;
|
||||
}
|
||||
const loadedMessages = res.data as ILoadMsgDTO[];
|
||||
|
||||
// Update messages to be displayed
|
||||
setMessages(loadedMessages.map( (loadMessage : ILoadMsgDTO) => {
|
||||
return {userId: loadMessage.senderId,
|
||||
type: (loadMessage.senderId === loggedUserContext?.loggedUser?._id ?"sent":"received"),
|
||||
text: loadMessage.message,
|
||||
userIcon:loadMessage.senderId === loggedUserContext?.loggedUser?._id?loggedUserContext?.loggedUser?.avatarImage:matchedUser?.matchedUser?.avatarImage
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if(localStorage.getItem("loggedUser") && localStorage.getItem("matchedUser")) fetchMessages();
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
socketContext?.socket?.on("receive_msg", (receiveMsgDTO: IReceiveMsgDTO) => {
|
||||
// Locally update the messages
|
||||
|
||||
const newMsgs = [...messages, {userId: matchedUser?.matchedUser?._id,type:"received", text:receiveMsgDTO.msg, userIcon:matchedUser?.matchedUser?.avatarImage}];
|
||||
setMessages(newMsgs);
|
||||
});
|
||||
|
||||
socketContext?.socket?.on("error_send_msg", (msg: any) => {
|
||||
if (EnvConfig.DEBUG) console.log(msg);
|
||||
});
|
||||
}, [
|
||||
matchedUser?.matchedUser?._id,
|
||||
matchedUser?.matchedUser?.avatarImage,
|
||||
messages,
|
||||
socketContext.socket,
|
||||
]);
|
||||
|
||||
const sendMsg = async (sendContactInfo: boolean = false) => {
|
||||
const contactMsg = `You wanna play? Let's play! Add me on Valorant! ${loggedUserContext?.loggedUser?.gameName}#${loggedUserContext?.loggedUser?.tagLine}`;
|
||||
|
||||
// Locally update the messages
|
||||
const newMsgs = [
|
||||
...messages,
|
||||
{
|
||||
userId: loggedUserContext?.loggedUser?._id,
|
||||
type: "sent",
|
||||
text: sendContactInfo ? contactMsg : typedMessage,
|
||||
userIcon: loggedUserContext?.loggedUser?.avatarImage,
|
||||
},
|
||||
];
|
||||
setMessages(newMsgs);
|
||||
|
||||
// Store the message in the database
|
||||
await ChatService.save({senderId:loggedUserContext?.loggedUser?._id, receiverId:matchedUser?.matchedUser?._id, message: typedMessage});
|
||||
|
||||
// Notify other users of the message
|
||||
socketContext?.socket?.emit("send_msg", matchedUser?.matchedUser?._id, sendContactInfo ? contactMsg : typedMessage);
|
||||
|
||||
setTypedMessage(""); // Clear the typed message
|
||||
};
|
||||
|
||||
const updateMsg = (e: any) => {
|
||||
setTypedMessage(e.target.value);
|
||||
};
|
||||
|
||||
function navigate(arg0: string) {
|
||||
throw new Error("Function not implemented.");
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Link onClick={() => { localStorage.removeItem("matchedUser") } } to={"/landing"}>
|
||||
{" "}
|
||||
<Exit />
|
||||
</Link>
|
||||
|
||||
<LeftColContainer>
|
||||
<Timer> 🕐 You have 10 minutes remaining! </Timer>
|
||||
|
||||
<ChatBox>
|
||||
{messages.map((msg: IMessage, index: number) => (
|
||||
<MessageContainer
|
||||
key={msg.userId + index.toString()}
|
||||
msgType={msg.type}
|
||||
senderImg={msg.userIcon}
|
||||
text={msg.text}
|
||||
/>
|
||||
))}
|
||||
</ChatBox>
|
||||
|
||||
<ChatInputContainer>
|
||||
<ChatInput
|
||||
value={typedMessage}
|
||||
placeholder="Message"
|
||||
onChange={updateMsg}
|
||||
></ChatInput>
|
||||
|
||||
<ChatBtn onClick={() => sendMsg()}>SEND</ChatBtn>
|
||||
</ChatInputContainer>
|
||||
</LeftColContainer>
|
||||
|
||||
<RightColContainer>
|
||||
<TopText>You're chatting with:</TopText>
|
||||
<ProfileCard
|
||||
imgSrc={
|
||||
matchedUser.matchedUser == null
|
||||
? "/images/icons/Jett_icon.webp"
|
||||
: matchedUser.matchedUser.avatarImage
|
||||
}
|
||||
userName={
|
||||
matchedUser.matchedUser == null
|
||||
? "HectorSalamanca"
|
||||
: matchedUser.matchedUser.displayName
|
||||
}
|
||||
basicInfo={
|
||||
matchedUser.matchedUser == null
|
||||
? "22F, US West"
|
||||
: matchedUser.matchedUser.age +
|
||||
" " +
|
||||
Gender[matchedUser.matchedUser.gender]
|
||||
}
|
||||
userType={
|
||||
matchedUser.matchedUser == null
|
||||
? 0
|
||||
: matchedUser.matchedUser.playerType
|
||||
}
|
||||
valRank={
|
||||
matchedUser.matchedUser == null
|
||||
? 6
|
||||
: matchedUser.matchedUser.rank[0]
|
||||
}
|
||||
valRankLvl={
|
||||
matchedUser.matchedUser == null
|
||||
? 1
|
||||
: matchedUser.matchedUser.rank[1]
|
||||
}
|
||||
chatRank="/images/reputation_ranks/ToxicWaste.png"
|
||||
aboutMe={
|
||||
matchedUser.matchedUser == null
|
||||
? "This is the about me section."
|
||||
: matchedUser.matchedUser.aboutMe
|
||||
}
|
||||
/>
|
||||
|
||||
<BtnContainer>
|
||||
<MobileTimer>🕐 You have 10 minutes remaining!</MobileTimer>
|
||||
<Btn onClick={() => sendMsg(true)} btnColor="#66c2a9">
|
||||
<BtnIcon imgSrc="/images/chat/share.png" />
|
||||
SHARE CONTACT
|
||||
</Btn>
|
||||
<Btn onClick={() => { localStorage.removeItem('matchedUser'); navigate('../landing'); }} btnColor="#f94b4b">
|
||||
<BtnIcon imgSrc="/images/chat/gonext.png" />
|
||||
<Link
|
||||
to="/landing"
|
||||
style={{ color: "#ffffff", textDecoration: "none" }}
|
||||
>
|
||||
{" "}
|
||||
GO NEXT
|
||||
</Link>
|
||||
</Btn>
|
||||
</BtnContainer>
|
||||
</RightColContainer>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const Exit = styled.img`
|
||||
content: url("images/chat/x.png");
|
||||
width: 1vw;
|
||||
height: 1vw;
|
||||
min-width: 15px;
|
||||
min-height: 15px;
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
padding: 1%;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const RightColContainer = styled.div`
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: min(5vw, 20px);
|
||||
@media all and (max-width: 1400px) {
|
||||
width: 100vw;
|
||||
height: 30vh;
|
||||
padding: 0;
|
||||
border-radius: 20px;
|
||||
background-color: #181818;
|
||||
}
|
||||
`;
|
||||
const LeftColContainer = styled.div`
|
||||
margin-right: 30px;
|
||||
align-items: center;
|
||||
@media all and (max-width: 1400px) {
|
||||
order: 1;
|
||||
margin-right: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const TopText = styled.p`
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: min(5vw, 20px);
|
||||
@media all and (max-width: 1400px) {
|
||||
font-size: min(5vw, 15px);
|
||||
}
|
||||
`;
|
||||
const Timer = styled.p`
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: min(5vw, 20px);
|
||||
@media all and (max-width: 1400px) {
|
||||
font-size: min(2vw, 15px);
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileTimer = styled.p`
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: min(5vw, 20px);
|
||||
display: none;
|
||||
@media all and (max-width: 1400px) {
|
||||
font-size: min(1.5vh, 15px);
|
||||
max-width: 50vw;
|
||||
text-align: left;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const BtnContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@media all and (max-width: 1400px) {
|
||||
margin: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const BtnIcon = styled.img<{ imgSrc: string }>`
|
||||
content: url(${(props) => props.imgSrc});
|
||||
width: 4vw;
|
||||
max-width: 20px;
|
||||
height: 4vw;
|
||||
max-height: 20px;
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
`;
|
||||
|
||||
const Btn = styled.div<{ btnColor: string }>`
|
||||
background-color: ${(props) => props.btnColor};
|
||||
width: 8vw;
|
||||
min-width: 150px;
|
||||
font-weight: 600;
|
||||
height: 6vh;
|
||||
font-size: min(2vw, 15px);
|
||||
border-radius: 20px;
|
||||
justify-content: center;
|
||||
margin: 20px;
|
||||
padding: 0.5%;
|
||||
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
filter: drop-shadow(0px 0px 10px ${(props) => props.btnColor});
|
||||
}
|
||||
@media all and (max-width: 1400px) {
|
||||
font-size: 40%;
|
||||
min-width: 70px;
|
||||
height: auto;
|
||||
padding: 5px;
|
||||
margin: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
`;
|
||||
const ChatInputContainer = styled.div`
|
||||
background-color: #182828;
|
||||
width: 100%;
|
||||
height: 6vh;
|
||||
margin-top: 20px;
|
||||
border-radius: 20px;
|
||||
color: #dedbdb;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
@media all and (max-width: 1400px) {
|
||||
width: 90vw;
|
||||
height: 5vh;
|
||||
margin-top: 0;
|
||||
padding: 1vh;
|
||||
}
|
||||
`;
|
||||
const ChatInput = styled.input`
|
||||
background-color: #282828;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 20px;
|
||||
color: #dedbdb;
|
||||
font-family: "Arimo", sans-serif;
|
||||
font-size: 20px;
|
||||
justify-content: center;
|
||||
display: block;
|
||||
outline: none;
|
||||
border: 0;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
padding: 2%;
|
||||
|
||||
@media all and (max-width: 1400px) {
|
||||
width: 90vw;
|
||||
height: 5vh;
|
||||
margin-top: 0;
|
||||
padding: 1vh;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChatBtn = styled.button.attrs({
|
||||
type: "submit",
|
||||
})`
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
background: none;
|
||||
font-family: "Arimo", sans-serif;
|
||||
font-size: 20px;
|
||||
right: 20px;
|
||||
border: 0;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
&:hover {
|
||||
filter: drop-shadow(0px 0px 5px #ffffff);
|
||||
}
|
||||
@media all and (max-width: 1400px) {
|
||||
margin-top: 0;
|
||||
padding: 1vh;
|
||||
font-size: 80%;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChatBox = styled.div`
|
||||
background-color: #282828;
|
||||
border-radius: 44px;
|
||||
overflow-y: scroll;
|
||||
width: 55vw;
|
||||
height: 70vh;
|
||||
padding: 5vh;
|
||||
margin: 10px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
justify-content: start;
|
||||
@media all and (max-width: 1400px) {
|
||||
width: 90vw;
|
||||
height: 50vh;
|
||||
padding: 1vh;
|
||||
order: 2;
|
||||
padding: 0;
|
||||
border-radius: 20px;
|
||||
background: none;
|
||||
}
|
||||
`;
|
||||
564
ui/src/components/Chat/ChatHistory.tsx
Normal file
564
ui/src/components/Chat/ChatHistory.tsx
Normal file
@@ -0,0 +1,564 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import Button from "../Shared/Button";
|
||||
import styled from "styled-components/macro";
|
||||
import { EmblaCarousel } from "./EmblaCarousel";
|
||||
import ProfileCardUpdated from "./ProfileCardUpdated";
|
||||
import { Slider } from "@mui/material";
|
||||
import { CommendationService } from "../../services/CommendationService";
|
||||
import { LoggedUserContext } from '../../contexts/LoggedUserContext';
|
||||
import { CustomToast } from "../Shared/CustomToast";
|
||||
import { toast } from "react-toastify";
|
||||
import { IUser } from "../../models/AuthModels";
|
||||
import { MatchingService, IMatchingResponse } from '../../services/MatchingService';
|
||||
import { ILoadMsgDTO, IMessage } from "../../models/ChatModels";
|
||||
import MessageContainer from "./MessageContainer";
|
||||
import { IChatResponse, ChatService } from '../../services/ChatService';
|
||||
import { Micellaneous } from "../../util/Micellaneous";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const SLIDE_COUNT = 5;
|
||||
const slides = Array.from(Array(SLIDE_COUNT).keys());
|
||||
|
||||
export const WidthContext = createContext<number>(1500);
|
||||
|
||||
export interface IHistoryEntry{
|
||||
key : number;
|
||||
user : IUser;
|
||||
}
|
||||
|
||||
function ChatHistory(props: Props): React.ReactElement {
|
||||
|
||||
// Constants
|
||||
const [width, setWidth] = useState(window.innerWidth);
|
||||
const breakpoint = 1000;
|
||||
const marks = [{value: 1, label: '1'}, {value: 2, label: '2'}, {value: 3, label: '3'},
|
||||
{value: 4, label: '4'}, {value: 5, label: '5'}, {value: 6, label: '6'},
|
||||
{value: 7, label: '7'}, {value: 8, label: '8'}, {value: 9, label: '9'},
|
||||
{value: 10, label: '10'}];
|
||||
|
||||
// Contexts
|
||||
const loggedUserContext = useContext(LoggedUserContext);
|
||||
|
||||
// State
|
||||
const [rateState, setRateState] = useState(0);
|
||||
const [rating, setRating] = useState(5);
|
||||
const [history, setHistory] = useState<IHistoryEntry[]>([]);
|
||||
const [filteredHistory, setFilteredHistory] = useState<IHistoryEntry[]>([]);
|
||||
const [messages, setMessages] = useState<IMessage[]>([]);
|
||||
const [main, setMain] = useState<number>(0)
|
||||
const [filter, setFilter] = useState<string>("");
|
||||
|
||||
// Use Effects
|
||||
React.useEffect(() => {
|
||||
|
||||
const fetchMatchHistory = async () : Promise <void> => {
|
||||
|
||||
const res : IMatchingResponse = await MatchingService.retrieveHistory(loggedUserContext?.loggedUser?._id);
|
||||
|
||||
if(res.statusCode !== 200){
|
||||
toast.error("Could not retrieve match history.")
|
||||
return;
|
||||
}
|
||||
|
||||
setHistory((res.data as IUser[]).map( (user : IUser, index : number) => {
|
||||
return {key: index, user : user};
|
||||
}));
|
||||
|
||||
setFilteredHistory((res.data as IUser[]).map( (user : IUser, index : number) => {
|
||||
return {key: index, user : user};
|
||||
}));
|
||||
}
|
||||
|
||||
fetchMatchHistory();
|
||||
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
const fetchMessages = async () : Promise<void> => {
|
||||
|
||||
const res : IChatResponse = await ChatService.retrieve({senderId : loggedUserContext?.loggedUser?._id, receiverId : history[main]?.user?._id});
|
||||
|
||||
if(res.statusCode !== 200){
|
||||
toast.error("Could not retrieve chat for match.")
|
||||
return;
|
||||
}
|
||||
const loadedMessages = res.data as ILoadMsgDTO[];
|
||||
|
||||
// Update messages to be displayed
|
||||
setMessages(loadedMessages.map( (loadMessage : ILoadMsgDTO) => {
|
||||
return {userId: loadMessage.senderId,
|
||||
type: (loadMessage.senderId === loggedUserContext?.loggedUser?._id ?"sent":"received"),
|
||||
text: loadMessage.message,
|
||||
userIcon:loadMessage.senderId === loggedUserContext?.loggedUser?._id?loggedUserContext?.loggedUser?.avatarImage:history[main].user.avatarImage
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if(loggedUserContext.loggedUser && history[main]) fetchMessages();
|
||||
|
||||
}, [loggedUserContext, history, main]);
|
||||
|
||||
// Other Functions
|
||||
|
||||
function mainChanged(main : number){
|
||||
setMain(main);
|
||||
}
|
||||
|
||||
function newRating() {
|
||||
setRateState(0);
|
||||
}
|
||||
|
||||
function startRating() {
|
||||
setRateState(1);
|
||||
}
|
||||
|
||||
function doneRating() {
|
||||
setRateState(2);
|
||||
}
|
||||
|
||||
function handleRating(event: Event) {
|
||||
let newRating = (event.target as HTMLInputElement).value;
|
||||
setRating(+newRating);
|
||||
}
|
||||
|
||||
async function commend() {
|
||||
|
||||
const response = await CommendationService.save({commenderId:loggedUserContext?.loggedUser?._id, commendedId: history[main].user._id, score:rating})
|
||||
|
||||
if(response.statusCode !== 200){
|
||||
toast.error(response.data as string);
|
||||
newRating();
|
||||
}else{
|
||||
doneRating();
|
||||
}
|
||||
}
|
||||
|
||||
function displayRating() {
|
||||
if (rateState === 0) {
|
||||
return <RateButton onClick={startRating}>RATE PLAYER</RateButton>;
|
||||
} else if (rateState === 1) {
|
||||
return (
|
||||
<RatingContainer>
|
||||
<label htmlFor="rating">RATE PLAYER</label>
|
||||
<CustomSlider
|
||||
size="small"
|
||||
defaultValue={5}
|
||||
min={1}
|
||||
max={10}
|
||||
step={1}
|
||||
marks={marks}
|
||||
valueLabelDisplay="off"
|
||||
onChange={(e: Event) => {
|
||||
handleRating(e);
|
||||
}}
|
||||
/>
|
||||
<Commend type="button" onClick={commend}>
|
||||
COMMEND
|
||||
</Commend>
|
||||
</RatingContainer>
|
||||
);
|
||||
} else if (rateState == 2) {
|
||||
return <h4>Rating Recorded!</h4>;
|
||||
}
|
||||
}
|
||||
|
||||
function handleFilter(e : any){
|
||||
console.log(e.target.value);
|
||||
setFilter(e.target.value);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleResizeWindow = () => setWidth(window.innerWidth);
|
||||
|
||||
window.addEventListener("resize", handleResizeWindow);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResizeWindow);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const newHistory = history.filter( (entry : IHistoryEntry) => {
|
||||
if(filter === "") return entry;
|
||||
if(entry.user.displayName.includes(filter)) return entry;
|
||||
});
|
||||
setFilteredHistory(newHistory);
|
||||
}, [filter]);
|
||||
|
||||
function navigate(arg0: string) {
|
||||
throw new Error("Function not implemented.");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomToast></CustomToast>
|
||||
<WidthContext.Provider value={width}>
|
||||
<MainWrapper>
|
||||
<MainContainer>
|
||||
<HistorySection>
|
||||
<Menu>
|
||||
{width > breakpoint && (
|
||||
<Button
|
||||
text={"BACK"}
|
||||
width={"160px"}
|
||||
height={"70px"}
|
||||
url={'../landing'}
|
||||
img_url={null}
|
||||
svg={true}
|
||||
></Button>
|
||||
)}
|
||||
{width > breakpoint && (
|
||||
<SearchContainer>
|
||||
<SearchIconWrapper>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
{/* <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> */}
|
||||
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352c79.5 0 144-64.5 144-144s-64.5-144-144-144S64 128.5 64 208s64.5 144 144 144z"/>
|
||||
</svg>
|
||||
{/* <SearchIcon url={"images/general/search.png"} /> */}
|
||||
</SearchIconWrapper>
|
||||
<SearchInput onChange={handleFilter} placeholder="Search Message History" />
|
||||
</SearchContainer>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
<PlayerCardsWrapper>
|
||||
<EmblaCarousel
|
||||
slides={[...slides]}
|
||||
history={filteredHistory}
|
||||
ClickHandler={newRating}
|
||||
mainChanged={mainChanged}
|
||||
/>
|
||||
{width < breakpoint && (
|
||||
<RatePlayerWrapper>
|
||||
<Button
|
||||
fontSize="2em"
|
||||
text={"BACK"}
|
||||
width={'auto'}
|
||||
height={"100%"}
|
||||
url={'../landing'}
|
||||
/>
|
||||
</RatePlayerWrapper>
|
||||
)}
|
||||
</PlayerCardsWrapper>
|
||||
|
||||
<ChatContainer>
|
||||
{messages.map((msg: IMessage, index: number) => (
|
||||
<MessageContainer
|
||||
key={msg.userId + index.toString()}
|
||||
msgType={msg.type}
|
||||
senderImg={msg.userIcon}
|
||||
text={msg.text}/>
|
||||
))}
|
||||
</ChatContainer>
|
||||
|
||||
</HistorySection>
|
||||
|
||||
{width > breakpoint && (
|
||||
<InfoCardSection>
|
||||
<RatePlayerWrapper>{displayRating()}</RatePlayerWrapper>
|
||||
<ProfileCardWrapper>
|
||||
<ProfileCardUpdated
|
||||
imgSrc= {history[main]?.user?.avatarImage}
|
||||
userName={history[main]?.user?.displayName}
|
||||
chatRank="images/reputation_ranks/ToxicWaste.png"
|
||||
userType={Micellaneous.playerTypeToString(history[main]?.user?.playerType)}
|
||||
valRank={`images/ranks/rank_${history[main]?.user?.rank[0]}_${history[main]?.user?.rank[1]}.webp`}
|
||||
aboutMe={history[main]?.user?.aboutMe}
|
||||
></ProfileCardUpdated>
|
||||
</ProfileCardWrapper>
|
||||
</InfoCardSection>
|
||||
)}
|
||||
</MainContainer>
|
||||
</MainWrapper>
|
||||
</WidthContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const RateButton = styled.button`
|
||||
color: white;
|
||||
background-color: #68c9ac;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
width: 160px;
|
||||
height: 70px;
|
||||
font-size: 16px;
|
||||
transition: 0.5s all;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px #68c9ac;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const Commend = styled.button`
|
||||
color: white;
|
||||
background-color: #68c9ac;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
width: 40%;
|
||||
margin-top: 2%;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const RatingContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const CustomSlider = styled(Slider)`
|
||||
|
||||
margin: 3% 0;
|
||||
|
||||
& .MuiSlider-thumb {
|
||||
background-color: #BD3944;
|
||||
height: 0.8vw;
|
||||
width: 0.3vw;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
& .MuiSlider-rail {
|
||||
color: #D9D9D9;
|
||||
opacity: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
& .MuiSlider-track{
|
||||
color: #BD3944;
|
||||
}
|
||||
|
||||
& .MuiSlider-mark{
|
||||
color: white;
|
||||
height: 0.5vw;
|
||||
width: 0.15vw;
|
||||
}
|
||||
|
||||
& .MuiSlider-markLabel{
|
||||
color: white;
|
||||
}
|
||||
`;
|
||||
|
||||
const MainWrapper = styled.div`
|
||||
// applies it to all the children
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: "Arimo";
|
||||
}
|
||||
font-weight: 200;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
`;
|
||||
|
||||
const MainContainer = styled.main`
|
||||
position: inherit;
|
||||
max-width: 1500px;
|
||||
width: 100%;
|
||||
|
||||
padding: 2%;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@media all and (max-width: 1000px) {
|
||||
flex-direction: column-reverse;
|
||||
padding: 5%;
|
||||
}
|
||||
`;
|
||||
|
||||
const HistorySection = styled.section`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 60%;
|
||||
height: 100%;
|
||||
|
||||
@media all and (max-width: 1000px) {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const InfoCardSection = styled.aside`
|
||||
position: relative;
|
||||
width: 37%;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ProfileCardWrapper = styled.div`
|
||||
position: inherit;
|
||||
height: 85%;
|
||||
width: 100%
|
||||
`;
|
||||
|
||||
const Menu = styled.nav`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const RatePlayerWrapper = styled.div`
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
|
||||
width: 100%;
|
||||
margin-bottom: 4%;
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
position: relative;
|
||||
width: 20%;
|
||||
height: 20%;
|
||||
}
|
||||
`;
|
||||
|
||||
const PlayerCardsWrapper = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 30%;
|
||||
|
||||
// Split screen
|
||||
@media all and (max-width: 1000px) {
|
||||
height: 40%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Mobile
|
||||
@media all and (max-width: 500px) {
|
||||
height: 30%;
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChatContainer = styled.div`
|
||||
overflow-y: scroll;
|
||||
margin: 10px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 70%;
|
||||
padding: 5%;
|
||||
outline: 1px red;
|
||||
background-color: #282828;
|
||||
border-radius: 44px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
height: 80%;
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const SearchContainer = styled.div`
|
||||
position: absolute;
|
||||
width: 40%;
|
||||
height: 40px;
|
||||
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
background: #282828;
|
||||
|
||||
border-radius: 44px;
|
||||
`;
|
||||
|
||||
const SearchInput = styled.input`
|
||||
position: relative;
|
||||
background: none;
|
||||
flex-grow: 1;
|
||||
border: none;
|
||||
|
||||
color: white;
|
||||
|
||||
// increase specificity
|
||||
&& {
|
||||
font-family: "Arimo";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 19px;
|
||||
line-height: 22px;
|
||||
text-decoration-color: blue;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&& :focus,
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* identical to box height */
|
||||
`;
|
||||
|
||||
const SearchIconWrapper = styled.div`
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
|
||||
& svg{
|
||||
fill: white;
|
||||
width: 1vw;
|
||||
height: 1vw;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
const SearchIcon = styled.img<{ url: string }>`
|
||||
position: relative;
|
||||
height: 50%;
|
||||
font-style: normal;
|
||||
& * {
|
||||
font-weight: 700;
|
||||
font-size: 26px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
|
||||
color: #ffffff;
|
||||
}
|
||||
aspect-ratio: 1;
|
||||
|
||||
content: url(${(props) => props.url});
|
||||
`;
|
||||
|
||||
export default ChatHistory;
|
||||
15
ui/src/components/Chat/Commend.tsx
Normal file
15
ui/src/components/Chat/Commend.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
myID: string;
|
||||
playerID: string;
|
||||
rating: number;
|
||||
};
|
||||
|
||||
export function Commend(props: Props) {
|
||||
let rateUser = {
|
||||
userID: props.myID,
|
||||
teammateID: props.playerID,
|
||||
rating: props.rating,
|
||||
};
|
||||
}
|
||||
210
ui/src/components/Chat/EmblaCarousel.tsx
Normal file
210
ui/src/components/Chat/EmblaCarousel.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React, { useState, useEffect, useCallback, useContext } from "react";
|
||||
import useEmblaCarousel from "embla-carousel-react";
|
||||
import "./embla.css";
|
||||
import styled from "styled-components/macro";
|
||||
import HistoryCard from "./HistoryCard";
|
||||
import { IHistoryEntry, WidthContext } from "./ChatHistory";
|
||||
|
||||
import ProfileCardUpdated from "./ProfileCardUpdated";
|
||||
|
||||
type Props = {
|
||||
history: Array<IHistoryEntry>;
|
||||
slides: Array<number>;
|
||||
ClickHandler: () => void;
|
||||
mainChanged : (main : number) => void;
|
||||
};
|
||||
|
||||
export const EmblaCarousel = (props: Props) => {
|
||||
let [viewportRef, embla] = useEmblaCarousel({
|
||||
align: "center",
|
||||
skipSnaps: false,
|
||||
draggable: false,
|
||||
});
|
||||
const [prevBtnEnabled, setPrevBtnEnabled] = useState(false);
|
||||
const [nextBtnEnabled, setNextBtnEnabled] = useState(false);
|
||||
const [main, setMain] = useState(0);
|
||||
const width = useContext(WidthContext);
|
||||
|
||||
const scrollPrev = useCallback(() => {
|
||||
embla && embla.scrollPrev();
|
||||
setMain(main - 1);
|
||||
props.ClickHandler();
|
||||
}, [embla, main]);
|
||||
|
||||
const scrollNext = useCallback(() => {
|
||||
embla && embla.scrollNext();
|
||||
setMain(main + 1);
|
||||
props.ClickHandler();
|
||||
}, [embla, main]);
|
||||
|
||||
const onSelect = useCallback(() => {
|
||||
if (!embla) return;
|
||||
setPrevBtnEnabled(embla.canScrollPrev());
|
||||
setNextBtnEnabled(embla.canScrollNext());
|
||||
}, [embla]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!embla) return;
|
||||
embla.on("select", onSelect);
|
||||
onSelect();
|
||||
}, [embla, onSelect]);
|
||||
|
||||
useEffect(() => {
|
||||
props.mainChanged(main);
|
||||
}, [main]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<EmblaViewPort ref={viewportRef}>
|
||||
<EmblaContainer>
|
||||
{props.history.map((entry: IHistoryEntry) =>
|
||||
width > 1000 ? (
|
||||
<WrapperOuter>
|
||||
<WrapperInner>
|
||||
<HistoryCard
|
||||
key={entry.user._id+entry.key}
|
||||
url={entry.user.avatarImage}
|
||||
username={entry.user.displayName}
|
||||
isMain={main === entry.key}
|
||||
zIndex={main === entry.key ? "3" : "1"}
|
||||
/>
|
||||
</WrapperInner>
|
||||
</WrapperOuter>
|
||||
) : (
|
||||
<WrapperOuter>
|
||||
<WrapperInner>
|
||||
<ProfileCardWrapper>
|
||||
<ProfileCardUpdated
|
||||
isMain={main === entry.key}
|
||||
imgSrc={entry.user.avatarImage}
|
||||
userName={entry.user.displayName}
|
||||
chatRank="images/reputation_ranks/ToxicWaste.png"
|
||||
userType="gamer"
|
||||
valRank="images/ranks/rank_7_3.webp"
|
||||
aboutMe="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
|
||||
></ProfileCardUpdated>
|
||||
</ProfileCardWrapper>
|
||||
</WrapperInner>
|
||||
</WrapperOuter>
|
||||
)
|
||||
)}
|
||||
</EmblaContainer>
|
||||
</EmblaViewPort>
|
||||
|
||||
<PrevButton onClick={scrollPrev} enabled={prevBtnEnabled} />
|
||||
<NextButton onClick={scrollNext} enabled={nextBtnEnabled} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
interface IButtonProps {
|
||||
enabled: boolean;
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export const PrevButton = (props: IButtonProps) => (
|
||||
<button
|
||||
className="embla__button embla__button--prev"
|
||||
onClick={props.onClick}
|
||||
disabled={!props.enabled}
|
||||
>
|
||||
<svg className="embla__button__svg" viewBox="137.718 -1.001 366.563 644">
|
||||
<path d="M428.36 12.5c16.67-16.67 43.76-16.67 60.42 0 16.67 16.67 16.67 43.76 0 60.42L241.7 320c148.25 148.24 230.61 230.6 247.08 247.08 16.67 16.66 16.67 43.75 0 60.42-16.67 16.66-43.76 16.67-60.42 0-27.72-27.71-249.45-249.37-277.16-277.08a42.308 42.308 0 0 1-12.48-30.34c0-11.1 4.1-22.05 12.48-30.42C206.63 234.23 400.64 40.21 428.36 12.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
|
||||
export const NextButton = (props: IButtonProps) => (
|
||||
<button
|
||||
className="embla__button embla__button--next"
|
||||
onClick={props.onClick}
|
||||
disabled={!props.enabled}
|
||||
>
|
||||
<svg className="embla__button__svg" viewBox="0 0 238.003 238.003">
|
||||
<path d="M181.776 107.719L78.705 4.648c-6.198-6.198-16.273-6.198-22.47 0s-6.198 16.273 0 22.47l91.883 91.883-91.883 91.883c-6.198 6.198-6.198 16.273 0 22.47s16.273 6.198 22.47 0l103.071-103.039a15.741 15.741 0 0 0 4.64-11.283c0-4.13-1.526-8.199-4.64-11.313z" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
padding: 2%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
height: 90%;
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const EmblaViewPort = styled.div`
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
width: 95%;
|
||||
|
||||
box-shadow: inset -100px 4px 40px rgba(24, 24, 24, 0.4);
|
||||
`;
|
||||
|
||||
// For all the cards
|
||||
const EmblaContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
/* This needs to be the same as the margin in History Card */
|
||||
margin-left: -1px;
|
||||
`;
|
||||
|
||||
const ProfileCardWrapper = styled.div`
|
||||
position: relative;
|
||||
padding: 1%;
|
||||
width: 90%;
|
||||
height: 95%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
// It's parent has no actual width
|
||||
const WrapperOuter = styled.div`
|
||||
/* This needs to be the same as the margin in .embla_container*/
|
||||
position: relative;
|
||||
padding-left: 1px;
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
min-height: 20vh;
|
||||
min-width: 95vw;
|
||||
}
|
||||
`;
|
||||
|
||||
const WrapperInner = styled.div`
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
height: 190px;
|
||||
width: 400px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
/* background-color: aliceblue; */
|
||||
}
|
||||
`;
|
||||
|
||||
// const EmblaSlide = styled.div<{ width: string }>`
|
||||
// position: relative;
|
||||
// flex: 0 0 ${(props) => props.width};
|
||||
// `;
|
||||
|
||||
export default EmblaCarousel;
|
||||
104
ui/src/components/Chat/HistoryCard.tsx
Normal file
104
ui/src/components/Chat/HistoryCard.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components/macro";
|
||||
import ProfilePicture from "../Shared/ProfilePicture";
|
||||
|
||||
type Props = {
|
||||
isMain: boolean;
|
||||
zIndex: string;
|
||||
username: string;
|
||||
// message: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function HistoryCard(props: Props): React.ReactElement {
|
||||
// const [width, setWidth] = useState("30%");
|
||||
|
||||
// useEffect(() => {
|
||||
// setWidth(props.width);
|
||||
// }, [props.width]);
|
||||
|
||||
return (
|
||||
<Container isMain={props.isMain} zIndex={props.zIndex}>
|
||||
<UserInfoContainer>
|
||||
<ProfilePicture size={"100%"} url={props.url} showBorder={false} />
|
||||
<UsernameWrapper>
|
||||
<Username>{props.username}</Username>
|
||||
</UsernameWrapper>
|
||||
</UserInfoContainer>
|
||||
{/* <MessagesWrapper>
|
||||
<Messages>{props.message}</Messages>
|
||||
</MessagesWrapper> */}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const Container = styled.div<{ isMain: boolean; zIndex: string }>`
|
||||
position: relative;
|
||||
width: 95%;
|
||||
height: 95%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #282828;
|
||||
box-shadow: ${(props) => (props.isMain ? "0px 0px 8px #66c2a9" : "none")};
|
||||
border-radius: 10px;
|
||||
z-index: ${(props) => props.zIndex};
|
||||
opacity: ${(props) => (props.isMain ? "1.0" : 0.2)};
|
||||
overflow: hidden;
|
||||
transition: all 1s;
|
||||
`;
|
||||
|
||||
const MessagesWrapper = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
width: 80%;
|
||||
height: 20%;
|
||||
`;
|
||||
|
||||
const Messages = styled.span`
|
||||
position: relative;
|
||||
|
||||
&& {
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
line-height: 23px;
|
||||
text-align: left;
|
||||
}
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
const UsernameWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 60%;
|
||||
margin-left: 5%;
|
||||
`;
|
||||
|
||||
const UserInfoContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
margin-bottom: 5%;
|
||||
width: 100%;
|
||||
height: 40%;
|
||||
`;
|
||||
|
||||
const Username = styled.span`
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-size: 24px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
61
ui/src/components/Chat/MessageContainer.tsx
Normal file
61
ui/src/components/Chat/MessageContainer.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import styled from "styled-components";
|
||||
import * as React from "react";
|
||||
|
||||
interface Props {
|
||||
msgType: string; //recieved or sent
|
||||
text: string;
|
||||
senderImg: string;
|
||||
}
|
||||
|
||||
export default function MessageContainer(props: Props) {
|
||||
return (
|
||||
<Wrapper msgType={props.msgType}>
|
||||
<Icon msgType={props.msgType} imgSrc={props.senderImg} />
|
||||
<ChatBubble msgType={props.msgType}>{props.text}</ChatBubble>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled.div<{ msgType: string }>`
|
||||
display: flex;
|
||||
margin: 10px;
|
||||
|
||||
justify-content: ${(props) =>
|
||||
props.msgType === "received" ? "start" : "end"};
|
||||
`;
|
||||
|
||||
const ChatBubble = styled.div<{ msgType: string }>`
|
||||
background-color: ${(props) =>
|
||||
props.msgType === "received" ? "#66c2a9" : "#FFFFFF"};
|
||||
order: ${(props) => (props.msgType === "received" ? 2 : 1)};
|
||||
max-width: 500px;
|
||||
padding: 1vw;
|
||||
border-radius: 20px;
|
||||
color: black;
|
||||
font-family: "Arimo", sans-serif;
|
||||
font-size: min(2vw, 20px);
|
||||
box-shadow: 0px 5px 6px #546466;
|
||||
@media all and (max-width: 1400px){
|
||||
padding: 2.5vw;
|
||||
font-size: max(1.5vw,12px);
|
||||
border-radius: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Icon = styled.img<{ imgSrc: string; msgType: string }>`
|
||||
content: url(${(props) => props.imgSrc});
|
||||
border-radius: 50%;
|
||||
width: 6vh;
|
||||
height: 6vh;
|
||||
margin: 5px;
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
aspect-ratio: 1;
|
||||
order: ${(props) => (props.msgType === "received" ? 1 : 2)};
|
||||
@media all and (max-width: 1400px) {
|
||||
|
||||
}
|
||||
@media all and (max-width: 800px){
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
167
ui/src/components/Chat/ProfileCard.tsx
Normal file
167
ui/src/components/Chat/ProfileCard.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
interface Props {
|
||||
imgSrc: string;
|
||||
userName: string;
|
||||
valRank?: number;
|
||||
valRankLvl?: number;
|
||||
chatRank: string;
|
||||
basicInfo?: string;
|
||||
userType: number;
|
||||
aboutMe?: string;
|
||||
}
|
||||
|
||||
export default function ProfileCard(props: Props): React.ReactElement<Props, any> {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Icon imgSrc={props.imgSrc} />
|
||||
<Username>{props.userName}</Username>
|
||||
<BasicInfo>{props.basicInfo}</BasicInfo>
|
||||
<Ranks>
|
||||
<RankLabel>
|
||||
<RankImg imgSrc={"/images/ranks/rank_"+props.valRank+"_"+props.valRankLvl+".webp"} />
|
||||
RANK
|
||||
</RankLabel>
|
||||
<RankLabel style={{ textAlign: "center" }}>
|
||||
<RankImg imgSrc={props.chatRank} />
|
||||
REPUTATION
|
||||
</RankLabel>
|
||||
</Ranks>
|
||||
<AboutContainer>
|
||||
<Label>ABOUT ME:</Label>
|
||||
<AboutMe>{props.aboutMe}</AboutMe>
|
||||
</AboutContainer>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled("div")`
|
||||
background-color: #282828;
|
||||
margin: 10px;
|
||||
width: 20vw;
|
||||
max-width: 400px;
|
||||
height: 70vh;
|
||||
padding: 5vh;
|
||||
border-radius: 44px;
|
||||
filter: drop-shadow(0px 0px 10px #66c2a9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: scroll;
|
||||
justify-content: center;
|
||||
@media all and (max-width: 1400px) {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
order: 1;
|
||||
flex-wrap: wrap;
|
||||
width: 90vw;
|
||||
max-width: 90vw;
|
||||
height: 13vh;
|
||||
padding: 2vw;
|
||||
filter: drop-shadow(0px 0px 5px #66c2a9);
|
||||
max-height: 200px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
@media all and (max-height: 1000px) {
|
||||
min-height: 10vw;
|
||||
}
|
||||
`;
|
||||
|
||||
const Icon = styled.img<{ imgSrc: string }>`
|
||||
content: url(${(props) => props.imgSrc});
|
||||
border-radius: 50%;
|
||||
width: 10vw;
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
aspect-ratio: 1;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
@media all and(max-height: 1000px) {
|
||||
width: 10%;
|
||||
height: 10%;
|
||||
}
|
||||
`;
|
||||
const Username = styled.p`
|
||||
text-align: center;
|
||||
font-size: min(3vw, 25px);
|
||||
margin: 5px;
|
||||
font-weight: 600;
|
||||
`;
|
||||
const BasicInfo = styled.p`
|
||||
text-align: center;
|
||||
font-size: min(15px, 2vw);
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
@media all and (max-width: 1400px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
const Ranks = styled.div`
|
||||
display: flex;
|
||||
width: 60%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
justify-content: space-between;
|
||||
@media all and (max-width: 1400px) {
|
||||
flex-direction: column;
|
||||
width: 10%;
|
||||
justify-content: center;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
`;
|
||||
const RankImg = styled.img<{ imgSrc: string }>`
|
||||
content: url(${(props) => props.imgSrc});
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 5vw;
|
||||
max-width: 50px;
|
||||
height: 5vw;
|
||||
max-height: 50px;
|
||||
@media all and(max-height: 1000px) {
|
||||
height: 10%;
|
||||
max-height: 10%;
|
||||
}
|
||||
`;
|
||||
|
||||
const AboutContainer = styled.div`
|
||||
text-align: left;
|
||||
overflow: scroll;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
@media all and (max-width: 1400px) {
|
||||
width: 40%;
|
||||
height: 90%;
|
||||
}
|
||||
`;
|
||||
|
||||
const AboutMe = styled.p`
|
||||
font-size: min(3vw, 20px);
|
||||
font-weight: 400;
|
||||
font-family: "Arimo", sans-serif;
|
||||
margin: 0;
|
||||
`;
|
||||
const Label = styled.p`
|
||||
text-align: left;
|
||||
font-size: min(20px, 1.2vw);
|
||||
font-weight: 600;
|
||||
@media all and (max-width: 1400px) {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
const RankLabel = styled(Label)`
|
||||
text-align: center;
|
||||
font-size: 60%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media all and (max-width: 1400px) {
|
||||
font-size: 30%;
|
||||
}
|
||||
`;
|
||||
232
ui/src/components/Chat/ProfileCardUpdated.tsx
Normal file
232
ui/src/components/Chat/ProfileCardUpdated.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import React, { useContext } from "react";
|
||||
import styled from "styled-components/macro";
|
||||
import { WidthContext } from "./ChatHistory";
|
||||
|
||||
interface Props {
|
||||
imgSrc: string;
|
||||
userName: string;
|
||||
valRank?: string;
|
||||
chatRank: string;
|
||||
userType: string;
|
||||
aboutMe?: string;
|
||||
isMain?: boolean;
|
||||
}
|
||||
|
||||
export default function ProfileCardUpdated(props: Props): React.ReactElement<Props, any> {
|
||||
const width = useContext(WidthContext);
|
||||
|
||||
return (
|
||||
<Wrapper isMain={props.isMain}>
|
||||
<UserImageContainer>
|
||||
{width > 500 && <UserIcon imgSrc={props.imgSrc} />}
|
||||
<div>
|
||||
<UsernameText>{props.userName}</UsernameText>
|
||||
</div>
|
||||
<RanksContainer>
|
||||
<RankWrapper>
|
||||
<RankImg imgSrc={props.valRank} />
|
||||
<RankLabel>RANK</RankLabel>
|
||||
</RankWrapper>
|
||||
<RankWrapper>
|
||||
<RankImg imgSrc={props.chatRank} />
|
||||
<RankLabel style={{ textAlign: "center" }}>REP</RankLabel>
|
||||
</RankWrapper>
|
||||
</RanksContainer>
|
||||
</UserImageContainer>
|
||||
|
||||
{width > 500 && (
|
||||
<AboutMeContainer>
|
||||
<AboutMeLabel>ABOUT ME:</AboutMeLabel>
|
||||
<AboutMeWrapper>
|
||||
<AboutMeText>{props.aboutMe}</AboutMeText>
|
||||
</AboutMeWrapper>
|
||||
</AboutMeContainer>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled("div")<{ isMain?: boolean }>`
|
||||
position: relative;
|
||||
background-color: #282828;
|
||||
border-radius: 44px;
|
||||
filter: drop-shadow(0px 0px 10px #66c2a9);
|
||||
|
||||
font-weight: 200;
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
|
||||
@media all and (min-width: 1000px) {
|
||||
aspect-ratio: 44/79;
|
||||
}
|
||||
|
||||
padding: 5%;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px) {
|
||||
/* height: 100%; */
|
||||
/* width: 100%; */
|
||||
|
||||
border-radius: 20px;
|
||||
|
||||
padding: 2%;
|
||||
|
||||
order: 1;
|
||||
flex-wrap: wrap;
|
||||
|
||||
filter: ${(props) => (props.isMain ? "drop-shadow(0px 0px 5px #66c2a9)" : "none")};
|
||||
}
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
filter: none;
|
||||
flex-direction: row;
|
||||
}
|
||||
`;
|
||||
|
||||
const UserImageContainer = styled.div`
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
height: 40%;
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const UserIcon = styled.img<{ imgSrc: string }>`
|
||||
position: relative;
|
||||
content: url(${(props) => props.imgSrc});
|
||||
aspect-ratio: 1/1;
|
||||
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
|
||||
margin-bottom: 5px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
width: 25%;
|
||||
margin-bottom: 5%;
|
||||
}
|
||||
`;
|
||||
|
||||
const UsernameText = styled.p`
|
||||
font-size: 2rem;
|
||||
|
||||
&& {
|
||||
margin-bottom: 5%;
|
||||
}
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
`;
|
||||
|
||||
const RanksContainer = styled.div`
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10%;
|
||||
|
||||
width: 100%;
|
||||
|
||||
margin-bottom: 5%;
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
width: 100%;
|
||||
height: 30%;
|
||||
}
|
||||
`;
|
||||
const RankImg = styled.img<{ imgSrc: string }>`
|
||||
content: url(${(props) => props.imgSrc});
|
||||
|
||||
position: relative;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
width: 5vw;
|
||||
max-width: 54px;
|
||||
height: 5vw;
|
||||
max-height: 54px;
|
||||
|
||||
aspect-ratio: 1/1;
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
min-width: 50px;
|
||||
min-height: 50px;
|
||||
}
|
||||
`;
|
||||
|
||||
const AboutMeContainer = styled.div`
|
||||
position: relative;
|
||||
height: 40%;
|
||||
@media all and (max-width: 1000px) {
|
||||
width: 40%;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
width: 40%;
|
||||
height: 90%;
|
||||
}
|
||||
`;
|
||||
|
||||
const AboutMeLabel = styled.p`
|
||||
text-align: left;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const AboutMeWrapper = styled.div`
|
||||
height: 90%;
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
|
||||
const AboutMeText = styled.p`
|
||||
position: relative;
|
||||
padding: 2%;
|
||||
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
font-family: "Arimo", sans-serif;
|
||||
`;
|
||||
|
||||
const RankLabel = styled(AboutMeLabel)`
|
||||
text-align: center;
|
||||
font-size: min(20px, 1.5vw);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const RankWrapper = styled.div``;
|
||||
69
ui/src/components/Chat/embla.css
Normal file
69
ui/src/components/Chat/embla.css
Normal file
@@ -0,0 +1,69 @@
|
||||
.embla {
|
||||
position: relative;
|
||||
background-color: #f7f7f7;
|
||||
padding: 20px;
|
||||
max-width: 670px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.embla__viewport.is-draggable {
|
||||
cursor: move;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.embla__viewport.is-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.embla__slide__img {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: auto;
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
max-width: none;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.embla__button {
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
touch-action: manipulation;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
fill: #1bcacd;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.embla__button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.embla__button__svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.embla__button--prev {
|
||||
left: 27px;
|
||||
}
|
||||
|
||||
.embla__button--next {
|
||||
right: 27px;
|
||||
}
|
||||
147
ui/src/components/Landing/FindDuo.tsx
Normal file
147
ui/src/components/Landing/FindDuo.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import * as React from "react";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
import { useState, useEffect } from "react";
|
||||
import { LoggedUserContext } from "../../contexts/LoggedUserContext";
|
||||
import { Micellaneous } from "../../util/Micellaneous";
|
||||
|
||||
export default function FindDuo() {
|
||||
// Contexts
|
||||
const loggedUserContext = React.useContext(LoggedUserContext);
|
||||
|
||||
// State
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(changeAgent, 750);
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
|
||||
function changeAgent() {
|
||||
index + 1 > 19 ? setIndex(0) : setIndex(index + 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<FindDuoContainer>
|
||||
<Picture
|
||||
icon={loggedUserContext?.loggedUser?.avatarImage ?? ""}
|
||||
></Picture>
|
||||
<Line></Line>
|
||||
<Picture icon={Micellaneous.getAgentIcon(index)} id="teammate">
|
||||
<p id="question-mark">?</p>
|
||||
</Picture>
|
||||
</FindDuoContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const FindDuoContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 0 15%;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const Picture = styled.div<{ icon: string }>`
|
||||
width: 9rem;
|
||||
height: 9rem;
|
||||
|
||||
background: url(${(props) => props.icon});
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-color: #266152;
|
||||
aspect-ratio: 1/1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border-color: rgb(102, 194, 169, 0.5);
|
||||
border-radius: 50%;
|
||||
border: 5px solid #66c2a9;
|
||||
z-index: 4;
|
||||
transition: all 0.5s ease-in-out;
|
||||
|
||||
& #question-mark {
|
||||
font-size: 5rem;
|
||||
font-weight: 600;
|
||||
|
||||
color: #c3c3c3;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
& #teammate {
|
||||
background: linear-gradient(rgba(0, 0, 0, 1), rgba(8, 71, 50, 0.2));
|
||||
}
|
||||
|
||||
@media (max-width: 1025px) {
|
||||
height: 8rem;
|
||||
width: 8rem;
|
||||
}
|
||||
@media (max-width: 769px) {
|
||||
height: 7.5rem;
|
||||
width: 7.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
height: 7rem;
|
||||
width: 7rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Bounce = keyframes`
|
||||
100%{
|
||||
transform: translateX(100%);
|
||||
z-index: 2;
|
||||
}
|
||||
0%{
|
||||
transform: translateX(-100%);
|
||||
z-index: 2;
|
||||
}
|
||||
`;
|
||||
|
||||
const Bounce1025 = keyframes`
|
||||
100%{
|
||||
transform: translateX(120%);
|
||||
z-index: 2;
|
||||
}
|
||||
0%{
|
||||
transform: translateX(-120%);
|
||||
z-index: 2;
|
||||
}
|
||||
`;
|
||||
|
||||
const Bounce769 = keyframes`
|
||||
100%{
|
||||
transform: translateY(115%);
|
||||
z-index: 2;
|
||||
}
|
||||
0%{
|
||||
transform: translateY(-115%);
|
||||
z-index: 2;
|
||||
}
|
||||
`;
|
||||
|
||||
const Line = styled.span`
|
||||
border: 2.5px solid #66c2a9;
|
||||
border-radius: 10px;
|
||||
background-color: #66c2a9;
|
||||
|
||||
width: 30%;
|
||||
animation: ${Bounce} 2.5s ease-in-out infinite alternate;
|
||||
|
||||
@media (max-width: 1025px) {
|
||||
width: 25%;
|
||||
animation: ${Bounce1025} 2.5s ease-in-out infinite alternate;
|
||||
}
|
||||
@media (max-width: 769px) {
|
||||
width: 1px;
|
||||
height: 100px;
|
||||
margin: 0 10%;
|
||||
border: 2.5px solid #66c2a9;
|
||||
animation: ${Bounce769} 2.5s ease-in-out infinite alternate;
|
||||
}
|
||||
`;
|
||||
559
ui/src/components/Landing/Landing.tsx
Normal file
559
ui/src/components/Landing/Landing.tsx
Normal file
@@ -0,0 +1,559 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import LandingCard from "./LandingCard";
|
||||
import { LoggedUserContext } from "../../contexts/LoggedUserContext";
|
||||
import { EnvConfig } from "../../util/EnvConfig";
|
||||
import { MatchedUserContext } from "../../contexts/MatchedUserContext";
|
||||
import { Micellaneous } from "../../util/Micellaneous";
|
||||
import { CustomToast } from "../Shared/CustomToast";
|
||||
import { FilterContext } from "../../contexts/FilterContext";
|
||||
import { toast } from "react-toastify";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { FilterPopup } from "../Shared/FilterPopup";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { SocketContext } from '../../contexts/SocketContext';
|
||||
|
||||
export default function Landing() {
|
||||
|
||||
const {state} = useLocation();
|
||||
|
||||
// State
|
||||
const [duoFound, setDuoFound] = useState<boolean>(false);
|
||||
const [findDuo, setFindDuo] = useState<boolean>(false);
|
||||
const [triggered, setTriggered] = React.useState(false);
|
||||
|
||||
const [bgAgent1, setBgAgent1] = useState("");
|
||||
const [bgAgent2, setBgAgent2] = useState("");
|
||||
const [logout, setLogout] = useState(false);
|
||||
|
||||
// Refs
|
||||
const pollingTimeout = React.useRef<NodeJS.Timeout>(null);
|
||||
|
||||
// Contexts
|
||||
const loggedUserContext = useContext(LoggedUserContext);
|
||||
const matchedUserContext = useContext(MatchedUserContext);
|
||||
const filterContext = useContext(FilterContext);
|
||||
const socketContext = useContext(SocketContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
// Use Effects
|
||||
|
||||
useEffect(() => {
|
||||
// Display a toast if we came from registration screen
|
||||
if(state?.justRegistered){
|
||||
toast.warning("Please edit your gender and age to be able to be better matched!")
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Messages from server
|
||||
socketContext?.socket?.on("error_user_connected", handleSuccessOrError);
|
||||
socketContext?.socket?.on("success_user_connected", handleSuccessOrError);
|
||||
socketContext?.socket?.on("error_find_matching", handleSuccessOrError);
|
||||
socketContext?.socket?.on("success_find_matching", handleSuccessOrError);
|
||||
socketContext?.socket?.on("error_stop_matching", handleSuccessOrError);
|
||||
socketContext?.socket?.on("success_stop_matching", handleSuccessOrError);
|
||||
socketContext?.socket?.on("match_found", handleMatchFound);
|
||||
|
||||
return () => {
|
||||
if (pollingTimeout) clearTimeout(pollingTimeout.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
let bgAgents = Micellaneous.getBackgroundAgents(
|
||||
loggedUserContext?.loggedUser?.avatarImage
|
||||
);
|
||||
setBgAgent1(bgAgents[0]);
|
||||
setBgAgent2(bgAgents[1]);
|
||||
}, []);
|
||||
|
||||
/* Handlers */
|
||||
|
||||
function handleCloseMe() {
|
||||
setTriggered(false);
|
||||
}
|
||||
|
||||
function handleSuccessOrError(res: any): void {
|
||||
if (EnvConfig.DEBUG) console.log(res);
|
||||
}
|
||||
|
||||
async function handleMatchFound(res: any): Promise<void> {
|
||||
// Store matched user in context
|
||||
matchedUserContext.updateMatchedUser(res.user);
|
||||
|
||||
// Stop timeout
|
||||
if (pollingTimeout) clearTimeout(pollingTimeout.current);
|
||||
|
||||
setDuoFound(true);
|
||||
}
|
||||
|
||||
const handleLogout = () =>{
|
||||
localStorage.clear();
|
||||
navigate('../login');
|
||||
socketContext.closeSocket();
|
||||
}
|
||||
|
||||
async function clickedFindDuo(): Promise<void> {
|
||||
|
||||
if(localStorage.getItem("matchedUser")){
|
||||
navigate('./chat')
|
||||
return;
|
||||
}
|
||||
|
||||
setFindDuo(true);
|
||||
|
||||
socketContext?.socket?.emit("find_matching", {
|
||||
userId: loggedUserContext?.loggedUser?._id,
|
||||
filters: filterContext.filters,
|
||||
} as any);
|
||||
|
||||
pollingTimeout.current = setTimeout(() => {
|
||||
toast.error("Could not find a match. Please try again later!");
|
||||
setFindDuo(false);
|
||||
socketContext?.socket?.emit("stop_matching", loggedUserContext?.loggedUser?._id);
|
||||
}, 300000); // 5 mins
|
||||
}
|
||||
|
||||
function clickedCancel(): void {
|
||||
setFindDuo(false);
|
||||
if (pollingTimeout) clearTimeout(pollingTimeout.current);
|
||||
socketContext?.socket?.emit("stop_matching", loggedUserContext?.loggedUser?._id);
|
||||
}
|
||||
|
||||
function clickedUser(): void{
|
||||
setLogout(!logout);
|
||||
}
|
||||
/* Helper Functions */
|
||||
|
||||
function getButton(): any {
|
||||
return findDuo ? (
|
||||
<Cancel onClick={clickedCancel}>✕ CANCEL</Cancel>
|
||||
) : (
|
||||
<FindDuo onClick={clickedFindDuo}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
id="magnifyingGlass"
|
||||
>
|
||||
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352c79.5 0 144-64.5 144-144s-64.5-144-144-144S64 128.5 64 208s64.5 144 144 144z" />
|
||||
</svg>
|
||||
FIND DUO
|
||||
</FindDuo>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function showChatButtons(): any {
|
||||
return findDuo || duoFound ? (
|
||||
<div></div>
|
||||
) : (
|
||||
<ButtonContainer>
|
||||
<div>
|
||||
<Button id="filter" onClick={() => setTriggered(true)}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
id="filterIcon"
|
||||
>
|
||||
<path d="M0 416c0-17.7 14.3-32 32-32l54.7 0c12.3-28.3 40.5-48 73.3-48s61 19.7 73.3 48L480 384c17.7 0 32 14.3 32 32s-14.3 32-32 32l-246.7 0c-12.3 28.3-40.5 48-73.3 48s-61-19.7-73.3-48L32 448c-17.7 0-32-14.3-32-32zm192 0c0-17.7-14.3-32-32-32s-32 14.3-32 32s14.3 32 32 32s32-14.3 32-32zM384 256c0-17.7-14.3-32-32-32s-32 14.3-32 32s14.3 32 32 32s32-14.3 32-32zm-32-80c32.8 0 61 19.7 73.3 48l54.7 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-54.7 0c-12.3 28.3-40.5 48-73.3 48s-61-19.7-73.3-48L32 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l246.7 0c12.3-28.3 40.5-48 73.3-48zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32s32-14.3 32-32s-14.3-32-32-32zm73.3 0L480 64c17.7 0 32 14.3 32 32s-14.3 32-32 32l-214.7 0c-12.3 28.3-40.5 48-73.3 48s-61-19.7-73.3-48L32 128C14.3 128 0 113.7 0 96S14.3 64 32 64l86.7 0C131 35.7 159.2 16 192 16s61 19.7 73.3 48z" />
|
||||
</svg>
|
||||
CHAT FILTERS
|
||||
</Button>
|
||||
</div>
|
||||
<History>
|
||||
<HistoryLink>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
id="historyIcon"
|
||||
>
|
||||
<path d="M75 75L41 41C25.9 25.9 0 36.6 0 57.9V168c0 13.3 10.7 24 24 24H134.1c21.4 0 32.1-25.9 17-41l-30.8-30.8C155 85.5 203 64 256 64c106 0 192 86 192 192s-86 192-192 192c-40.8 0-78.6-12.7-109.7-34.4c-14.5-10.1-34.4-6.6-44.6 7.9s-6.6 34.4 7.9 44.6C151.2 495 201.7 512 256 512c141.4 0 256-114.6 256-256S397.4 0 256 0C185.3 0 121.3 28.7 75 75zm181 53c-13.3 0-24 10.7-24 24V256c0 6.4 2.5 12.5 7 17l72 72c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-65-65V152c0-13.3-10.7-24-24-24z" />
|
||||
</svg>
|
||||
<Link
|
||||
style={{
|
||||
color: "#ffffff",
|
||||
textDecoration: "none",
|
||||
fontWeight: "600",
|
||||
}}
|
||||
to={"/history"}
|
||||
>CHAT HISTORY </Link>
|
||||
</HistoryLink>
|
||||
</History>
|
||||
</ButtonContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function showLogout(){
|
||||
return logout ?
|
||||
(<User>
|
||||
<StopLogout onClick={clickedUser}>✕</StopLogout>
|
||||
<Logout onClick={handleLogout}>LOGOUT</Logout>
|
||||
</User>
|
||||
)
|
||||
:
|
||||
(<User onClick={clickedUser}>
|
||||
<p id="username">
|
||||
{Micellaneous.toTitleCase(
|
||||
loggedUserContext?.loggedUser?.displayName
|
||||
) ?? "<username>"}
|
||||
</p>
|
||||
<img id="profilePic"
|
||||
src={loggedUserContext?.loggedUser?.avatarImage}
|
||||
alt="Player Icon">
|
||||
</img>
|
||||
</User>);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomToast></CustomToast>
|
||||
<LandingPage>
|
||||
<FilterPopup
|
||||
closeMe={handleCloseMe}
|
||||
triggered={triggered}
|
||||
></FilterPopup>
|
||||
<Nav>
|
||||
<Logo>
|
||||
<h2 id="valorant">VALORANT</h2>
|
||||
<h1 id="duofinder">DUOFINDER</h1>
|
||||
</Logo>
|
||||
<UserDiv>
|
||||
{showLogout()}
|
||||
</UserDiv>
|
||||
</Nav>
|
||||
<LandingContent>
|
||||
<Agent src={bgAgent1}></Agent>
|
||||
<Container>
|
||||
<LandingCard
|
||||
findDuo={findDuo}
|
||||
duoFound={duoFound}
|
||||
imgSrc={loggedUserContext?.loggedUser?.avatarImage}
|
||||
/>
|
||||
{showChatButtons()}
|
||||
{getButton()}
|
||||
</Container>
|
||||
<Agent src={bgAgent2}></Agent>
|
||||
</LandingContent>
|
||||
</LandingPage>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const ButtonContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 0%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ButtonImages = styled.img`
|
||||
/* height: 100%; */
|
||||
filter: invert();
|
||||
width: 2%;
|
||||
`;
|
||||
|
||||
const Button = styled.button`
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
/* height: 100%; */
|
||||
/* margin: 5%; */
|
||||
background: none;
|
||||
padding: 0% 10% 5% 10%;
|
||||
/* background: none; */
|
||||
color: white;
|
||||
border: 0px;
|
||||
width: 200px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& #filterIcon {
|
||||
fill: white;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 100%;
|
||||
padding-right: 5%;
|
||||
margin-bottom: -2.5%;
|
||||
}
|
||||
|
||||
@media (max-width: 769px) {
|
||||
margin-top: 5%;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
`;
|
||||
|
||||
const LandingPage = styled.div`
|
||||
background-color: #181818;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
min-height: 100vh;
|
||||
min-width: 100vw;
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
`;
|
||||
|
||||
const Nav = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding-top: 1rem;
|
||||
transition: all 0.25s ease-in-out;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
position: static;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Logo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
font-family: "valorant";
|
||||
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
& #valorant {
|
||||
color: #f94b4b;
|
||||
font-size: 2rem;
|
||||
margin: 0px;
|
||||
padding-bottom: 5px;
|
||||
font-weight: 200;
|
||||
transition: all 0.5s ease-in-out;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
& #duofinder {
|
||||
color: white;
|
||||
font-size: 3rem;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
font-weight: 200;
|
||||
transition: all 0.5s ease-in-out;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 769px) {
|
||||
position: static;
|
||||
transform: translateX(0);
|
||||
}
|
||||
`;
|
||||
|
||||
const UserDiv = styled.div`
|
||||
margin-left: auto;
|
||||
padding-right: 2rem;
|
||||
cursor:pointer;
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 300;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
padding-right: 0;
|
||||
margin: 2.5% auto;
|
||||
}
|
||||
`
|
||||
|
||||
const User = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
transition: all 0.25s ease-in-out;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
& #username {
|
||||
color: white;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
& #profilePic {
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
background-color: #425852;
|
||||
@media (max-width: 769px) {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StopLogout = styled.button`
|
||||
color:white;
|
||||
background:transparent;
|
||||
border: none;
|
||||
transition:0.5s all;
|
||||
&:hover{
|
||||
cursor:pointer;
|
||||
color: #f94b4b;
|
||||
}
|
||||
`
|
||||
|
||||
const Logout = styled.button`
|
||||
background-color:transparent;
|
||||
color:white;
|
||||
border:none;
|
||||
border-radius: 3px;
|
||||
transition: 0.5s all;
|
||||
cursor:pointer;
|
||||
height:56px;
|
||||
|
||||
&:hover{
|
||||
color: #f94b4b;
|
||||
}
|
||||
`
|
||||
|
||||
const LandingContent = styled.div`
|
||||
display: flex;
|
||||
flex: row;
|
||||
justify-content: space-evenly;
|
||||
height: 80vh;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const FindDuo = styled.button`
|
||||
background-color: #66c2a9;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1em;
|
||||
padding: 10px 20px;
|
||||
transition: 0.5s;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 150px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 7.5px #66c2a9;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& #magnifyingGlass {
|
||||
fill: white;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 3px 10px 5px 0px;
|
||||
}
|
||||
|
||||
@media (max-width: 769px) {
|
||||
margin: 5%;
|
||||
}
|
||||
`;
|
||||
|
||||
const Cancel = styled.button`
|
||||
background-color: #66c2a9;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-family: "Poppins", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1em;
|
||||
padding: 10px 20px;
|
||||
transition: 0.5s;
|
||||
|
||||
width: 150px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 7.5px #66c2a9;
|
||||
cursor: pointer;
|
||||
}
|
||||
@media (max-width: 769px) {
|
||||
margin: 5%;
|
||||
}
|
||||
`;
|
||||
|
||||
const HistoryButtonWrapper = styled.div`
|
||||
margin-left: 15px;
|
||||
`;
|
||||
|
||||
const History = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const HistoryLink = styled.div`
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
background: none;
|
||||
padding: 0% 10% 5% 10%;
|
||||
line-break: 100%;
|
||||
border: 0px;
|
||||
width: 200px;
|
||||
|
||||
& #historyIcon {
|
||||
fill: white;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 100%;
|
||||
padding-right: 5%;
|
||||
margin-bottom: -2.5%;
|
||||
}
|
||||
|
||||
@media (max-width: 769px) {
|
||||
font-size: 0.75rem;
|
||||
margin-top: 5%;
|
||||
}
|
||||
`;
|
||||
|
||||
const Agent = styled.img`
|
||||
filter: brightness(35%) drop-shadow(0 0 7.5px #66c2aa6c);
|
||||
width: 20vw;
|
||||
height: 80vh;
|
||||
object-fit: cover;
|
||||
|
||||
visibility: visible;
|
||||
opacity: 100;
|
||||
transition: visibility 1s, opacity 1s;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: visibility 1s, opacity 1s;
|
||||
}
|
||||
`;
|
||||
function navigate(arg0: string) {
|
||||
throw new Error("Function not implemented.");
|
||||
}
|
||||
|
||||
48
ui/src/components/Landing/LandingCard.tsx
Normal file
48
ui/src/components/Landing/LandingCard.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Profile from "./Profile";
|
||||
import FindDuo from "./FindDuo";
|
||||
import MatchFound from "./MatchFound";
|
||||
|
||||
type Props = {
|
||||
findDuo: boolean;
|
||||
duoFound: boolean;
|
||||
imgSrc: string;
|
||||
};
|
||||
|
||||
export default function LandingCard(props: Props) {
|
||||
/* Helper Functions */
|
||||
|
||||
const displayCard = () => {
|
||||
if (!props.findDuo) {
|
||||
return <Profile></Profile>;
|
||||
} else if (props.findDuo && !props.duoFound) {
|
||||
return <FindDuo></FindDuo>;
|
||||
} else if (props.findDuo && props.duoFound) {
|
||||
return <MatchFound></MatchFound>;
|
||||
}
|
||||
};
|
||||
|
||||
return <Card>{displayCard()}</Card>;
|
||||
}
|
||||
|
||||
const Card = styled.div`
|
||||
background-color: #282828;
|
||||
margin: 5%;
|
||||
width: 50vw;
|
||||
height: 55vh;
|
||||
|
||||
border-radius: 46px;
|
||||
box-shadow: 0 0 7.5px #66c2a9;
|
||||
text-align: center;
|
||||
color: white;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
width: 380px;
|
||||
height: 65vh;
|
||||
}
|
||||
`;
|
||||
152
ui/src/components/Landing/MatchFound.tsx
Normal file
152
ui/src/components/Landing/MatchFound.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { LoggedUserContext } from "../../contexts/LoggedUserContext";
|
||||
import { MatchedUserContext } from "../../contexts/MatchedUserContext";
|
||||
|
||||
export default function MatchFound() {
|
||||
// Contexts
|
||||
const loggedUserContext = useContext(LoggedUserContext);
|
||||
const matchedUserContext = useContext(MatchedUserContext);
|
||||
|
||||
// State
|
||||
const [countdown, setCountdown] = useState(3);
|
||||
|
||||
/* Navigation */
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (countdown > 0) {
|
||||
// Use setTimeout to schedule an update to the countdown state
|
||||
// every 1 second. When the countdown reaches 0, clear the
|
||||
// timeout so the interval stops.
|
||||
const timeout = setTimeout(() => {
|
||||
setCountdown(countdown - 1);
|
||||
}, 1000);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
navigate("../chat");
|
||||
}, 1000);
|
||||
}
|
||||
}, [countdown]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MatchContainer>
|
||||
<MatchFoundText>Match Found</MatchFoundText>
|
||||
<FindDuoContainer>
|
||||
<ProfileDiv>
|
||||
<Teammate
|
||||
icon={loggedUserContext?.loggedUser?.avatarImage}
|
||||
></Teammate>
|
||||
<Username>
|
||||
{loggedUserContext?.loggedUser?.displayName ?? "<username 1>"}
|
||||
</Username>
|
||||
</ProfileDiv>
|
||||
|
||||
<CountDownText>{countdown}</CountDownText>
|
||||
<ProfileDiv>
|
||||
<Teammate
|
||||
icon={matchedUserContext?.matchedUser?.avatarImage}
|
||||
></Teammate>
|
||||
<Username>
|
||||
{matchedUserContext?.matchedUser?.displayName ?? "<username 2>"}
|
||||
</Username>
|
||||
</ProfileDiv>
|
||||
</FindDuoContainer>
|
||||
</MatchContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const MatchContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const ProfileDiv = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const MatchFoundText = styled.p`
|
||||
font-family: "valorant";
|
||||
font-size: 2.8vw;
|
||||
margin: 0;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
margin-bottom: 5%;
|
||||
}
|
||||
`;
|
||||
|
||||
const Username = styled.p`
|
||||
margin-bottom: 0;
|
||||
font-size: 1rem;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
margin-top: 5%;
|
||||
}
|
||||
`;
|
||||
|
||||
const CountDownText = styled.p`
|
||||
color: #f94b4b;
|
||||
font-size: 5vw;
|
||||
width: 5vw;
|
||||
font-family: "valorant";
|
||||
margin: 0;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
margin: 7.5% 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Teammate = styled.img<{ icon: string }>`
|
||||
height: 9rem;
|
||||
width: 9rem;
|
||||
background: url(${(props) => props.icon});
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
border-color: rgb(102, 194, 169, 0.5);
|
||||
border-radius: 50%;
|
||||
border: 5px solid #66c2a9;
|
||||
|
||||
z-index: 4;
|
||||
transition: all 0.5s ease-in-out;
|
||||
background-color: #266152;
|
||||
|
||||
@media (max-width: 1025px) {
|
||||
height: 7rem;
|
||||
width: 7rem;
|
||||
}
|
||||
@media (max-width: 769px) {
|
||||
height: 6rem;
|
||||
width: 6rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const FindDuoContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 2.5% 10%;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 5% 0;
|
||||
}
|
||||
`;
|
||||
516
ui/src/components/Landing/Profile.tsx
Normal file
516
ui/src/components/Landing/Profile.tsx
Normal file
@@ -0,0 +1,516 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import styled from "styled-components";
|
||||
import { LoggedUserContext } from "../../contexts/LoggedUserContext";
|
||||
import { GameMode, Gender } from "../../models/FiltersModels";
|
||||
import { AuthService, IAuthResponse } from "../../services/AuthService";
|
||||
import { Micellaneous } from "../../util/Micellaneous";
|
||||
|
||||
// Main User Homescreen
|
||||
export default function Profile(): React.ReactElement {
|
||||
// Context
|
||||
const loggedUserContext = useContext(LoggedUserContext);
|
||||
|
||||
// State
|
||||
const [generalEdit, setGeneralEdit] = useState(false);
|
||||
|
||||
const [displayName, setDisplayName] = useState(
|
||||
loggedUserContext?.loggedUser?.displayName
|
||||
);
|
||||
const [age, setAge] = useState(loggedUserContext?.loggedUser?.age);
|
||||
const [gender, setGender] = useState(loggedUserContext?.loggedUser?.gender);
|
||||
const [playerType, setPlayerType] = useState(
|
||||
loggedUserContext?.loggedUser?.playerType
|
||||
);
|
||||
const [aboutMe, setAboutMe] = useState(
|
||||
loggedUserContext?.loggedUser?.aboutMe
|
||||
);
|
||||
const [profilePic, setProfilePic] = useState(
|
||||
loggedUserContext?.loggedUser?.avatarImage
|
||||
);
|
||||
const [charRemaining, setCharRemaining] = useState(150);
|
||||
|
||||
// Handlers
|
||||
const edit = async () => {
|
||||
|
||||
let shouldSave = generalEdit;
|
||||
setGeneralEdit(!generalEdit);
|
||||
|
||||
if(shouldSave){
|
||||
|
||||
const newValues = {
|
||||
displayName: displayName,
|
||||
age: age,
|
||||
gender: gender,
|
||||
playerType: playerType,
|
||||
aboutMe: aboutMe,
|
||||
avatarImage: profilePic
|
||||
};
|
||||
|
||||
const res : IAuthResponse = await AuthService.update({
|
||||
userId: loggedUserContext?.loggedUser?._id,
|
||||
...newValues,
|
||||
});
|
||||
|
||||
if(res.statusCode === 200){
|
||||
toast.success("Information updated succesfully.");
|
||||
return;
|
||||
}
|
||||
|
||||
loggedUserContext.updateLoggedUser({
|
||||
...loggedUserContext?.loggedUser,
|
||||
...newValues,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Pick a new (random) profile pic, only if editing is enabled
|
||||
const changePfp = () => {
|
||||
if (generalEdit){
|
||||
setProfilePic(Micellaneous.getAgentIcon(0, true));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisplayNameChange = (e: any) => {
|
||||
if (generalEdit) setDisplayName(e.target.value);
|
||||
};
|
||||
|
||||
const handleGenderChange = (e: any) => {
|
||||
if (generalEdit) setGender(e.target.value);
|
||||
};
|
||||
|
||||
const handleAgeChange = (e: any) => {
|
||||
if (generalEdit) setAge(e.target.value);
|
||||
};
|
||||
|
||||
const handleAboutMeChange = (e: any) => {
|
||||
if (generalEdit) setAboutMe(e.target.value);
|
||||
let charRemaining = 150 - e.target.value.length;
|
||||
setCharRemaining(charRemaining);
|
||||
};
|
||||
|
||||
const handlePlayerTypeChange = (e : any) => {
|
||||
if(generalEdit) setPlayerType(e.target.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<ProfilePage>
|
||||
<Edit
|
||||
genE={generalEdit}
|
||||
src="./images/general/edit.png"
|
||||
onClick={() => edit()}
|
||||
></Edit>
|
||||
<GridContainer>
|
||||
<ProfileContainer>
|
||||
<Pfp
|
||||
genE={generalEdit}
|
||||
src={profilePic}
|
||||
onClick={() => changePfp()}
|
||||
></Pfp>
|
||||
<PersonInfo>
|
||||
{/* <UsernameDiv> */}
|
||||
<Input
|
||||
style={{marginBottom:'10%'}}
|
||||
genE={generalEdit}
|
||||
placeholder={displayName ?? "Username"}
|
||||
autoComplete={"off"}
|
||||
maxLength={15}
|
||||
disabled={!generalEdit}
|
||||
onChange={handleDisplayNameChange}
|
||||
></Input>
|
||||
{/* </UsernameDiv> */}
|
||||
|
||||
<InfoInputs>
|
||||
<InfoInputsInner>
|
||||
<Age
|
||||
style={{width: 'auto', margin:'0 2%'}}
|
||||
genE={generalEdit}
|
||||
disabled={!generalEdit}
|
||||
type="number"
|
||||
placeholder={!age || age === 0?'--':`${age}`}
|
||||
value = {!age?18:age}
|
||||
min="18"
|
||||
max="99"
|
||||
onChange={handleAgeChange}
|
||||
></Age>
|
||||
<Drops
|
||||
style={{margin:'0 2%'}}
|
||||
value={gender}
|
||||
genE={generalEdit}
|
||||
disabled={!generalEdit}
|
||||
onChange={handleGenderChange}
|
||||
>
|
||||
<option value={Gender.unknown}>{"Gender"}</option>
|
||||
<option value={Gender.woman}>
|
||||
{Micellaneous.genderToString(Gender.woman, generalEdit)}
|
||||
</option>
|
||||
<option value={Gender.man}>
|
||||
{Micellaneous.genderToString(Gender.man, generalEdit)}
|
||||
</option>
|
||||
<option value={Gender.nonBinary}>
|
||||
{Micellaneous.genderToString(Gender.nonBinary, generalEdit)}
|
||||
</option>
|
||||
</Drops>
|
||||
<ServerPref> { Micellaneous.serverPreferenceToString(loggedUserContext?.loggedUser?.region)}</ServerPref>
|
||||
|
||||
</InfoInputsInner>
|
||||
<Drops style={{display: 'block', marginLeft:'auto', marginRight:'auto', width: 'auto', marginTop:'5%'}} value={playerType} genE={generalEdit} disabled={!generalEdit} onChange={handlePlayerTypeChange}>
|
||||
<option value={GameMode.competitive}>{Micellaneous.playerTypeToString(GameMode.competitive)}</option>
|
||||
<option value={GameMode.casual}>{Micellaneous.playerTypeToString(GameMode.casual)}</option>
|
||||
</Drops>
|
||||
</InfoInputs>
|
||||
</PersonInfo>
|
||||
</ProfileContainer>
|
||||
|
||||
<DetailsContainer>
|
||||
<BioContainer>
|
||||
<Label>ABOUT ME</Label>
|
||||
<TextArea
|
||||
onChange={handleAboutMeChange}
|
||||
value={aboutMe}
|
||||
genE={generalEdit}
|
||||
autoComplete="off"
|
||||
placeholder={"There's nothing here! Edit your profile to liven things up!"}
|
||||
disabled={!generalEdit}
|
||||
rows={6}
|
||||
maxLength={150}
|
||||
></TextArea>
|
||||
<CharRemaining genE={generalEdit}>
|
||||
{charRemaining}/150
|
||||
</CharRemaining>
|
||||
</BioContainer>
|
||||
|
||||
<RankInfo>
|
||||
<Ranks>
|
||||
<RankLabel>
|
||||
<Heading>REPUTATION</Heading>
|
||||
<RankImg imgSrc="/images/reputation_ranks/ToxicWaste.png"></RankImg>
|
||||
</RankLabel>
|
||||
</Ranks>
|
||||
<Ranks>
|
||||
<RankLabel></RankLabel>
|
||||
<Heading>RANK</Heading>
|
||||
<RankImg
|
||||
imgSrc={loggedUserContext && loggedUserContext.loggedUser && loggedUserContext.loggedUser.rank?`images/ranks/rank_${loggedUserContext?.loggedUser?.rank[0]}_${loggedUserContext?.loggedUser?.rank[1]}.webp`:"images/ranks/rank_1_1.webp"}
|
||||
></RankImg>
|
||||
</Ranks>
|
||||
</RankInfo>
|
||||
</DetailsContainer>
|
||||
</GridContainer>
|
||||
</ProfilePage>
|
||||
);
|
||||
}
|
||||
|
||||
const ProfilePage = styled.div``;
|
||||
const InfoInputs = styled.div`
|
||||
@media (max-width: 769px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const InfoInputsInner = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const CharRemaining = styled.p<{ genE: boolean }>`
|
||||
color: ${(props) => (props.genE ? "#4a4a4a" : "#282828")};
|
||||
font-size: 0.75rem;
|
||||
font-weight: 300;
|
||||
margin: 0 15% 0 auto;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
font-size: 0.5rem;
|
||||
margin-right: 5%;
|
||||
}
|
||||
`;
|
||||
|
||||
const Heading = styled.p`
|
||||
margin-top: 0%;
|
||||
margin-bottom: 10%;
|
||||
`;
|
||||
|
||||
const GridContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const ProfileContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
width: 50%;
|
||||
`;
|
||||
|
||||
const PersonInfo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 10%;
|
||||
@media (max-width: 769px) {
|
||||
padding-top: 5%;
|
||||
}
|
||||
`;
|
||||
|
||||
const DetailsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
width: 50%;
|
||||
@media (max-width: 769px) {
|
||||
width: 100%;
|
||||
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
`;
|
||||
|
||||
const BioContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
text-align: left;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const RankInfo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-right: 35%;
|
||||
z-index: 2;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
margin: -19.25% 0 5% 0;
|
||||
|
||||
justify-content: space-between;
|
||||
}
|
||||
`;
|
||||
|
||||
const Drops = styled.select<{ genE: boolean }>`
|
||||
background-color: ${(props) => (props.genE ? "#383838" : "#282828")};
|
||||
-webkit-appearance: ${(props) => (props.genE ? "" : "none")};
|
||||
-moz-appearance: ${(props) => (props.genE ? "" : "none")};
|
||||
border: 0px;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
height: 30px;
|
||||
width: ${(props) => (props.genE ? "75px" : "40px")};
|
||||
transition: 0.5s all;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 2%;
|
||||
opacity: 100%;
|
||||
|
||||
:focus {
|
||||
box-shadow: 0 0 5px #60d6b5;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
@media (max-width: 769px) {
|
||||
height: 20px;
|
||||
width: 40px;
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Age = styled.input<{ genE: boolean }>`
|
||||
background-color: ${(props) => (props.genE ? "#383838" : "#282828")};
|
||||
border: 0px;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
font-family: Arial;
|
||||
text-align: center;
|
||||
height: 28px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 200;
|
||||
transition: 0.5s all;
|
||||
padding:0;
|
||||
width: auto;
|
||||
::placeholder {
|
||||
color: white;
|
||||
}
|
||||
:focus {
|
||||
box-shadow: 0 0 5px #60d6b5;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
@media (max-width: 769px) {
|
||||
height: 18px;
|
||||
margin-top: 5px;
|
||||
width: 30px;
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Input = styled.input<{ genE: boolean }>`
|
||||
background-color: ${(props) => (props.genE ? "#383838" : "#282828")};
|
||||
color: white;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
height: 30px;
|
||||
width: 200px;
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
transition: 0.5s all;
|
||||
::placeholder {
|
||||
color: white;
|
||||
}
|
||||
:focus {
|
||||
box-shadow: 0 0 5px #60d6b5;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
@media (max-width: 1025px) {
|
||||
font-size: 1rem;
|
||||
width: 150px;
|
||||
}
|
||||
@media (max-width: 769px) {
|
||||
height: 20px;
|
||||
width: 100px;
|
||||
}
|
||||
`;
|
||||
|
||||
const TextArea = styled.textarea<{ genE: boolean }>`
|
||||
background-color: ${(props) => (props.genE ? "#383838" : "#282828")};
|
||||
color: white;
|
||||
width: inherit;
|
||||
margin-right: 15%;
|
||||
font-family: "Poppins", sans-serif;
|
||||
resize: none;
|
||||
border: 0px;
|
||||
border-radius: 3px;
|
||||
transition: 0.5s all;
|
||||
font-size: 1rem;
|
||||
font-weight: 200;
|
||||
height: 70%;
|
||||
|
||||
overflow: auto;
|
||||
:focus {
|
||||
box-shadow: 0 0 5px #60d6b5;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1025px) {
|
||||
height: 60%;
|
||||
}
|
||||
@media (max-width: 769px) {
|
||||
height: 10vh;
|
||||
margin: 2.5% 10%;
|
||||
font-size: 0.7rem;
|
||||
|
||||
::placeholder {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Edit = styled.img<{ genE: boolean }>`
|
||||
filter: ${(props) =>
|
||||
props.genE ? "drop-shadow(2px 2px 10px red) invert()" : "invert()"};
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 80%;
|
||||
|
||||
:hover {
|
||||
filter: drop-shadow(2px 2px 10px red) invert();
|
||||
cursor: pointer;
|
||||
}
|
||||
@media (max-width: 769px) {
|
||||
margin-top: 5%;
|
||||
margin-right: 5%;
|
||||
}
|
||||
`;
|
||||
|
||||
const Pfp = styled.img<{ genE: boolean }>`
|
||||
filter: ${(props) =>
|
||||
props.genE ? "drop-shadow(1px 1px 8px #66c2a9) brightness(100%)" : ""};
|
||||
aspect-ratio: 1/1;
|
||||
height: 9rem;
|
||||
width: 9rem;
|
||||
border: 5px solid #66c2a9;
|
||||
border-radius: 50%;
|
||||
background-color: #266152;
|
||||
transition: 0.5s all;
|
||||
|
||||
@media (max-width: 1025px) {
|
||||
height: 7.5rem;
|
||||
width: 7.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Label = styled.p`
|
||||
font-size: min(20px, 1.2vw);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
|
||||
@media (max-width: 1025px) {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const RankLabel = styled.div`
|
||||
padding-left: 0px;
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media (max-width: 1025px) {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Ranks = styled.div`
|
||||
margin-top: 5%;
|
||||
margin-left: 0;
|
||||
|
||||
@media (max-width: 769px) {
|
||||
margin-left: 11%;
|
||||
margin-right: 11%;
|
||||
}
|
||||
|
||||
@media (max-width: 769px) {
|
||||
}
|
||||
`;
|
||||
|
||||
const RankImg = styled.img<{ imgSrc: string }>`
|
||||
content: url(${(props) => props.imgSrc});
|
||||
justify-content: center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 5vw;
|
||||
max-width: 50px;
|
||||
height: 5vw;
|
||||
max-height: 50px;
|
||||
@media all and(max-height: 1000px) {
|
||||
height: 10%;
|
||||
max-height: 10%;
|
||||
}
|
||||
`;
|
||||
|
||||
const ServerPref = styled.span`
|
||||
margin: 0 2%;
|
||||
font-size: 0.75rem;
|
||||
@media (max-width: 769px) {
|
||||
margin-top: 5px;
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
`;
|
||||
103
ui/src/components/Shared/Button.tsx
Normal file
103
ui/src/components/Shared/Button.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
img_url?: string;
|
||||
fontSize?: string;
|
||||
url?: string;
|
||||
svg?: boolean;
|
||||
};
|
||||
|
||||
function Button(props: Props): React.ReactElement {
|
||||
let navigate = useNavigate();
|
||||
const onClickHandler = () => {
|
||||
navigate(`${props.url}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper onClick={onClickHandler} height={props.height} width={props.width}>
|
||||
{props.img_url ? <Image url={props.img_url} size={"40px"} /> :
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
||||
{/* <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> */}
|
||||
<path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.2 288 416 288c17.7 0 32-14.3 32-32s-14.3-32-32-32l-306.7 0L214.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"/>
|
||||
</svg>
|
||||
}
|
||||
<Text fontSize={props.fontSize}>{props.text}</Text>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled.div<{ width?: string; height?: string }>`
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
|
||||
align-items: center;
|
||||
padding: 1%;
|
||||
|
||||
background: #66c2a9;
|
||||
background-blend-mode: darken;
|
||||
mix-blend-mode: normal;
|
||||
box-shadow: 0px 3px 3px -2px rgba(255, 255, 255, 0.35);
|
||||
border-radius: 10px;
|
||||
|
||||
aspect-ratio: 16/7;
|
||||
height: ${(props) => props.height};
|
||||
width: ${(props) => props.width};
|
||||
transition: 0.5s all;
|
||||
|
||||
:hover {
|
||||
box-shadow: 0 0 7.5px #66c2a9;
|
||||
|
||||
cursor: pointer;
|
||||
// For no text selection
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-khtml-user-select: none; /* Konqueror HTML */
|
||||
-moz-user-select: none; /* Old versions of Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently
|
||||
supported by Chrome, Edge, Opera and Firefox */
|
||||
}
|
||||
|
||||
svg{
|
||||
fill: white;
|
||||
height: 2vw;
|
||||
width: 2vw;
|
||||
}
|
||||
|
||||
|
||||
`;
|
||||
|
||||
const Image = styled.img<{ url: string; size: string }>`
|
||||
position: relative;
|
||||
width: ${(props) => props.size};
|
||||
height: ${(props) => props.size};
|
||||
content: url(${(props) => props.url});
|
||||
`;
|
||||
|
||||
const Text = styled.div<{ fontSize: string }>`
|
||||
font-family: "Arimo";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-size: ${(props) => (props.fontSize ? props.fontSize : "1.5em")};
|
||||
|
||||
text-align: center;
|
||||
|
||||
color: #ffffff;
|
||||
|
||||
mix-blend-mode: normal;
|
||||
|
||||
@media all and (max-width: 1400px) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media all and (max-width: 500px) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Button;
|
||||
8
ui/src/components/Shared/CustomToast.tsx
Normal file
8
ui/src/components/Shared/CustomToast.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as React from 'react'
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
export function CustomToast() : React.ReactElement {
|
||||
return <ToastContainer toastStyle={{ backgroundColor: "black" }} position="top-right" autoClose={1800} hideProgressBar={true} newestOnTop={false}
|
||||
closeOnClick rtl={false} theme="dark"/>
|
||||
}
|
||||
468
ui/src/components/Shared/FilterPopup.tsx
Normal file
468
ui/src/components/Shared/FilterPopup.tsx
Normal file
@@ -0,0 +1,468 @@
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components';
|
||||
import { LoggedUserContext } from '../../contexts/LoggedUserContext';
|
||||
import { GenderPicker } from './GenderPicker';
|
||||
import { RankSlider } from './RankSlider';
|
||||
import { FilterContext } from '../../contexts/FilterContext';
|
||||
import { CustomToast } from './CustomToast';
|
||||
import { toast } from 'react-toastify';
|
||||
import { FiltersService, IFiltersResponse } from '../../services/FiltersService';
|
||||
import { GameMode, ServerPreference } from '../../models/FiltersModels';
|
||||
import { Micellaneous } from '../../util/Micellaneous';
|
||||
|
||||
type Props = {
|
||||
triggered : boolean;
|
||||
closeMe : () => void;
|
||||
}
|
||||
|
||||
type PopupProps = {
|
||||
triggered : boolean;
|
||||
}
|
||||
|
||||
export function FilterPopup(props : Props) : React.ReactElement<Props, any> {
|
||||
|
||||
/* Logged user and filter contexts */
|
||||
const loggedUserContext = React.useContext(LoggedUserContext);
|
||||
const filterContex = React.useContext(FilterContext);
|
||||
|
||||
/* Age Range Height */
|
||||
const [ageRangeHeight, setAgeRangeHeight] = React.useState<number>(0);
|
||||
const [compChecked, setCompChecked] = React.useState<boolean>(true);
|
||||
|
||||
/* Refs */
|
||||
const serverPrefDiv = React.useRef(null);
|
||||
const gameModeTitle = React.useRef(null);
|
||||
|
||||
const minAgeInput = React.useRef(null);
|
||||
const maxAgeInput = React.useRef(null);
|
||||
|
||||
// Use effect
|
||||
|
||||
React.useEffect(() => {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
let serverPrefDivHeight = serverPrefDiv?.current?.clientHeight ?? 0
|
||||
let gameModeTitleHeight = gameModeTitle?.current?.clientHeight ?? 0
|
||||
setAgeRangeHeight(serverPrefDivHeight + gameModeTitleHeight);
|
||||
function handleWindowResize() {
|
||||
let serverPrefDivHeight = serverPrefDiv?.current?.clientHeight ?? 0
|
||||
let gameModeTitleHeight = gameModeTitle?.current?.clientHeight ?? 0
|
||||
setAgeRangeHeight(serverPrefDivHeight + gameModeTitleHeight);
|
||||
}
|
||||
window.addEventListener('resize', handleWindowResize);
|
||||
|
||||
setCompChecked(filterContex?.filters?.gameMode === GameMode.competitive ?? true);
|
||||
}, []);
|
||||
|
||||
/* Handlers */
|
||||
|
||||
function handleServerPrefChange(event : any){
|
||||
filterContex.updateServerPreference(Number(event.target.value));
|
||||
}
|
||||
|
||||
function handleGameModeChange(event : any, gameMode : GameMode){
|
||||
setCompChecked(gameMode === GameMode.competitive);
|
||||
filterContex.updateGameMode(gameMode);
|
||||
}
|
||||
|
||||
function handleAgeRangeChange(event : any, isMin : boolean){
|
||||
|
||||
// Check that value is a number
|
||||
if(isNaN(Number(event.target.value))){
|
||||
toast.error(`The ${isMin?'min':'max'} age must be an integer number.`);
|
||||
if(isMin) minAgeInput.current.value = ""
|
||||
else maxAgeInput.current.value = ""
|
||||
return;
|
||||
}
|
||||
|
||||
let newAges : number[] = null;
|
||||
if(isMin){
|
||||
newAges = [Number(event.target.value), filterContex.filters.ageRange[1]];
|
||||
}else{
|
||||
newAges = [filterContex.filters.ageRange[0], Number(event.target.value)];
|
||||
}
|
||||
|
||||
filterContex.updateAgeRange(newAges);
|
||||
}
|
||||
|
||||
async function handleSave(){
|
||||
try{
|
||||
|
||||
if(filterContex.filters.ageRange[0] < 18){
|
||||
toast.error(`The min age must be greater than or equal to 18`);
|
||||
minAgeInput.current.value = ""
|
||||
filterContex.updateAgeRange([18, 25]);
|
||||
return;
|
||||
}else if(filterContex.filters.ageRange[0] > filterContex.filters.ageRange[1]){
|
||||
toast.error(`The min age must be less than or equal to the max age.`);
|
||||
minAgeInput.current.value = ""
|
||||
filterContex.updateAgeRange([18, filterContex.filters.ageRange[1]]);
|
||||
return;
|
||||
}else if(filterContex.filters.ageRange[1] < filterContex.filters.ageRange[0]){
|
||||
toast.error(`The max age must be greater than or equal to the min age.`);
|
||||
maxAgeInput.current.value = ""
|
||||
filterContex.updateAgeRange([filterContex.filters.ageRange[0], 25]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Call API to attempt save of filters
|
||||
const filterResponse : IFiltersResponse = await FiltersService.upsert({userId:loggedUserContext.loggedUser._id, filters:filterContex.filters});
|
||||
|
||||
if(filterResponse.statusCode !== 200){ // Username already in use or Email already in use
|
||||
toast.error(filterResponse.data);
|
||||
return;
|
||||
}else{
|
||||
toast.success("Filters saved successfully");
|
||||
props.closeMe();
|
||||
}
|
||||
}catch(err){
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
return ( <>
|
||||
<CustomToast/>
|
||||
<Popup triggered={props.triggered}>
|
||||
<PopupContent>
|
||||
<div id='header'>
|
||||
<p id='title'>CHAT FILTERS</p>
|
||||
<p id='subtitle'>Choose who you want to match with</p>
|
||||
</div>
|
||||
<div id='body'>
|
||||
<div id='left'>
|
||||
<div style={{height: '25%'}} id='server-pref' className='row' ref={serverPrefDiv}>
|
||||
<p>Server Preferences:</p>
|
||||
<div className='selects'>
|
||||
<select value={filterContex?.filters?.serverPreference ?? ServerPreference.na} className="sever-preferences" onChange={handleServerPrefChange}>
|
||||
<option value={ServerPreference.na}>{Micellaneous.serverPreferenceToString(ServerPreference.na, true)}</option>
|
||||
<option value={ServerPreference.eu}>{Micellaneous.serverPreferenceToString(ServerPreference.eu, true)}</option>
|
||||
<option value={ServerPreference.ap}>{Micellaneous.serverPreferenceToString(ServerPreference.ap, true)}</option>
|
||||
<option value={ServerPreference.kr}>{Micellaneous.serverPreferenceToString(ServerPreference.kr, true)}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{height: '25%'}} id='game-mode' className='row'>
|
||||
<p ref={gameModeTitle}>Game Mode:</p>
|
||||
<div className="radios">
|
||||
<div className='radio-group'>
|
||||
<input type="radio"value="Competitive" onChange={(e:any) => handleGameModeChange(e, GameMode.competitive)} checked={compChecked}/>
|
||||
<label>Competitive</label>
|
||||
</div>
|
||||
<div className='radio-group'>
|
||||
<input type="radio" value="Casual" onChange={(e:any) => handleGameModeChange(e, GameMode.casual)} checked={!compChecked}/>
|
||||
<label>Casual</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{height: '50%'}} id='rank-disparity' className='row'>
|
||||
<p>Rank Disparity:</p>
|
||||
<div className="ranks">
|
||||
<RankSlider/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='right'>
|
||||
<div id='age-range' style={{height: `calc(100% - (100% - ${ageRangeHeight}px))`}} className='row'>
|
||||
<p>Age Range:</p>
|
||||
<div className='ages'>
|
||||
<input ref={minAgeInput} placeholder={`${filterContex?.filters?.ageRange[0]}` ?? 'Min'} type='text' maxLength={2} onChange={(e:any) => handleAgeRangeChange(e, true)}></input>
|
||||
<p>to</p>
|
||||
<input ref={maxAgeInput} placeholder={`${filterContex?.filters?.ageRange[1]}` ?? 'Max'} type='text' maxLength={2} onChange={(e:any) => handleAgeRangeChange(e, false)}></input>
|
||||
</div>
|
||||
</div>
|
||||
<div id='match-me-with' style={{height: `calc(100% - ${ageRangeHeight}px)`}} className='row'>
|
||||
<p>Match me with:</p>
|
||||
<div className='genders'>
|
||||
<GenderPicker></GenderPicker>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='footer'>
|
||||
<button id='cancel-btn' onClick={() => {props.closeMe()}}>CANCEL</button>
|
||||
<button id='save-btn' onClick={handleSave}>SAVE</button>
|
||||
</div>
|
||||
</PopupContent>
|
||||
</Popup>
|
||||
</>);
|
||||
}
|
||||
|
||||
const Popup = styled.div( (props : PopupProps) => `
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgb(0,0,0,0.7);
|
||||
display: ${props.triggered?'flex':'none'};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 5;
|
||||
`);
|
||||
|
||||
const PopupContent = styled.div`
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
width: 35vw;
|
||||
height: 30vw;
|
||||
background-color: #282828;
|
||||
border-radius: 10%;
|
||||
padding: 2vw;
|
||||
transition: all 0.25s ease-in-out;
|
||||
|
||||
& #header{
|
||||
text-align: center;
|
||||
height: 25%;
|
||||
|
||||
& #title{
|
||||
font-size: 2.5vw;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& #subtitle{
|
||||
font-size: 1.2vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& #body{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 60%;
|
||||
overflow: hidden;
|
||||
|
||||
& p {
|
||||
font-size: 1.0vw;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& #left, #right{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
& .row{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& #left{
|
||||
width: 70%;
|
||||
|
||||
& .row{
|
||||
height: 33.33%;
|
||||
}
|
||||
|
||||
/* Server Preferences */
|
||||
& #server-pref{
|
||||
|
||||
.selects{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
& select {
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: 0.5vw;
|
||||
border-radius: 12px;
|
||||
background-color: #D9D9D9;
|
||||
font-size: 0.7vw;
|
||||
justify-content: left;
|
||||
|
||||
@media screen and (max-width: 950px) and (orientation: portrait){
|
||||
width: auto;
|
||||
height: auto;
|
||||
font-size: 2.0vw;
|
||||
transform: translate(-25%,0) scale(0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Game Mode */
|
||||
& #game-mode{
|
||||
|
||||
& .radios{
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
& .radio-group{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
background-color: #D9D9D9;
|
||||
padding: 0.2vw 0;
|
||||
padding-right: 1vw;
|
||||
border-radius: 10px;
|
||||
color: black;
|
||||
width: auto;
|
||||
height: 1.5vw;
|
||||
font-size: 0.8vw;
|
||||
margin-right: 2%;
|
||||
|
||||
& input{
|
||||
accent-color: black;
|
||||
height: 1vw;
|
||||
width: 1vw;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
min-width: 3px;
|
||||
min-height: 3px;
|
||||
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus{
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
& label{
|
||||
margin-left: 0.1vw;
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Rank Disparity */
|
||||
& #rank-disparity{
|
||||
width: 100%;
|
||||
|
||||
& .ranks{
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
justify-content: left;
|
||||
margin-top: 3%;
|
||||
/* align-items: center; */
|
||||
|
||||
& input{
|
||||
width: 85%;
|
||||
height: 10%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& #right {
|
||||
width: 30%;
|
||||
|
||||
& .row{
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
/* Age Range */
|
||||
& #age-range{
|
||||
display: flex;
|
||||
|
||||
& .ages{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: left;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
|
||||
& input{
|
||||
width: 20%;
|
||||
height: auto;
|
||||
padding: 0.6vw;
|
||||
border-radius: 35%;
|
||||
background-color: #D9D9D9;
|
||||
font-size: 0.8vw;
|
||||
border: none;
|
||||
|
||||
&::placeholder{
|
||||
text-align: center;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&:focus{
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
& p{
|
||||
font-size: 1.0vw;
|
||||
font-weight: normal;
|
||||
margin: 0 5%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Match Me With */
|
||||
& #match-me-with{
|
||||
|
||||
& .genders{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: left;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: 7%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& #footer{
|
||||
height: 15%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
& button{
|
||||
margin: 3%;
|
||||
padding: 2% 0;
|
||||
width: 7vw;
|
||||
font-size: 1.2vw;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
& #save-btn{
|
||||
background-color: #66C2A9;
|
||||
|
||||
&:hover{
|
||||
background-color: #1cce9f;
|
||||
}
|
||||
}
|
||||
|
||||
& #cancel-btn{
|
||||
background-color: #F94B4B;
|
||||
|
||||
&:hover{
|
||||
background-color: #cb1e1e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 950px) and (orientation: portrait){
|
||||
transform: scale(2.0);
|
||||
}
|
||||
`;
|
||||
191
ui/src/components/Shared/GenderPicker.tsx
Normal file
191
ui/src/components/Shared/GenderPicker.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import * as React from 'react'
|
||||
import { toast } from 'react-toastify';
|
||||
import styled from 'styled-components';
|
||||
import { FilterContext } from '../../contexts/FilterContext';
|
||||
import { Gender } from '../../models/FiltersModels';
|
||||
import { CustomToast } from './CustomToast';
|
||||
|
||||
interface IPos{
|
||||
left : number;
|
||||
top : number;
|
||||
}
|
||||
|
||||
export function GenderPicker() : React.ReactElement{
|
||||
|
||||
/* Filter context */
|
||||
const filterContex = React.useContext(FilterContext);
|
||||
|
||||
/* State */
|
||||
const [horizontalLinePos, setHorizontalLinePos] = React.useState<IPos>({left:0,top:0});
|
||||
const [genders, setGenders] = React.useState<boolean[]>([true, true, true, true]);
|
||||
|
||||
/* Refs */
|
||||
|
||||
const allGendersInput = React.useRef(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
setHorizontalLinePos({
|
||||
left: allGendersInput?.current?.offsetLeft ?? 0,
|
||||
top: allGendersInput?.current?.offsetTop + 1 ?? 0
|
||||
});
|
||||
function handleWindowResize() {
|
||||
setHorizontalLinePos({
|
||||
left: allGendersInput?.current?.offsetLeft ?? 0,
|
||||
top: allGendersInput?.current?.offsetTop + 1 ?? 0
|
||||
});
|
||||
}
|
||||
window.addEventListener('resize', handleWindowResize);
|
||||
|
||||
setGenders(filterContex?.filters?.genders ?? [true, true, true, true]);
|
||||
}, []);
|
||||
|
||||
/* Handlers */
|
||||
|
||||
function handleGenderChange(event : any, gender : Gender){
|
||||
|
||||
// Make sure that at least one gender is selected
|
||||
let newGenders : boolean[] = [genders[Gender.allGenders], genders[Gender.woman], genders[Gender.man], genders[Gender.nonBinary]];
|
||||
if(gender === Gender.allGenders && event.target.checked){ // if all genders is selected, select all other checkboxes
|
||||
newGenders[Gender.allGenders] = true;
|
||||
newGenders[Gender.man] = true;
|
||||
newGenders[Gender.woman] = true;
|
||||
newGenders[Gender.nonBinary] = true;
|
||||
}else if(gender === Gender.allGenders && !event.target.checked){ // if all gender is deselected, deselect all but one
|
||||
newGenders[Gender.allGenders] = false;
|
||||
newGenders[Gender.man] = false;
|
||||
newGenders[Gender.woman] = true;
|
||||
newGenders[Gender.nonBinary] = false;
|
||||
}else if(genders[Gender.allGenders] && gender !== Gender.allGenders && !event.target.checked){ // if all genders is selected, and one other checkbox is unselected
|
||||
newGenders[Gender.allGenders] = false;
|
||||
newGenders[Gender.man] = genders[Gender.man];
|
||||
newGenders[Gender.woman] = genders[Gender.woman];
|
||||
newGenders[Gender.nonBinary] = genders[Gender.nonBinary];
|
||||
newGenders[gender] = event.target.checked;
|
||||
}else if(gender !== Gender.allGenders && event.target.checked && !genders[Gender.allGenders]){
|
||||
newGenders[gender] = true;
|
||||
newGenders[Gender.allGenders] = (genders[Gender.man] && genders[Gender.woman]) ||
|
||||
(genders[Gender.nonBinary] && genders[Gender.woman]) ||
|
||||
(genders[Gender.man] && genders[Gender.nonBinary])
|
||||
}else{
|
||||
newGenders[Gender.allGenders] = genders[Gender.allGenders];
|
||||
newGenders[Gender.man] = genders[Gender.man];
|
||||
newGenders[Gender.woman] = genders[Gender.woman];
|
||||
newGenders[Gender.nonBinary] = genders[Gender.nonBinary];
|
||||
newGenders[gender] = event.target.checked;
|
||||
}
|
||||
|
||||
// Make sure there is at least one gender selected
|
||||
if(!(newGenders[Gender.woman] || newGenders[Gender.man] || newGenders[Gender.nonBinary])){
|
||||
toast.error("At least one gender must be selected");
|
||||
return;
|
||||
}
|
||||
|
||||
setGenders(newGenders);
|
||||
filterContex.updateGender(newGenders);
|
||||
}
|
||||
|
||||
/* Helpers */
|
||||
|
||||
// Add a function to load things based on filters
|
||||
|
||||
return (<>
|
||||
<CustomToast/>
|
||||
<PickerContainer>
|
||||
<div className='checkbox-row'>
|
||||
<input type="checkbox" ref={allGendersInput} onChange={(e:any) => handleGenderChange(e, Gender.allGenders)} checked={genders[Gender.allGenders]}></input>
|
||||
<label>All Genders</label>
|
||||
</div>
|
||||
<div className='checkbox-row indent'>
|
||||
<input type="checkbox" onChange={(e:any) => handleGenderChange(e, Gender.woman)} checked={genders[Gender.woman]}></input>
|
||||
<label>Woman</label>
|
||||
</div>
|
||||
<div className='checkbox-row indent'>
|
||||
<input type="checkbox" onChange={(e:any) => handleGenderChange(e, Gender.man)} checked={genders[Gender.man]}></input>
|
||||
<label>Man</label>
|
||||
</div>
|
||||
<div className='checkbox-row indent'>
|
||||
<input type="checkbox" onChange={(e:any) => handleGenderChange(e, Gender.nonBinary)} checked={genders[Gender.nonBinary]}></input>
|
||||
<label>NB</label>
|
||||
</div>
|
||||
<div className='vertical-line' style={{top:horizontalLinePos.top, left:horizontalLinePos.left}}></div>
|
||||
<div id='horizontal1' className='horizontal-line' style={{top:horizontalLinePos.top, left:horizontalLinePos.left}}></div>
|
||||
<div id='horizontal2' className='horizontal-line' style={{top:horizontalLinePos.top, left:horizontalLinePos.left}}></div>
|
||||
<div id='horizontal3' className='horizontal-line' style={{top:horizontalLinePos.top, left:horizontalLinePos.left}}></div>
|
||||
</PickerContainer>
|
||||
</>);
|
||||
}
|
||||
|
||||
const PickerContainer = styled.div`
|
||||
height: auto;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
& input{
|
||||
height: 1.0vw;
|
||||
width: 1.0vw;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
z-index: 2;
|
||||
min-width: 5px;
|
||||
min-height: 5px;
|
||||
}
|
||||
|
||||
& label{
|
||||
font-size: 1.0vw;
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
& .checkbox-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
width: auto;
|
||||
margin-bottom: 4%;
|
||||
}
|
||||
|
||||
& .indent{
|
||||
margin-left: 15%;
|
||||
}
|
||||
|
||||
& .vertical-line{
|
||||
position: absolute;
|
||||
width: 0.45vw;
|
||||
height: 5.65vw;
|
||||
border-right: 1px solid #66C2A9;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
& .horizontal-line{
|
||||
position: absolute;
|
||||
border-bottom: 1px solid #66C2A9;
|
||||
width: 1vw;
|
||||
margin-left: 5.5%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
& #horizontal1{
|
||||
height: 2.2vw;
|
||||
|
||||
@media screen and (max-width: 950px) {
|
||||
height: 2.0vw;
|
||||
}
|
||||
}
|
||||
|
||||
& #horizontal2{
|
||||
height: 3.85vw;
|
||||
|
||||
@media screen and (max-width: 950px) {
|
||||
height: 3.65vw;
|
||||
}
|
||||
}
|
||||
|
||||
& #horizontal3{
|
||||
height: 5.6vw;
|
||||
|
||||
@media screen and (max-width: 950px) {
|
||||
height: 5.4vw;
|
||||
}
|
||||
}
|
||||
`;
|
||||
32
ui/src/components/Shared/ProfilePicture.tsx
Normal file
32
ui/src/components/Shared/ProfilePicture.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
size: string;
|
||||
url: string;
|
||||
showBorder: boolean;
|
||||
};
|
||||
|
||||
export default function ProfilePicture(props: Props): React.ReactElement {
|
||||
return (
|
||||
<Wrapper size={props.size}>
|
||||
<Image url={props.url} />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled.div<{ size: string }>`
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
height: ${(props) => props.size || "100%"};
|
||||
`;
|
||||
|
||||
const Image = styled("img")<{ url: string }>`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
|
||||
content: url(${(props) => props.url});
|
||||
`;
|
||||
155
ui/src/components/Shared/RankSlider.tsx
Normal file
155
ui/src/components/Shared/RankSlider.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { Slider } from '@mui/material';
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components';
|
||||
import { FilterContext } from '../../contexts/FilterContext';
|
||||
import { RankLevel, RankType } from '../../models/FiltersModels';
|
||||
|
||||
export function RankSlider() : React.ReactElement{
|
||||
|
||||
/* Filter Context */
|
||||
const filterContex = React.useContext(FilterContext);
|
||||
|
||||
/* Constants */
|
||||
const marks = [{value: 0.8, label: ''}, {value: 1,label: ''},{value: 1.2,label: ''},{value: 1.8,label: ''},
|
||||
{value: 2,label: ''},{value: 2.2,label: ''},{value: 2.8,label: ''},{value: 3,label: ''},
|
||||
{value: 3.2,label: ''},{value: 3.8,label: ''},{value: 4,label: ''},{value: 4.2,label: ''},
|
||||
{value: 4.8,label: ''},{value: 5,label: ''},{value: 5.2,label: ''},{value: 5.8,label: ''},
|
||||
{value: 6,label: ''},{value: 6.2,label: ''},{value: 6.8,label: ''},{value: 7,label: ''},
|
||||
{value: 7.2,label: ''},{value: 7.8,label: ''},{value: 8,label: ''},{value: 8.2,label: ''},
|
||||
{value: 9,label: ''}];
|
||||
|
||||
const [value, setValue] = React.useState<number[]>([5, 6]);
|
||||
|
||||
/* Use Effect */
|
||||
React.useEffect(() => {
|
||||
setValue([toSliderRank(filterContex?.filters?.rankDisparity[0], filterContex?.filters?.rankDisparity[1]),
|
||||
toSliderRank(filterContex?.filters?.rankDisparity[2], filterContex?.filters?.rankDisparity[3])]);
|
||||
}, []);
|
||||
|
||||
/* Handlers */
|
||||
|
||||
const handleChange = (event: Event, newValue: number | number[], activeThumb: number) : void => {
|
||||
if (!Array.isArray(newValue) || newValue[0] >= newValue[1] || !inCorrectRange(newValue[0]) || !inCorrectRange(newValue[1])) {
|
||||
return;
|
||||
}
|
||||
setValue([newValue[0], newValue[1]]);
|
||||
|
||||
// Change filter context here
|
||||
filterContex.updateRankDisparity([...toRank(newValue[0]), ...toRank(newValue[1])]);
|
||||
};
|
||||
|
||||
/* Helper Functions */
|
||||
|
||||
const inCorrectRange = (value : number) : boolean => {
|
||||
let result : boolean = (value === 0.8 || value === 1 || value === 1.2);
|
||||
result ||= (value === 1.8 || value === 2 || value === 2.2);
|
||||
result ||= (value === 2.8 || value === 3 || value === 3.2);
|
||||
result ||= (value === 3.8 || value === 4 || value === 4.2);
|
||||
result ||= (value === 4.8 || value === 5 || value === 5.2);
|
||||
result ||= (value === 5.8 || value === 6 || value === 6.2);
|
||||
result ||= (value === 6.8 || value === 7 || value === 7.2);
|
||||
result ||= (value === 7.8 || value === 8 || value === 8.2);
|
||||
result ||= value === 9.0;
|
||||
return result;
|
||||
}
|
||||
|
||||
function toRankLevel(val : number) : RankLevel {
|
||||
let decimalPart = Number((val % 1).toFixed(2));
|
||||
let ans : RankLevel;
|
||||
if(decimalPart === 0.80) ans = RankLevel.one
|
||||
else if(decimalPart === 0.00) ans = RankLevel.two
|
||||
else ans = RankLevel.three
|
||||
return ans;
|
||||
}
|
||||
|
||||
function toRankType(val : number) : RankType{
|
||||
let integerPart = val - Number((val % 1).toFixed(2));
|
||||
let decimalPart = Number((val % 1).toFixed(2));
|
||||
if(decimalPart === 0.80) integerPart += 1;
|
||||
return integerPart
|
||||
}
|
||||
|
||||
function toSliderRank(rankType : RankType, rankLevel : RankLevel) : number {
|
||||
|
||||
let decimal : number = 0.0;
|
||||
if(rankLevel === RankLevel.one) decimal = -0.20;
|
||||
else decimal = 0.20;
|
||||
|
||||
return rankType + decimal;
|
||||
}
|
||||
|
||||
function toRank(val : number) : number[] {
|
||||
return [toRankType(val), toRankLevel(val)]
|
||||
}
|
||||
|
||||
return (
|
||||
<OuterContainer>
|
||||
<CustomSlider value={value} onChange={handleChange} valueLabelDisplay="off"
|
||||
disableSwap min={0.6} max={9.2} step={0.1} marks={marks}/>
|
||||
<RankIcons>
|
||||
<img style={{marginLeft:'2.7%'}} src={'images/ranks/rank_1_1.webp'} alt="rank 1"></img>
|
||||
<img style={{marginLeft:'5.0%'}} src={'/images/ranks/rank_2_1.webp'} alt="rank 2"></img>
|
||||
<img style={{marginLeft:'4.0%'}} src={'/images/ranks/rank_3_1.webp'} alt="rank 3"></img>
|
||||
<img style={{marginLeft:'5.0%'}} src={'/images/ranks/rank_4_1.webp'} alt="rank 4"></img>
|
||||
<img style={{marginLeft:'5.3%'}} src={'/images/ranks/rank_5_1.webp'} alt="rank 5"></img>
|
||||
<img style={{marginLeft:'5.0%'}} src={'/images/ranks/rank_6_1.webp'} alt="rank 6"></img>
|
||||
<img style={{marginLeft:'4.8%'}} src={'images/ranks/rank_7_1.webp'} alt="rank 7"></img>
|
||||
<img style={{marginLeft:'4.8%'}} src={'images/ranks/rank_8_1.webp'} alt="rank 8"></img>
|
||||
<img style={{marginLeft:'4.6%'}} src={'/images/ranks/rank_9_1.webp'} alt="rank 9"></img>
|
||||
</RankIcons>
|
||||
</OuterContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const OuterContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 90%;
|
||||
height: 4vw;
|
||||
`;
|
||||
|
||||
const CustomSlider = styled(Slider)`
|
||||
margin-left: 0.2vw;
|
||||
margin-bottom: 6%;
|
||||
height: 0.1vw!important;
|
||||
padding: 0!important;
|
||||
|
||||
|
||||
& .MuiSlider-thumb {
|
||||
background-color: #BD3944;
|
||||
height: 0.8vw;
|
||||
width: 0.3vw;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
& .MuiSlider-rail {
|
||||
color: #D9D9D9;
|
||||
height: 0.2vw;
|
||||
opacity: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
& .MuiSlider-track{
|
||||
color: #BD3944;
|
||||
}
|
||||
|
||||
& .MuiSlider-mark{
|
||||
color: white;
|
||||
height: 0.5vw;
|
||||
width: 0.15vw;
|
||||
margin-top: 4%;
|
||||
}
|
||||
`;
|
||||
|
||||
const RankIcons = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: auto;
|
||||
width: auto;
|
||||
|
||||
& img{
|
||||
height: 1.5vw;
|
||||
width: auto;
|
||||
padding: 0.4vw 0 0 0;
|
||||
}
|
||||
`;
|
||||
96
ui/src/components/Start/Agents.tsx
Normal file
96
ui/src/components/Start/Agents.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components';
|
||||
import { FormType } from './Form';
|
||||
|
||||
type Props = {
|
||||
formType : FormType
|
||||
}
|
||||
|
||||
export function Agents(props : Props) : React.ReactElement<Props, any>{
|
||||
|
||||
const { innerWidth: width, innerHeight: height } = window;
|
||||
const [imgHeight, setImgHeight] = React.useState(0);
|
||||
const imgRef = React.useRef(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
setImgHeight(imgRef.current?.clientHeight);
|
||||
function handleWindowResize() {
|
||||
setImgHeight(imgRef.current?.clientHeight);
|
||||
}
|
||||
window.addEventListener('resize', handleWindowResize);
|
||||
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AgentContainer formType={props.formType}>
|
||||
<div className='text-container' style={{height: (imgHeight === 0 || (window.matchMedia("(orientation: landscape)").matches && (width <= 900 || height <= 700)))?'10%':`calc(100% - ${imgHeight}px)`}}>
|
||||
<p>Find teammates to play Valorant with! Climb up the ranks with like-minded players and make long lasting friendships.</p>
|
||||
</div>
|
||||
<img ref={imgRef} alt="Group of Agents"></img>
|
||||
</AgentContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const AgentContainer = styled.div((props : Props) =>`
|
||||
width: 67%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
& div{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 5% 0;
|
||||
|
||||
& p {
|
||||
color: white;
|
||||
font-size: 2.2vh;
|
||||
text-align: center;
|
||||
padding: 0 10%;
|
||||
|
||||
@media (max-width: 950px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& img{
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: ${props.formType === FormType.Login?'88%':'86%'};
|
||||
|
||||
content:url(${props.formType === FormType.Login?"/images/start_screen/agents_5.png":"/images/start_screen/agents_2.png"});
|
||||
|
||||
@media only screen and (max-width: 950px){
|
||||
width: 100%;
|
||||
height: auto;
|
||||
content:url(${"/images/start_screen/agents_3.png"});
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 950px){
|
||||
position:absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 700px){
|
||||
content:url($"/images/start_screen/agent_1.png"});
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 700px) and (orientation: landscape){
|
||||
width: auto;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 950px) {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position:absolute;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
}
|
||||
`);
|
||||
343
ui/src/components/Start/Form.tsx
Normal file
343
ui/src/components/Start/Form.tsx
Normal file
@@ -0,0 +1,343 @@
|
||||
import * as React from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { AuthService, IAuthResponse } from '../../services/AuthService';
|
||||
import { LoggedUserContext } from '../../contexts/LoggedUserContext';
|
||||
import { CustomToast } from '../Shared/CustomToast';
|
||||
import { IUser } from '../../models/AuthModels';
|
||||
import { FiltersService, IFiltersResponse } from '../../services/FiltersService';
|
||||
import { FilterContext } from '../../contexts/FilterContext';
|
||||
import { IFilters } from '../../models/FiltersModels';
|
||||
import { Micellaneous } from '../../util/Micellaneous';
|
||||
import { SocketContext } from '../../contexts/SocketContext';
|
||||
|
||||
export enum FormType {
|
||||
Login = 0,
|
||||
Registration = 1,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
formType: FormType;
|
||||
};
|
||||
|
||||
interface INav{
|
||||
justRegistered : boolean;
|
||||
}
|
||||
|
||||
|
||||
export function Form(props: Props): React.ReactElement<Props, any> {
|
||||
|
||||
/* Logged User Context */
|
||||
const loggedUserContext = React.useContext(LoggedUserContext);
|
||||
const filterContex = React.useContext(FilterContext);
|
||||
const socketContext = React.useContext(SocketContext);
|
||||
|
||||
/* Form State */
|
||||
const [displayName, setDisplayName] = React.useState('');
|
||||
const [gameName, setGameName] = React.useState('');
|
||||
const [tag, setTag] = React.useState('');
|
||||
const [email, setEmail] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [confirmPassword, setConfirmPassword] = React.useState('');
|
||||
const [navigateNext, setNavigateNext] = React.useState<INav>(null);
|
||||
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
|
||||
/* Navigation */
|
||||
const navigate = useNavigate();
|
||||
|
||||
/* Use Effect */
|
||||
React.useEffect(() => {
|
||||
if(loggedUserContext.loggedUser && navigateNext){
|
||||
socketContext.updateSocket(loggedUserContext?.loggedUser?._id);
|
||||
}
|
||||
}, [loggedUserContext.loggedUser, navigateNext]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if(socketContext.socket && navigateNext){
|
||||
console.log(socketContext.socket);
|
||||
navigate('../landing', { state: {
|
||||
justRegistered: navigateNext.justRegistered,
|
||||
}});
|
||||
}
|
||||
}, [socketContext.socket, navigateNext])
|
||||
|
||||
/* Handlers */
|
||||
|
||||
// Form
|
||||
const handleDisplayNameChange = (e : any) => { setDisplayName(e.target.value.toLowerCase()); }
|
||||
const handleGameNameChange = (e : any) => { setGameName(e.target.value); }
|
||||
const handleTagChange = (e : any) => { setTag(e.target.value.toUpperCase()); }
|
||||
|
||||
const handleEmailChange = (e : any) => { setEmail(e.target.value.toLowerCase()); }
|
||||
const handlePasswordChange = (e : any) => { setPassword(e.target.value); }
|
||||
const handleConfirmPasswordChange = (e : any) => { setConfirmPassword(e.target.value); }
|
||||
|
||||
// Button
|
||||
const handleButtonClick = async () : Promise<void> => {
|
||||
|
||||
if(loading){ // avoid multiple requests sent to api
|
||||
toast.error("Please wait, your request is being processed.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Email and password
|
||||
if(!email){
|
||||
toast.error("Please provide email.");
|
||||
return;
|
||||
}else if(!password){
|
||||
toast.error("Please provide a password.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(props.formType === FormType.Registration){
|
||||
|
||||
// Password, GameName and Tag validation
|
||||
if(!gameName){
|
||||
toast.error("Please provide a game name");
|
||||
return;
|
||||
}else if(!tag){
|
||||
toast.error("Please provide a tag");
|
||||
return;
|
||||
}else if(!confirmPassword){
|
||||
toast.error("Please confirm your password.");
|
||||
return;
|
||||
}else if(password !== confirmPassword){
|
||||
toast.error("Confirm password does not match password");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
// Call API to attempt registration
|
||||
let newUser = { displayName: displayName, gameName : gameName, tagLine : tag,
|
||||
email : email, password : password, avatarImage : Micellaneous.getAgentIcon(0, true)};
|
||||
const authResponse : IAuthResponse = await AuthService.register(newUser);
|
||||
|
||||
if(authResponse.statusCode !== 201){ // Username already in use or Email already in use
|
||||
toast.error(authResponse.data as String);
|
||||
setLoading(false);
|
||||
return;
|
||||
}else{
|
||||
|
||||
loggedUserContext.updateLoggedUser(authResponse.data as IUser);
|
||||
|
||||
// Creating default filters for new user
|
||||
const filtersResponse : IFiltersResponse = await FiltersService.upsert({userId: ((authResponse.data as IUser)._id as string), filters: null});
|
||||
filterContex.updateFilter(filtersResponse.data as IFilters);
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
setNavigateNext({justRegistered: true});
|
||||
}else{
|
||||
|
||||
setLoading(true);
|
||||
|
||||
// Call API to attempt login
|
||||
const authResponse : IAuthResponse = await AuthService.login({email:email, password:password});
|
||||
|
||||
if(authResponse.statusCode !== 200){ // Wrong email and password combination
|
||||
toast.error(authResponse.data as String);
|
||||
setLoading(false);
|
||||
return;
|
||||
}else{
|
||||
|
||||
loggedUserContext.updateLoggedUser(authResponse.data as IUser);
|
||||
|
||||
// Retrieving existing filters for user
|
||||
const filtersResponse : IFiltersResponse = await FiltersService.retrieve({userId: ((authResponse.data as IUser)._id as string)});
|
||||
filterContex.updateFilter(filtersResponse.data as IFilters);
|
||||
|
||||
setLoading(false);
|
||||
|
||||
setNavigateNext({justRegistered: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomToast></CustomToast>
|
||||
<OuterForm>
|
||||
<InnerForm>
|
||||
<Title>
|
||||
<p id="title1">VALORANT</p>
|
||||
<p id="title2">DUOFINDER</p>
|
||||
</Title>
|
||||
<Fields>
|
||||
<p id="subtitle">{props.formType === FormType.Registration?'CREATE ACCOUNT':'LOGIN'}</p>
|
||||
{props.formType === FormType.Registration?<input type='text' placeholder="DISPLAY NAME (optional)" onChange={handleDisplayNameChange}></input>:''}
|
||||
{props.formType === FormType.Registration?<InputPair>
|
||||
<input id='game-name' type='text' placeholder="GAME NAME" onChange={handleGameNameChange}></input>
|
||||
<input id='tag' type='text' placeholder="TAG" onChange={handleTagChange}></input>
|
||||
</InputPair>:''}
|
||||
<input type='email' placeholder="EMAIL" onChange={handleEmailChange}></input>
|
||||
<input type='password' placeholder="PASSWORD" onChange={handlePasswordChange}></input>
|
||||
{props.formType === FormType.Registration?<input type='password' placeholder="CONFIRM PASSWORD" onChange={handleConfirmPasswordChange}></input>:''}
|
||||
{props.formType === FormType.Registration?
|
||||
<p className='question'>ALREADY HAVE AN ACCOUNT?<Link to='/login'> LOGIN</Link></p>:
|
||||
<p className="question">DON'T HAVE AN ACCOUNT?<Link to='/register'> REGISTER</Link></p>}
|
||||
</Fields>
|
||||
<button onClick={handleButtonClick}>{props.formType === FormType.Registration?'SIGNUP':'START'}</button>
|
||||
</InnerForm>
|
||||
</OuterForm>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const OuterForm = styled.div`
|
||||
width: 33vw;
|
||||
background-color: #181818;
|
||||
|
||||
@media screen and (max-width: 950px) {
|
||||
border-radius: 10px;
|
||||
padding: 5vw 2vw;
|
||||
transform: scale(1.5);
|
||||
background-color: rgb(24, 24, 24, 0.8);
|
||||
box-shadow: 0 0 1rem 0.1rem rgb(74, 183, 190);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
transform: scale(1.7);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
@media screen and (max-height: 400px) and (orientation:landscape){
|
||||
transform: scale(1.0)
|
||||
}
|
||||
`;
|
||||
|
||||
const InnerForm = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
& button {
|
||||
padding: 10px;
|
||||
background-color: #f94b4b;
|
||||
border: none;
|
||||
border-radius: 0.5vw;
|
||||
font-size: 1.3vw;
|
||||
color: white;
|
||||
width: auto;
|
||||
padding: 3%;
|
||||
transition: 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: #cb1e1e;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
text-align: center;
|
||||
|
||||
& p {
|
||||
margin: 0;
|
||||
display: block;
|
||||
font-family: "valorant";
|
||||
}
|
||||
|
||||
& #title1{
|
||||
font-size: 3.5vw;
|
||||
color: #F94B4B;
|
||||
}
|
||||
|
||||
& #title2{
|
||||
font-size: 4.5vw;
|
||||
}
|
||||
`;
|
||||
|
||||
const Fields = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: auto;
|
||||
margin: 15% 0;
|
||||
|
||||
& #subtitle {
|
||||
font-size: 1.2vw;
|
||||
}
|
||||
|
||||
& .question {
|
||||
font-size: 1vw;
|
||||
|
||||
& a {
|
||||
color: #f94b4b;
|
||||
text-decoration: none;
|
||||
transition: 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: #cb1e1e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& input {
|
||||
padding: 1vw;
|
||||
border: none;
|
||||
background-color: #e6e3e3;
|
||||
border-radius: 0.5vw;
|
||||
width: 15vw;
|
||||
height: 10px;
|
||||
margin: 5px;
|
||||
transition: 0.2s ease-in-out;
|
||||
font-size: 1vw;
|
||||
|
||||
&::placeholder {
|
||||
color: black;
|
||||
font-size: 0.8vw;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: default;
|
||||
background-color: #bcbaba;
|
||||
}
|
||||
|
||||
& input{
|
||||
padding: 1vw;
|
||||
border: none;
|
||||
background-color: #e6e3e3;
|
||||
border-radius: 0.5vw;
|
||||
width: 15vw;
|
||||
height: 10px;
|
||||
margin: 5px;
|
||||
transition: 0.2s ease-in-out;
|
||||
font-size: 1vw;
|
||||
&::placeholder{
|
||||
color: black;
|
||||
font-size: 0.8vw;
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
cursor: default;
|
||||
background-color: #bcbaba;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const InputPair = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
& #game-name{
|
||||
width: 9.5vw;
|
||||
}
|
||||
|
||||
& #tag{
|
||||
width: 3vw;
|
||||
}
|
||||
`;
|
||||
49
ui/src/components/Start/Start.tsx
Normal file
49
ui/src/components/Start/Start.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react'
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { Agents } from './Agents';
|
||||
import { Form, FormType } from './Form';
|
||||
|
||||
|
||||
type Props = {
|
||||
formType : FormType
|
||||
}
|
||||
|
||||
export default function Start(props : Props) : React.ReactElement{
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
React.useEffect(() => {
|
||||
if(localStorage.getItem('loggedUser')){
|
||||
navigate('../landing');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StartPage>
|
||||
<Agents formType={props.formType}></Agents>
|
||||
<Form formType={props.formType}></Form>
|
||||
</StartPage>
|
||||
);
|
||||
}
|
||||
|
||||
const StartPage = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: black;
|
||||
min-width: 100vw;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
|
||||
@media (max-width: 950px) {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 950px) and (orientation: landscape){
|
||||
height: 100vw;
|
||||
width: 100vw;
|
||||
}
|
||||
`;
|
||||
60
ui/src/contexts/FilterContext.tsx
Normal file
60
ui/src/contexts/FilterContext.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import { GameMode, IFilters } from "../models/FiltersModels";
|
||||
import { EnvConfig } from "../util/EnvConfig";
|
||||
|
||||
export type FiltersContextType = {
|
||||
filters : IFilters,
|
||||
updateServerPreference : (serverPreference : number) => void,
|
||||
updateGameMode : (gameMode : GameMode) => void,
|
||||
updateRankDisparity : (rankDisparity : number[]) => void,
|
||||
updateAgeRange : (ageRange : number[]) => void,
|
||||
updateGender : (genders : boolean[]) => void,
|
||||
updateFilter : (filters : IFilters) => void
|
||||
};
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const FilterContext = React.createContext<FiltersContextType | null>(null);
|
||||
|
||||
export function FilterProvider({children} : Props) {
|
||||
|
||||
const [filters, setFilters] = React.useState<IFilters | null>(localStorage.getItem("filters")?JSON.parse(localStorage.getItem("filters")):null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if(EnvConfig.DEBUG) console.log(`Filters: ${filters ?? "NULL"}`);
|
||||
if(filters) localStorage.setItem('filters', JSON.stringify(filters));
|
||||
}, [filters]);
|
||||
|
||||
function updateServerPreference (serverPreference : number) : void{
|
||||
setFilters({...filters, serverPreference: serverPreference});
|
||||
}
|
||||
|
||||
function updateGameMode(gameMode : GameMode) : void {
|
||||
setFilters({...filters, gameMode: gameMode});
|
||||
}
|
||||
|
||||
function updateRankDisparity(rankDisparity : number[]) : void{
|
||||
setFilters({...filters, rankDisparity: rankDisparity});
|
||||
}
|
||||
|
||||
function updateAgeRange(ageRange : number[]) : void{
|
||||
setFilters({...filters, ageRange: ageRange});
|
||||
}
|
||||
|
||||
function updateGender(genders : boolean[]) : void{
|
||||
setFilters({...filters, genders: genders});
|
||||
}
|
||||
|
||||
function updateFilter(filters : IFilters) : void{
|
||||
setFilters(filters);
|
||||
}
|
||||
|
||||
const value : FiltersContextType = {filters:filters, updateServerPreference:updateServerPreference,
|
||||
updateGameMode:updateGameMode, updateRankDisparity:updateRankDisparity,
|
||||
updateAgeRange:updateAgeRange, updateGender:updateGender,
|
||||
updateFilter:updateFilter}
|
||||
|
||||
return <FilterContext.Provider value={value}>{children}</FilterContext.Provider>
|
||||
};
|
||||
32
ui/src/contexts/LoggedUserContext.tsx
Normal file
32
ui/src/contexts/LoggedUserContext.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { IUser } from "../models/AuthModels";
|
||||
import { EnvConfig } from '../util/EnvConfig';
|
||||
|
||||
export type LoggedUserContextType = {
|
||||
loggedUser : IUser,
|
||||
updateLoggedUser : (loggedUser : IUser) => void
|
||||
};
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const LoggedUserContext = React.createContext<LoggedUserContextType | null>(null);
|
||||
|
||||
export function LoggedUserProvider({children} : Props) {
|
||||
|
||||
const [loggedUser, setLoggedUser] = React.useState<IUser | null>(localStorage.getItem("loggedUser")?JSON.parse(localStorage.getItem("loggedUser")):null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if(EnvConfig.DEBUG) console.log(`LoggeddUser: ${loggedUser ?? "NULL"}`);
|
||||
if(loggedUser) localStorage.setItem('loggedUser', JSON.stringify(loggedUser));
|
||||
}, [loggedUser]);
|
||||
|
||||
const updateLoggedUser = (loggedUser: IUser) : void => {
|
||||
setLoggedUser(loggedUser);
|
||||
}
|
||||
|
||||
const value : LoggedUserContextType = {loggedUser:loggedUser, updateLoggedUser:updateLoggedUser}
|
||||
|
||||
return <LoggedUserContext.Provider value={value}>{children}</LoggedUserContext.Provider>
|
||||
};
|
||||
32
ui/src/contexts/MatchedUserContext.tsx
Normal file
32
ui/src/contexts/MatchedUserContext.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { IUser } from "../models/AuthModels";
|
||||
import { EnvConfig } from '../util/EnvConfig';
|
||||
|
||||
export type MatchedUserContextType = {
|
||||
matchedUser : IUser,
|
||||
updateMatchedUser : (matchedUser : IUser) => void
|
||||
};
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const MatchedUserContext = React.createContext<MatchedUserContextType | null>(null);
|
||||
|
||||
export function MatchedUserProvider({children} : Props) {
|
||||
|
||||
const [matchedUser, setMatchedUser] = React.useState<IUser | null>(localStorage.getItem("matchedUser")?JSON.parse(localStorage.getItem("matchedUser")):null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if(EnvConfig.DEBUG) console.log(`Matched User: ${matchedUser ?? "NULL"}`);
|
||||
if(matchedUser) localStorage.setItem('matchedUser', JSON.stringify(matchedUser));
|
||||
}, [matchedUser]);
|
||||
|
||||
const updateMatchedUser = (loggedUser: IUser) : void => {
|
||||
setMatchedUser(loggedUser);
|
||||
}
|
||||
|
||||
const value : MatchedUserContextType = {matchedUser:matchedUser, updateMatchedUser:updateMatchedUser}
|
||||
|
||||
return <MatchedUserContext.Provider value={value}>{children}</MatchedUserContext.Provider>
|
||||
};
|
||||
38
ui/src/contexts/SocketContext.tsx
Normal file
38
ui/src/contexts/SocketContext.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { EnvConfig } from "../util/EnvConfig";
|
||||
|
||||
export type SocketContextType = {
|
||||
socket : Socket,
|
||||
updateSocket : (userId : string) => void
|
||||
closeSocket : () => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const SocketContext = React.createContext<SocketContextType | null>(null);
|
||||
|
||||
export function SocketProvider({children} : Props) {
|
||||
|
||||
const [socket, setSocket] = React.useState<Socket | null>(localStorage.getItem("loggedUser")?io(EnvConfig.SOCKET_URL, { query: { userId: `${JSON.parse(localStorage.getItem("loggedUser"))._id}` }, transports: ["websocket"], reconnectionDelayMax: 1000}):null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if(EnvConfig.DEBUG) console.log(`Socket: ${socket?.id ?? "NULL"}`);
|
||||
}, [socket]);
|
||||
|
||||
const closeSocket = () : void => {
|
||||
if(socket){
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
const updateUserId = (userId: string) : void => {
|
||||
setSocket(io(EnvConfig.SOCKET_URL, { query: { userId: `${userId}` }, transports: ["websocket"], reconnectionDelayMax: 1000}));
|
||||
}
|
||||
|
||||
const value : SocketContextType = {socket: socket, updateSocket: updateUserId, closeSocket: closeSocket}
|
||||
|
||||
return <SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
||||
};
|
||||
10
ui/src/index.tsx
Normal file
10
ui/src/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
// <React.StrictMode>
|
||||
<App/>
|
||||
// </React.StrictMode>
|
||||
);
|
||||
47
ui/src/models/AuthModels.ts
Normal file
47
ui/src/models/AuthModels.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/* DTOs */
|
||||
|
||||
import { GameMode, Gender } from "./FiltersModels";
|
||||
|
||||
export interface IRegisterDTO{
|
||||
displayName : string;
|
||||
gameName : string;
|
||||
tagLine : string;
|
||||
email : string;
|
||||
password : string;
|
||||
avatarImage : string;
|
||||
}
|
||||
|
||||
export interface ILoginDTO{
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface IUpdateDTO{
|
||||
userId: string,
|
||||
displayName : string,
|
||||
age : number,
|
||||
gender : Gender,
|
||||
playerType : GameMode,
|
||||
aboutMe: string,
|
||||
avatarImage: string
|
||||
}
|
||||
|
||||
/* View Models */
|
||||
|
||||
export interface IUser{
|
||||
_id : string;
|
||||
riotId : string;
|
||||
displayName : string;
|
||||
gameName : string;
|
||||
tagLine : string;
|
||||
email : string;
|
||||
avatarImage : string;
|
||||
rank : number[];
|
||||
accountLevel : number;
|
||||
region : number;
|
||||
age : number;
|
||||
gender : number;
|
||||
reputation : number;
|
||||
playerType : number;
|
||||
aboutMe: string;
|
||||
}
|
||||
31
ui/src/models/ChatModels.ts
Normal file
31
ui/src/models/ChatModels.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// DTOs
|
||||
|
||||
export interface IReceiveMsgDTO{
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export interface ISaveMsgDTO{
|
||||
senderId : string,
|
||||
receiverId : string
|
||||
message : string
|
||||
}
|
||||
|
||||
export interface IRetrieveMsgsDTO{
|
||||
senderId : string,
|
||||
receiverId : string
|
||||
}
|
||||
|
||||
export interface ILoadMsgDTO{
|
||||
senderId : string,
|
||||
receiverId : string,
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Models
|
||||
|
||||
export interface IMessage {
|
||||
userId : string;
|
||||
type: string;
|
||||
text: string;
|
||||
userIcon: string;
|
||||
}
|
||||
5
ui/src/models/CommendationModels.ts
Normal file
5
ui/src/models/CommendationModels.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface ISaveCommendDTO{
|
||||
commenderId : string,
|
||||
commendedId : string,
|
||||
score : number
|
||||
}
|
||||
65
ui/src/models/FiltersModels.ts
Normal file
65
ui/src/models/FiltersModels.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/* Related */
|
||||
|
||||
export enum ServerPreference{
|
||||
na=0,
|
||||
eu=1,
|
||||
ap=2,
|
||||
kr=3
|
||||
}
|
||||
|
||||
export interface AgeRange{
|
||||
minAge: number;
|
||||
maxAge: number;
|
||||
}
|
||||
|
||||
export enum GameMode{
|
||||
competitive = 0,
|
||||
casual = 1
|
||||
}
|
||||
|
||||
export enum RankType{
|
||||
iron = 1,
|
||||
bronze = 2,
|
||||
silver = 3,
|
||||
gold = 4,
|
||||
platinum = 5,
|
||||
diamond = 6,
|
||||
ascendant = 7,
|
||||
immortal = 8,
|
||||
radiant = 9
|
||||
}
|
||||
|
||||
export enum RankLevel{
|
||||
one = 1,
|
||||
two = 2,
|
||||
three = 3
|
||||
}
|
||||
|
||||
export enum Gender{
|
||||
unknown = -1,
|
||||
allGenders = 0,
|
||||
woman = 1,
|
||||
man = 2,
|
||||
nonBinary = 3
|
||||
}
|
||||
|
||||
/* Filters */
|
||||
|
||||
export interface IFilters {
|
||||
serverPreference: ServerPreference;
|
||||
gameMode : GameMode;
|
||||
rankDisparity : number[];
|
||||
ageRange : number[];
|
||||
genders : boolean[];
|
||||
}
|
||||
|
||||
/* DTOs */
|
||||
|
||||
export interface IUpsertDTO{
|
||||
userId : string,
|
||||
filters : IFilters
|
||||
}
|
||||
|
||||
export interface IRetrieveDTO{
|
||||
userId : string
|
||||
}
|
||||
47
ui/src/services/AuthService.ts
Normal file
47
ui/src/services/AuthService.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import axios from 'axios';
|
||||
import { ILoginDTO, IRegisterDTO, IUpdateDTO, IUser } from '../models/AuthModels';
|
||||
import { ApiConfig } from '../util/ApiConfig';
|
||||
|
||||
export interface IAuthResponse {
|
||||
data : IUser | string;
|
||||
statusCode : number;
|
||||
}
|
||||
|
||||
export class AuthService{
|
||||
|
||||
public static login = async (loginDTO : ILoginDTO) : Promise<IAuthResponse> => {
|
||||
try{
|
||||
let response = await axios({method: 'post', url: ApiConfig.loginRoute(), data: loginDTO});
|
||||
return {data:response?.data, statusCode:response?.status};
|
||||
}catch(err){
|
||||
return {data:err?.response?.data, statusCode:err?.response?.status};
|
||||
}
|
||||
}
|
||||
|
||||
public static register = async (registerDTO : IRegisterDTO) : Promise<IAuthResponse> => {
|
||||
try{
|
||||
let response = await axios({method: 'post', url: ApiConfig.registerRoute(),data: registerDTO});
|
||||
return {data:response?.data, statusCode:response?.status};
|
||||
}catch(err){
|
||||
return {data:err?.response?.data, statusCode:err?.response?.status};
|
||||
}
|
||||
}
|
||||
|
||||
public static update = async (updateDTO : IUpdateDTO) : Promise<IAuthResponse> => {
|
||||
try{
|
||||
let response = await axios({method: 'put', url: ApiConfig.updateUserRoute(),data: updateDTO});
|
||||
return {data:response?.data, statusCode:response?.status};
|
||||
}catch(err){
|
||||
return {data:err?.response?.data, statusCode:err?.response?.status};
|
||||
}
|
||||
}
|
||||
|
||||
public static find = async (userId : string) : Promise<IAuthResponse> => {
|
||||
try{
|
||||
let response = await axios({method: 'get', url: ApiConfig.findUserRoute(userId)});
|
||||
return {data:response?.data, statusCode:response?.status};
|
||||
}catch(err){
|
||||
return {data:err?.response?.data, statusCode:err?.response?.status};
|
||||
}
|
||||
}
|
||||
}
|
||||
29
ui/src/services/ChatService.ts
Normal file
29
ui/src/services/ChatService.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import axios from 'axios';
|
||||
import { IRetrieveMsgsDTO, ISaveMsgDTO } from '../models/ChatModels';
|
||||
import { ApiConfig } from '../util/ApiConfig';
|
||||
|
||||
export interface IChatResponse {
|
||||
data : any | string;
|
||||
statusCode : number;
|
||||
}
|
||||
|
||||
export class ChatService{
|
||||
|
||||
public static save = async (saveMsgDTO : ISaveMsgDTO) : Promise<IChatResponse> => {
|
||||
try{
|
||||
let response = await axios({method: 'post', url: ApiConfig.saveMessageRoute(), data: saveMsgDTO});
|
||||
return {data:response?.data, statusCode:response?.status};
|
||||
}catch(err){
|
||||
return {data:err?.response?.data, statusCode:err?.response?.status};
|
||||
}
|
||||
}
|
||||
|
||||
public static retrieve = async (retrieveMsgsDTO : IRetrieveMsgsDTO) : Promise<IChatResponse> => {
|
||||
try{
|
||||
let response = await axios({method: 'post', url: ApiConfig.retrieveMessagesRoute(), data: retrieveMsgsDTO});
|
||||
return {data:response?.data, statusCode:response?.status};
|
||||
}catch(err){
|
||||
return {data:err?.response?.data, statusCode:err?.response?.status};
|
||||
}
|
||||
}
|
||||
}
|
||||
21
ui/src/services/CommendationService.ts
Normal file
21
ui/src/services/CommendationService.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import axios from 'axios';
|
||||
import { ISaveCommendDTO } from '../models/CommendationModels';
|
||||
import { ApiConfig } from '../util/ApiConfig';
|
||||
|
||||
export interface ICommedationResponse {
|
||||
data : any;
|
||||
statusCode : number;
|
||||
}
|
||||
|
||||
export class CommendationService{
|
||||
|
||||
public static save = async (saveCommendDTO : ISaveCommendDTO) : Promise<ICommedationResponse> => {
|
||||
|
||||
try{
|
||||
let response = await axios({method: 'post', url: ApiConfig.saveCommendRoute(), data: saveCommendDTO});
|
||||
return {data:response?.data, statusCode:response?.status};
|
||||
}catch(err){
|
||||
return {data:err?.response?.data, statusCode:err?.response?.status};
|
||||
}
|
||||
}
|
||||
}
|
||||
31
ui/src/services/FiltersService.ts
Normal file
31
ui/src/services/FiltersService.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import axios from 'axios';
|
||||
import { IRetrieveDTO, IUpsertDTO } from '../models/FiltersModels';
|
||||
import { ApiConfig } from '../util/ApiConfig';
|
||||
|
||||
export interface IFiltersResponse {
|
||||
data : any;
|
||||
statusCode : number;
|
||||
}
|
||||
|
||||
export class FiltersService{
|
||||
|
||||
public static retrieve = async (retrieveDTO : IRetrieveDTO) : Promise<IFiltersResponse> => {
|
||||
|
||||
try{
|
||||
let response = await axios({method: 'get', url: ApiConfig.retrieveFiltersRoute(retrieveDTO)});
|
||||
return {data:response?.data, statusCode:response?.status};
|
||||
}catch(err){
|
||||
return {data:err?.response?.data, statusCode:err?.response?.status};
|
||||
}
|
||||
}
|
||||
|
||||
public static upsert = async (upsertDTO : IUpsertDTO) : Promise<IFiltersResponse> => {
|
||||
|
||||
try{
|
||||
let response = await axios({method: 'post', url: ApiConfig.upsertFiltersRoute(), data: upsertDTO});
|
||||
return {data:response?.data, statusCode:response?.status};
|
||||
}catch(err){
|
||||
return {data:err?.response?.data, statusCode:err?.response?.status};
|
||||
}
|
||||
}
|
||||
}
|
||||
21
ui/src/services/MatchingService.ts
Normal file
21
ui/src/services/MatchingService.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import axios from 'axios';
|
||||
import { IUser } from '../models/AuthModels';
|
||||
import { ApiConfig } from '../util/ApiConfig';
|
||||
|
||||
export interface IMatchingResponse {
|
||||
data : IUser[] | string;
|
||||
statusCode : number;
|
||||
}
|
||||
|
||||
export class MatchingService{
|
||||
|
||||
public static retrieveHistory = async (userId : string) : Promise<IMatchingResponse> => {
|
||||
|
||||
try{
|
||||
let response = await axios({method: 'get', url: ApiConfig.retrieveMatchHistory(userId)});
|
||||
return {data:response?.data, statusCode:response?.status};
|
||||
}catch(err){
|
||||
return {data:err?.response?.data, statusCode:err?.response?.status};
|
||||
}
|
||||
}
|
||||
}
|
||||
27
ui/src/util/ApiConfig.ts
Normal file
27
ui/src/util/ApiConfig.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { EnvConfig } from "./EnvConfig";
|
||||
import { IRetrieveDTO } from '../models/FiltersModels';
|
||||
|
||||
export class ApiConfig{
|
||||
|
||||
private static baseRoute : string = EnvConfig.API_URL;
|
||||
|
||||
/* User */
|
||||
public static loginRoute = () => this.baseRoute + `/users/login`;
|
||||
public static registerRoute = () => this.baseRoute + `/users/register`;
|
||||
public static updateUserRoute = () => this.baseRoute + `/users/update`;
|
||||
public static findUserRoute = (userId : string) => this.baseRoute + `users/${userId}`;
|
||||
|
||||
/* Chat */
|
||||
public static saveMessageRoute = () => this.baseRoute + `/chats/save`;
|
||||
public static retrieveMessagesRoute = () => this.baseRoute + `/chats/retrieve`;
|
||||
|
||||
/* History */
|
||||
public static retrieveMatchHistory = (userId : string) => this.baseRoute + `/matchings/${userId}`;
|
||||
|
||||
/* Commend */
|
||||
public static saveCommendRoute = () => this.baseRoute + `/commendations`;
|
||||
|
||||
/* Filters */
|
||||
public static upsertFiltersRoute = () => this.baseRoute + `/filters`;
|
||||
public static retrieveFiltersRoute = (retrieveDTO : IRetrieveDTO) => this.baseRoute + `/filters/${retrieveDTO.userId}`;
|
||||
}
|
||||
15
ui/src/util/EnvConfig.ts
Normal file
15
ui/src/util/EnvConfig.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export class EnvConfig{
|
||||
|
||||
/* Enviroment variables for api config */
|
||||
private static API_HOST : string = process.env.REACT_APP_API_HOST ?? "";
|
||||
private static API_PORT : string = process.env.REACT_APP_API_PORT ?? "";
|
||||
public static API_URL : string = `http://${EnvConfig.API_HOST}:${EnvConfig.API_PORT}`;
|
||||
|
||||
/* Enviroment variables for socket.io config */
|
||||
private static SOCKET_HOST : string = process.env.REACT_APP_SOCKET_HOST ?? "";
|
||||
private static SOCKET_PORT : string = process.env.REACT_APP_SOCKET_PORT ?? "";
|
||||
public static SOCKET_URL : string = `http://${EnvConfig.SOCKET_HOST}:${EnvConfig.SOCKET_PORT}`;
|
||||
|
||||
/* Others */
|
||||
public static DEBUG : boolean = ( (process.env.REACT_APP_DEBUG??'true') === 'true');
|
||||
}
|
||||
173
ui/src/util/Micellaneous.ts
Normal file
173
ui/src/util/Micellaneous.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
GameMode,
|
||||
Gender,
|
||||
RankLevel,
|
||||
RankType,
|
||||
ServerPreference,
|
||||
} from "../models/FiltersModels";
|
||||
|
||||
export class Micellaneous {
|
||||
private static agentIcons: Array<string> = [
|
||||
"/images/icons/Astra_icon.webp",
|
||||
"/images/icons/Breach_icon.webp",
|
||||
"/images/icons/Brimstone_icon.webp",
|
||||
"/images/icons/Chamber_icon.webp",
|
||||
"/images/icons/Cypher_icon.webp",
|
||||
"/images/icons/Fade_icon.webp",
|
||||
"/images/icons/Harbor_icon.webp",
|
||||
"/images/icons/Jett_icon.webp",
|
||||
"/images/icons/KAYO_icon.webp",
|
||||
"/images/icons/Killjoy_icon.webp",
|
||||
"/images/icons/Neon_icon.webp",
|
||||
"/images/icons/Omen_icon.webp",
|
||||
"/images/icons/Phoenix_icon.webp",
|
||||
"/images/icons/Raze_icon.webp",
|
||||
"/images/icons/Reyna_icon.webp",
|
||||
"/images/icons/Sage_icon.webp",
|
||||
"/images/icons/Skye_icon.webp",
|
||||
"/images/icons/Sova_icon.webp",
|
||||
"/images/icons/Viper_icon.webp",
|
||||
"/images/icons/Yoru_icon.webp",
|
||||
];
|
||||
|
||||
private static backgroundAgents: Array<string> = [
|
||||
"/images/background/Astra.png",
|
||||
"/images/background/Breach.png",
|
||||
"/images/background/Brimstone.png",
|
||||
"/images/background/Chamber.png",
|
||||
"/images/background/Cypher.png",
|
||||
"/images/background/Fade.png",
|
||||
"/images/background/Harbor.png",
|
||||
"/images/background/Jett.png",
|
||||
"/images/background/KAYO.png",
|
||||
"/images/background/Killjoy.png",
|
||||
"/images/background/Neon.png",
|
||||
"/images/background/Omen.png",
|
||||
"/images/background/Phoenix.png",
|
||||
"/images/background/Raze.png",
|
||||
"/images/background/Reyna.png",
|
||||
"/images/background/Sage.png",
|
||||
"/images/background/Skye.png",
|
||||
"/images/background/Sova.png",
|
||||
"/images/background/Viper.png",
|
||||
"/images/background/Yoru.png",
|
||||
];
|
||||
|
||||
// Converts a string to Title Case
|
||||
public static toTitleCase(str: string) {
|
||||
return str?.replace(/\w\S*/g, function (txt) {
|
||||
return txt?.charAt(0).toUpperCase() + txt?.substr(1).toLowerCase();
|
||||
});
|
||||
}
|
||||
|
||||
// Map rank to string
|
||||
public static rankToString(rank: number[]) {
|
||||
let rankType = "";
|
||||
let rankLevel = "";
|
||||
|
||||
switch (rank[0]) {
|
||||
case RankType.iron:
|
||||
rankType = "Iron";
|
||||
break;
|
||||
case RankType.bronze:
|
||||
rankType = "Bronze";
|
||||
break;
|
||||
case RankType.silver:
|
||||
rankType = "Silver";
|
||||
break;
|
||||
case RankType.gold:
|
||||
rankType = "Gold";
|
||||
break;
|
||||
case RankType.platinum:
|
||||
rankType = "Platinum";
|
||||
break;
|
||||
case RankType.diamond:
|
||||
rankType = "Diamond";
|
||||
break;
|
||||
case RankType.ascendant:
|
||||
rankType = "Ascendant";
|
||||
break;
|
||||
case RankType.immortal:
|
||||
rankType = "Immortal";
|
||||
break;
|
||||
case RankType.radiant:
|
||||
rankType = "Radiant";
|
||||
break;
|
||||
}
|
||||
|
||||
switch (rank[1]) {
|
||||
case RankLevel.one:
|
||||
rankLevel = " 1";
|
||||
break;
|
||||
case RankLevel.two:
|
||||
rankLevel = " 2";
|
||||
break;
|
||||
case RankLevel.three:
|
||||
rankLevel = " 3";
|
||||
break;
|
||||
}
|
||||
|
||||
return rankType + rankLevel;
|
||||
}
|
||||
|
||||
// Map server preference to string
|
||||
public static serverPreferenceToString(
|
||||
serverPref: ServerPreference,
|
||||
longVersion: boolean = false
|
||||
) {
|
||||
switch (serverPref) {
|
||||
case ServerPreference.na:
|
||||
return longVersion ? "N. America" : "NA";
|
||||
case ServerPreference.eu:
|
||||
return longVersion ? "Europe" : "EU";
|
||||
case ServerPreference.ap:
|
||||
return longVersion ? "Asia Pacific" : "AP";
|
||||
case ServerPreference.kr:
|
||||
return longVersion ? "Korea" : "KR";
|
||||
}
|
||||
}
|
||||
|
||||
// Map gender to string
|
||||
public static genderToString(gender: Gender, longVersion: boolean = false) {
|
||||
switch (gender) {
|
||||
case Gender.woman:
|
||||
return longVersion ? "Woman" : "W";
|
||||
case Gender.man:
|
||||
return longVersion ? "Man" : "M";
|
||||
case Gender.nonBinary:
|
||||
return longVersion ? "Non-Binary" : "NB";
|
||||
}
|
||||
}
|
||||
|
||||
// Get a specific icon
|
||||
public static getAgentIcon(idx: number, randomly: boolean = false) {
|
||||
if (!randomly) {
|
||||
if (idx < 0 || idx >= Micellaneous.agentIcons.length) idx = 0;
|
||||
return Micellaneous.agentIcons[idx];
|
||||
} else {
|
||||
idx = Math.floor(Math.random() * (Micellaneous.agentIcons.length - 1));
|
||||
return Micellaneous.agentIcons[idx];
|
||||
}
|
||||
}
|
||||
|
||||
public static getBackgroundAgents(iconSrc: string) {
|
||||
const bgAgent1: number = Micellaneous.agentIcons.indexOf(iconSrc);
|
||||
if (bgAgent1 !== -1) {
|
||||
let bgAgent2: number = bgAgent1;
|
||||
while (bgAgent1 === bgAgent2) {
|
||||
bgAgent2 = Math.floor(Math.random() * 20);
|
||||
}
|
||||
const bgAgents = [
|
||||
Micellaneous.backgroundAgents[bgAgent1],
|
||||
Micellaneous.backgroundAgents[bgAgent2],
|
||||
];
|
||||
return bgAgents;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static playerTypeToString(playerType: GameMode) {
|
||||
if (playerType === GameMode.casual) return "Casual";
|
||||
else return "Competitive";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user