Skip to content

Build a modern social app with a stunning UI with a native mobile feel, a special tech stack, an infinite scroll feature, and amazing performance using React JS, Appwrite, TypeScript, and more.

Notifications You must be signed in to change notification settings

TarakaKoda/Snapgram

Repository files navigation


Project Banner
react.js appwrite tailwindcss reactquery typescript

A Social Media Application

  1. πŸ€– Introduction
  2. βš™οΈ Tech Stack
  3. πŸ”‹ Features
  4. 🀸 Quick Start
  5. πŸ•ΈοΈ Snippets
  6. πŸ”— Links
  7. πŸ“Έ Screenshots

Explore social media with this user-friendly platform that has a nice look and lots of features. Easily create and explore posts, and enjoy a strong authentication system and quick data fetching using React Query for a smooth user experience.

  • React.js
  • Appwrite
  • React Query
  • TypeScript
  • Shadcn
  • Tailwind CSS

πŸ‘‰ Authentication System: A robust authentication system ensuring security and user privacy

πŸ‘‰ Explore Page: Homepage for users to explore posts, with a featured section for top creators

πŸ‘‰ Like, Comment and Save Functionality: Enable users to like Additionally, introduced a new comment feature to enhance user interaction and save posts, with dedicated pages for managing liked and saved content

πŸ‘‰ Detailed Post Page: A detailed post page displaying content and related posts for an immersive user experience

πŸ‘‰ Profile Page: A user profile page showcasing liked posts and providing options to edit the profile

πŸ‘‰ Browse Other Users: Allow users to browse and explore other users' profiles and posts

πŸ‘‰ Create Post Page: Implement a user-friendly create post page with effortless file management, storage, and drag-drop feature

πŸ‘‰ Edit Post Functionality: Provide users with the ability to edit the content of their posts at any time

πŸ‘‰ Responsive UI with Bottom Bar: A responsive UI with a bottom bar, enhancing the mobile app feel for seamless navigation

πŸ‘‰ React Query Integration: Incorporate the React Query (Tanstack Query) data fetching library for, Auto caching to enhance performance, Parallel queries for efficient data retrieval, First-class Mutations, etc

πŸ‘‰ Backend as a Service (BaaS) - Appwrite: Utilize Appwrite as a Backend as a Service solution for streamlined backend development, offering features like authentication, database, file storage, and more

and many more, including code architecture and reusability

Follow these steps to set up the project locally on your machine.

Prerequisites

Make sure you have the following installed on your machine:

Cloning the Repository

git clone https://github.com/TarakaKoda/Snapgram.git

Installation

Install the project dependencies using npm:

npm install

Set Up Environment Variables

Create a new file named .env in the root of your project and add the following content:

VITE_APPWRITE_URL=
VITE_APPWRITE_PROJECT_ID=
VITE_APPWRITE_DATABASE_ID=
VITE_APPWRITE_STORAGE_ID=
VITE_APPWRITE_USER_COLLECTION_ID=
VITE_APPWRITE_POST_COLLECTION_ID=
VITE_APPWRITE_SAVES_COLLECTION_ID=
VITE_APPWRITE_COMMENT_COLLECTION_ID =

Replace the placeholder values with your actual Appwrite credentials. You can obtain these credentials by signing up on the Appwrite website.

Running the Project

npm start

Open http://localhost:3000 in your browser to view the project.

constants.index.ts
export const sidebarLinks = [
  {
    imgURL: "/assets/icons/home.svg",
    route: "/",
    label: "Home",
  },
  {
    imgURL: "/assets/icons/wallpaper.svg",
    route: "/explore",
    label: "Explore",
  },
  {
    imgURL: "/assets/icons/people.svg",
    route: "/all-users",
    label: "People",
  },
  {
    imgURL: "/assets/icons/bookmark.svg",
    route: "/saved",
    label: "Saved",
  },
  {
    imgURL: "/assets/icons/gallery-add.svg",
    route: "/create-post",
    label: "Create Post",
  },
];

export const bottombarLinks = [
  {
    imgURL: "/assets/icons/home.svg",
    route: "/",
    label: "Home",
  },
  {
    imgURL: "/assets/icons/wallpaper.svg",
    route: "/explore",
    label: "Explore",
  },
  {
    imgURL: "/assets/icons/bookmark.svg",
    route: "/saved",
    label: "Saved",
  },
  {
    imgURL: "/assets/icons/gallery-add.svg",
    route: "/create-post",
    label: "Create",
  },
];
globals.css
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap");

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  * {
    @apply m-0 box-border list-none scroll-smooth p-0;
  }

  body {
    @apply min-h-screen bg-dark-1 font-inter text-white;
  }
}

@layer utilities {
  /* TYPOGRAPHY */
  .h1-bold {
    @apply text-[36px] font-bold leading-[140%] tracking-tighter;
  }

  .h1-semibold {
    @apply text-[36px] font-semibold leading-[140%] tracking-tighter;
  }

  .h2-bold {
    @apply text-[30px] font-bold leading-[140%] tracking-tighter;
  }

  .h3-bold {
    @apply text-[24px] font-bold leading-[140%] tracking-tighter;
  }

  .base-semibold {
    @apply text-[16px] font-semibold leading-[140%] tracking-tighter;
  }

  .base-medium {
    @apply text-[16px] font-medium leading-[140%];
  }

  .base-regular {
    @apply text-[16px] font-normal leading-[140%];
  }

  .body-bold {
    @apply text-[18px] font-bold leading-[140%];
  }

  .body-medium {
    @apply text-[18px] font-medium leading-[140%];
  }

  .small-semibold {
    @apply text-[14px] font-semibold leading-[140%] tracking-tighter;
  }

  .small-medium {
    @apply text-[14px] font-medium leading-[140%];
  }

  .small-regular {
    @apply text-[14px] font-normal leading-[140%];
  }

  .subtle-semibold {
    @apply text-[12px] font-semibold leading-[140%];
  }

  .tiny-medium {
    @apply text-[10px] font-medium leading-[140%];
  }

  /* UTILITIES */
  .invert-white {
    @apply brightness-0 invert transition;
  }

  .flex-center {
    @apply flex items-center justify-center;
  }

  .flex-between {
    @apply flex items-center justify-between;
  }

  .flex-start {
    @apply flex items-center justify-start;
  }

  .custom-scrollbar::-webkit-scrollbar {
    width: 3px;
    height: 3px;
    border-radius: 2px;
  }

  .custom-scrollbar::-webkit-scrollbar-track {
    background: #09090a;
  }

  .custom-scrollbar::-webkit-scrollbar-thumb {
    background: #5c5c7b;
    border-radius: 50px;
  }

  .custom-scrollbar::-webkit-scrollbar-thumb:hover {
    background: #7878a3;
  }

  .common-container {
    @apply custom-scrollbar flex flex-1 flex-col items-center gap-10 overflow-scroll px-5 py-10 md:px-8 lg:p-14;
  }

  /* All Users */
  .user-container {
    @apply flex w-full max-w-5xl flex-col items-start gap-6 md:gap-9;
  }

  .user-grid {
    @apply grid w-full max-w-5xl grid-cols-1 gap-7 xs:grid-cols-2 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3;
  }

  /* Explore */
  .explore-container {
    @apply custom-scrollbar flex flex-1 flex-col items-center overflow-scroll px-5 py-10 md:p-14;
  }

  .explore-inner_container {
    @apply flex w-full max-w-5xl flex-col items-center gap-6 md:gap-9;
  }

  .explore-search {
    @apply h-12 border-none bg-dark-4 ring-offset-0 placeholder:text-light-4 focus-visible:ring-0 focus-visible:ring-offset-0 !important;
  }

  /* Home */
  .home-container {
    @apply custom-scrollbar flex flex-1 flex-col items-center gap-10 overflow-scroll px-5 py-10 md:px-8 lg:p-14;
  }

  .home-posts {
    @apply flex w-full max-w-screen-sm flex-col items-center gap-6 md:gap-9;
  }

  .home-creators {
    @apply custom-scrollbar hidden w-72 flex-col gap-10 overflow-scroll px-6 py-10  xl:flex 2xl:w-465;
  }

  /* Post Details */
  .post_details-container {
    @apply custom-scrollbar flex flex-1 flex-col items-center gap-10 overflow-scroll px-5 py-10 md:p-14;
  }

  .post_details-card {
    @apply flex w-full max-w-5xl flex-col rounded-[30px] border border-dark-4 bg-dark-2 xl:flex-row xl:rounded-l-[24px];
  }

  .post_details-img {
    @apply h-80 rounded-t-[30px] bg-dark-1 object-cover p-5 lg:h-[480px] xl:w-[48%] xl:rounded-l-[24px] xl:rounded-tr-none;
  }

  .post_details-info {
    @apply flex flex-1 flex-col items-start gap-5 rounded-[30px] bg-dark-2 p-8 lg:gap-7;
  }

  .post_details-delete_btn {
    @apply small-medium lg:base-medium flex gap-3 p-0  text-light-1 hover:bg-transparent hover:text-light-1;
  }

  /* Profile */
  .profile-container {
    @apply custom-scrollbar flex flex-1 flex-col items-center gap-10 overflow-scroll px-5 py-10 md:p-14;
  }

  .profile-inner_container {
    @apply relative flex w-full max-w-5xl flex-col items-center gap-8 md:mb-8 xl:flex-row xl:items-start;
  }

  .profile-tab {
    @apply flex-center w-48 flex-1 gap-3 bg-dark-2  py-4 transition xl:flex-initial;
  }

  /* Saved */
  .saved-container {
    @apply custom-scrollbar flex flex-1 flex-col items-center gap-10 overflow-scroll px-5 py-10 md:p-14;
  }

  /* Bottom bar */
  .bottom-bar {
    @apply flex-between sticky bottom-0 z-50 w-full rounded-t-[20px] bg-dark-2 px-5 py-4 md:hidden;
  }

  /* File uploader */
  .file_uploader-img {
    @apply h-80 w-full rounded-[24px] object-cover object-top lg:h-[480px];
  }

  .file_uploader-label {
    @apply small-regular w-full border-t border-t-dark-4 p-4 text-center text-light-4;
  }

  .file_uploader-box {
    @apply flex-center h-80 flex-col p-7 lg:h-[612px];
  }

  /* Grid Post List */
  .grid-container {
    @apply grid w-full max-w-5xl grid-cols-1 gap-7 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3;
  }

  .grid-post_link {
    @apply flex h-full w-full cursor-pointer overflow-hidden rounded-[24px] border border-dark-4;
  }

  .grid-post_user {
    @apply flex-between absolute bottom-0 w-full gap-2 rounded-b-[24px] bg-gradient-to-t from-dark-3 to-transparent p-5;
  }

  /* Left sidebar */
  .leftsidebar {
    @apply hidden min-w-[270px] flex-col justify-between bg-dark-2 px-6 py-10 md:flex;
  }

  .leftsidebar-link {
    @apply base-medium rounded-lg transition hover:bg-primary-500;
  }

  /* Post Card */
  .post-card {
    @apply w-full max-w-screen-sm rounded-3xl border border-dark-4 bg-dark-2 p-5 lg:p-7;
  }

  .post-card_img {
    @apply mb-5 h-64 w-full rounded-[24px] object-cover xs:h-[400px] lg:h-[450px];
  }

  /* Topbar */
  .topbar {
    @apply sticky top-0 z-50 w-full bg-dark-2 md:hidden;
  }

  /* User card */
  .user-card {
    @apply flex-center flex-col gap-4 rounded-[20px] border border-dark-4 px-5 py-8;
  }
}

@layer components {
  /* SHADCN COMPONENTS */
  /* Form */
  .shad-form_label {
    @apply text-white !important;
  }

  .shad-form_message {
    @apply text-red !important;
  }

  .shad-input {
    @apply h-12 border-none bg-dark-4 ring-offset-light-3 placeholder:text-light-4 focus-visible:ring-1 focus-visible:ring-offset-1 !important;
  }

  .shad-textarea {
    @apply h-36 rounded-xl border-none bg-dark-3 ring-offset-light-3 focus-visible:ring-1 focus-visible:ring-offset-1 !important;
  }

  /* Button */
  .shad-button_primary {
    @apply flex gap-2 bg-primary-500 text-light-1 hover:bg-primary-500 !important;
  }

  .shad-button_dark_4 {
    @apply flex h-12 gap-2 bg-dark-4 px-5 text-light-1 !important;
  }

  .shad-button_ghost {
    @apply flex items-center justify-start gap-4 hover:bg-transparent hover:text-white !important;
  }
}
queryKeys.ts
export enum QUERY_KEYS {
  // AUTH KEYS
  CREATE_USER_ACCOUNT = "createUserAccount",

  // USER KEYS
  GET_CURRENT_USER = "getCurrentUser",
  GET_USERS = "getUsers",
  GET_USER_BY_ID = "getUserById",

  // POST KEYS
  GET_POSTS = "getPosts",
  GET_INFINITE_POSTS = "getInfinitePosts",
  GET_RECENT_POSTS = "getRecentPosts",
  GET_POST_BY_ID = "getPostById",
  GET_USER_POSTS = "getUserPosts",
  GET_FILE_PREVIEW = "getFilePreview",

  //  SEARCH KEYS
  SEARCH_POSTS = "getSearchPosts",
}
tailwind.config.js
/** @type {import('tailwindcss').Config} */
const defaultTheme = require("tailwindcss/defaultTheme");

module.exports = {
  darkMode: ["class"],
  content: [
    "./pages/**/*.{ts,tsx}",
    "./components/**/*.{ts,tsx}",
    "./app/**/*.{ts,tsx}",
    "./src/**/*.{ts,tsx}",
  ],
  theme: {
    container: {
      center: true,
      padding: "2rem",
      screens: {
        "2xl": "1400px",
      },
    },
    extend: {
      colors: {
        "primary-500": "#877EFF",
        "primary-600": "#5D5FEF",
        "secondary-500": "#FFB620",
        "off-white": "#D0DFFF",
        red: "#FF5A5A",
        "dark-1": "#000000",
        "dark-2": "#09090A",
        "dark-3": "#101012",
        "dark-4": "#1F1F22",
        "light-1": "#FFFFFF",
        "light-2": "#EFEFEF",
        "light-3": "#7878A3",
        "light-4": "#5C5C7B",
      },
      screens: {
        xs: "480px",
      },
      width: {
        420: "420px",
        465: "465px",
      },
      fontFamily: {
        inter: ["Inter", "sans-serif"],
      },
      keyframes: {
        "accordion-down": {
          from: { height: 0 },
          to: { height: "var(--radix-accordion-content-height)" },
        },
        "accordion-up": {
          from: { height: "var(--radix-accordion-content-height)" },
          to: { height: 0 },
        },
      },
      animation: {
        "accordion-down": "accordion-down 0.2s ease-out",
        "accordion-up": "accordion-up 0.2s ease-out",
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
};
types.index.ts
export type INavLink = {
  imgURL: string;
  route: string;
  label: string;
};

export type IUpdateUser = {
  userId: string;
  name: string;
  bio: string;
  imageId: string;
  imageUrl: URL | string;
  file: File[];
};

export type INewPost = {
  userId: string;
  caption: string;
  file: File[];
  location?: string;
  tags?: string;
};

export type IUpdatePost = {
  postId: string;
  caption: string;
  imageId: string;
  imageUrl: URL;
  file: File[];
  location?: string;
  tags?: string;
};

export type IUser = {
  id: string;
  name: string;
  username: string;
  email: string;
  imageUrl: string;
  bio: string;
};

export type INewUser = {
  name: string;
  email: string;
  username: string;
  password: string;
};
useDebounce.ts
import { useEffect, useState } from "react";

// https://codesandbox.io/s/react-query-debounce-ted8o?file=/src/useDebounce.js
export default function useDebounce<T>(value: T, delay: number): T {
  // State and setters for debounced value
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    // Update debounced value after delay
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // Cancel the timeout if value changes (also on delay change or unmount)
    // This is how we prevent debounced value from updating if value is changed ...
    // .. within the delay period. Timeout gets cleared and restarted.
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]); // Only re-call effect if value or delay changes

  return debouncedValue;
}
utils.ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

export const convertFileToUrl = (file: File) => URL.createObjectURL(file);

export function formatDateString(dateString: string) {
  const options: Intl.DateTimeFormatOptions = {
    year: "numeric",
    month: "short",
    day: "numeric",
  };

  const date = new Date(dateString);
  const formattedDate = date.toLocaleDateString("en-US", options);

  const time = date.toLocaleTimeString([], {
    hour: "numeric",
    minute: "2-digit",
  });

  return `${formattedDate} at ${time}`;
}

//
export const multiFormatDateString = (timestamp: string = ""): string => {
  const timestampNum = Math.round(new Date(timestamp).getTime() / 1000);
  const date: Date = new Date(timestampNum * 1000);
  const now: Date = new Date();

  const diff: number = now.getTime() - date.getTime();
  const diffInSeconds: number = diff / 1000;
  const diffInMinutes: number = diffInSeconds / 60;
  const diffInHours: number = diffInMinutes / 60;
  const diffInDays: number = diffInHours / 24;

  switch (true) {
    case Math.floor(diffInDays) >= 30:
      return formatDateString(timestamp);
    case Math.floor(diffInDays) === 1:
      return `${Math.floor(diffInDays)} day ago`;
    case Math.floor(diffInDays) > 1 && diffInDays < 30:
      return `${Math.floor(diffInDays)} days ago`;
    case Math.floor(diffInHours) >= 1:
      return `${Math.floor(diffInHours)} hours ago`;
    case Math.floor(diffInMinutes) >= 1:
      return `${Math.floor(diffInMinutes)} minutes ago`;
    default:
      return "Just now";
  }
};

export const checkIsLiked = (likeList: string[], userId: string) => {
  return likeList.includes(userId);
};

Assets used in the project are here

πŸ“„ SIGN UP PAGE

Desktop View πŸ’»

Signup Page

Tablet View πŸ“±

Signup Page

Mobile View πŸ“±

Signup Page Mobile View

πŸ“ƒ SIGN IN PAGE

Desktop View πŸ’»

Sign in Page

Tablet View πŸ“±

Sign in Page

Mobile View πŸ“±

Sign in Page Mobile view

🏠 HOME PAGE

Desktop View πŸ’»

Home Page Desktop View

Tablet View πŸ“±

Home Page Tablet View

Mobile View πŸ“±

Home Page Mobile View

❀️ Like Functionality

Mobile View πŸ“±

Post Mobile View

Tablet View πŸ“±

Liked Post Tablet View

Desktop View πŸ’»

All Users who liked the post Desktop View

πŸ’Ύ Save Functionality

Mobile View πŸ“±

Saving a Post Mobile View

Tablet View πŸ“±

Saved Post Tablet View

Desktop View πŸ’»

Saved Posts Page Desktop View

πŸ–ΌοΈ Detailed Post

Desktop View πŸ’»

Post details Desktop View

Tablet View πŸ“±

Post Details Tablet View

Mobile View πŸ“±

Post Details Mobile View

πŸ‘€ Profile & πŸ“ Edit Profile

Desktop View πŸ’»

Profile Desktop View

Mobile View πŸ“±

Profile Mobile View

Tablet View πŸ“±

Edit Profile Tablet View

πŸ‘₯ All Users

Mobile View πŸ“±

All Users Mobile View

Tablet View πŸ“±

All Users Tablet View

Desktop View πŸ’»

All Users Desktop View

πŸ–ΌοΈ Create & πŸ“ Update Post

Mobile View πŸ“±

Create Post Mobile View

Tablet View πŸ“±

Create Post Tablet View

Desktop View πŸ’»

Update Post Desktop View

About

Build a modern social app with a stunning UI with a native mobile feel, a special tech stack, an infinite scroll feature, and amazing performance using React JS, Appwrite, TypeScript, and more.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published