├── client
├── src
│ ├── index.css
│ ├── lib
│ │ └── axios.js
│ ├── main.jsx
│ ├── socket
│ │ └── socket.client.js
│ ├── store
│ │ ├── useUserStore.js
│ │ ├── useMessageStore.js
│ │ ├── useAuthStore.js
│ │ └── useMatchStore.js
│ ├── components
│ │ ├── SwipeFeedback.jsx
│ │ ├── SwipeArea.jsx
│ │ ├── LoginForm.jsx
│ │ ├── MessageInput.jsx
│ │ ├── Sidebar.jsx
│ │ ├── Header.jsx
│ │ └── SignUpForm.jsx
│ ├── App.jsx
│ └── pages
│ │ ├── AuthPage.jsx
│ │ ├── HomePage.jsx
│ │ ├── ChatPage.jsx
│ │ └── ProfilePage.jsx
├── public
│ ├── avatar.png
│ ├── male
│ │ ├── 1.jpg
│ │ ├── 2.jpg
│ │ ├── 3.jpg
│ │ ├── 4.jpg
│ │ ├── 5.jpg
│ │ ├── 6.jpg
│ │ ├── 7.jpg
│ │ ├── 8.jpg
│ │ └── 9.jpg
│ ├── female
│ │ ├── 1.jpg
│ │ ├── 2.jpg
│ │ ├── 3.jpg
│ │ ├── 4.jpg
│ │ ├── 5.jpg
│ │ ├── 6.jpg
│ │ ├── 7.jpg
│ │ ├── 8.jpg
│ │ ├── 9.jpg
│ │ ├── 10.jpg
│ │ ├── 11.jpg
│ │ └── 12.jpg
│ ├── screenshot-for-readme.png
│ └── vite.svg
├── postcss.config.js
├── vite.config.js
├── tailwind.config.js
├── index.html
├── README.md
├── eslint.config.js
└── package.json
├── api
├── routes
│ ├── userRoutes.js
│ ├── messageRoutes.js
│ ├── authRoutes.js
│ └── matchRoutes.js
├── config
│ ├── cloudinary.js
│ └── db.js
├── models
│ ├── Message.js
│ └── User.js
├── socket
│ └── socket.server.js
├── middleware
│ └── auth.js
├── controllers
│ ├── userController.js
│ ├── messageController.js
│ ├── authController.js
│ └── matchController.js
├── server.js
└── seeds
│ └── user.js
├── .gitignore
├── package.json
└── README.md
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/client/public/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/avatar.png
--------------------------------------------------------------------------------
/client/public/male/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/male/1.jpg
--------------------------------------------------------------------------------
/client/public/male/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/male/2.jpg
--------------------------------------------------------------------------------
/client/public/male/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/male/3.jpg
--------------------------------------------------------------------------------
/client/public/male/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/male/4.jpg
--------------------------------------------------------------------------------
/client/public/male/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/male/5.jpg
--------------------------------------------------------------------------------
/client/public/male/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/male/6.jpg
--------------------------------------------------------------------------------
/client/public/male/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/male/7.jpg
--------------------------------------------------------------------------------
/client/public/male/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/male/8.jpg
--------------------------------------------------------------------------------
/client/public/male/9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/male/9.jpg
--------------------------------------------------------------------------------
/client/public/female/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/female/1.jpg
--------------------------------------------------------------------------------
/client/public/female/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/female/2.jpg
--------------------------------------------------------------------------------
/client/public/female/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/female/3.jpg
--------------------------------------------------------------------------------
/client/public/female/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/female/4.jpg
--------------------------------------------------------------------------------
/client/public/female/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/female/5.jpg
--------------------------------------------------------------------------------
/client/public/female/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/female/6.jpg
--------------------------------------------------------------------------------
/client/public/female/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/female/7.jpg
--------------------------------------------------------------------------------
/client/public/female/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/female/8.jpg
--------------------------------------------------------------------------------
/client/public/female/9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/female/9.jpg
--------------------------------------------------------------------------------
/client/public/female/10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/female/10.jpg
--------------------------------------------------------------------------------
/client/public/female/11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/female/11.jpg
--------------------------------------------------------------------------------
/client/public/female/12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/female/12.jpg
--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/client/public/screenshot-for-readme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/burakorkmez/tinder-clone/HEAD/client/public/screenshot-for-readme.png
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | });
8 |
--------------------------------------------------------------------------------
/client/src/lib/axios.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const BASE_URL = import.meta.env.MODE === "development" ? "http://localhost:5000/api" : "/api";
4 |
5 | export const axiosInstance = axios.create({
6 | baseURL: BASE_URL,
7 | withCredentials: true,
8 | });
9 |
--------------------------------------------------------------------------------
/client/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import daisyui from "daisyui";
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | export default {
5 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
6 | theme: {
7 | extend: {},
8 | },
9 | plugins: [daisyui],
10 | };
11 |
--------------------------------------------------------------------------------
/api/routes/userRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { protectRoute } from "../middleware/auth.js";
3 | import { updateProfile } from "../controllers/userController.js";
4 |
5 | const router = express.Router();
6 |
7 | router.put("/update", protectRoute, updateProfile);
8 |
9 | export default router;
10 |
--------------------------------------------------------------------------------
/api/config/cloudinary.js:
--------------------------------------------------------------------------------
1 | import { v2 as cloudinary } from "cloudinary";
2 | import dotenv from "dotenv";
3 |
4 | dotenv.config();
5 |
6 | cloudinary.config({
7 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
8 | api_key: process.env.CLOUDINARY_API_KEY,
9 | api_secret: process.env.CLOUDINARY_API_SECRET,
10 | });
11 |
12 | export default cloudinary;
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | .env
16 |
17 | # Editor directories and files
18 | .vscode/*
19 | !.vscode/extensions.json
20 | .idea
21 | .DS_Store
22 | *.suo
23 | *.ntvs*
24 | *.njsproj
25 | *.sln
26 | *.sw?
27 |
--------------------------------------------------------------------------------
/client/src/main.jsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 | import App from "./App.jsx";
4 | import "./index.css";
5 |
6 | import { BrowserRouter } from "react-router-dom";
7 |
8 | createRoot(document.getElementById("root")).render(
9 |
10 |
11 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/api/config/db.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | export const connectDB = async () => {
4 | try {
5 | const conn = await mongoose.connect(process.env.MONGO_URI);
6 | console.log(`MongoDB Connected: ${conn.connection.host}`);
7 | } catch (error) {
8 | console.log("Error connecting to MongoDB: ", error);
9 | process.exit(1); // exit process with failure, 0 for success
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/api/routes/messageRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { protectRoute } from "../middleware/auth.js";
3 | import { getConversation, sendMessage } from "../controllers/messageController.js";
4 |
5 | const router = express.Router();
6 |
7 | router.use(protectRoute);
8 |
9 | router.post("/send", sendMessage);
10 | router.get("/conversation/:userId", getConversation);
11 |
12 | export default router;
13 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/api/routes/authRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { signup, login, logout } from "../controllers/authController.js";
3 | import { protectRoute } from "../middleware/auth.js";
4 |
5 | const router = express.Router();
6 |
7 | router.post("/signup", signup);
8 | router.post("/login", login);
9 | router.post("/logout", logout);
10 |
11 | router.get("/me", protectRoute, (req, res) => {
12 | res.send({
13 | success: true,
14 | user: req.user,
15 | });
16 | });
17 |
18 | export default router;
19 |
--------------------------------------------------------------------------------
/api/models/Message.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const messageSchema = new mongoose.Schema(
4 | {
5 | sender: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: "User",
8 | required: true,
9 | },
10 | receiver: {
11 | type: mongoose.Schema.Types.ObjectId,
12 | ref: "User",
13 | required: true,
14 | },
15 | content: {
16 | type: String,
17 | required: true,
18 | },
19 | },
20 | { timestamps: true }
21 | );
22 |
23 | const Message = mongoose.model("Message", messageSchema);
24 |
25 | export default Message;
26 |
--------------------------------------------------------------------------------
/api/routes/matchRoutes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { protectRoute } from "../middleware/auth.js";
3 | import { getMatches, getUserProfiles, swipeLeft, swipeRight } from "../controllers/matchController.js";
4 |
5 | const router = express.Router();
6 |
7 | router.post("/swipe-right/:likedUserId", protectRoute, swipeRight);
8 | router.post("/swipe-left/:dislikedUserId", protectRoute, swipeLeft);
9 |
10 | router.get("/", protectRoute, getMatches);
11 | router.get("/user-profiles", protectRoute, getUserProfiles);
12 |
13 | export default router;
14 |
--------------------------------------------------------------------------------
/client/src/socket/socket.client.js:
--------------------------------------------------------------------------------
1 | import io from "socket.io-client";
2 |
3 | const SOCKET_URL = import.meta.env.MODE === "development" ? "http://localhost:5000" : "/";
4 |
5 | let socket = null;
6 |
7 | export const initializeSocket = (userId) => {
8 | if (socket) {
9 | socket.disconnect();
10 | }
11 |
12 | socket = io(SOCKET_URL, {
13 | auth: { userId },
14 | });
15 | };
16 |
17 | export const getSocket = () => {
18 | if (!socket) {
19 | throw new Error("Socket not initialized");
20 | }
21 | return socket;
22 | };
23 |
24 | export const disconnectSocket = () => {
25 | if (socket) {
26 | socket.disconnect();
27 | socket = null;
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/client/src/store/useUserStore.js:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { axiosInstance } from "../lib/axios";
3 | import toast from "react-hot-toast";
4 | import { useAuthStore } from "./useAuthStore";
5 |
6 | export const useUserStore = create((set) => ({
7 | loading: false,
8 |
9 | updateProfile: async (data) => {
10 | try {
11 | set({ loading: true });
12 | const res = await axiosInstance.put("/users/update", data);
13 | useAuthStore.getState().setAuthUser(res.data.user);
14 | toast.success("Profile updated successfully");
15 | } catch (error) {
16 | toast.error(error.response.data.message || "Something went wrong");
17 | } finally {
18 | set({ loading: false });
19 | }
20 | },
21 | }));
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tinder-clone",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "nodemon api/server.js",
8 | "build": "npm install && npm install --prefix client && npm run build --prefix client",
9 | "start": "node api/server.js"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "type": "module",
14 | "license": "ISC",
15 | "dependencies": {
16 | "bcryptjs": "^2.4.3",
17 | "cloudinary": "^2.5.0",
18 | "cookie-parser": "^1.4.6",
19 | "cors": "^2.8.5",
20 | "dotenv": "^16.4.5",
21 | "express": "^4.21.0",
22 | "jsonwebtoken": "^9.0.2",
23 | "mongoose": "^8.7.0",
24 | "socket.io": "^4.8.0"
25 | },
26 | "devDependencies": {
27 | "nodemon": "^3.1.7"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/client/src/components/SwipeFeedback.jsx:
--------------------------------------------------------------------------------
1 | import { useMatchStore } from "../store/useMatchStore";
2 |
3 | const getFeedbackStyle = (swipeFeedback) => {
4 | if (swipeFeedback === "liked") return "text-green-500";
5 | if (swipeFeedback === "passed") return "text-red-500";
6 | if (swipeFeedback === "matched") return "text-pink-500";
7 | return "";
8 | };
9 |
10 | const getFeedbackText = (swipeFeedback) => {
11 | if (swipeFeedback === "liked") return "Liked!";
12 | if (swipeFeedback === "passed") return "Passed";
13 | if (swipeFeedback === "matched") return "It's a Match!";
14 | return "";
15 | };
16 |
17 | const SwipeFeedback = () => {
18 | const { swipeFeedback } = useMatchStore();
19 |
20 | return (
21 |
26 | {getFeedbackText(swipeFeedback)}
27 |
28 | );
29 | };
30 | export default SwipeFeedback;
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Tinder Clone ✨
2 |
3 | 
4 |
5 | About This Course:
6 |
7 | - 🔐 Authentication System with JWT
8 | - 🛡️ Route Protection
9 | - 👤 User Profile Creation and Updates
10 | - 🖼️ Image Upload for Profiles
11 | - 🔄 Swipe Right/Left Feature
12 | - 💬 Real-time Chat Messaging
13 | - 🔔 Real-time Notifications
14 | - 🤝 Matching Algorithm
15 | - 📱 Responsive Mobile Design
16 | - ⌛ And a lot more...
17 |
18 | ### Setup .env file
19 |
20 | ```bash
21 | PORT=5000
22 | MONGO_URI=
23 |
24 | JWT_SECRET=
25 |
26 | NODE_ENV=development
27 | CLIENT_URL=http://localhost:5173
28 |
29 | CLOUDINARY_API_KEY=
30 | CLOUDINARY_API_SECRET=
31 | CLOUDINARY_CLOUD_NAME=
32 |
33 | ```
34 |
35 | ### Run this app locally
36 |
37 | - Set `NODE_ENV=production` and build the app 👇
38 |
39 | ```shell
40 | npm run build
41 | ```
42 |
43 | ### Start the app
44 |
45 | ```shell
46 | npm run start
47 | ```
48 |
--------------------------------------------------------------------------------
/api/socket/socket.server.js:
--------------------------------------------------------------------------------
1 | import { Server } from "socket.io";
2 |
3 | let io;
4 |
5 | const connectedUsers = new Map();
6 | // { userId: socketId }
7 |
8 | export const initializeSocket = (httpServer) => {
9 | io = new Server(httpServer, {
10 | cors: {
11 | origin: process.env.CLIENT_URL,
12 | credentials: true,
13 | },
14 | });
15 |
16 | io.use((socket, next) => {
17 | const userId = socket.handshake.auth.userId;
18 | if (!userId) return next(new Error("Invalid user ID"));
19 |
20 | socket.userId = userId;
21 | next();
22 | });
23 |
24 | io.on("connection", (socket) => {
25 | console.log(`User connected with socket id: ${socket.id}`);
26 | connectedUsers.set(socket.userId, socket.id);
27 |
28 | socket.on("disconnect", () => {
29 | console.log(`User disconnected with socket id: ${socket.id}`);
30 | connectedUsers.delete(socket.userId);
31 | });
32 | });
33 | };
34 |
35 | export const getIO = () => {
36 | if (!io) {
37 | throw new Error("Socket.io not initialized!");
38 | }
39 | return io;
40 | };
41 |
42 | export const getConnectedUsers = () => connectedUsers;
43 |
--------------------------------------------------------------------------------
/client/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import globals from "globals";
3 | import react from "eslint-plugin-react";
4 | import reactHooks from "eslint-plugin-react-hooks";
5 | import reactRefresh from "eslint-plugin-react-refresh";
6 |
7 | export default [
8 | { ignores: ["dist"] },
9 | {
10 | files: ["**/*.{js,jsx}"],
11 | languageOptions: {
12 | ecmaVersion: 2020,
13 | globals: globals.browser,
14 | parserOptions: {
15 | ecmaVersion: "latest",
16 | ecmaFeatures: { jsx: true },
17 | sourceType: "module",
18 | },
19 | },
20 | settings: { react: { version: "18.3" } },
21 | plugins: {
22 | react,
23 | "react-hooks": reactHooks,
24 | "react-refresh": reactRefresh,
25 | },
26 | rules: {
27 | ...js.configs.recommended.rules,
28 | ...react.configs.recommended.rules,
29 | ...react.configs["jsx-runtime"].rules,
30 | ...reactHooks.configs.recommended.rules,
31 | "react/jsx-no-target-blank": "off",
32 | "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
33 | "react/prop-types": "off",
34 | },
35 | },
36 | ];
37 |
--------------------------------------------------------------------------------
/api/middleware/auth.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import User from "../models/User.js";
3 |
4 | export const protectRoute = async (req, res, next) => {
5 | try {
6 | const token = req.cookies.jwt;
7 |
8 | if (!token) {
9 | return res.status(401).json({
10 | success: false,
11 | message: "Not authorized - No token provided",
12 | });
13 | }
14 |
15 | const decoded = jwt.verify(token, process.env.JWT_SECRET);
16 |
17 | if (!decoded) {
18 | return res.status(401).json({
19 | success: false,
20 | message: "Not authorized - Invalid token",
21 | });
22 | }
23 |
24 | const currentUser = await User.findById(decoded.id);
25 |
26 | req.user = currentUser;
27 |
28 | next();
29 | } catch (error) {
30 | console.log("Error in auth middleware: ", error);
31 |
32 | if (error instanceof jwt.JsonWebTokenError) {
33 | return res.status(401).json({
34 | success: false,
35 | message: "Not authorized - Invalid token",
36 | });
37 | } else {
38 | return res.status(500).json({
39 | success: false,
40 | message: "Internal server error",
41 | });
42 | }
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/api/controllers/userController.js:
--------------------------------------------------------------------------------
1 | import cloudinary from "../config/cloudinary.js";
2 | import User from "../models/User.js";
3 |
4 | export const updateProfile = async (req, res) => {
5 | // image => cloudinary -> image.cloudinary.your => mongodb
6 |
7 | try {
8 | const { image, ...otherData } = req.body;
9 |
10 | let updatedData = otherData;
11 |
12 | if (image) {
13 | // base64 format
14 | if (image.startsWith("data:image")) {
15 | try {
16 | const uploadResponse = await cloudinary.uploader.upload(image);
17 | updatedData.image = uploadResponse.secure_url;
18 | } catch (error) {
19 | console.error("Error uploading image:", uploadError);
20 |
21 | return res.status(400).json({
22 | success: false,
23 | message: "Error uploading image",
24 | });
25 | }
26 | }
27 | }
28 |
29 | const updatedUser = await User.findByIdAndUpdate(req.user.id, updatedData, { new: true });
30 |
31 | res.status(200).json({
32 | success: true,
33 | user: updatedUser,
34 | });
35 | } catch (error) {
36 | console.log("Error in updateProfile: ", error);
37 | res.status(500).json({
38 | success: false,
39 | message: "Internal server error",
40 | });
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@react-spring/web": "^9.7.5",
14 | "axios": "^1.7.7",
15 | "emoji-picker-react": "^4.12.0",
16 | "lucide-react": "^0.447.0",
17 | "react": "^18.3.1",
18 | "react-dom": "^18.3.1",
19 | "react-hot-toast": "^2.4.1",
20 | "react-router-dom": "^6.26.2",
21 | "react-tinder-card": "^1.6.4",
22 | "socket.io-client": "^4.8.0",
23 | "zustand": "^5.0.0-rc.2"
24 | },
25 | "devDependencies": {
26 | "@eslint/js": "^9.11.1",
27 | "@types/react": "^18.3.10",
28 | "@types/react-dom": "^18.3.0",
29 | "@vitejs/plugin-react": "^4.3.2",
30 | "autoprefixer": "^10.4.20",
31 | "daisyui": "^4.12.12",
32 | "eslint": "^9.11.1",
33 | "eslint-plugin-react": "^7.37.0",
34 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
35 | "eslint-plugin-react-refresh": "^0.4.12",
36 | "globals": "^15.9.0",
37 | "postcss": "^8.4.47",
38 | "tailwindcss": "^3.4.13",
39 | "vite": "^5.4.8"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/client/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate, Route, Routes } from "react-router-dom";
2 |
3 | import HomePage from "./pages/HomePage";
4 | import AuthPage from "./pages/AuthPage";
5 | import ProfilePage from "./pages/ProfilePage";
6 | import ChatPage from "./pages/ChatPage";
7 | import { useAuthStore } from "./store/useAuthStore";
8 | import { useEffect } from "react";
9 | import { Toaster } from "react-hot-toast";
10 |
11 | function App() {
12 | const { checkAuth, authUser, checkingAuth } = useAuthStore();
13 |
14 | useEffect(() => {
15 | checkAuth();
16 | }, [checkAuth]);
17 |
18 | if (checkingAuth) return null;
19 |
20 | return (
21 |
22 |
23 | : } />
24 | : } />
25 | : } />
26 | : } />
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | export default App;
35 |
--------------------------------------------------------------------------------
/client/src/pages/AuthPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | import LoginForm from "../components/LoginForm";
4 | import SignUpForm from "../components/SignUpForm";
5 |
6 | const AuthPage = () => {
7 | const [isLogin, setIsLogin] = useState(true);
8 |
9 | return (
10 |
15 |
16 |
17 | {isLogin ? "Sign in to Swipe" : "Create a Swipe account"}
18 |
19 |
20 |
21 | {isLogin ?
:
}
22 |
23 |
24 |
25 | {isLogin ? "New to Swipe?" : "Already have an account?"}
26 |
27 |
28 |
34 |
35 |
36 |
37 |
38 | );
39 | };
40 | export default AuthPage;
41 |
--------------------------------------------------------------------------------
/client/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/api/server.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import dotenv from "dotenv";
3 | import cookieParser from "cookie-parser";
4 | import cors from "cors";
5 | import path from "path";
6 | import { createServer } from "http";
7 |
8 | // routes
9 | import authRoutes from "./routes/authRoutes.js";
10 | import userRoutes from "./routes/userRoutes.js";
11 | import matchRoutes from "./routes/matchRoutes.js";
12 | import messageRoutes from "./routes/messageRoutes.js";
13 |
14 | import { connectDB } from "./config/db.js";
15 | import { initializeSocket } from "./socket/socket.server.js";
16 |
17 | dotenv.config();
18 |
19 | const app = express();
20 | const httpServer = createServer(app);
21 | const PORT = process.env.PORT || 5000;
22 |
23 | const __dirname = path.resolve();
24 |
25 | initializeSocket(httpServer);
26 |
27 | app.use(express.json());
28 | app.use(cookieParser());
29 | app.use(
30 | cors({
31 | origin: process.env.CLIENT_URL,
32 | credentials: true,
33 | })
34 | );
35 |
36 | app.use("/api/auth", authRoutes);
37 | app.use("/api/users", userRoutes);
38 | app.use("/api/matches", matchRoutes);
39 | app.use("/api/messages", messageRoutes);
40 |
41 | if (process.env.NODE_ENV === "production") {
42 | app.use(express.static(path.join(__dirname, "/client/dist")));
43 |
44 | app.get("*", (req, res) => {
45 | res.sendFile(path.resolve(__dirname, "client", "dist", "index.html"));
46 | });
47 | }
48 |
49 | httpServer.listen(PORT, () => {
50 | console.log("Server started at this port:" + PORT);
51 | connectDB();
52 | });
53 |
--------------------------------------------------------------------------------
/client/src/components/SwipeArea.jsx:
--------------------------------------------------------------------------------
1 | import TinderCard from "react-tinder-card";
2 | import { useMatchStore } from "../store/useMatchStore";
3 |
4 | const SwipeArea = () => {
5 | const { userProfiles, swipeRight, swipeLeft } = useMatchStore();
6 |
7 | const handleSwipe = (dir, user) => {
8 | if (dir === "right") swipeRight(user);
9 | else if (dir === "left") swipeLeft(user);
10 | };
11 |
12 | return (
13 |
14 | {userProfiles.map((user) => (
15 |
handleSwipe(dir, user)}
19 | swipeRequirementType='position'
20 | swipeThreshold={100}
21 | preventSwipe={["up", "down"]}
22 | >
23 |
27 |
28 |
33 |
34 |
35 |
36 | {user.name}, {user.age}
37 |
38 |
{user.bio}
39 |
40 |
41 |
42 | ))}
43 |
44 | );
45 | };
46 | export default SwipeArea;
47 |
--------------------------------------------------------------------------------
/api/controllers/messageController.js:
--------------------------------------------------------------------------------
1 | import Message from "../models/Message.js";
2 | import { getConnectedUsers, getIO } from "../socket/socket.server.js";
3 |
4 | export const sendMessage = async (req, res) => {
5 | try {
6 | const { content, receiverId } = req.body;
7 |
8 | const newMessage = await Message.create({
9 | sender: req.user.id,
10 | receiver: receiverId,
11 | content,
12 | });
13 |
14 | const io = getIO();
15 | const connectedUsers = getConnectedUsers();
16 | const receiverSocketId = connectedUsers.get(receiverId);
17 |
18 | if (receiverSocketId) {
19 | io.to(receiverSocketId).emit("newMessage", {
20 | message: newMessage,
21 | });
22 | }
23 |
24 | res.status(201).json({
25 | success: true,
26 | message: newMessage,
27 | });
28 | } catch (error) {
29 | console.log("Error in sendMessage: ", error);
30 | res.status(500).json({
31 | success: false,
32 | message: "Internal server error",
33 | });
34 | }
35 | };
36 |
37 | export const getConversation = async (req, res) => {
38 | const { userId } = req.params;
39 | try {
40 | const messages = await Message.find({
41 | $or: [
42 | { sender: req.user._id, receiver: userId },
43 | { sender: userId, receiver: req.user._id },
44 | ],
45 | }).sort("createdAt");
46 |
47 | res.status(200).json({
48 | success: true,
49 | messages,
50 | });
51 | } catch (error) {
52 | console.log("Error in getConversation: ", error);
53 | res.status(500).json({
54 | success: false,
55 | message: "Internal server error",
56 | });
57 | }
58 | };
59 |
--------------------------------------------------------------------------------
/client/src/store/useMessageStore.js:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { axiosInstance } from "../lib/axios";
3 | import toast from "react-hot-toast";
4 | import { getSocket } from "../socket/socket.client";
5 | import { useAuthStore } from "./useAuthStore";
6 |
7 | export const useMessageStore = create((set) => ({
8 | messages: [],
9 | loading: true,
10 |
11 | sendMessage: async (receiverId, content) => {
12 | try {
13 | // mockup a message, show it in the chat immediately
14 | set((state) => ({
15 | messages: [
16 | ...state.messages,
17 | { _id: Date.now(), sender: useAuthStore.getState().authUser._id, content },
18 | ],
19 | }));
20 | const res = await axiosInstance.post("/messages/send", { receiverId, content });
21 | console.log("message sent", res.data);
22 | } catch (error) {
23 | toast.error(error.response.data.message || "Something went wrong");
24 | }
25 | },
26 |
27 | getMessages: async (userId) => {
28 | try {
29 | set({ loading: true });
30 | const res = await axiosInstance.get(`/messages/conversation/${userId}`);
31 | set({ messages: res.data.messages });
32 | } catch (error) {
33 | console.log(error);
34 | set({ messages: [] });
35 | } finally {
36 | set({ loading: false });
37 | }
38 | },
39 |
40 | subscribeToMessages: () => {
41 | const socket = getSocket();
42 | socket.on("newMessage", ({ message }) => {
43 | set((state) => ({ messages: [...state.messages, message] }));
44 | });
45 | },
46 |
47 | unsubscribeFromMessages: () => {
48 | const socket = getSocket();
49 | socket.off("newMessage");
50 | },
51 | }));
52 |
--------------------------------------------------------------------------------
/api/models/User.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import bcrypt from "bcryptjs";
3 |
4 | const userSchema = new mongoose.Schema(
5 | {
6 | name: {
7 | type: String,
8 | required: true,
9 | },
10 | email: {
11 | type: String,
12 | required: true,
13 | unique: true,
14 | },
15 | password: {
16 | type: String,
17 | required: true,
18 | },
19 | age: {
20 | type: Number,
21 | required: true,
22 | },
23 | gender: {
24 | type: String,
25 | required: true,
26 | enum: ["male", "female"],
27 | },
28 | genderPreference: {
29 | type: String,
30 | required: true,
31 | enum: ["male", "female", "both"],
32 | },
33 | bio: { type: String, default: "" },
34 | image: { type: String, default: "" },
35 | likes: [
36 | {
37 | type: mongoose.Schema.Types.ObjectId,
38 | ref: "User",
39 | },
40 | ],
41 | dislikes: [
42 | {
43 | type: mongoose.Schema.Types.ObjectId,
44 | ref: "User",
45 | },
46 | ],
47 | matches: [
48 | {
49 | type: mongoose.Schema.Types.ObjectId,
50 | ref: "User",
51 | },
52 | ],
53 | },
54 | { timestamps: true }
55 | );
56 |
57 | userSchema.pre("save", async function (next) {
58 | // MAKE SURE TO ADD THIS IF CHECK!!! 👇 I forgot to add this in the tutorial
59 | // only hash if password is modified.
60 | if (!this.isModified("password")) {
61 | return next();
62 | }
63 |
64 | this.password = await bcrypt.hash(this.password, 10);
65 | next();
66 | });
67 |
68 | userSchema.methods.matchPassword = async function (enteredPassword) {
69 | return await bcrypt.compare(enteredPassword, this.password);
70 | };
71 |
72 | const User = mongoose.model("User", userSchema);
73 |
74 | export default User;
75 |
--------------------------------------------------------------------------------
/client/src/store/useAuthStore.js:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { axiosInstance } from "../lib/axios";
3 | import toast from "react-hot-toast";
4 | import { disconnectSocket, initializeSocket } from "../socket/socket.client";
5 |
6 | export const useAuthStore = create((set) => ({
7 | authUser: null,
8 | checkingAuth: true,
9 | loading: false,
10 |
11 | signup: async (signupData) => {
12 | try {
13 | set({ loading: true });
14 | const res = await axiosInstance.post("/auth/signup", signupData);
15 | set({ authUser: res.data.user });
16 | initializeSocket(res.data.user._id);
17 |
18 | toast.success("Account created successfully");
19 | } catch (error) {
20 | toast.error(error.response.data.message || "Something went wrong");
21 | } finally {
22 | set({ loading: false });
23 | }
24 | },
25 | login: async (loginData) => {
26 | try {
27 | set({ loading: true });
28 | const res = await axiosInstance.post("/auth/login", loginData);
29 | set({ authUser: res.data.user });
30 | initializeSocket(res.data.user._id);
31 | toast.success("Logged in successfully");
32 | } catch (error) {
33 | toast.error(error.response.data.message || "Something went wrong");
34 | } finally {
35 | set({ loading: false });
36 | }
37 | },
38 | logout: async () => {
39 | try {
40 | const res = await axiosInstance.post("/auth/logout");
41 | disconnectSocket();
42 | if (res.status === 200) set({ authUser: null });
43 | } catch (error) {
44 | toast.error(error.response.data.message || "Something went wrong");
45 | }
46 | },
47 | checkAuth: async () => {
48 | try {
49 | const res = await axiosInstance.get("/auth/me");
50 | initializeSocket(res.data.user._id);
51 | set({ authUser: res.data.user });
52 | } catch (error) {
53 | set({ authUser: null });
54 | console.log(error);
55 | } finally {
56 | set({ checkingAuth: false });
57 | }
58 | },
59 |
60 | setAuthUser: (user) => set({ authUser: user }),
61 | }));
62 |
--------------------------------------------------------------------------------
/client/src/components/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useAuthStore } from "../store/useAuthStore";
3 |
4 | const LoginForm = () => {
5 | const [email, setEmail] = useState("");
6 | const [password, setPassword] = useState("");
7 |
8 | const { login, loading } = useAuthStore();
9 |
10 | return (
11 |
67 | );
68 | };
69 | export default LoginForm;
70 |
--------------------------------------------------------------------------------
/client/src/components/MessageInput.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 | import { useMessageStore } from "../store/useMessageStore";
3 | import { Send, Smile } from "lucide-react";
4 | import EmojiPicker from "emoji-picker-react";
5 |
6 | const MessageInput = ({ match }) => {
7 | const [message, setMessage] = useState("");
8 | const [showEmojiPicker, setShowEmojiPicker] = useState(false);
9 | const emojiPickerRef = useRef(null);
10 |
11 | const { sendMessage } = useMessageStore();
12 |
13 | const handleSendMessage = (e) => {
14 | e.preventDefault();
15 | if (message.trim()) {
16 | sendMessage(match._id, message);
17 | setMessage("");
18 | }
19 | };
20 |
21 | useEffect(() => {
22 | const handleClickOutside = (event) => {
23 | if (emojiPickerRef.current && !emojiPickerRef.current.contains(event.target)) {
24 | setShowEmojiPicker(false);
25 | }
26 | };
27 |
28 | document.addEventListener("mousedown", handleClickOutside);
29 | return () => {
30 | document.removeEventListener("mousedown", handleClickOutside);
31 | };
32 | }, []);
33 |
34 | return (
35 |
70 | );
71 | };
72 | export default MessageInput;
73 |
--------------------------------------------------------------------------------
/api/seeds/user.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import bcrypt from "bcryptjs";
3 | import User from "../models/User.js";
4 | import dotenv from "dotenv";
5 |
6 | dotenv.config();
7 |
8 | const maleNames = ["James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph", "Thomas"];
9 |
10 | const femaleNames = [
11 | "Mary",
12 | "Patricia",
13 | "Jennifer",
14 | "Linda",
15 | "Elizabeth",
16 | "Barbara",
17 | "Susan",
18 | "Jessica",
19 | "Sarah",
20 | "Karen",
21 | "Nancy",
22 | "Lisa",
23 | ];
24 |
25 | const genderPreferences = ["male", "female", "both"];
26 |
27 | const bioDescriptors = [
28 | "Coffee addict",
29 | "Cat lover",
30 | "Dog person",
31 | "Foodie",
32 | "Gym rat",
33 | "Bookworm",
34 | "Movie buff",
35 | "Music lover",
36 | "Travel junkie",
37 | "Beach bum",
38 | "City slicker",
39 | "Outdoor enthusiast",
40 | "Netflix binger",
41 | "Yoga enthusiast",
42 | "Craft beer connoisseur",
43 | "Sushi fanatic",
44 | "Adventure seeker",
45 | "Night owl",
46 | "Early bird",
47 | "Aspiring chef",
48 | ];
49 |
50 | const generateBio = () => {
51 | const descriptors = bioDescriptors.sort(() => 0.5 - Math.random()).slice(0, 3);
52 | return descriptors.join(" | ");
53 | };
54 |
55 | const generateRandomUser = (gender, index) => {
56 | const names = gender === "male" ? maleNames : femaleNames;
57 | const name = names[index];
58 | const age = Math.floor(Math.random() * (45 - 21 + 1) + 21);
59 | return {
60 | name,
61 | email: `${name.toLowerCase()}${age}@example.com`,
62 | password: bcrypt.hashSync("password123", 10),
63 | age,
64 | gender,
65 | genderPreference: genderPreferences[Math.floor(Math.random() * genderPreferences.length)],
66 | bio: generateBio(),
67 | image: `/${gender}/${index + 1}.jpg`,
68 | };
69 | };
70 |
71 | const seedUsers = async () => {
72 | try {
73 | await mongoose.connect(process.env.MONGO_URI);
74 |
75 | await User.deleteMany({});
76 |
77 | const maleUsers = maleNames.map((_, i) => generateRandomUser("male", i));
78 | const femaleUsers = femaleNames.map((_, i) => generateRandomUser("female", i));
79 |
80 | const allUsers = [...maleUsers, ...femaleUsers];
81 |
82 | await User.insertMany(allUsers);
83 |
84 | console.log("Database seeded successfully with users having concise bios");
85 | } catch (error) {
86 | console.error("Error seeding database:", error);
87 | } finally {
88 | mongoose.disconnect();
89 | }
90 | };
91 |
92 | seedUsers();
93 |
--------------------------------------------------------------------------------
/client/src/store/useMatchStore.js:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { axiosInstance } from "../lib/axios";
3 | import toast from "react-hot-toast";
4 | import { getSocket } from "../socket/socket.client";
5 |
6 | export const useMatchStore = create((set) => ({
7 | matches: [],
8 | isLoadingMyMatches: false,
9 | isLoadingUserProfiles: false,
10 | userProfiles: [],
11 | swipeFeedback: null,
12 |
13 | getMyMatches: async () => {
14 | try {
15 | set({ isLoadingMyMatches: true });
16 | const res = await axiosInstance.get("/matches");
17 | set({ matches: res.data.matches });
18 | } catch (error) {
19 | set({ matches: [] });
20 | toast.error(error.response.data.message || "Something went wrong");
21 | } finally {
22 | set({ isLoadingMyMatches: false });
23 | }
24 | },
25 |
26 | getUserProfiles: async () => {
27 | try {
28 | set({ isLoadingUserProfiles: true });
29 | const res = await axiosInstance.get("/matches/user-profiles");
30 | set({ userProfiles: res.data.users });
31 | } catch (error) {
32 | set({ userProfiles: [] });
33 | toast.error(error.response.data.message || "Something went wrong");
34 | } finally {
35 | set({ isLoadingUserProfiles: false });
36 | }
37 | },
38 |
39 | swipeLeft: async (user) => {
40 | try {
41 | set({ swipeFeedback: "passed" });
42 | await axiosInstance.post("/matches/swipe-left/" + user._id);
43 | } catch (error) {
44 | console.log(error);
45 | toast.error("Failed to swipe left");
46 | } finally {
47 | setTimeout(() => set({ swipeFeedback: null }), 1500);
48 | }
49 | },
50 | swipeRight: async (user) => {
51 | try {
52 | set({ swipeFeedback: "liked" });
53 | await axiosInstance.post("/matches/swipe-right/" + user._id);
54 | } catch (error) {
55 | console.log(error);
56 | toast.error("Failed to swipe right");
57 | } finally {
58 | setTimeout(() => set({ swipeFeedback: null }), 1500);
59 | }
60 | },
61 |
62 | subscribeToNewMatches: () => {
63 | try {
64 | const socket = getSocket();
65 |
66 | socket.on("newMatch", (newMatch) => {
67 | set((state) => ({
68 | matches: [...state.matches, newMatch],
69 | }));
70 | toast.success("You got a new match!");
71 | });
72 | } catch (error) {
73 | console.log(error);
74 | }
75 | },
76 |
77 | unsubscribeFromNewMatches: () => {
78 | try {
79 | const socket = getSocket();
80 | socket.off("newMatch");
81 | } catch (error) {
82 | console.error(error);
83 | }
84 | },
85 | }));
86 |
--------------------------------------------------------------------------------
/client/src/pages/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import Sidebar from "../components/Sidebar";
3 | import { Header } from "../components/Header";
4 | import { useMatchStore } from "../store/useMatchStore";
5 | import { Frown } from "lucide-react";
6 |
7 | import SwipeArea from "../components/SwipeArea";
8 | import SwipeFeedback from "../components/SwipeFeedback";
9 | import { useAuthStore } from "../store/useAuthStore";
10 |
11 | const HomePage = () => {
12 | const { isLoadingUserProfiles, getUserProfiles, userProfiles, subscribeToNewMatches, unsubscribeFromNewMatches } =
13 | useMatchStore();
14 |
15 | const { authUser } = useAuthStore();
16 |
17 | useEffect(() => {
18 | getUserProfiles();
19 | }, [getUserProfiles]);
20 |
21 | useEffect(() => {
22 | authUser && subscribeToNewMatches();
23 |
24 | return () => {
25 | unsubscribeFromNewMatches();
26 | };
27 | }, [subscribeToNewMatches, unsubscribeFromNewMatches, authUser]);
28 |
29 | return (
30 |
35 |
36 |
37 |
38 |
39 | {userProfiles.length > 0 && !isLoadingUserProfiles && (
40 | <>
41 |
42 |
43 | >
44 | )}
45 |
46 | {userProfiles.length === 0 && !isLoadingUserProfiles && }
47 |
48 | {isLoadingUserProfiles && }
49 |
50 |
51 |
52 | );
53 | };
54 | export default HomePage;
55 |
56 | const NoMoreProfiles = () => (
57 |
58 |
59 |
Woah there, speedy fingers!
60 |
Bro are you OK? Maybe it's time to touch some grass.
61 |
62 | );
63 |
64 | const LoadingUI = () => {
65 | return (
66 |
79 | );
80 | };
81 |
--------------------------------------------------------------------------------
/api/controllers/authController.js:
--------------------------------------------------------------------------------
1 | import User from "../models/User.js";
2 | import jwt from "jsonwebtoken";
3 |
4 | const signToken = (id) => {
5 | // jwt token
6 | return jwt.sign({ id }, process.env.JWT_SECRET, {
7 | expiresIn: "7d",
8 | });
9 | };
10 |
11 | export const signup = async (req, res) => {
12 | const { name, email, password, age, gender, genderPreference } = req.body;
13 | try {
14 | if (!name || !email || !password || !age || !gender || !genderPreference) {
15 | return res.status(400).json({
16 | success: false,
17 | message: "All fields are required",
18 | });
19 | }
20 |
21 | if (age < 18) {
22 | return res.status(400).json({
23 | success: false,
24 | message: "You must at lest 18 years old",
25 | });
26 | }
27 |
28 | if (password.length < 6) {
29 | return res.status(400).json({
30 | success: false,
31 | message: "Password must be at least 6 characters",
32 | });
33 | }
34 |
35 | const newUser = await User.create({
36 | name,
37 | email,
38 | password,
39 | age,
40 | gender,
41 | genderPreference,
42 | });
43 |
44 | const token = signToken(newUser._id);
45 |
46 | res.cookie("jwt", token, {
47 | maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds
48 | httpOnly: true, // prevents XSS attacks
49 | sameSite: "strict", // prevents CSRF attacks
50 | secure: process.env.NODE_ENV === "production",
51 | });
52 |
53 | res.status(201).json({
54 | success: true,
55 | user: newUser,
56 | });
57 | } catch (error) {
58 | console.log("Error in signup controller:", error);
59 | res.status(500).json({ success: false, message: "Server error" });
60 | }
61 | };
62 | export const login = async (req, res) => {
63 | const { email, password } = req.body;
64 | try {
65 | if (!email || !password) {
66 | return res.status(400).json({
67 | success: false,
68 | message: "All fields are required",
69 | });
70 | }
71 |
72 | const user = await User.findOne({ email }).select("+password");
73 |
74 | if (!user || !(await user.matchPassword(password))) {
75 | return res.status(401).json({
76 | success: false,
77 | message: "Invalid email or password",
78 | });
79 | }
80 |
81 | const token = signToken(user._id);
82 |
83 | res.cookie("jwt", token, {
84 | maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds
85 | httpOnly: true, // prevents XSS attacks
86 | sameSite: "strict", // prevents CSRF attacks
87 | secure: process.env.NODE_ENV === "production",
88 | });
89 |
90 | res.status(200).json({
91 | success: true,
92 | user,
93 | });
94 | } catch (error) {
95 | console.log("Error in login controller:", error);
96 | res.status(500).json({ success: false, message: "Server error" });
97 | }
98 | };
99 | export const logout = async (req, res) => {
100 | res.clearCookie("jwt");
101 | res.status(200).json({ success: true, message: "Logged out successfully" });
102 | };
103 |
--------------------------------------------------------------------------------
/client/src/components/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { Heart, Loader, MessageCircle, X } from "lucide-react";
3 | import { Link } from "react-router-dom";
4 | import { useMatchStore } from "../store/useMatchStore";
5 |
6 | const Sidebar = () => {
7 | const [isOpen, setIsOpen] = useState(false);
8 |
9 | const toggleSidebar = () => setIsOpen(!isOpen);
10 |
11 | const { getMyMatches, matches, isLoadingMyMatches } = useMatchStore();
12 |
13 | useEffect(() => {
14 | getMyMatches();
15 | }, [getMyMatches]);
16 |
17 | return (
18 | <>
19 |
27 |
28 | {/* Header */}
29 |
30 |
Matches
31 |
37 |
38 |
39 |
40 | {isLoadingMyMatches ? (
41 |
42 | ) : matches.length === 0 ? (
43 |
44 | ) : (
45 | matches.map((match) => (
46 |
47 |
48 |

53 |
54 |
{match.name}
55 |
56 |
57 | ))
58 | )}
59 |
60 |
61 |
62 |
63 |
69 | >
70 | );
71 | };
72 | export default Sidebar;
73 |
74 | const NoMatchesFound = () => (
75 |
76 |
77 |
No Matches Yet
78 |
79 | Don't worry! Your perfect match is just around the corner. Keep swiping!
80 |
81 |
82 | );
83 |
84 | const LoadingState = () => (
85 |
86 |
87 |
Loading Matches
88 |
We're finding your perfect matches. This might take a moment...
89 |
90 | );
91 |
--------------------------------------------------------------------------------
/api/controllers/matchController.js:
--------------------------------------------------------------------------------
1 | import User from "../models/User.js";
2 | import { getConnectedUsers, getIO } from "../socket/socket.server.js";
3 |
4 | export const swipeRight = async (req, res) => {
5 | try {
6 | const { likedUserId } = req.params;
7 | const currentUser = await User.findById(req.user.id);
8 | const likedUser = await User.findById(likedUserId);
9 |
10 | if (!likedUser) {
11 | return res.status(404).json({
12 | success: false,
13 | message: "User not found",
14 | });
15 | }
16 |
17 | if (!currentUser.likes.includes(likedUserId)) {
18 | currentUser.likes.push(likedUserId);
19 | await currentUser.save();
20 |
21 | // if the other user already liked us, it's a match, so let's update both users
22 | if (likedUser.likes.includes(currentUser.id)) {
23 | currentUser.matches.push(likedUserId);
24 | likedUser.matches.push(currentUser.id);
25 |
26 | await Promise.all([await currentUser.save(), await likedUser.save()]);
27 |
28 | // send notification in real-time with socket.io
29 | const connectedUsers = getConnectedUsers();
30 | const io = getIO();
31 |
32 | const likedUserSocketId = connectedUsers.get(likedUserId);
33 |
34 | if (likedUserSocketId) {
35 | io.to(likedUserSocketId).emit("newMatch", {
36 | _id: currentUser._id,
37 | name: currentUser.name,
38 | image: currentUser.image,
39 | });
40 | }
41 |
42 | const currentSocketId = connectedUsers.get(currentUser._id.toString());
43 | if (currentSocketId) {
44 | io.to(currentSocketId).emit("newMatch", {
45 | _id: likedUser._id,
46 | name: likedUser.name,
47 | image: likedUser.image,
48 | });
49 | }
50 | }
51 | }
52 |
53 | res.status(200).json({
54 | success: true,
55 | user: currentUser,
56 | });
57 | } catch (error) {
58 | console.log("Error in swipeRight: ", error);
59 |
60 | res.status(500).json({
61 | success: false,
62 | message: "Internal server error",
63 | });
64 | }
65 | };
66 |
67 | export const swipeLeft = async (req, res) => {
68 | try {
69 | const { dislikedUserId } = req.params;
70 | const currentUser = await User.findById(req.user.id);
71 |
72 | if (!currentUser.dislikes.includes(dislikedUserId)) {
73 | currentUser.dislikes.push(dislikedUserId);
74 | await currentUser.save();
75 | }
76 |
77 | res.status(200).json({
78 | success: true,
79 | user: currentUser,
80 | });
81 | } catch (error) {
82 | console.log("Error in swipeLeft: ", error);
83 |
84 | res.status(500).json({
85 | success: false,
86 | message: "Internal server error",
87 | });
88 | }
89 | };
90 |
91 | export const getMatches = async (req, res) => {
92 | try {
93 | const user = await User.findById(req.user.id).populate("matches", "name image");
94 |
95 | res.status(200).json({
96 | success: true,
97 | matches: user.matches,
98 | });
99 | } catch (error) {
100 | console.log("Error in getMatches: ", error);
101 |
102 | res.status(500).json({
103 | success: false,
104 | message: "Internal server error",
105 | });
106 | }
107 | };
108 |
109 | export const getUserProfiles = async (req, res) => {
110 | try {
111 | const currentUser = await User.findById(req.user.id);
112 |
113 | const users = await User.find({
114 | $and: [
115 | { _id: { $ne: currentUser.id } },
116 | { _id: { $nin: currentUser.likes } },
117 | { _id: { $nin: currentUser.dislikes } },
118 | { _id: { $nin: currentUser.matches } },
119 | {
120 | gender:
121 | currentUser.genderPreference === "both"
122 | ? { $in: ["male", "female"] }
123 | : currentUser.genderPreference,
124 | },
125 | { genderPreference: { $in: [currentUser.gender, "both"] } },
126 | ],
127 | });
128 |
129 | res.status(200).json({
130 | success: true,
131 | users,
132 | });
133 | } catch (error) {
134 | console.log("Error in getUserProfiles: ", error);
135 |
136 | res.status(500).json({
137 | success: false,
138 | message: "Internal server error",
139 | });
140 | }
141 | };
142 |
--------------------------------------------------------------------------------
/client/src/pages/ChatPage.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { Header } from "../components/Header";
3 | import { useAuthStore } from "../store/useAuthStore";
4 | import { useMatchStore } from "../store/useMatchStore";
5 | import { useMessageStore } from "../store/useMessageStore";
6 | import { Link, useParams } from "react-router-dom";
7 | import { Loader, UserX } from "lucide-react";
8 | import MessageInput from "../components/MessageInput";
9 |
10 | const ChatPage = () => {
11 | const { getMyMatches, matches, isLoadingMyMatches } = useMatchStore();
12 | const { messages, getMessages, subscribeToMessages, unsubscribeFromMessages } = useMessageStore();
13 | const { authUser } = useAuthStore();
14 |
15 | const { id } = useParams();
16 |
17 | const match = matches.find((m) => m?._id === id);
18 |
19 | useEffect(() => {
20 | if (authUser && id) {
21 | getMyMatches();
22 | getMessages(id);
23 | subscribeToMessages();
24 | }
25 |
26 | return () => {
27 | unsubscribeFromMessages();
28 | };
29 | }, [getMyMatches, authUser, getMessages, subscribeToMessages, unsubscribeFromMessages, id]);
30 |
31 | if (isLoadingMyMatches) return ;
32 | if (!match) return ;
33 |
34 | return (
35 |
36 |
37 |
38 |
39 |
40 |

44 |
{match.name}
45 |
46 |
47 |
48 | {messages.length === 0 ? (
49 |
Start your conversation with {match.name}
50 | ) : (
51 | messages.map((msg) => (
52 |
56 |
63 | {msg.content}
64 |
65 |
66 | ))
67 | )}
68 |
69 |
70 |
71 |
72 | );
73 | };
74 | export default ChatPage;
75 |
76 | const MatchNotFound = () => (
77 |
78 |
79 |
80 |
Match Not Found
81 |
Oops! It seems this match doesn't exist or has been removed.
82 |
87 | Go Back To Home
88 |
89 |
90 |
91 | );
92 |
93 | const LoadingMessagesUI = () => (
94 |
95 |
96 |
97 |
Loading Chat
98 |
Please wait while we fetch your conversation...
99 |
110 |
111 |
112 | );
113 |
--------------------------------------------------------------------------------
/client/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 | import { useAuthStore } from "../store/useAuthStore";
3 | import { Link } from "react-router-dom";
4 | import { Flame, User, LogOut, Menu } from "lucide-react";
5 |
6 | export const Header = () => {
7 | const { authUser, logout } = useAuthStore();
8 | const [dropdownOpen, setDropdownOpen] = useState(false);
9 | const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
10 | const dropdownRef = useRef(null);
11 |
12 | useEffect(() => {
13 | const handleClickOutside = (event) => {
14 | if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
15 | setDropdownOpen(false);
16 | }
17 | };
18 |
19 | document.addEventListener("mousedown", handleClickOutside);
20 |
21 | return () => document.removeEventListener("mousedown", handleClickOutside);
22 | }, []);
23 |
24 | return (
25 |
145 | );
146 | };
147 |
--------------------------------------------------------------------------------
/client/src/components/SignUpForm.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useAuthStore } from "../store/useAuthStore";
3 |
4 | const SignUpForm = () => {
5 | const [name, setName] = useState("");
6 | const [email, setEmail] = useState("");
7 | const [password, setPassword] = useState("");
8 | const [gender, setGender] = useState("");
9 | const [age, setAge] = useState("");
10 | const [genderPreference, setGenderPreference] = useState("");
11 |
12 | const { signup, loading } = useAuthStore();
13 |
14 | return (
15 |
194 | );
195 | };
196 | export default SignUpForm;
197 |
--------------------------------------------------------------------------------
/client/src/pages/ProfilePage.jsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from "react";
2 | import { Header } from "../components/Header";
3 | import { useAuthStore } from "../store/useAuthStore";
4 | import { useUserStore } from "../store/useUserStore";
5 |
6 | const ProfilePage = () => {
7 | const { authUser } = useAuthStore();
8 | const [name, setName] = useState(authUser.name || "");
9 | const [bio, setBio] = useState(authUser.bio || "");
10 | const [age, setAge] = useState(authUser.age || "");
11 | const [gender, setGender] = useState(authUser.gender || "");
12 | const [genderPreference, setGenderPreference] = useState(authUser.genderPreference || []);
13 | const [image, setImage] = useState(authUser.image || null);
14 |
15 | const fileInputRef = useRef(null);
16 |
17 | const { loading, updateProfile } = useUserStore();
18 |
19 | const handleSubmit = (e) => {
20 | e.preventDefault();
21 | updateProfile({ name, bio, age, gender, genderPreference, image });
22 | };
23 |
24 | const handleImageChange = (e) => {
25 | const file = e.target.files[0];
26 | if (file) {
27 | const reader = new FileReader();
28 | reader.onloadend = () => {
29 | setImage(reader.result);
30 | };
31 |
32 | reader.readAsDataURL(file);
33 | }
34 | };
35 |
36 | console.log(image);
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
Your Profile
45 |
46 |
47 |
181 |
182 |
183 | );
184 | };
185 | export default ProfilePage;
186 |
--------------------------------------------------------------------------------