This project is created for educational purposes, to get a deeper understanding of how JavaScript frontend frameworks function and how they are implemented under the hood.
It's live and available at messenger-app.artemplv.dev.
QuickChat AI is a messaging application with real-time group chats and an AI chatbot inside.
It's a minimum-dependency single-page application using no framework on the frontend. Component's lifecycle and frontend routing are implemented using TypeScript
.
Handlebars
library is used for HTML templates and Sass
preprocessor is used for styling.
Express.js
is used to implement the API server. Data is stored and manipulated with MongoDB
and Mongoose
.
Cloudinary services are utilized to handle media attachments.
The backend runs in Node.js
environment, the source code is at quickchat-api.
The AI chatbot is implemented using OpenAI API
.
Jest
and Sinon
libraries are used to test components, router, and network requests.
Users can communicate with a chatbot that is created using OpenAI API. In those chats, context is also used, so users can have a comprehensive conversation experience. Users are able to reset the context.
Note: Chatbot "lives" in the app as another user and can be invited to any chat by its username.
Users can send pictures from their devices.
The app is seamlessly accessible and user-friendly on smartphones.
Users can sign up for an account that is secured with password encryption.
![user sign in view](https://private-user-images.githubusercontent.com/48654322/266468417-f2e0ced4-f266-494b-9d0d-990f1c5d9e93.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjU0MDEyNzAsIm5iZiI6MTcyNTQwMDk3MCwicGF0aCI6Ii80ODY1NDMyMi8yNjY0Njg0MTctZjJlMGNlZDQtZjI2Ni00OTRiLTlkMGQtOTkwZjFjNWQ5ZTkzLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA5MDMlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwOTAzVDIyMDI1MFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTY4NTgxMjcwYjA2YzMzNzkwNmM4YjFjYzg2MjM4NmQwNTcyZWU2MTc0NmYxY2M2ZmQ2YWE4MjA5NDIzNzNkN2ImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.zQ3P4jDzUMJ1bmZZKC-jB2raxDQHWI_JXC3wY9ygRXA)
Here is how the event bus
pattern is implemented. It is then utilized to create a component's lifecycle.
class EventBus {
listeners: { [key: string]: Function[] };
constructor() {
this.listeners = {};
}
on(event: string, callback: Function): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
off(event: string, callback: Function): void {
if (!this.listeners[event]) {
throw new Error(`Missing event: ${event}`);
}
this.listeners[event] = this.listeners[event].filter(
(listener) => listener !== callback,
);
}
emit(event: string, ...args: any): void {
if (!this.listeners[event]) {
throw new Error(`Missing event: ${event}`);
}
this.listeners[event].forEach((listener) => {
listener(...args);
});
}
}
Block
is a base class from which all components inherit. It's similar to React's Component class.
Here is an example of how some lifecycle methods are implemented.
class Block {
static EVENTS = {
INIT: 'init',
FLOW_CDM: 'flow:component-did-mount',
FLOW_RENDER: 'flow:render',
FLOW_CDR: 'flow:component-did-render',
FLOW_CDU: 'flow:component-did-update',
};
...
private _componentDidMount(): void {
this.componentDidMount();
this.eventBus().emit(Block.EVENTS.FLOW_RENDER);
}
private _componentDidRender(): void {
this.componentDidRender();
}
private _componentDidUpdate(): void {
if (this.componentDidUpdate()) {
this.eventBus().emit(Block.EVENTS.FLOW_RENDER);
}
}
private _render() {
const block = this.render();
this.element.innerHTML = block;
this.eventBus().emit(Block.EVENTS.FLOW_CDR);
}
...
This is an example of how lifecycle methods are used in components. Here's what happens:
- After
ChatsPage
component is mounted, it fetches chats information and establishes a websocket connection with a server. - After the component is rendered, event listeners are attached to specific elements.
- When the component's props change, the
componentDidUpdate
method is triggered. In this case, it executes some logic when a user switches chats. It returnstrue
to trigger the component to rerender after an update.
class ChatsPage extends Block {
...
componentDidMount() {
this.getChats();
this.addSocketConnection();
window.addEventListener('sessionStorageUpdate', this.onLoginLogout());
}
componentDidRender() {
this.addListeners();
}
componentDidUpdate() {
if (this._state.chatId !== this.props.chatId) {
this._state.chatId = this.props.chatId;
this.getChat();
this._socket.send({
chatId: this._state.chatId,
content: '0',
type: 'get old',
});
}
return true;
}
...
A custom client router is implemented in the app. For example, this is how route changes and redirections are handled.
_onRoute(pathname: string) {
if (pathname === '/') {
return this.go('/chats');
}
const route = this.getRoute(pathname);
if (!route) {
return this.go('/404');
}
if (route.withAuth && !sessionStorage.getItem('token')) {
return this.go('/login');
}
if (this._currentRoute) {
this._currentRoute.leave();
}
this._currentRoute = route;
route.render();
}