├── pic1.png ├── pic2.png ├── app ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── postcss.config.mjs ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── next.config.ts ├── eslint.config.mjs ├── .gitignore ├── package.json ├── tsconfig.json ├── README.md └── .github └── workflows └── nextjs.yml /pic1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HamiParsa/My-Messenger/HEAD/pic1.png -------------------------------------------------------------------------------- /pic2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HamiParsa/My-Messenger/HEAD/pic2.png -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HamiParsa/My-Messenger/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | output: 'export', // export as static files 5 | trailingSlash: true, // ensures pages like /about/ exist 6 | images: { 7 | unoptimized: true, // disable Next.js Image Optimization 8 | }, 9 | }; 10 | 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, globalIgnores } from "eslint/config"; 2 | import nextVitals from "eslint-config-next/core-web-vitals"; 3 | import nextTs from "eslint-config-next/typescript"; 4 | 5 | const eslintConfig = defineConfig([ 6 | ...nextVitals, 7 | ...nextTs, 8 | // Override default ignores of eslint-config-next. 9 | globalIgnores([ 10 | // Default ignores of eslint-config-next: 11 | ".next/**", 12 | "out/**", 13 | "build/**", 14 | "next-env.d.ts", 15 | ]), 16 | ]); 17 | 18 | export default eslintConfig; 19 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: var(--font-geist-sans); 12 | --font-mono: var(--font-geist-mono); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #0a0a0a; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | background: var(--background); 24 | color: var(--foreground); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "telegram", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "eslint" 10 | }, 11 | "dependencies": { 12 | "next": "16.0.0", 13 | "react": "19.2.0", 14 | "react-dom": "19.2.0", 15 | "react-icons": "^5.5.0" 16 | }, 17 | "devDependencies": { 18 | "@tailwindcss/postcss": "^4", 19 | "@types/node": "^20", 20 | "@types/react": "^19", 21 | "@types/react-dom": "^19", 22 | "eslint": "^9", 23 | "eslint-config-next": "16.0.0", 24 | "tailwindcss": "^4", 25 | "typescript": "^5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "MyMessenger", 17 | description: "Generated by create next app", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "react-jsx", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "./*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts", 36 | ".next/dev/types/**/*.ts", 37 | "**/*.mts", 38 | ".next\\dev/types/**/*.ts", 39 | ".next\\dev/types/**/*.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

💬 MyMessenger - Frontend Demo

3 |

Explore the clean and interactive frontend of a messaging app with dark mode, responsive layout, and persistent local storage using IndexedDB. Built with Next.js, React, and TypeScript.

4 | 5 | 6 | 7 |
8 | 9 | --- 10 | 11 | ## 🔥 About MyMessenger 12 | 13 | **MyMessenger** is a sleek, frontend-only messaging app that uses **Next.js**, **React**, and **TypeScript** for a modern chat experience. 14 | With features like message persistence, image attachments, emoji picker, and typing indicator, this demo simulates a real-time messaging experience entirely on the client-side. 15 | 16 | The app utilizes **IndexedDB** for local message and image storage, so your messages persist even after page reloads. 17 | 18 | --- 19 | 20 | ## 🚀 Features 21 | 22 | 32 | 33 | --- 34 | 35 | ## 🧠 Tech Stack 36 | 37 | | Technology | Purpose | 38 | |------------|---------| 39 | | ⚛️ **React** | Dynamic UI and chat logic | 40 | | 🧭 **Next.js** | File-based routing & optimization | 41 | | 🟦 **TypeScript** | Type safety and structured code | 42 | | 🎨 **Tailwind CSS** | Styling and responsive design | 43 | | 🗂️ **IndexedDB** | Persistent local storage for messages and images | 44 | | 😎 **react-icons** | Small, clean icons for UI elements | 45 | | 💬 **Next.js Client Component** | Entire app built as a client-side component | 46 | 47 | --- 48 | 49 | ## 💡 Description 50 | 51 | Users can: 52 | 53 | - Switch between contacts and view individual chat windows 🖱️ 54 | - Send text messages and upload images with previews 🖼️ 55 | - All chat history, including images, is stored in the browser using **IndexedDB** for persistence 🔄 56 | - Use the built-in **emoji picker** to enhance messages 😎 57 | - See a **typing indicator** when the user is typing ⏳ 58 | - Interact with the UI in a smooth, responsive experience 💬 59 | 60 | This project demonstrates **modern front-end techniques** with persistent local storage, UI interactivity, and responsive design. 61 | 62 | --- 63 | 64 | ## 🖼️ Project Preview 65 | 66 |
67 | 68 |

69 | 70 |
71 | 72 | --- 73 | 74 | ## 👨‍💻 Author 75 | 76 | **Developed by:** [Your Name](https://github.com/hamiparsa) 77 | 💬 Front-End Developer | Building interactive, immersive web experiences 78 | 79 | --- 80 | 81 |
82 | 83 |
84 | 85 | -------------------------------------------------------------------------------- /.github/workflows/nextjs.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Next.js site to GitHub Pages 2 | # 3 | # To get started with Next.js see: https://nextjs.org/docs/getting-started 4 | # 5 | name: Deploy Next.js site to Pages 6 | 7 | on: 8 | # Runs on pushes targeting the default branch 9 | push: 10 | branches: ["main"] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 22 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 23 | concurrency: 24 | group: "pages" 25 | cancel-in-progress: false 26 | 27 | jobs: 28 | # Build job 29 | build: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Detect package manager 35 | id: detect-package-manager 36 | run: | 37 | if [ -f "${{ github.workspace }}/yarn.lock" ]; then 38 | echo "manager=yarn" >> $GITHUB_OUTPUT 39 | echo "command=install" >> $GITHUB_OUTPUT 40 | echo "runner=yarn" >> $GITHUB_OUTPUT 41 | exit 0 42 | elif [ -f "${{ github.workspace }}/package.json" ]; then 43 | echo "manager=npm" >> $GITHUB_OUTPUT 44 | echo "command=ci" >> $GITHUB_OUTPUT 45 | echo "runner=npx --no-install" >> $GITHUB_OUTPUT 46 | exit 0 47 | else 48 | echo "Unable to determine package manager" 49 | exit 1 50 | fi 51 | - name: Setup Node 52 | uses: actions/setup-node@v4 53 | with: 54 | node-version: "20" 55 | cache: ${{ steps.detect-package-manager.outputs.manager }} 56 | - name: Setup Pages 57 | uses: actions/configure-pages@v5 58 | with: 59 | # Automatically inject basePath in your Next.js configuration file and disable 60 | # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized). 61 | # 62 | # You may remove this line if you want to manage the configuration yourself. 63 | static_site_generator: next 64 | - name: Restore cache 65 | uses: actions/cache@v4 66 | with: 67 | path: | 68 | .next/cache 69 | # Generate a new cache whenever packages or source files change. 70 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} 71 | # If source files changed but packages didn't, rebuild from a prior cache. 72 | restore-keys: | 73 | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- 74 | - name: Install dependencies 75 | run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} 76 | - name: Build with Next.js 77 | run: ${{ steps.detect-package-manager.outputs.runner }} next build 78 | - name: Upload artifact 79 | uses: actions/upload-pages-artifact@v3 80 | with: 81 | path: ./out 82 | 83 | # Deployment job 84 | deploy: 85 | environment: 86 | name: github-pages 87 | url: ${{ steps.deployment.outputs.page_url }} 88 | runs-on: ubuntu-latest 89 | needs: build 90 | steps: 91 | - name: Deploy to GitHub Pages 92 | id: deployment 93 | uses: actions/deploy-pages@v4 94 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useEffect, useRef } from "react"; 4 | import { MdSend, MdArrowBack } from "react-icons/md"; 5 | import { FaSmile, FaPaperclip } from "react-icons/fa"; 6 | import Image from "next/image"; 7 | import { TbMessageChatbotFilled } from "react-icons/tb"; 8 | 9 | // ---------- Types ---------- 10 | type Message = { id: string; fromMe: boolean; text?: string; fileDataUrl?: string; time: string }; 11 | type Contact = { id: number; name: string; avatar: string; online: boolean; messages: Message[] }; 12 | 13 | // ---------- Helpers ---------- 14 | const formatTime = () => { 15 | const d = new Date(); 16 | return `${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}`; 17 | }; 18 | 19 | // ---------- IndexedDB ---------- 20 | const DB_NAME = "chatAppDB"; 21 | const STORE_NAME = "contacts"; 22 | 23 | function openDB() { 24 | return new Promise((resolve, reject) => { 25 | const request = indexedDB.open(DB_NAME, 1); 26 | request.onupgradeneeded = () => { 27 | const db = request.result; 28 | if (!db.objectStoreNames.contains(STORE_NAME)) { 29 | db.createObjectStore(STORE_NAME, { keyPath: "id" }); 30 | } 31 | }; 32 | request.onsuccess = () => resolve(request.result); 33 | request.onerror = () => reject(request.error); 34 | }); 35 | } 36 | 37 | async function saveContactsToDB(contacts: Contact[]) { 38 | const db = await openDB(); 39 | const tx = db.transaction(STORE_NAME, "readwrite"); 40 | const store = tx.objectStore(STORE_NAME); 41 | contacts.forEach(c => store.put(c)); 42 | await new Promise((resolve, reject) => { 43 | tx.oncomplete = () => resolve(); 44 | tx.onerror = () => reject(tx.error); 45 | }); 46 | } 47 | 48 | async function loadContactsFromDB(): Promise { 49 | const db = await openDB(); 50 | const tx = db.transaction(STORE_NAME, "readonly"); 51 | const store = tx.objectStore(STORE_NAME); 52 | return new Promise((resolve, reject) => { 53 | const request = store.getAll(); 54 | request.onsuccess = () => resolve(request.result as Contact[]); 55 | request.onerror = () => reject(request.error); 56 | }); 57 | } 58 | 59 | // ---------- Main ---------- 60 | export default function FullChatApp() { 61 | const [activeContactId, setActiveContactId] = useState(null); 62 | const [contacts, setContacts] = useState([]); 63 | const [input, setInput] = useState(""); 64 | const [files, setFiles] = useState([]); 65 | const [showEmoji, setShowEmoji] = useState(false); 66 | const messagesEndRef = useRef(null); 67 | 68 | useEffect(() => { 69 | (async () => { 70 | const saved = await loadContactsFromDB(); 71 | if (saved.length > 0) setContacts(saved); 72 | else { 73 | setContacts([ 74 | { id: 1, name: "Alice", avatar: "https://i.pravatar.cc/150?img=1", online: true, messages: [] }, 75 | { id: 2, name: "Bob", avatar: "https://i.pravatar.cc/150?img=2", online: false, messages: [] }, 76 | { id: 3, name: "Charlie", avatar: "https://i.pravatar.cc/150?img=3", online: true, messages: [] }, 77 | ]); 78 | } 79 | })(); 80 | }, []); 81 | 82 | useEffect(() => { 83 | messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); 84 | }, [activeContactId, contacts]); 85 | 86 | const activeContact = contacts.find(c => c.id === activeContactId); 87 | 88 | const sendMessage = async () => { 89 | if ((!input.trim() && files.length === 0) || activeContactId === null) return; 90 | const now = formatTime(); 91 | const newMessages: Message[] = []; 92 | 93 | if (input.trim()) 94 | newMessages.push({ id: String(Date.now()), fromMe: true, text: input, time: now }); 95 | 96 | for (const f of files) { 97 | if (!f.type.startsWith("image/")) continue; 98 | const dataUrl = await new Promise((resolve) => { 99 | const reader = new FileReader(); 100 | reader.onload = () => resolve(reader.result as string); 101 | reader.readAsDataURL(f); 102 | }); 103 | newMessages.push({ id: String(Date.now() + Math.random()), fromMe: true, fileDataUrl: dataUrl, time: now }); 104 | } 105 | 106 | const updated = contacts.map(c => 107 | c.id === activeContactId ? { ...c, messages: [...c.messages, ...newMessages] } : c 108 | ); 109 | setContacts(updated); 110 | setInput(""); 111 | setFiles([]); 112 | setShowEmoji(false); 113 | await saveContactsToDB(updated); 114 | 115 | // auto reply 116 | setTimeout(async () => { 117 | const reply: Message = { 118 | id: String(Date.now() + 1000), 119 | fromMe: false, 120 | text: "Received 👍", 121 | time: formatTime(), 122 | }; 123 | const updated2 = updated.map(c => 124 | c.id === activeContactId ? { ...c, messages: [...c.messages, reply] } : c 125 | ); 126 | setContacts(updated2); 127 | await saveContactsToDB(updated2); 128 | }, 1000); 129 | }; 130 | 131 | const insertEmoji = (emoji: string) => setInput(prev => prev + emoji); 132 | 133 | return ( 134 |
135 | {/* Contact List */} 136 | {activeContactId === null && ( 137 |
138 |
139 |
140 | MyMessenger 141 |
142 |
143 |
144 | {contacts.map(c => { 145 | const last = c.messages[c.messages.length - 1]; 146 | return ( 147 |
setActiveContactId(c.id)} 150 | className="flex items-center gap-3 p-4 cursor-pointer hover:bg-white/10 transition-all" 151 | > 152 | {c.name} 153 |
154 |
155 | {c.name} 156 | {last?.time} 157 |
158 |
{last?.text || "No messages yet"}
159 |
160 | {c.online &&
} 161 |
162 | ); 163 | })} 164 |
165 |
166 | )} 167 | 168 | {/* Chat Window */} 169 | {activeContactId !== null && activeContact && ( 170 |
171 | {/* Header */} 172 |
173 | 176 | {activeContact.name} 177 |
178 |
{activeContact.name}
179 |
{activeContact.online ? "online" : "offline"}
180 |
181 |
182 | 183 | {/* Messages */} 184 |
185 | {activeContact.messages.map(m => ( 186 |
190 | {!m.fromMe && ( 191 | {activeContact.name} 192 | )} 193 |
199 | {m.text} 200 | {m.fileDataUrl && ( 201 | sent image 202 | )} 203 |
{m.time}
204 |
205 |
206 | ))} 207 |
208 |
209 | 210 | {/* Input */} 211 |
212 | {showEmoji && ( 213 |
214 | {["😀","😂","😍","👍","🎉","😢","😎","❤️","🔥"].map(e => ( 215 | 216 | ))} 217 |
218 | )} 219 |
220 | 221 | 225 | setInput(e.target.value)} 228 | onKeyDown={e => { if (e.key === "Enter") sendMessage(); }} 229 | placeholder="Type a message..." 230 | className="flex-1 rounded-full px-4 py-2 bg-white/10 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition" 231 | /> 232 | 235 |
236 |
237 |
238 | )} 239 |
240 | ); 241 | } 242 | --------------------------------------------------------------------------------