Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
strange969 committed Mar 14, 2023
0 parents commit e0fe935
Show file tree
Hide file tree
Showing 26 changed files with 3,297 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
api/.env
client/.env
.idea
api/node_modules
client/node_modules
api/uploads/*
24 changes: 24 additions & 0 deletions client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
13 changes: 13 additions & 0 deletions client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"autoprefixer": "^10.4.13",
"axios": "^1.3.2",
"lodash": "^4.17.21",
"postcss": "^8.4.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.2.4"
},
"devDependencies": {
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@vitejs/plugin-react": "^3.1.0",
"vite": "^4.1.0"
}
}
6 changes: 6 additions & 0 deletions client/postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
1 change: 1 addition & 0 deletions client/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import axios from "axios";
import {UserContextProvider} from "./UserContext";
import Routes from "./Routes";

function App() {
axios.defaults.baseURL = 'http://localhost:4040';
axios.defaults.withCredentials = true;
return (
<UserContextProvider>
<Routes />
</UserContextProvider>
)
}

export default App
20 changes: 20 additions & 0 deletions client/src/Avatar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default function Avatar({userId,username,online}) {
const colors = ['bg-teal-200', 'bg-red-200',
'bg-green-200', 'bg-purple-200',
'bg-blue-200', 'bg-yellow-200',
'bg-orange-200', 'bg-pink-200', 'bg-fuchsia-200', 'bg-rose-200'];
const userIdBase10 = parseInt(userId.substring(10), 16);
const colorIndex = userIdBase10 % colors.length;
const color = colors[colorIndex];
return (
<div className={"w-8 h-8 relative rounded-full flex items-center "+color}>
<div className="text-center w-full opacity-70">{username[0]}</div>
{online && (
<div className="absolute w-3 h-3 bg-green-400 bottom-0 right-0 rounded-full border border-white"></div>
)}
{!online && (
<div className="absolute w-3 h-3 bg-gray-400 bottom-0 right-0 rounded-full border border-white"></div>
)}
</div>
);
}
213 changes: 213 additions & 0 deletions client/src/Chat.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import {useContext, useEffect, useRef, useState} from "react";
import Avatar from "./Avatar";
import Logo from "./Logo";
import {UserContext} from "./UserContext.jsx";
import {uniqBy} from "lodash";
import axios from "axios";
import Contact from "./Contact";

export default function Chat() {
const [ws,setWs] = useState(null);
const [onlinePeople,setOnlinePeople] = useState({});
const [offlinePeople,setOfflinePeople] = useState({});
const [selectedUserId,setSelectedUserId] = useState(null);
const [newMessageText,setNewMessageText] = useState('');
const [messages,setMessages] = useState([]);
const {username,id,setId,setUsername} = useContext(UserContext);
const divUnderMessages = useRef();
useEffect(() => {
connectToWs();
}, [selectedUserId]);
function connectToWs() {
const ws = new WebSocket('ws://localhost:4040');
setWs(ws);
ws.addEventListener('message', handleMessage);
ws.addEventListener('close', () => {
setTimeout(() => {
console.log('Disconnected. Trying to reconnect.');
connectToWs();
}, 1000);
});
}
function showOnlinePeople(peopleArray) {
const people = {};
peopleArray.forEach(({userId,username}) => {
people[userId] = username;
});
setOnlinePeople(people);
}
function handleMessage(ev) {
const messageData = JSON.parse(ev.data);
console.log({ev,messageData});
if ('online' in messageData) {
showOnlinePeople(messageData.online);
} else if ('text' in messageData) {
if (messageData.sender === selectedUserId) {
setMessages(prev => ([...prev, {...messageData}]));
}
}
}
function logout() {
axios.post('/logout').then(() => {
setWs(null);
setId(null);
setUsername(null);
});
}
function sendMessage(ev, file = null) {
if (ev) ev.preventDefault();
ws.send(JSON.stringify({
recipient: selectedUserId,
text: newMessageText,
file,
}));
if (file) {
axios.get('/messages/'+selectedUserId).then(res => {
setMessages(res.data);
});
} else {
setNewMessageText('');
setMessages(prev => ([...prev,{
text: newMessageText,
sender: id,
recipient: selectedUserId,
_id: Date.now(),
}]));
}
}
function sendFile(ev) {
const reader = new FileReader();
reader.readAsDataURL(ev.target.files[0]);
reader.onload = () => {
sendMessage(null, {
name: ev.target.files[0].name,
data: reader.result,
});
};
}

useEffect(() => {
const div = divUnderMessages.current;
if (div) {
div.scrollIntoView({behavior:'smooth', block:'end'});
}
}, [messages]);

useEffect(() => {
axios.get('/people').then(res => {
const offlinePeopleArr = res.data
.filter(p => p._id !== id)
.filter(p => !Object.keys(onlinePeople).includes(p._id));
const offlinePeople = {};
offlinePeopleArr.forEach(p => {
offlinePeople[p._id] = p;
});
setOfflinePeople(offlinePeople);
});
}, [onlinePeople]);

useEffect(() => {
if (selectedUserId) {
axios.get('/messages/'+selectedUserId).then(res => {
setMessages(res.data);
});
}
}, [selectedUserId]);

const onlinePeopleExclOurUser = {...onlinePeople};
delete onlinePeopleExclOurUser[id];

const messagesWithoutDupes = uniqBy(messages, '_id');

return (
<div className="flex h-screen">
<div className="bg-white w-1/3 flex flex-col">
<div className="flex-grow">
<Logo />
{Object.keys(onlinePeopleExclOurUser).map(userId => (
<Contact
key={userId}
id={userId}
online={true}
username={onlinePeopleExclOurUser[userId]}
onClick={() => {setSelectedUserId(userId);console.log({userId})}}
selected={userId === selectedUserId} />
))}
{Object.keys(offlinePeople).map(userId => (
<Contact
key={userId}
id={userId}
online={false}
username={offlinePeople[userId].username}
onClick={() => setSelectedUserId(userId)}
selected={userId === selectedUserId} />
))}
</div>
<div className="p-2 text-center flex items-center justify-center">
<span className="mr-2 text-sm text-gray-600 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-4 h-4">
<path fillRule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clipRule="evenodd" />
</svg>
{username}
</span>
<button
onClick={logout}
className="text-sm bg-blue-100 py-1 px-2 text-gray-500 border rounded-sm">logout</button>
</div>
</div>
<div className="flex flex-col bg-blue-50 w-2/3 p-2">
<div className="flex-grow">
{!selectedUserId && (
<div className="flex h-full flex-grow items-center justify-center">
<div className="text-gray-300">&larr; Select a person from the sidebar</div>
</div>
)}
{!!selectedUserId && (
<div className="relative h-full">
<div className="overflow-y-scroll absolute top-0 left-0 right-0 bottom-2">
{messagesWithoutDupes.map(message => (
<div key={message._id} className={(message.sender === id ? 'text-right': 'text-left')}>
<div className={"text-left inline-block p-2 my-2 rounded-md text-sm " +(message.sender === id ? 'bg-blue-500 text-white':'bg-white text-gray-500')}>
{message.text}
{message.file && (
<div className="">
<a target="_blank" className="flex items-center gap-1 border-b" href={axios.defaults.baseURL + '/uploads/' + message.file}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-4 h-4">
<path fillRule="evenodd" d="M18.97 3.659a2.25 2.25 0 00-3.182 0l-10.94 10.94a3.75 3.75 0 105.304 5.303l7.693-7.693a.75.75 0 011.06 1.06l-7.693 7.693a5.25 5.25 0 11-7.424-7.424l10.939-10.94a3.75 3.75 0 115.303 5.304L9.097 18.835l-.008.008-.007.007-.002.002-.003.002A2.25 2.25 0 015.91 15.66l7.81-7.81a.75.75 0 011.061 1.06l-7.81 7.81a.75.75 0 001.054 1.068L18.97 6.84a2.25 2.25 0 000-3.182z" clipRule="evenodd" />
</svg>
{message.file}
</a>
</div>
)}
</div>
</div>
))}
<div ref={divUnderMessages}></div>
</div>
</div>
)}
</div>
{!!selectedUserId && (
<form className="flex gap-2" onSubmit={sendMessage}>
<input type="text"
value={newMessageText}
onChange={ev => setNewMessageText(ev.target.value)}
placeholder="Type your message here"
className="bg-white flex-grow border rounded-sm p-2"/>
<label className="bg-blue-200 p-2 text-gray-600 cursor-pointer rounded-sm border border-blue-200">
<input type="file" className="hidden" onChange={sendFile} />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6">
<path fillRule="evenodd" d="M18.97 3.659a2.25 2.25 0 00-3.182 0l-10.94 10.94a3.75 3.75 0 105.304 5.303l7.693-7.693a.75.75 0 011.06 1.06l-7.693 7.693a5.25 5.25 0 11-7.424-7.424l10.939-10.94a3.75 3.75 0 115.303 5.304L9.097 18.835l-.008.008-.007.007-.002.002-.003.002A2.25 2.25 0 015.91 15.66l7.81-7.81a.75.75 0 011.061 1.06l-7.81 7.81a.75.75 0 001.054 1.068L18.97 6.84a2.25 2.25 0 000-3.182z" clipRule="evenodd" />
</svg>
</label>
<button type="submit" className="bg-blue-500 p-2 text-white rounded-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
</svg>
</button>
</form>
)}
</div>
</div>
);
}
16 changes: 16 additions & 0 deletions client/src/Contact.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Avatar from "./Avatar.jsx";

export default function Contact({id,username,onClick,selected,online}) {
return (
<div key={id} onClick={() => onClick(id)}
className={"border-b border-gray-100 flex items-center gap-2 cursor-pointer "+(selected ? 'bg-blue-50' : '')}>
{selected && (
<div className="w-1 bg-blue-500 h-12 rounded-r-md"></div>
)}
<div className="flex gap-2 py-2 pl-4 items-center">
<Avatar online={online} username={username} userId={id} />
<span className="text-gray-800">{username}</span>
</div>
</div>
);
}
11 changes: 11 additions & 0 deletions client/src/Logo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function Logo() {
return (
<div className="text-blue-600 font-bold flex gap-2 p-4">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6">
<path d="M4.913 2.658c2.075-.27 4.19-.408 6.337-.408 2.147 0 4.262.139 6.337.408 1.922.25 3.291 1.861 3.405 3.727a4.403 4.403 0 00-1.032-.211 50.89 50.89 0 00-8.42 0c-2.358.196-4.04 2.19-4.04 4.434v4.286a4.47 4.47 0 002.433 3.984L7.28 21.53A.75.75 0 016 21v-4.03a48.527 48.527 0 01-1.087-.128C2.905 16.58 1.5 14.833 1.5 12.862V6.638c0-1.97 1.405-3.718 3.413-3.979z" />
<path d="M15.75 7.5c-1.376 0-2.739.057-4.086.169C10.124 7.797 9 9.103 9 10.609v4.285c0 1.507 1.128 2.814 2.67 2.94 1.243.102 2.5.157 3.768.165l2.782 2.781a.75.75 0 001.28-.53v-2.39l.33-.026c1.542-.125 2.67-1.433 2.67-2.94v-4.286c0-1.505-1.125-2.811-2.664-2.94A49.392 49.392 0 0015.75 7.5z" />
</svg>
MernChat
</div>
);
}
Loading

0 comments on commit e0fe935

Please sign in to comment.