From ae6332ac83afde90556685b034998f71c291b4b5 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 29 Jan 2026 11:39:36 +0000 Subject: [PATCH] Version 1 --- backend/src/db/db.config.js | 10 +- .../db/seeders/20260129000000-chat-demo.js | 88 +++++++ frontend/src/components/NavBarItem.tsx | 5 +- frontend/src/layouts/Authenticated.tsx | 5 +- frontend/src/menuAside.ts | 8 +- frontend/src/pages/chat.tsx | 214 ++++++++++++++++ frontend/src/pages/index.tsx | 240 +++++++----------- 7 files changed, 409 insertions(+), 161 deletions(-) create mode 100644 backend/src/db/seeders/20260129000000-chat-demo.js create mode 100644 frontend/src/pages/chat.tsx diff --git a/backend/src/db/db.config.js b/backend/src/db/db.config.js index 5a2f718..3b59ba0 100644 --- a/backend/src/db/db.config.js +++ b/backend/src/db/db.config.js @@ -1,5 +1,3 @@ - - module.exports = { production: { dialect: 'postgres', @@ -12,10 +10,10 @@ module.exports = { seederStorage: 'sequelize', }, development: { - username: 'postgres', + username: process.env.DB_USER || 'postgres', dialect: 'postgres', - password: '', - database: 'db_app_draft', + password: process.env.DB_PASS || '', + database: process.env.DB_NAME || 'db_app_draft', host: process.env.DB_HOST || 'localhost', logging: console.log, seederStorage: 'sequelize', @@ -30,4 +28,4 @@ module.exports = { logging: console.log, seederStorage: 'sequelize', } -}; +}; \ No newline at end of file diff --git a/backend/src/db/seeders/20260129000000-chat-demo.js b/backend/src/db/seeders/20260129000000-chat-demo.js new file mode 100644 index 0000000..38fa9dc --- /dev/null +++ b/backend/src/db/seeders/20260129000000-chat-demo.js @@ -0,0 +1,88 @@ +'use strict'; +const { v4: uuidv4 } = require('uuid'); + +const adminId = '193bf4b5-9f07-4bd5-9a43-e7e41f3e96af'; +const johnId = 'af5a87be-8f9c-4630-902a-37a60b7005ba'; +const clientId = '5bc531ab-611f-41f3-9373-b7cc5d09c93d'; + +const convId1 = uuidv4(); +const convId2 = uuidv4(); + +module.exports = { + up: async (queryInterface, Sequelize) => { + try { + // Create Conversations + await queryInterface.bulkInsert('conversations', [ + { + id: convId1, + title: 'Project Nexus Sync', + is_group: true, + status: 'active', + last_message_preview: 'The video call module is ready for testing!', + createdAt: new Date(), + updatedAt: new Date() + }, + { + id: convId2, + title: 'John Doe', + is_group: false, + status: 'active', + last_message_preview: 'Hey! Did you check the new landing page?', + createdAt: new Date(), + updatedAt: new Date() + } + ]); + + // Create Participants (Many-to-Many through table) + await queryInterface.bulkInsert('conversationsParticipantsUsers', [ + { conversations_participantsId: convId1, users_custom_permissionsId: adminId, createdAt: new Date(), updatedAt: new Date() }, + { conversations_participantsId: convId1, users_custom_permissionsId: johnId, createdAt: new Date(), updatedAt: new Date() }, + { conversations_participantsId: convId1, users_custom_permissionsId: clientId, createdAt: new Date(), updatedAt: new Date() }, + { conversations_participantsId: convId2, users_custom_permissionsId: adminId, createdAt: new Date(), updatedAt: new Date() }, + { conversations_participantsId: convId2, users_custom_permissionsId: johnId, createdAt: new Date(), updatedAt: new Date() } + ]); + + // Create Messages + await queryInterface.bulkInsert('messages', [ + { + id: uuidv4(), + content: 'Hello everyone! Welcome to Nexus Chat.', + conversationId: convId1, + senderId: adminId, + status: 'read', + sent_at: new Date(), + createdAt: new Date(Date.now() - 3600000), + updatedAt: new Date() + }, + { + id: uuidv4(), + content: 'The video call module is ready for testing!', + conversationId: convId1, + senderId: johnId, + status: 'sent', + sent_at: new Date(), + createdAt: new Date(Date.now() - 1800000), + updatedAt: new Date() + }, + { + id: uuidv4(), + content: 'Hey! Did you check the new landing page?', + conversationId: convId2, + senderId: johnId, + status: 'read', + sent_at: new Date(), + createdAt: new Date(Date.now() - 900000), + updatedAt: new Date() + } + ]); + } catch (error) { + console.error('Error during chat demo seeding:', error); + } + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete('messages', null, {}); + await queryInterface.bulkDelete('conversationsParticipantsUsers', null, {}); + await queryInterface.bulkDelete('conversations', null, {}); + } +}; diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..4ced3eb 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, {useEffect, useRef, useState} from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' @@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) { } return
{NavBarItemComponentContents}
-} +} \ No newline at end of file diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..26c3572 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' @@ -126,4 +125,4 @@ export default function LayoutAuthenticated({ ) -} +} \ No newline at end of file diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index f7655d2..32fdee1 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -7,7 +7,11 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiViewDashboardOutline, label: 'Dashboard', }, - + { + href: '/chat', + icon: icon.mdiChatProcessingOutline, + label: 'Nexus Chat', + }, { href: '/users/users-list', label: 'Users', @@ -72,4 +76,4 @@ const menuAside: MenuAsideItem[] = [ }, ] -export default menuAside +export default menuAside \ No newline at end of file diff --git a/frontend/src/pages/chat.tsx b/frontend/src/pages/chat.tsx new file mode 100644 index 0000000..eb54d11 --- /dev/null +++ b/frontend/src/pages/chat.tsx @@ -0,0 +1,214 @@ +import React, { useEffect, useState, useRef } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import LayoutAuthenticated from '../layouts/Authenticated'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import { fetch as fetchConversations } from '../stores/conversations/conversationsSlice'; +import { fetch as fetchMessages, create as createMessage } from '../stores/messages/messagesSlice'; +import { mdiSend, mdiVideo, mdiMagnify, mdiDotsVertical, mdiPaperclip, mdiChatProcessingOutline } from '@mdi/js'; +import BaseIcon from '../components/BaseIcon'; +import moment from 'moment'; + +export default function ChatPage() { + const dispatch = useAppDispatch(); + const { conversations, loading: loadingConversations } = useAppSelector((state) => state.conversations); + const { messages, loading: loadingMessages } = useAppSelector((state) => state.messages); + const { currentUser } = useAppSelector((state) => state.auth); + + const [selectedConversation, setSelectedConversation] = useState(null); + const [messageText, setMessageText] = useState(''); + const messagesEndRef = useRef(null); + + useEffect(() => { + dispatch(fetchConversations({ query: '?limit=100' })); + }, [dispatch]); + + useEffect(() => { + if (selectedConversation) { + dispatch(fetchMessages({ query: `?conversation=${selectedConversation.id}&limit=100` })); + } + }, [selectedConversation, dispatch]); + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + const handleSendMessage = async (e: React.FormEvent) => { + e.preventDefault(); + if (!messageText.trim() || !selectedConversation || !currentUser) return; + + const data = { + content: messageText, + conversation: selectedConversation.id, + sender: currentUser.id, + sent_at: new Date(), + status: 'sent' + }; + + await dispatch(createMessage(data)); + setMessageText(''); + dispatch(fetchMessages({ query: `?conversation=${selectedConversation.id}&limit=100` })); + }; + + return ( +
+ + Nexus Chat | Messages + + + {/* Sidebar: Conversations List */} +
+
+

Messages

+
+ + +
+
+
+ {conversations?.map((conv: any) => ( +
setSelectedConversation(conv)} + className={`p-4 flex items-center space-x-3 cursor-pointer transition-all border-l-4 ${ + selectedConversation?.id === conv.id + ? 'bg-indigo-50 dark:bg-indigo-900/20 border-indigo-600' + : 'hover:bg-gray-50 dark:hover:bg-dark-700 border-transparent' + }`} + > +
+ {conv.title?.charAt(0) || 'C'} +
+
+
+

{conv.title || 'Direct Message'}

+ + {conv.updatedAt ? moment(conv.updatedAt).format('HH:mm') : ''} + +
+

+ {conv.last_message_preview || 'No messages yet...'} +

+
+
+ ))} + {(!conversations || conversations.length === 0) && !loadingConversations && ( +
+ No conversations found. +
+ )} +
+
+ + {/* Main Chat Area */} +
+ {selectedConversation ? ( + <> + {/* Chat Header */} +
+
+
+ {selectedConversation.title?.charAt(0) || 'C'} +
+
+

{selectedConversation.title || 'Conversation'}

+

Online

+
+
+
+ + +
+
+ + {/* Messages List */} +
+ {messages?.map((msg: any) => { + const isMe = msg.senderId === currentUser?.id; + return ( +
+
+
+

{msg.content}

+
+
+ + {moment(msg.createdAt).format('HH:mm')} + + {isMe && ( + Read + )} +
+
+
+ ); + })} + {!loadingMessages && (!messages || messages.length === 0) && ( +
+ +

No messages in this conversation yet.

+
+ )} +
+
+ + {/* Input Area */} +
+
+ +
+ setMessageText(e.target.value)} + placeholder="Type a message..." + className="w-full px-4 py-3 bg-gray-50 dark:bg-dark-800 border border-transparent focus:bg-white dark:focus:bg-dark-700 focus:border-indigo-500 rounded-2xl text-sm outline-none transition-all" + /> +
+ +
+
+ + ) : ( +
+
+ +
+

Select a conversation

+

+ Pick one from the list or start a new conversation to begin chatting. +

+
+ )} +
+
+ ); +} + +ChatPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 2fdd258..bc5cff7 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,112 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; import Link from 'next/link'; -import BaseButton from '../components/BaseButton'; -import CardBox from '../components/CardBox'; -import SectionFullScreen from '../components/SectionFullScreen'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; -import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; - - -export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('image'); - const [contentPosition, setContentPosition] = useState('right'); - const textColor = useAppSelector((state) => state.style.linkColor); - - const title = 'App Draft' - - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); - - const imageBlock = (image) => ( -
- -
- ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- - -
) - } - }; +import { mdiChatProcessingOutline, mdiVideoOutline, mdiShieldLockOutline, mdiArrowRight } from '@mdi/js'; +import BaseIcon from '../components/BaseIcon'; +export default function LandingPage() { return ( -
+
- {getPageTitle('Starter Page')} + {getPageTitle('Nexus Chat - Next Gen Messaging')} - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - - - -
+ {/* Navigation */} +
- -
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
+
+ + Login + + + Get Started + +
+ + {/* Hero Section */} +
+
+

+ Connect beyond
+ + boundaries. + +

+

+ Experience seamless messaging and HD video calls with military-grade security. + The future of communication is here. +

+
+ + Launch Chat + + + + Admin UI + +
+
+ + {/* Features Grid */} +
+
+
+
+ +
+

Instant Messaging

+

+ Real-time message delivery with rich media support, read receipts, and typing indicators. +

+
+
+
+ +
+

HD Video Calls

+

+ Crystal clear video and audio quality with low latency, anywhere in the world. +

+
+
+
+ +
+

Private & Secure

+

+ End-to-end encryption ensures your conversations stay between you and your contacts. +

+
+
+
+ + {/* Footer */} +
+

© 2026 Nexus Chat Inc. Built with passion for connection.

+
); } -Starter.getLayout = function getLayout(page: ReactElement) { +LandingPage.getLayout = function getLayout(page: ReactElement) { return {page}; }; -