Add files via upload

This commit is contained in:
Richard Gingrich
2024-08-06 15:55:00 -06:00
committed by GitHub
parent 5ca105d4f2
commit 4edbe1190a
46 changed files with 35418 additions and 0 deletions

75
ui/README.md Normal file
View File

@@ -0,0 +1,75 @@
# SENG 513 - UI / API
The following System Requirements and Installation steps are required for running the project locally. <br>
If you are accessing the project remotely ([vivideradicator.ca](http://www.vivideradicator.ca)), you may skip to the Usage section of this document. <br>
## System Requirements
Ensure that the following are already installed on your operating system of choice. <br>
> Node JS <br>
> NPM <br>
> MongoDB
## Installation
User Interface
1. Clone or download the code found within the GitHub SENG513-UI repository, to a path/of/your/choosing <br>
1b. This can be done from within VS Code, via `git clone https://github.com/Rafael1321/SENG513-UI.git` <br>
2. Using the command prompt, `cd ~path/of/your/choosing/SENG513-UI` will navigate you to the proper directory <br>
3. Install all required project dependencies using `npm i` <br>
4. Run the application with `npm start` <br>
</br>
API <br>
Note that you must be running MongoDB before beginning this step. <br>
Instructions can be found at https://www.mongodb.com/docs/manual/installation/ <br>
1. Clone or download the code found within the GitHub SENG513-API repository, to a path/of/your/choosing <br>
1b. This can be done from within VS Code, via `git clone https://github.com/Rafael1321/SENG513-API.git` <br>
2. Using the command prompt, `cd ~path/of/your/choosing/SENG513-API` will navigate you to the proper directory <br>
3. Install all required project dependencies using `npm i` <br>
4. Run the application with `npm start` <br>
## Usage
The following steps can be used to experience the full functionality of the application. </br>
1. You will be greeted by the login/account registration page <br>
1b. If you do not yet have an account on DuoFinder, feel free to create one now. The 2 starter accounts below may also be used <br>
DO NOT use real password information here, it is not yet encrypted <br>
1c. Profile 1: email = vivideradicator@gmail.com password = seng513grading <br>
1d. Profile 2: email = me@vivideradicator.ca password = seng513grading <br>
2. Enter your email address and passwords into their respective fields <br>
3. Once logged in, the profile card can be seen. Clicking the edit button in the top right will enable customizing of your profile <br>
3b. You may change your profile picture, bio, gender, and age. Customize your profile as desired, and save the changes by pressing the same edit button again <br>
3c. Set your desired filters by pushing the 'Chat Filters' button, which will limit who you can match with based on your wishes <br>
4. To connect with another user, press the 'Find Duo' button <br>
4b. If you are wishing to test the functionality by yourself, simply log into another account on a seperate window and search for a match on both accounts <br>
5. Once connected, you may send messages in real-time to the other user. Pushing the 'Share Contact' button will automatically send your in-game information to the other user <br>
5b. Move onto another connection with the "Next" button, or return to your profile page by clicking the 'x' in the top-right corner <br>
6. Once back on your profile page, view your past matches with the 'Chat History' button <br>
6b. You may re-read past messages and give a rating to anyone you have talked to previously. To give a rating, press the top-right 'Rate Player' button <br>
7. Enjoy!
## Version History
*v1.0* <br>
Front-end only. Focus on creating a visually appealing and easy-to-use UI. <br>
Use-state-based system, where changes were not saved and lost upon refreshing/exiting the app. <br>
<br>
*v2.0* </br>
Introduced save states and a backend. User information now saved. <br>
*v2.1* </br>
Real-time data share between users made possible with sockets. <br>
<br>
*v3.0* <br>
Polishing of the app, squashing of several major bugs. <br>
Small UI adjustments, ensuring all systems work as they should. <br>
<br>
## Contributors and Credit
Gaganjot Brar <br>
Harkamal Randhawa <br>
Martha Ibarra <br>
Rafael Flores Souza <br>
Richard Gingrich <br>
Tyler Chen

29826
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

60
ui/package.json Normal file
View File

@@ -0,0 +1,60 @@
{
"name": "seng513-ui",
"version": "1.0.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@mui/material": "^5.10.17",
"axios": "^1.2.0",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-styled-components": "^2.0.7",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"embla-carousel": "^7.0.5",
"embla-carousel-autoplay": "^7.0.5",
"embla-carousel-react": "^7.0.5",
"env-cmd": "^10.1.0",
"@mui/styled-engine-sc": "^5.10.16",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"react-toastify": "^9.1.1",
"socket.io-client": "^4.5.4",
"styled-components": "^5.3.6",
"usehooks-ts": "^2.6.0"
},
"scripts": {
"magic": "webpack",
"start": "env-cmd -f ./.env react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@types/react-router-dom": "^5.3.3",
"@types/styled-components": "^5.1.26",
"ts-loader": "^9.3.1",
"webpack-cli": "^4.10.0"
}
}

35
ui/src/App.tsx Normal file
View 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;

View 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;
}
`;

View 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;

View 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,
};
}

View 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;

View 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;
`;

View 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;
}
`;

View 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%;
}
`;

View 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``;

View 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;
}

View 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">&#63;</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;
}
`;

View 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}>&#10005; 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}>&#10005;</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.");
}

View 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;
}
`;

View 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;
}
`;

View 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;
}
`;

View 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;

View 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"/>
}

View 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);
}
`;

View 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;
}
}
`;

View 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});
`;

View 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;
}
`;

View 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;
}
`);

View 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;
}
`;

View 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;
}
`;

View 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>
};

View 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>
};

View 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>
};

View 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
View 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>
);

View 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;
}

View 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;
}

View File

@@ -0,0 +1,5 @@
export interface ISaveCommendDTO{
commenderId : string,
commendedId : string,
score : number
}

View 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
}

View 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};
}
}
}

View 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};
}
}
}

View 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};
}
}
}

View 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};
}
}
}

View 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
View 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
View 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
View 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";
}
}

13
ui/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true
}
}

21
ui/webpack.config.js Normal file
View File

@@ -0,0 +1,21 @@
const path = require('path');
module.exports = {
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: ["styled-components"]
};