├── 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 |
23 | 🎨 Dark Theme — polished dark UI with smooth animations
24 | 📱 Responsive Design — optimized for mobile, tablet, and desktop
25 | 📧 Message Persistence — store messages and images locally in IndexedDB
26 | 🖼️ Image Attachments — upload and preview images before sending
27 | 😎 Emoji Picker — easily insert emojis into messages
28 | ⏳ Typing Indicator — shows typing status while typing a message
29 | 🖱️ Interactive Contact List — easily switch between chats
30 | 🔄 Auto-Reply Simulation — simulate receiving a reply after a delay
31 |
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 |
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 |
setActiveContactId(null)} className="mr-4 text-gray-300 hover:text-white transition">
174 |
175 |
176 |
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 |
192 | )}
193 |
199 | {m.text}
200 | {m.fileDataUrl && (
201 |
202 | )}
203 |
{m.time}
204 |
205 |
206 | ))}
207 |
208 |
209 |
210 | {/* Input */}
211 |
237 |
238 | )}
239 |
240 | );
241 | }
242 |
--------------------------------------------------------------------------------