├── bun.lockb
├── app
├── icon1.png
├── icon2.png
├── favicon.ico
├── apple-icon.png
├── (auth)
│ ├── sign-in
│ │ └── [[...sign-in]]
│ │ │ └── page.tsx
│ └── sign-up
│ │ └── [[...sign-up]]
│ │ └── page.tsx
├── (root)
│ ├── (home)
│ │ ├── previous
│ │ │ └── page.tsx
│ │ ├── upcoming
│ │ │ └── page.tsx
│ │ ├── recordings
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── personal-room
│ │ │ └── page.tsx
│ ├── layout.tsx
│ └── meeting
│ │ └── [id]
│ │ └── page.tsx
├── layout.tsx
└── globals.css
├── .github
├── images
│ ├── img1.png
│ ├── img2.png
│ ├── img3.png
│ ├── img4.png
│ ├── img_main.png
│ └── stats.svg
├── dependabot.yml
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── update-bun-lockfile.yml
├── public
├── images
│ ├── avatar-3.png
│ ├── avatar-4.png
│ ├── avatar-5.png
│ ├── avatar-1.jpeg
│ ├── avatar-2.jpeg
│ └── hero-background.png
└── icons
│ ├── three-dots.svg
│ ├── home.svg
│ ├── hamburger.svg
│ ├── play.svg
│ ├── video.svg
│ ├── recordings.svg
│ ├── add-meeting.svg
│ ├── add-personal.svg
│ ├── github.svg
│ ├── loading-circle.svg
│ ├── schedule.svg
│ ├── copy.svg
│ ├── previous.svg
│ ├── upcoming.svg
│ ├── share.svg
│ ├── join-meeting.svg
│ ├── call-ended.svg
│ ├── logo.svg
│ ├── yoom-logo.svg
│ └── checked.svg
├── next.config.mjs
├── .eslintrc.json
├── postcss.config.mjs
├── .prettierrc.json
├── lib
└── utils.ts
├── components
├── loader.tsx
├── ui
│ ├── textarea.tsx
│ ├── input.tsx
│ ├── toaster.tsx
│ ├── button.tsx
│ ├── dialog.tsx
│ ├── sheet.tsx
│ ├── use-toast.ts
│ ├── toast.tsx
│ └── dropdown-menu.tsx
├── end-call-button.tsx
├── home-card.tsx
├── navbar.tsx
├── sidebar.tsx
├── modals
│ └── meeting-modal.tsx
├── meeting-setup.tsx
├── mobile-nav.tsx
├── meeting-card.tsx
├── call-list.tsx
├── meeting-room.tsx
└── meeting-type-list.tsx
├── environment.d.ts
├── components.json
├── middleware.ts
├── .gitignore
├── .env.example
├── tsconfig.json
├── SECURITY.md
├── constants
└── index.ts
├── hooks
├── use-get-call-by-id.ts
└── use-get-calls.ts
├── actions
└── stream.actions.ts
├── config
└── index.ts
├── LICENSE
├── providers
└── stream-client-provider.tsx
├── tailwind.config.ts
├── package.json
├── CODE_OF_CONDUCT.md
└── README.md
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/bun.lockb
--------------------------------------------------------------------------------
/app/icon1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/app/icon1.png
--------------------------------------------------------------------------------
/app/icon2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/app/icon2.png
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/app/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/app/apple-icon.png
--------------------------------------------------------------------------------
/.github/images/img1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/.github/images/img1.png
--------------------------------------------------------------------------------
/.github/images/img2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/.github/images/img2.png
--------------------------------------------------------------------------------
/.github/images/img3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/.github/images/img3.png
--------------------------------------------------------------------------------
/.github/images/img4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/.github/images/img4.png
--------------------------------------------------------------------------------
/public/images/avatar-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/public/images/avatar-3.png
--------------------------------------------------------------------------------
/public/images/avatar-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/public/images/avatar-4.png
--------------------------------------------------------------------------------
/public/images/avatar-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/public/images/avatar-5.png
--------------------------------------------------------------------------------
/.github/images/img_main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/.github/images/img_main.png
--------------------------------------------------------------------------------
/public/images/avatar-1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/public/images/avatar-1.jpeg
--------------------------------------------------------------------------------
/public/images/avatar-2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/public/images/avatar-2.jpeg
--------------------------------------------------------------------------------
/public/images/hero-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamillm/ts-zoom/HEAD/public/images/hero-background.png
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "plugins": ["unused-imports"],
4 | "rules": {
5 | "unused-imports/no-unused-imports": "error"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "semi": true,
4 | "tabWidth": 2,
5 | "singleQuote": false,
6 | "jsxSingleQuote": false,
7 | "plugins": ["prettier-plugin-tailwindcss"]
8 | }
9 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/app/(auth)/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from "@clerk/nextjs";
2 |
3 | const SignInPage = () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
11 | export default SignInPage;
12 |
--------------------------------------------------------------------------------
/app/(auth)/sign-up/[[...sign-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from "@clerk/nextjs";
2 |
3 | const SignUpPage = () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
11 | export default SignUpPage;
12 |
--------------------------------------------------------------------------------
/components/loader.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | export const Loader = () => {
4 | return (
5 |
6 |
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/public/icons/three-dots.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/(root)/(home)/previous/page.tsx:
--------------------------------------------------------------------------------
1 | import { CallList } from "@/components/call-list";
2 |
3 | const PreviousPage = () => {
4 | return (
5 |
10 | );
11 | };
12 |
13 | export default PreviousPage;
14 |
--------------------------------------------------------------------------------
/app/(root)/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren } from "react";
2 |
3 | import { StreamClientProvider } from "@/providers/stream-client-provider";
4 |
5 | const RootLayout = ({ children }: PropsWithChildren) => {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | };
12 |
13 | export default RootLayout;
14 |
--------------------------------------------------------------------------------
/environment.d.ts:
--------------------------------------------------------------------------------
1 | // This file is needed to support autocomplete for process.env
2 | export {};
3 |
4 | declare global {
5 | namespace NodeJS {
6 | interface ProcessEnv {
7 | // stream api keys
8 | NEXT_PUBLIC_STREAM_API_KEY: string;
9 | STREAM_SECRET_KEY: string;
10 |
11 | // app base url
12 | NEXT_PUBLIC_BASE_URL: string;
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/(root)/(home)/upcoming/page.tsx:
--------------------------------------------------------------------------------
1 | import { CallList } from "@/components/call-list";
2 |
3 | const UpcomingPage = () => {
4 | return (
5 |
10 | );
11 | };
12 |
13 | export default UpcomingPage;
14 |
--------------------------------------------------------------------------------
/app/(root)/(home)/recordings/page.tsx:
--------------------------------------------------------------------------------
1 | import { CallList } from "@/components/call-list";
2 |
3 | const RecordingsPage = () => {
4 | return (
5 |
6 | Recordings
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default RecordingsPage;
14 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/public/icons/home.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
2 |
3 | const protectedRoutes = createRouteMatcher([
4 | "/",
5 | "/upcoming",
6 | "/previous",
7 | "/recordings",
8 | "/personal-room",
9 | "/meeting(.*)",
10 | ]);
11 |
12 | export default clerkMiddleware((auth, req) => {
13 | if (protectedRoutes(req)) auth().protect();
14 | });
15 |
16 | export const config = {
17 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
18 | };
19 |
--------------------------------------------------------------------------------
/.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.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "monthly"
12 | versioning-strategy: increase
13 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # .env.local
2 |
3 | # disabled next.js telemetry
4 | NEXT_TELEMETRY_DISABLED=1
5 |
6 | # clerk auth keys
7 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
8 | CLERK_SECRET_KEY=sk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
9 |
10 | # clerk auth redirect urls
11 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
12 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
13 |
14 | # stream api keys
15 | NEXT_PUBLIC_STREAM_API_KEY=xxxxxxxxxxxxxxxxxxxxxx
16 | STREAM_SECRET_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
17 |
18 | # app base url
19 | NEXT_PUBLIC_BASE_URL=http://localhost:3000
20 |
--------------------------------------------------------------------------------
/app/(root)/(home)/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren } from "react";
2 |
3 | import { Navbar } from "@/components/navbar";
4 | import { Sidebar } from "@/components/sidebar";
5 |
6 | const HomeLayout = ({ children }: PropsWithChildren) => {
7 | return (
8 |
9 |
10 |
11 |
18 |
19 | );
20 | };
21 |
22 | export default HomeLayout;
23 |
--------------------------------------------------------------------------------
/public/icons/hamburger.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Use this section to tell people about which versions of your project are
6 | currently being supported with security updates.
7 |
8 | | Version | Supported |
9 | | ------- | ------------------ |
10 | | 5.1.x | :white_check_mark: |
11 | | 5.0.x | :x: |
12 | | 4.0.x | :white_check_mark: |
13 | | < 4.0 | :x: |
14 |
15 | ## Reporting a Vulnerability
16 |
17 | Use this section to tell people how to report a vulnerability.
18 |
19 | Tell them where to go, how often they can expect to get an update on a
20 | reported vulnerability, what to expect if the vulnerability is accepted or
21 | declined, etc.
22 |
--------------------------------------------------------------------------------
/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const SIDEBAR_LINKS = [
2 | {
3 | label: "Home",
4 | route: "/",
5 | imgUrl: "/icons/home.svg",
6 | },
7 | {
8 | label: "Upcoming",
9 | route: "/upcoming",
10 | imgUrl: "/icons/upcoming.svg",
11 | },
12 | {
13 | label: "Previous",
14 | route: "/previous",
15 | imgUrl: "/icons/previous.svg",
16 | },
17 | {
18 | label: "Recordings",
19 | route: "/recordings",
20 | imgUrl: "/icons/video.svg",
21 | },
22 | {
23 | label: "Personal Room",
24 | route: "/personal-room",
25 | imgUrl: "/icons/add-personal.svg",
26 | },
27 | ] as const;
28 |
29 | export const avatarImages = [
30 | "/images/avatar-1.jpeg",
31 | "/images/avatar-2.jpeg",
32 | "/images/avatar-3.png",
33 | "/images/avatar-4.png",
34 | "/images/avatar-5.png",
35 | ] as const;
36 |
--------------------------------------------------------------------------------
/public/icons/play.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/.github/workflows/update-bun-lockfile.yml:
--------------------------------------------------------------------------------
1 | name: 'Dependabot: Update bun.lockb'
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - "package.json"
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | update-bun-lockb:
13 | name: "Update bun.lockb"
14 | if: github.actor == 'dependabot[bot]'
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: oven-sh/setup-bun@v1
18 | - uses: actions/checkout@v4
19 | with:
20 | fetch-depth: 0
21 | ref: ${{ github.event.pull_request.head.ref }}
22 | - run: |
23 | bun install
24 | git add bun.lockb
25 | git config --global user.name 'dependabot[bot]'
26 | git config --global user.email 'dependabot[bot]@users.noreply.github.com'
27 | git commit --amend --no-edit
28 | git push --force
29 |
--------------------------------------------------------------------------------
/hooks/use-get-call-by-id.ts:
--------------------------------------------------------------------------------
1 | import { useStreamVideoClient, type Call } from "@stream-io/video-react-sdk";
2 | import { useEffect, useState } from "react";
3 |
4 | export const useGetCallById = (id: string | string[]) => {
5 | const [call, setCall] = useState();
6 | const [isCallLoading, setIsCallLoading] = useState(true);
7 |
8 | const streamClient = useStreamVideoClient();
9 |
10 | useEffect(() => {
11 | if (!streamClient) return;
12 |
13 | const loadCall = async () => {
14 | const { calls } = await streamClient.queryCalls({
15 | filter_conditions: {
16 | id,
17 | },
18 | });
19 |
20 | if (calls.length > 0) setCall(calls[0]);
21 |
22 | setIsCallLoading(false);
23 | };
24 |
25 | loadCall();
26 | }, [streamClient, id]);
27 |
28 | return { call, isCallLoading };
29 | };
30 |
--------------------------------------------------------------------------------
/public/icons/video.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | }
21 | );
22 | Textarea.displayName = "Textarea";
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/public/icons/recordings.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/actions/stream.actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { currentUser } from "@clerk/nextjs/server";
4 | import { StreamClient } from "@stream-io/node-sdk";
5 |
6 | const apiKey = process.env.NEXT_PUBLIC_STREAM_API_KEY;
7 | const apiSecret = process.env.STREAM_SECRET_KEY;
8 |
9 | export const tokenProvider = async () => {
10 | const user = await currentUser();
11 |
12 | if (!user || !user?.id) throw new Error("Unauthorized.");
13 | if (!apiKey) throw new Error("Stream api key missing.");
14 | if (!apiSecret) throw new Error("Stream api secret missing.");
15 |
16 | const streamClient = new StreamClient(apiKey, apiSecret);
17 |
18 | // token is valid for an hour
19 | const exp = Math.round(new Date().getTime() / 1000) + 60 * 60;
20 | const issued = Math.floor(Date.now() / 1000) - 60;
21 |
22 | const token = streamClient.createToken(user.id, exp, issued);
23 |
24 | return token;
25 | };
26 |
--------------------------------------------------------------------------------
/public/icons/add-meeting.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/icons/add-personal.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/public/icons/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast";
11 | import { useToast } from "@/components/ui/use-toast";
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/components/end-call-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCall, useCallStateHooks } from "@stream-io/video-react-sdk";
4 | import { useRouter } from "next/navigation";
5 |
6 | import { Button } from "@/components/ui/button";
7 |
8 | export const EndCallButton = () => {
9 | const call = useCall();
10 | const router = useRouter();
11 |
12 | const { useLocalParticipant } = useCallStateHooks();
13 | const localParticipant = useLocalParticipant();
14 |
15 | const isMeetingOwner =
16 | localParticipant &&
17 | call?.state.createdBy &&
18 | localParticipant.userId === call.state.createdBy.id;
19 |
20 | if (!isMeetingOwner) return;
21 |
22 | return (
23 |
24 |
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/public/icons/loading-circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 |
28 | - OS: [e.g. iOS]
29 | - Browser [e.g. chrome, safari]
30 | - Version [e.g. 22]
31 |
32 | **Smartphone (please complete the following information):**
33 |
34 | - Device: [e.g. iPhone6]
35 | - OS: [e.g. iOS8.1]
36 | - Browser [e.g. stock browser, safari]
37 | - Version [e.g. 22]
38 |
39 | **Additional context**
40 | Add any other context about the problem here.
41 |
--------------------------------------------------------------------------------
/components/home-card.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import Image from "next/image";
3 |
4 | type HomeCardProps = {
5 | className?: string;
6 | img: string;
7 | title: string;
8 | description: string;
9 | handleClick: () => void;
10 | };
11 |
12 | export const HomeCard = ({
13 | className,
14 | img,
15 | title,
16 | description,
17 | handleClick,
18 | }: HomeCardProps) => {
19 | return (
20 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/config/index.ts:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 |
3 | export const siteConfig: Metadata = {
4 | title: "Yoom",
5 | description: "Modern Next.js powered Video calling app",
6 | keywords: [
7 | "reactjs",
8 | "nextjs",
9 | "vercel",
10 | "react",
11 | "getstream",
12 | "stream",
13 | "shadcn",
14 | "shadcn-ui",
15 | "radix-ui",
16 | "cn",
17 | "clsx",
18 | "zoom-clone",
19 | "realtime-video",
20 | "realtime-chat",
21 | "live-audio",
22 | "live-video",
23 | "live-chat",
24 | "radix-toast",
25 | "lucide-react",
26 | "next-themes",
27 | "postcss",
28 | "prettier",
29 | "react-dom",
30 | "tailwindcss",
31 | "tailwindcss-animate",
32 | "ui/ux",
33 | "js",
34 | "ts",
35 | "javascript",
36 | "typescript",
37 | "eslint",
38 | "html",
39 | "css",
40 | ] as Array,
41 | authors: {
42 | name: "James Lee",
43 | url: "https://github.com/tsolutionsx",
44 | },
45 | } as const;
46 |
47 | export const links = {
48 | sourceCode: "https://github.com/tsolutionsx/zoom-clone",
49 | } as const;
50 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 James Lee
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/public/icons/schedule.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/icons/copy.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/providers/stream-client-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useUser } from "@clerk/nextjs";
4 | import { StreamVideo, StreamVideoClient } from "@stream-io/video-react-sdk";
5 | import { useEffect, useState, type PropsWithChildren } from "react";
6 |
7 | import { tokenProvider } from "@/actions/stream.actions";
8 | import { Loader } from "@/components/loader";
9 |
10 | const apiKey = process.env.NEXT_PUBLIC_STREAM_API_KEY;
11 |
12 | export const StreamClientProvider = ({ children }: PropsWithChildren) => {
13 | const [videoClient, setVideoClient] = useState();
14 | const { user, isLoaded } = useUser();
15 |
16 | useEffect(() => {
17 | if (!isLoaded || !user) return;
18 | if (!apiKey) throw new Error("Stream api key missing.");
19 |
20 | const client = new StreamVideoClient({
21 | apiKey,
22 | user: {
23 | id: user?.id,
24 | name: user?.username || user?.id,
25 | image: user?.imageUrl,
26 | },
27 | tokenProvider,
28 | });
29 |
30 | setVideoClient(client);
31 | }, [user, isLoaded]);
32 |
33 | if (!videoClient) return ;
34 |
35 | return {children};
36 | };
37 |
--------------------------------------------------------------------------------
/public/icons/previous.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/public/icons/upcoming.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/app/(root)/meeting/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useUser } from "@clerk/nextjs";
4 | import { StreamCall, StreamTheme } from "@stream-io/video-react-sdk";
5 | import { useState } from "react";
6 |
7 | import { Loader } from "@/components/loader";
8 | import { MeetingRoom } from "@/components/meeting-room";
9 | import { MeetingSetup } from "@/components/meeting-setup";
10 | import { useGetCallById } from "@/hooks/use-get-call-by-id";
11 |
12 | type MeetingIdPageProps = {
13 | params: {
14 | id: string;
15 | };
16 | };
17 |
18 | const MeetingIdPage = ({ params }: MeetingIdPageProps) => {
19 | const [isSetupComplete, setIsSetupComplete] = useState(false);
20 | const { user, isLoaded } = useUser();
21 |
22 | const { call, isCallLoading } = useGetCallById(params.id);
23 |
24 | if (!isLoaded || isCallLoading) return ;
25 |
26 | return (
27 |
28 |
29 |
30 | {!isSetupComplete ? (
31 |
32 | ) : (
33 |
34 | )}
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default MeetingIdPage;
42 |
--------------------------------------------------------------------------------
/components/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { SignedIn, UserButton } from "@clerk/nextjs";
2 | import Image from "next/image";
3 | import Link from "next/link";
4 |
5 | import { links } from "@/config";
6 |
7 | import { MobileNav } from "./mobile-nav";
8 |
9 | export const Navbar = () => {
10 | return (
11 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import Link from "next/link";
5 | import { usePathname } from "next/navigation";
6 |
7 | import { SIDEBAR_LINKS } from "@/constants";
8 | import { cn } from "@/lib/utils";
9 |
10 | export const Sidebar = () => {
11 | const pathname = usePathname();
12 |
13 | return (
14 |
15 |
16 | {SIDEBAR_LINKS.map((item) => {
17 | const isActive =
18 | pathname === item.route || pathname.startsWith(`${item.route}/`);
19 |
20 | return (
21 |
31 |
37 |
38 |
39 | {item.label}
40 |
41 |
42 | );
43 | })}
44 |
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { ClerkProvider } from "@clerk/nextjs";
2 | import type { Metadata, Viewport } from "next";
3 | import { Inter } from "next/font/google";
4 | import type { PropsWithChildren } from "react";
5 |
6 | import "@stream-io/video-react-sdk/dist/css/styles.css";
7 | import "react-datepicker/dist/react-datepicker.css";
8 |
9 | import { Toaster } from "@/components/ui/toaster";
10 | import { siteConfig } from "@/config";
11 | import { cn } from "@/lib/utils";
12 |
13 | import "./globals.css";
14 |
15 | const inter = Inter({ subsets: ["latin"] });
16 |
17 | export const viewport: Viewport = {
18 | themeColor: "#0E78F9",
19 | colorScheme: "dark",
20 | };
21 |
22 | export const metadata: Metadata = siteConfig;
23 |
24 | const AppLayout = ({ children }: Readonly) => {
25 | return (
26 |
27 |
42 |
43 | {children}
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default AppLayout;
52 |
--------------------------------------------------------------------------------
/hooks/use-get-calls.ts:
--------------------------------------------------------------------------------
1 | import { useUser } from "@clerk/nextjs";
2 | import { Call, useStreamVideoClient } from "@stream-io/video-react-sdk";
3 | import { useEffect, useState } from "react";
4 |
5 | export const useGetCalls = () => {
6 | const { user } = useUser();
7 | const client = useStreamVideoClient();
8 |
9 | const [calls, setCalls] = useState();
10 | const [isLoading, setIsLoading] = useState(false);
11 |
12 | useEffect(() => {
13 | const loadCalls = async () => {
14 | if (!client || !user?.id) return;
15 |
16 | setIsLoading(true);
17 |
18 | try {
19 | const { calls } = await client.queryCalls({
20 | sort: [{ field: "starts_at", direction: -1 }],
21 | filter_conditions: {
22 | starts_at: { $exists: true },
23 | $or: [
24 | { created_by_user_id: user.id },
25 | { members: { $in: [user.id] } },
26 | ],
27 | },
28 | });
29 |
30 | setCalls(calls);
31 | } catch (error) {
32 | console.error(error);
33 | } finally {
34 | setIsLoading(false);
35 | }
36 | };
37 |
38 | loadCalls();
39 | }, [client, user?.id]);
40 |
41 | const now = new Date();
42 |
43 | const endedCalls = calls?.filter(({ state: { startsAt, endedAt } }: Call) => {
44 | return (startsAt && new Date(startsAt) < now) || !!endedAt;
45 | });
46 |
47 | const upcomingCalls = calls?.filter(({ state: { startsAt } }: Call) => {
48 | return startsAt && new Date(startsAt) > now;
49 | });
50 |
51 | return { endedCalls, upcomingCalls, callRecordings: calls, isLoading };
52 | };
53 |
--------------------------------------------------------------------------------
/app/(root)/(home)/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { MeetingTypeList } from "@/components/meeting-type-list";
4 | import { useGetCalls } from "@/hooks/use-get-calls";
5 |
6 | const HomePage = () => {
7 | const now = new Date();
8 | const { upcomingCalls } = useGetCalls();
9 |
10 | const time = now.toLocaleTimeString("en-US", {
11 | hour: "2-digit",
12 | minute: "2-digit",
13 | });
14 |
15 | const date = new Intl.DateTimeFormat("en-US", { dateStyle: "full" }).format(
16 | now
17 | );
18 |
19 | return (
20 |
21 |
22 |
23 |
24 | {upcomingCalls?.length === 0
25 | ? "No upcoming meeting"
26 | : upcomingCalls?.length &&
27 | `Upcoming meeting at:
28 | ${upcomingCalls[
29 | upcomingCalls.length - 1
30 | ].state?.startsAt?.toLocaleTimeString("en-US", {
31 | hour: "2-digit",
32 | minute: "2-digit",
33 | })}`}
34 |
35 |
36 |
37 |
{time}
38 |
39 |
{date}
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default HomePage;
50 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{ts,tsx}",
7 | "./components/**/*.{ts,tsx}",
8 | "./app/**/*.{ts,tsx}",
9 | "./src/**/*.{ts,tsx}",
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | dark: {
23 | 1: "#1C1F2E",
24 | 2: "#161925",
25 | 3: "#252A41",
26 | 4: "#1E2757",
27 | },
28 | blue: {
29 | 1: "#0E78F9",
30 | },
31 | sky: {
32 | 1: "#C9DDFF",
33 | 2: "#ECF0FF",
34 | 3: "#F5FCFF",
35 | },
36 | orange: {
37 | 1: "#FF742E",
38 | },
39 | purple: {
40 | 1: "#830EF9",
41 | },
42 | yellow: {
43 | 1: "#F9A90E",
44 | },
45 | },
46 | keyframes: {
47 | "accordion-down": {
48 | from: { height: "0" },
49 | to: { height: "var(--radix-accordion-content-height)" },
50 | },
51 | "accordion-up": {
52 | from: { height: "var(--radix-accordion-content-height)" },
53 | to: { height: "0" },
54 | },
55 | },
56 | animation: {
57 | "accordion-down": "accordion-down 0.2s ease-out",
58 | "accordion-up": "accordion-up 0.2s ease-out",
59 | },
60 | backgroundImage: {
61 | hero: "url('/images/hero-background.png')",
62 | },
63 | },
64 | },
65 | plugins: [require("tailwindcss-animate")],
66 | } satisfies Config;
67 |
68 | export default config;
69 |
--------------------------------------------------------------------------------
/public/icons/share.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/components/modals/meeting-modal.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import type { PropsWithChildren } from "react";
3 |
4 | import { Button } from "@/components/ui/button";
5 | import { Dialog, DialogContent } from "@/components/ui/dialog";
6 | import { cn } from "@/lib/utils";
7 |
8 | type MeetingModalProps = {
9 | isOpen: boolean;
10 | onClose: () => void;
11 | title: string;
12 | className?: string;
13 | handleClick?: () => void;
14 | buttonText?: string;
15 | image?: string;
16 | buttonIcon?: string;
17 | isLoading?: boolean;
18 | };
19 |
20 | export const MeetingModal = ({
21 | isOpen,
22 | onClose,
23 | title,
24 | className,
25 | children,
26 | handleClick,
27 | buttonText,
28 | image,
29 | buttonIcon,
30 | isLoading = false,
31 | }: PropsWithChildren) => {
32 | return (
33 |
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/components/meeting-setup.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | DeviceSettings,
5 | VideoPreview,
6 | useCall,
7 | } from "@stream-io/video-react-sdk";
8 | import { useEffect, useState } from "react";
9 |
10 | import { Button } from "@/components/ui/button";
11 |
12 | type MeetingSetupProps = {
13 | setIsSetupComplete: (isSetupComplete: boolean) => void;
14 | };
15 |
16 | export const MeetingSetup = ({ setIsSetupComplete }: MeetingSetupProps) => {
17 | const [isMicCamToggledOn, setIsMicCamToggledOn] = useState(false);
18 |
19 | const call = useCall();
20 |
21 | if (!call)
22 | throw new Error("useCall must be used within StreamCall component.");
23 |
24 | useEffect(() => {
25 | if (isMicCamToggledOn) {
26 | call?.camera.disable();
27 | call?.microphone.disable();
28 | } else {
29 | call?.camera.enable();
30 | call?.microphone.enable();
31 | }
32 | }, [isMicCamToggledOn, call?.camera, call?.microphone]);
33 |
34 | return (
35 |
36 |
Setup
37 |
38 |
39 |
40 |
41 |
49 |
50 |
51 |
52 |
53 |
63 |
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button";
45 | return (
46 |
51 | );
52 | }
53 | );
54 | Button.displayName = "Button";
55 |
56 | export { Button, buttonVariants };
57 |
--------------------------------------------------------------------------------
/public/icons/join-meeting.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | * {
6 | margin: 0;
7 | padding: 0;
8 | box-sizing: border-box;
9 | }
10 |
11 | /* ======== stream css overrides ======== */
12 | .str-video__call-stats {
13 | max-width: 500px;
14 | position: relative;
15 | }
16 |
17 | .str-video__speaker-layout__wrapper {
18 | max-height: 700px;
19 | }
20 |
21 | .str-video__participant-details {
22 | color: white;
23 | }
24 |
25 | .str-video__menu-container {
26 | color: white;
27 | }
28 |
29 | .str-video__notification {
30 | color: white;
31 | }
32 |
33 | .str-video__participant-list {
34 | background-color: #1c1f2e;
35 | padding: 10px;
36 | border-radius: 10px;
37 | color: white;
38 | height: 100%;
39 | }
40 |
41 | .str-video__call-controls__button {
42 | height: 40px;
43 | }
44 |
45 | .glassmorphism {
46 | background: rgba(255, 255, 255, 0.25);
47 | backdrop-filter: blur(4px);
48 | -webkit-backdrop-filter: blur(4px);
49 | }
50 | .glassmorphism2 {
51 | background: rgba(18, 17, 17, 0.25);
52 | backdrop-filter: blur(8px);
53 | -webkit-backdrop-filter: blur(8px);
54 | }
55 |
56 | /* ==== clerk class override ===== */
57 |
58 | .cl-userButtonPopoverActionButtonIcon {
59 | color: white;
60 | }
61 |
62 | .cl-logoBox {
63 | height: 40px;
64 | }
65 | .cl-dividerLine {
66 | background: #252a41;
67 | height: 2px;
68 | }
69 |
70 | .cl-socialButtonsIconButton {
71 | border: 3px solid #565761;
72 | }
73 |
74 | .cl-internal-wkkub3 {
75 | color: white;
76 | }
77 | .cl-userButtonPopoverActionButton {
78 | color: white;
79 | }
80 |
81 | .cl-providerIcon__github {
82 | filter: invert(1);
83 | }
84 |
85 | /* =============================== */
86 |
87 | @layer utilities {
88 | .flex-center {
89 | @apply flex items-center justify-center;
90 | }
91 |
92 | .flex-between {
93 | @apply flex items-center justify-between;
94 | }
95 | }
96 |
97 | /* animation */
98 |
99 | .show-block {
100 | width: 100%;
101 | max-width: 350px;
102 | display: block;
103 | animation: show 0.7s forwards linear;
104 | }
105 |
106 | @keyframes show {
107 | 0% {
108 | animation-timing-function: ease-in;
109 | width: 0%;
110 | }
111 |
112 | 100% {
113 | animation-timing-function: ease-in;
114 | width: 100%;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zoom-clone",
3 | "version": "1.0.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "author": {
12 | "name": "James Lee",
13 | "email": "techsolutionsx7419@gmail.com",
14 | "url": "https://github.com/tsolutionsx"
15 | },
16 | "description": "Modern Next.js powered Video calling app",
17 | "keywords": [
18 | "reactjs",
19 | "nextjs",
20 | "vercel",
21 | "react",
22 | "getstream",
23 | "stream",
24 | "shadcn",
25 | "shadcn-ui",
26 | "radix-ui",
27 | "cn",
28 | "clsx",
29 | "zoom-clone",
30 | "realtime-video",
31 | "realtime-chat",
32 | "live-audio",
33 | "live-video",
34 | "live-chat",
35 | "radix-toast",
36 | "lucide-react",
37 | "next-themes",
38 | "postcss",
39 | "prettier",
40 | "react-dom",
41 | "tailwindcss",
42 | "tailwindcss-animate",
43 | "ui/ux",
44 | "js",
45 | "ts",
46 | "javascript",
47 | "typescript",
48 | "eslint",
49 | "html",
50 | "css"
51 | ],
52 | "license": "MIT",
53 | "repository": {
54 | "type": "git",
55 | "url": "https://github.com/tsolutionsx/zoom-clone.git"
56 | },
57 | "homepage": "https://github.com/tsolutionsx/zoom-clone#readme",
58 | "bugs": {
59 | "url": "https://github.com/tsolutionsx/zoom-clone/issues",
60 | "email": "techsolutionsx7419@gmail.com"
61 | },
62 |
63 | "dependencies": {
64 | "@clerk/nextjs": "^5.2.1",
65 | "@radix-ui/react-dialog": "^1.1.1",
66 | "@radix-ui/react-dropdown-menu": "^2.0.6",
67 | "@radix-ui/react-slot": "^1.0.2",
68 | "@radix-ui/react-toast": "^1.1.5",
69 | "@stream-io/node-sdk": "^0.3.0",
70 | "@stream-io/video-react-sdk": "^1.0.15",
71 | "class-variance-authority": "^0.7.0",
72 | "clsx": "^2.1.1",
73 | "lucide-react": "^0.381.0",
74 | "next": "14.2.3",
75 | "react": "^18",
76 | "react-datepicker": "^6.9.0",
77 | "react-dom": "^18",
78 | "tailwind-merge": "^2.3.0",
79 | "tailwindcss-animate": "^1.0.7"
80 | },
81 | "devDependencies": {
82 | "@types/node": "^20",
83 | "@types/react": "^18",
84 | "@types/react-datepicker": "^6.2.0",
85 | "@types/react-dom": "^18",
86 | "eslint": "^9",
87 | "eslint-config-next": "14.2.3",
88 | "eslint-plugin-unused-imports": "^4.0.0",
89 | "postcss": "^8",
90 | "prettier": "^3.3.2",
91 | "prettier-plugin-tailwindcss": "^0.6.5",
92 | "tailwindcss": "^3.4.3",
93 | "typescript": "^5"
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/components/mobile-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import Link from "next/link";
5 | import { usePathname } from "next/navigation";
6 |
7 | import {
8 | Sheet,
9 | SheetClose,
10 | SheetContent,
11 | SheetTrigger,
12 | } from "@/components/ui/sheet";
13 | import { SIDEBAR_LINKS } from "@/constants";
14 | import { cn } from "@/lib/utils";
15 |
16 | export const MobileNav = () => {
17 | const pathname = usePathname();
18 |
19 | return (
20 |
21 |
22 |
23 |
30 |
31 |
32 |
33 |
34 |
35 |
42 |
43 | Yoom
44 |
45 |
46 |
47 |
48 |
49 | {SIDEBAR_LINKS.map((item) => {
50 | const isActive =
51 | pathname === item.route ||
52 | pathname.startsWith(`${item.route}/`);
53 |
54 | return (
55 |
56 |
65 |
71 |
72 | {item.label}
73 |
74 |
75 | );
76 | })}
77 |
78 |
79 |
80 |
81 |
82 | );
83 | };
84 |
--------------------------------------------------------------------------------
/app/(root)/(home)/personal-room/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useUser } from "@clerk/nextjs";
4 | import { useStreamVideoClient } from "@stream-io/video-react-sdk";
5 | import { useRouter } from "next/navigation";
6 |
7 | import { Loader } from "@/components/loader";
8 | import { Button } from "@/components/ui/button";
9 | import { useToast } from "@/components/ui/use-toast";
10 | import { useGetCallById } from "@/hooks/use-get-call-by-id";
11 |
12 | type TableProps = {
13 | title: string;
14 | description: string;
15 | };
16 |
17 | const Table = ({ title, description }: TableProps) => (
18 |
19 |
20 | {title}:
21 |
22 |
23 | {description}
24 |
25 |
26 | );
27 |
28 | const PersonalRoomPage = () => {
29 | const router = useRouter();
30 | const { toast } = useToast();
31 | const { user, isLoaded } = useUser();
32 | const streamClient = useStreamVideoClient();
33 |
34 | const meetingId = user?.id;
35 | const { call } = useGetCallById(meetingId!);
36 |
37 | if (!user || !user?.username || !user?.id || !isLoaded) return ;
38 |
39 | const meetingLink = `${process.env.NEXT_PUBLIC_BASE_URL}/meeting/${meetingId}?personal=true`;
40 |
41 | const startRoom = async () => {
42 | if (!streamClient || !user || !user?.id) return;
43 |
44 | if (!call) {
45 | const newCall = streamClient.call("default", meetingId!);
46 | await newCall.getOrCreate({
47 | data: {
48 | starts_at: new Date().toISOString(),
49 | },
50 | });
51 | }
52 |
53 | router.push(`/meeting/${meetingId}?personal=true`);
54 | };
55 |
56 | return (
57 |
58 | Personal Room
59 |
60 |
65 |
66 |
67 |
70 |
71 |
82 |
83 |
84 | );
85 | };
86 |
87 | export default PersonalRoomPage;
88 |
--------------------------------------------------------------------------------
/components/meeting-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 |
5 | import { Button } from "@/components/ui/button";
6 | import { useToast } from "@/components/ui/use-toast";
7 | import { avatarImages } from "@/constants";
8 | import { cn } from "@/lib/utils";
9 |
10 | type MeetingCardProps = {
11 | title: string;
12 | date: string;
13 | icon: string;
14 | isPreviousMeeting?: boolean;
15 | buttonIcon1?: string;
16 | buttonText?: string;
17 | handleClick: () => void;
18 | link: string;
19 | };
20 |
21 | export const MeetingCard = ({
22 | icon,
23 | title,
24 | date,
25 | isPreviousMeeting,
26 | buttonIcon1,
27 | handleClick,
28 | link,
29 | buttonText,
30 | }: MeetingCardProps) => {
31 | const { toast } = useToast();
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
{title}
40 |
{date}
41 |
42 |
43 |
44 |
45 |
46 | {avatarImages.map((img, index) => (
47 |
0 })}
54 | style={{ top: 0, left: index * 28 }}
55 | />
56 | ))}
57 |
58 | +5
59 |
60 |
61 | {!isPreviousMeeting && (
62 |
63 |
69 |
86 |
87 | )}
88 |
89 |
90 | );
91 | };
92 |
--------------------------------------------------------------------------------
/public/icons/call-ended.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/components/call-list.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { type Call, type CallRecording } from "@stream-io/video-react-sdk";
4 | import { useRouter } from "next/navigation";
5 | import { useEffect, useState } from "react";
6 |
7 | import { useToast } from "@/components/ui/use-toast";
8 | import { useGetCalls } from "@/hooks/use-get-calls";
9 |
10 | import { Loader } from "./loader";
11 | import { MeetingCard } from "./meeting-card";
12 |
13 | type CallListType = {
14 | type: "ended" | "upcoming" | "recordings";
15 | };
16 |
17 | export const CallList = ({ type }: CallListType) => {
18 | const router = useRouter();
19 | const { toast } = useToast();
20 |
21 | const [recordings, setRecordings] = useState([]);
22 |
23 | const { endedCalls, upcomingCalls, callRecordings, isLoading } =
24 | useGetCalls();
25 |
26 | const getCalls = () => {
27 | switch (type) {
28 | case "ended":
29 | return endedCalls;
30 |
31 | case "recordings":
32 | return recordings;
33 |
34 | case "upcoming":
35 | return upcomingCalls;
36 |
37 | default:
38 | return [];
39 | }
40 | };
41 |
42 | const getNoCallsMessage = () => {
43 | switch (type) {
44 | case "ended":
45 | return "No previoud calls.";
46 |
47 | case "recordings":
48 | return "No recordings.";
49 |
50 | case "upcoming":
51 | return "No upcoming calls.";
52 |
53 | default:
54 | return "No calls.";
55 | }
56 | };
57 |
58 | useEffect(() => {
59 | const fetchRecordings = async () => {
60 | if (!callRecordings) return;
61 |
62 | try {
63 | const callData = await Promise.all(
64 | callRecordings.map((call) => call.queryRecordings())
65 | );
66 |
67 | const recordings = callData
68 | .filter((call) => call.recordings.length > 0)
69 | .flatMap((call) => call.recordings);
70 |
71 | setRecordings(recordings);
72 | } catch (error) {
73 | toast({ title: "Try again later." });
74 | }
75 | };
76 |
77 | if (type === "recordings") fetchRecordings();
78 | }, [type, callRecordings, toast]);
79 |
80 | const calls = getCalls();
81 | const noCallsMessage = getNoCallsMessage();
82 |
83 | if (isLoading) return ;
84 |
85 | return (
86 |
87 | {calls && calls.length > 0 ? (
88 | calls.map((call: Call | CallRecording, i) => (
89 | router.push(`${(call as CallRecording).url}`)
113 | : () => router.push(`/meeting/${(call as Call).id}`)
114 | }
115 | link={
116 | type === "recordings"
117 | ? (call as CallRecording).url
118 | : `${process.env.NEXT_PUBLIC_BASE_URL}/meeting/${
119 | (call as Call).id
120 | }`
121 | }
122 | />
123 | ))
124 | ) : (
125 | {noCallsMessage}
126 | )}
127 |
128 | );
129 | };
130 |
--------------------------------------------------------------------------------
/components/meeting-room.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | CallControls,
5 | CallParticipantsList,
6 | CallStatsButton,
7 | CallingState,
8 | PaginatedGridLayout,
9 | SpeakerLayout,
10 | useCallStateHooks,
11 | } from "@stream-io/video-react-sdk";
12 | import { LayoutList, Users } from "lucide-react";
13 | import { useRouter, useSearchParams } from "next/navigation";
14 | import { useState } from "react";
15 |
16 | import {
17 | DropdownMenu,
18 | DropdownMenuContent,
19 | DropdownMenuItem,
20 | DropdownMenuSeparator,
21 | DropdownMenuTrigger,
22 | } from "@/components/ui/dropdown-menu";
23 | import { cn } from "@/lib/utils";
24 |
25 | import { EndCallButton } from "./end-call-button";
26 | import { Loader } from "./loader";
27 |
28 | type CallLayoutType = "grid" | "speaker-left" | "speaker-right";
29 |
30 | export const MeetingRoom = () => {
31 | const router = useRouter();
32 | const searchParams = useSearchParams();
33 | const [showParticipants, setShowParticipants] = useState(false);
34 | const [layout, setLayout] = useState("speaker-left");
35 |
36 | const { useCallCallingState } = useCallStateHooks();
37 | const callingState = useCallCallingState();
38 |
39 | const isPersonalRoom = !!searchParams.get("personal");
40 |
41 | if (callingState !== CallingState.JOINED) return ;
42 |
43 | const CallLayout = () => {
44 | switch (layout) {
45 | case "grid":
46 | return ;
47 | case "speaker-right":
48 | return ;
49 | default:
50 | return ;
51 | }
52 | };
53 |
54 | return (
55 |
56 |
57 |
58 |
59 |
60 |
61 |
66 | setShowParticipants(false)} />
67 |
68 |
69 |
70 |
71 |
router.push("/")} />
72 |
73 |
74 |
75 |
79 |
80 |
81 |
82 |
83 | {["Grid", "Speaker Left", "Speaker Right"].map((item, i) => (
84 |
85 |
88 | setLayout(
89 | item.toLowerCase().replace(" ", "-") as CallLayoutType
90 | )
91 | }
92 | >
93 | {item}
94 |
95 |
96 |
97 |
98 | ))}
99 |
100 |
101 |
102 |
103 |
104 |
114 |
115 | {!isPersonalRoom && }
116 |
117 |
118 | );
119 | };
120 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { X } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = DialogPrimitive.Portal;
14 |
15 | const DialogClose = DialogPrimitive.Close;
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ));
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | );
68 | DialogHeader.displayName = "DialogHeader";
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | );
82 | DialogFooter.displayName = "DialogFooter";
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ));
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | };
123 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SheetPrimitive from "@radix-ui/react-dialog";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 | import { X } from "lucide-react";
7 |
8 | import { cn } from "@/lib/utils";
9 |
10 | const Sheet = SheetPrimitive.Root;
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger;
13 |
14 | const SheetClose = SheetPrimitive.Close;
15 |
16 | const SheetPortal = SheetPrimitive.Portal;
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ));
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | );
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ));
75 | SheetContent.displayName = SheetPrimitive.Content.displayName;
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | );
89 | SheetHeader.displayName = "SheetHeader";
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | );
103 | SheetFooter.displayName = "SheetFooter";
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ));
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName;
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ));
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName;
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | };
141 |
--------------------------------------------------------------------------------
/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react";
5 |
6 | import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
7 |
8 | const TOAST_LIMIT = 1;
9 | const TOAST_REMOVE_DELAY = 1000000;
10 |
11 | type ToasterToast = ToastProps & {
12 | id: string;
13 | title?: React.ReactNode;
14 | description?: React.ReactNode;
15 | action?: ToastActionElement;
16 | };
17 |
18 | const actionTypes = {
19 | ADD_TOAST: "ADD_TOAST",
20 | UPDATE_TOAST: "UPDATE_TOAST",
21 | DISMISS_TOAST: "DISMISS_TOAST",
22 | REMOVE_TOAST: "REMOVE_TOAST",
23 | } as const;
24 |
25 | let count = 0;
26 |
27 | function genId() {
28 | count = (count + 1) % Number.MAX_SAFE_INTEGER;
29 | return count.toString();
30 | }
31 |
32 | type ActionType = typeof actionTypes;
33 |
34 | type Action =
35 | | {
36 | type: ActionType["ADD_TOAST"];
37 | toast: ToasterToast;
38 | }
39 | | {
40 | type: ActionType["UPDATE_TOAST"];
41 | toast: Partial;
42 | }
43 | | {
44 | type: ActionType["DISMISS_TOAST"];
45 | toastId?: ToasterToast["id"];
46 | }
47 | | {
48 | type: ActionType["REMOVE_TOAST"];
49 | toastId?: ToasterToast["id"];
50 | };
51 |
52 | interface State {
53 | toasts: ToasterToast[];
54 | }
55 |
56 | const toastTimeouts = new Map>();
57 |
58 | const addToRemoveQueue = (toastId: string) => {
59 | if (toastTimeouts.has(toastId)) {
60 | return;
61 | }
62 |
63 | const timeout = setTimeout(() => {
64 | toastTimeouts.delete(toastId);
65 | dispatch({
66 | type: "REMOVE_TOAST",
67 | toastId: toastId,
68 | });
69 | }, TOAST_REMOVE_DELAY);
70 |
71 | toastTimeouts.set(toastId, timeout);
72 | };
73 |
74 | export const reducer = (state: State, action: Action): State => {
75 | switch (action.type) {
76 | case "ADD_TOAST":
77 | return {
78 | ...state,
79 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
80 | };
81 |
82 | case "UPDATE_TOAST":
83 | return {
84 | ...state,
85 | toasts: state.toasts.map((t) =>
86 | t.id === action.toast.id ? { ...t, ...action.toast } : t
87 | ),
88 | };
89 |
90 | case "DISMISS_TOAST": {
91 | const { toastId } = action;
92 |
93 | // ! Side effects ! - This could be extracted into a dismissToast() action,
94 | // but I'll keep it here for simplicity
95 | if (toastId) {
96 | addToRemoveQueue(toastId);
97 | } else {
98 | state.toasts.forEach((toast) => {
99 | addToRemoveQueue(toast.id);
100 | });
101 | }
102 |
103 | return {
104 | ...state,
105 | toasts: state.toasts.map((t) =>
106 | t.id === toastId || toastId === undefined
107 | ? {
108 | ...t,
109 | open: false,
110 | }
111 | : t
112 | ),
113 | };
114 | }
115 | case "REMOVE_TOAST":
116 | if (action.toastId === undefined) {
117 | return {
118 | ...state,
119 | toasts: [],
120 | };
121 | }
122 | return {
123 | ...state,
124 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
125 | };
126 | }
127 | };
128 |
129 | const listeners: Array<(state: State) => void> = [];
130 |
131 | let memoryState: State = { toasts: [] };
132 |
133 | function dispatch(action: Action) {
134 | memoryState = reducer(memoryState, action);
135 | listeners.forEach((listener) => {
136 | listener(memoryState);
137 | });
138 | }
139 |
140 | type Toast = Omit;
141 |
142 | function toast({ ...props }: Toast) {
143 | const id = genId();
144 |
145 | const update = (props: ToasterToast) =>
146 | dispatch({
147 | type: "UPDATE_TOAST",
148 | toast: { ...props, id },
149 | });
150 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
151 |
152 | dispatch({
153 | type: "ADD_TOAST",
154 | toast: {
155 | ...props,
156 | id,
157 | open: true,
158 | onOpenChange: (open) => {
159 | if (!open) dismiss();
160 | },
161 | },
162 | });
163 |
164 | return {
165 | id: id,
166 | dismiss,
167 | update,
168 | };
169 | }
170 |
171 | function useToast() {
172 | const [state, setState] = React.useState(memoryState);
173 |
174 | React.useEffect(() => {
175 | listeners.push(setState);
176 | return () => {
177 | const index = listeners.indexOf(setState);
178 | if (index > -1) {
179 | listeners.splice(index, 1);
180 | }
181 | };
182 | }, [state]);
183 |
184 | return {
185 | ...state,
186 | toast,
187 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
188 | };
189 | }
190 |
191 | export { useToast, toast };
192 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ToastPrimitives from "@radix-ui/react-toast";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 | import { X } from "lucide-react";
7 |
8 | import { cn } from "@/lib/utils";
9 |
10 | const ToastProvider = ToastPrimitives.Provider;
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ));
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border-none bg-dark-1 text-white",
33 | destructive: "border-none bg-rose-500 text-white",
34 | },
35 | },
36 | defaultVariants: {
37 | variant: "default",
38 | },
39 | }
40 | );
41 |
42 | const Toast = React.forwardRef<
43 | React.ElementRef,
44 | React.ComponentPropsWithoutRef &
45 | VariantProps
46 | >(({ className, variant, ...props }, ref) => {
47 | return (
48 |
53 | );
54 | });
55 | Toast.displayName = ToastPrimitives.Root.displayName;
56 |
57 | const ToastAction = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
69 | ));
70 | ToastAction.displayName = ToastPrimitives.Action.displayName;
71 |
72 | const ToastClose = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >(({ className, ...props }, ref) => (
76 |
85 |
86 |
87 | ));
88 | ToastClose.displayName = ToastPrimitives.Close.displayName;
89 |
90 | const ToastTitle = React.forwardRef<
91 | React.ElementRef,
92 | React.ComponentPropsWithoutRef
93 | >(({ className, ...props }, ref) => (
94 |
99 | ));
100 | ToastTitle.displayName = ToastPrimitives.Title.displayName;
101 |
102 | const ToastDescription = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ));
112 | ToastDescription.displayName = ToastPrimitives.Description.displayName;
113 |
114 | type ToastProps = React.ComponentPropsWithoutRef;
115 |
116 | type ToastActionElement = React.ReactElement;
117 |
118 | export {
119 | type ToastProps,
120 | type ToastActionElement,
121 | ToastProvider,
122 | ToastViewport,
123 | Toast,
124 | ToastTitle,
125 | ToastDescription,
126 | ToastClose,
127 | ToastAction,
128 | };
129 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct - Yoom
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to make participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to a positive environment for our
15 | community include:
16 |
17 | - Demonstrating empathy and kindness toward other people
18 | - Being respectful of differing opinions, viewpoints, and experiences
19 | - Giving and gracefully accepting constructive feedback
20 | - Accepting responsibility and apologizing to those affected by our mistakes,
21 | and learning from the experience
22 | - Focusing on what is best not just for us as individuals, but for the
23 | overall community
24 |
25 | Examples of unacceptable behavior include:
26 |
27 | - The use of sexualized language or imagery, and sexual attention or
28 | advances
29 | - Trolling, insulting or derogatory comments, and personal or political attacks
30 | - Public or private harassment
31 | - Publishing others' private information, such as a physical or email
32 | address, without their explicit permission
33 | - Other conduct which could reasonably be considered inappropriate in a
34 | professional setting
35 |
36 | ## Our Responsibilities
37 |
38 | Project maintainers are responsible for clarifying and enforcing our standards of
39 | acceptable behavior and will take appropriate and fair corrective action in
40 | response to any behavior that they deem inappropriate,
41 | threatening, offensive, or harmful.
42 |
43 | Project maintainers have the right and responsibility to remove, edit, or reject
44 | comments, commits, code, wiki edits, issues, and other contributions that are
45 | not aligned to this Code of Conduct, and will
46 | communicate reasons for moderation decisions when appropriate.
47 |
48 | ## Scope
49 |
50 | This Code of Conduct applies within all community spaces, and also applies when
51 | an individual is officially representing the community in public spaces.
52 | Examples of representing our community include using an official e-mail address,
53 | posting via an official social media account, or acting as an appointed
54 | representative at an online or offline event.
55 |
56 | ## Enforcement
57 |
58 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
59 | reported to the community leaders responsible for enforcement at .
60 | All complaints will be reviewed and investigated promptly and fairly.
61 |
62 | All community leaders are obligated to respect the privacy and security of the
63 | reporter of any incident.
64 |
65 | ## Enforcement Guidelines
66 |
67 | Community leaders will follow these Community Impact Guidelines in determining
68 | the consequences for any action they deem in violation of this Code of Conduct:
69 |
70 | ### 1. Correction
71 |
72 | **Community Impact**: Use of inappropriate language or other behavior deemed
73 | unprofessional or unwelcome in the community.
74 |
75 | **Consequence**: A private, written warning from community leaders, providing
76 | clarity around the nature of the violation and an explanation of why the
77 | behavior was inappropriate. A public apology may be requested.
78 |
79 | ### 2. Warning
80 |
81 | **Community Impact**: A violation through a single incident or series
82 | of actions.
83 |
84 | **Consequence**: A warning with consequences for continued behavior. No
85 | interaction with the people involved, including unsolicited interaction with
86 | those enforcing the Code of Conduct, for a specified period of time. This
87 | includes avoiding interactions in community spaces as well as external channels
88 | like social media. Violating these terms may lead to a temporary or
89 | permanent ban.
90 |
91 | ### 3. Temporary Ban
92 |
93 | **Community Impact**: A serious violation of community standards, including
94 | sustained inappropriate behavior.
95 |
96 | **Consequence**: A temporary ban from any sort of interaction or public
97 | communication with the community for a specified period of time. No public or
98 | private interaction with the people involved, including unsolicited interaction
99 | with those enforcing the Code of Conduct, is allowed during this period.
100 | Violating these terms may lead to a permanent ban.
101 |
102 | ### 4. Permanent Ban
103 |
104 | **Community Impact**: Demonstrating a pattern of violation of community
105 | standards, including sustained inappropriate behavior, harassment of an
106 | individual, or aggression toward or disparagement of classes of individuals.
107 |
108 | **Consequence**: A permanent ban from any sort of public interaction within
109 | the community.
110 |
111 | ## Attribution
112 |
113 | This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org/), version
114 | [1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.md) and
115 | [2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct/code_of_conduct.md),
116 | and was generated by [contributing-gen](https://github.com/bttger/contributing-gen).
117 |
--------------------------------------------------------------------------------
/components/meeting-type-list.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useUser } from "@clerk/nextjs";
4 | import { type Call, useStreamVideoClient } from "@stream-io/video-react-sdk";
5 | import { useRouter } from "next/navigation";
6 | import { useState } from "react";
7 | import ReactDatePicker from "react-datepicker";
8 |
9 | import { MeetingModal } from "@/components/modals/meeting-modal";
10 | import { Input } from "@/components/ui/input";
11 | import { Textarea } from "@/components/ui/textarea";
12 | import { useToast } from "@/components/ui/use-toast";
13 |
14 | import { HomeCard } from "./home-card";
15 | import { Loader } from "./loader";
16 |
17 | type MeetingState =
18 | | "isScheduleMeeting"
19 | | "isJoiningMeeting"
20 | | "isInstantMeeting"
21 | | undefined;
22 |
23 | export const MeetingTypeList = () => {
24 | const router = useRouter();
25 | const { toast } = useToast();
26 |
27 | const [isLoading, setIsLoading] = useState(false);
28 | const [callDetails, setCallDetails] = useState();
29 | const [meetingState, setMeetingState] = useState(undefined);
30 | const [values, setValues] = useState({
31 | dateTime: new Date(),
32 | description: "",
33 | link: "",
34 | });
35 |
36 | const { user } = useUser();
37 | const streamClient = useStreamVideoClient();
38 |
39 | const createMeeting = async () => {
40 | if (!streamClient || !user || !user?.id) return;
41 |
42 | try {
43 | setIsLoading(true);
44 |
45 | if (!values.dateTime) {
46 | return toast({
47 | title: "Please select a date and time.",
48 | variant: "destructive",
49 | });
50 | }
51 |
52 | const id = crypto.randomUUID();
53 | const call = streamClient.call("default", id);
54 |
55 | if (!call) throw new Error("Failed to create call.");
56 |
57 | const startsAt =
58 | values.dateTime.toISOString() || new Date(Date.now()).toISOString();
59 | const description = values.description || "Instant meeting";
60 |
61 | await call.getOrCreate({
62 | data: {
63 | starts_at: startsAt,
64 | custom: {
65 | description,
66 | },
67 | },
68 | });
69 |
70 | setCallDetails(call);
71 |
72 | if (!values?.description) {
73 | router.push(`/meeting/${call.id}`);
74 | }
75 |
76 | toast({
77 | title: "Meeting created.",
78 | });
79 | } catch (error) {
80 | console.error("CREATE_MEETING: ", error);
81 |
82 | toast({
83 | title: "Failed to create meeting.",
84 | variant: "destructive",
85 | });
86 | } finally {
87 | setIsLoading(false);
88 | }
89 | };
90 |
91 | if (!streamClient || !user || !user?.id) return ;
92 |
93 | const meetingLink = `${process.env.NEXT_PUBLIC_BASE_URL}/meeting/${callDetails?.id}`;
94 |
95 | return (
96 |
216 | );
217 | };
218 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5 | import { Check, ChevronRight, Circle } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root;
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group;
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean;
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ));
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName;
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ));
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName;
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ));
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean;
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ));
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ));
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName;
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ));
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean;
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ));
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ));
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | );
181 | };
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | };
201 |
--------------------------------------------------------------------------------
/public/icons/logo.svg:
--------------------------------------------------------------------------------
1 |
102 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Yoom - Modern Next.js powered Video calling app
4 |
5 |
6 |
7 |
8 |
9 |
10 | # :notebook_with_decorative_cover: Table of Contents
11 |
12 |
13 |
14 | - [Folder Structure](#bangbang-folder-structure)
15 | - [Getting Started](#toolbox-getting-started)
16 | - [Screenshots](#camera-screenshots)
17 | - [Tech Stack](#gear-tech-stack)
18 | - [Stats](#wrench-stats)
19 | - [Contribute](#raised_hands-contribute)
20 | - [Acknowledgements](#gem-acknowledgements)
21 | - [Buy Me a Coffee](#coffee-buy-me-a-coffee)
22 | - [Follow Me](#rocket-follow-me)
23 | - [Learn More](#books-learn-more)
24 | - [Deploy on Netlify](#page_with_curl-deploy-on-netlify)
25 | - [Give A Star](#star-give-a-star)
26 | - [Star History](#star2-star-history)
27 | - [Give A Star](#star-give-a-star)
28 |
29 |
30 |
31 | ## :bangbang: Folder Structure
32 |
33 | Here is the folder structure of this app.
34 |
35 | ```bash
36 | zoom-clone/
37 | |- actions/
38 | |- stream.actions.ts
39 | |- app/
40 | |-- (auth)/
41 | |--- sign-in/[[...sign-in]]/
42 | |--- sign-up/[[...sign-up]]/
43 | |-- (root)/
44 | |--- (home)
45 | |--- meeting
46 | |--- layout.tsx
47 | |-- apple-icon.png
48 | |-- favicon.ico
49 | |-- globals.css
50 | |-- icon1.png
51 | |-- icon2.png
52 | |-- layout.tsx
53 | |- components/
54 | |-- modals/
55 | |-- ui/
56 | |-- call-list.tsx
57 | |-- end-call-button.tsx
58 | |-- home-card.tsx
59 | |-- loader.tsx
60 | |-- meeting-card.tsx
61 | |-- meeting-room.tsx
62 | |-- meeting-setup.tsx
63 | |-- meeting-type-list.tsx
64 | |-- mobile-nav.tsx
65 | |-- navbar.tsx
66 | |-- sidebar.tsx
67 | |- config/
68 | |-- index.ts
69 | |- constants/
70 | |-- index.ts
71 | |- hooks/
72 | |-- use-get-call-by-id.ts
73 | |-- use-get-calls.ts
74 | |- lib/
75 | |-- utils.ts
76 | |- public/
77 | |-- icons/
78 | |-- images/
79 | |- scripts/
80 | |-- prod.ts
81 | |-- reset.ts
82 | |-- seed.ts
83 | |- store/
84 | |-- use-exit-modal.ts
85 | |-- use-hearts-modal.ts
86 | |-- use-practice-modal.ts
87 | |- types/
88 | |-- canvas.ts
89 | |- .env.example
90 | |- .env.local
91 | |- .eslintrc.js
92 | |- .gitignore
93 | |- .prettierrc.json
94 | |- components.json
95 | |- environment.d.ts
96 | |- middleware.ts
97 | |- next.config.mjs
98 | |- package-lock.json
99 | |- package.json
100 | |- postcss.config.js
101 | |- tailwind.config.ts
102 | |- tsconfig.json
103 | ```
104 |
105 |
106 |
107 | ## :toolbox: Getting Started
108 |
109 | 1. Make sure **Git** and **NodeJS** is installed.
110 | 2. Clone this repository to your local computer.
111 | 3. Create `.env.local` file in **root** directory.
112 | 4. Contents of `.env.local`:
113 |
114 | ```env
115 | # .env.local
116 |
117 | # disabled next.js telemetry
118 | NEXT_TELEMETRY_DISABLED=1
119 |
120 | # clerk auth keys
121 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
122 | CLERK_SECRET_KEY=sk_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
123 |
124 | # clerk auth redirect urls
125 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
126 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
127 |
128 | # stream api keys
129 | NEXT_PUBLIC_STREAM_API_KEY=xxxxxxxxxxxxxxxxxxxxxx
130 | STREAM_SECRET_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
131 |
132 | # app base url
133 | NEXT_PUBLIC_BASE_URL=http://localhost:3000
134 |
135 | ```
136 |
137 | ### 5. Obtain Clerk Authentication Keys
138 |
139 | 1. **Source**: Clerk Dashboard or Settings Page
140 | 2. **Procedure**:
141 | - Log in to your Clerk account.
142 | - Navigate to the dashboard or settings page.
143 | - Look for the section related to authentication keys.
144 | - Copy the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` provided in that section.
145 |
146 | ### 6. Specify Public App URL
147 |
148 | 1. **Procedure**:
149 | - Replace `http://localhost:3000` with the URL of your deployed application.
150 |
151 | ### 7. Set Up Stream
152 |
153 | 1. **Create a Stream Account**:
154 |
155 | - If you don't have a Stream account, sign up at [GetStream.io](https://getstream.io/).
156 |
157 | 2. **Create a New App**:
158 | - After logging in, navigate to the Stream dashboard.
159 | - Click on "Create App" to set up a new application for Zoom-Clone.
160 | - Provide a name for your app and select the appropriate region.
161 |
162 | ### 8. Obtain the Stream API Key and Secret Key
163 |
164 | 1. **Navigate to the App Settings**:
165 |
166 | - In your Stream dashboard, select the app you created for Zoom-Clone.
167 | - Go to the "Overview" or "Keys" section.
168 |
169 | 2. **Copy the API Key**:
170 |
171 | - You will find the "API Key" listed in the app details. Copy this key.
172 |
173 | 3. **Copy the Secret Key**:
174 | - In the same section, you will find the "Secret Key". Copy this key as well.
175 |
176 | ### 9. Save and Secure:
177 |
178 | - Save the changes to the `.env.local` file.
179 |
180 | 10. Install Project Dependencies using `npm install --legacy-peer-deps` or `yarn install --legacy-peer-deps`.
181 |
182 | 11. Now app is fully configured 👍 and you can start using this app using either one of `npm run dev` or `yarn dev`.
183 |
184 | **NOTE:** Please make sure to keep your API keys and configuration values secure and do not expose them publicly.
185 |
186 | ## :camera: Screenshots
187 |
188 | 
189 |
190 | 
191 |
192 | 
193 |
194 | ## :gear: Tech Stack
195 |
196 | [](https://react.dev/ "React JS") [](https://nextjs.org/ "Next JS") [](https://www.typescriptlang.org/ "Typescript") [](https://tailwindcss.com/ "Tailwind CSS") [](https://vercel.app/ "Vercel")
197 |
198 | ## :wrench: Stats
199 |
200 | [](https://pagespeed.web.dev/analysis?url=https://clone-yoom.netlify.app "Stats for Yoom")
201 |
202 | ## :raised_hands: Contribute
203 |
204 | You might encounter some bugs while using this app. You are more than welcome to contribute. Just submit changes via pull request and I will review them before merging. Make sure you follow community guidelines.
205 |
206 | ## :gem: Acknowledgements
207 |
208 | Useful resources and dependencies that are used in Yoom.
209 |
210 | - [@clerk/nextjs](https://www.npmjs.com/package/@clerk/nextjs): ^5.1.3
211 | - [@radix-ui/react-dialog](https://www.npmjs.com/package/@radix-ui/react-dialog): ^1.0.5
212 | - [@radix-ui/react-dropdown-menu](https://www.npmjs.com/package/@radix-ui/react-dropdown-menu): ^2.0.6
213 | - [@radix-ui/react-slot](https://www.npmjs.com/package/@radix-ui/react-slot): ^1.0.2
214 | - [@radix-ui/react-toast](https://www.npmjs.com/package/@radix-ui/react-toast): ^1.1.5
215 | - [@stream-io/node-sdk](https://www.npmjs.com/package/@stream-io/node-sdk): ^0.2.3
216 | - [@stream-io/video-react-sdk](https://www.npmjs.com/package/@stream-io/video-react-sdk): ^1.0.13
217 | - [class-variance-authority](https://www.npmjs.com/package/class-variance-authority): ^0.7.0
218 | - [clsx](https://www.npmjs.com/package/clsx): ^2.1.1
219 | - [lucide-react](https://www.npmjs.com/package/lucide-react): ^0.379.0
220 | - [next](https://www.npmjs.com/package/next): 14.2.3
221 | - [react](https://www.npmjs.com/package/react): ^18
222 | - [react-datepicker](https://www.npmjs.com/package/react-datepicker): ^6.9.0
223 | - [react-dom](https://www.npmjs.com/package/react-dom): ^18
224 | - [tailwind-merge](https://www.npmjs.com/package/tailwind-merge): ^2.3.0
225 | - [tailwindcss-animate](https://www.npmjs.com/package/tailwindcss-animate): ^1.0.7
226 | - [@types/node](https://www.npmjs.com/package/@types/node): ^20
227 | - [@types/react](https://www.npmjs.com/package/@types/react): ^18
228 | - [@types/react-datepicker](https://www.npmjs.com/package/@types/react-datepicker): ^6.2.0
229 | - [@types/react-dom](https://www.npmjs.com/package/@types/react-dom): ^18
230 | - [eslint](https://www.npmjs.com/package/eslint): ^8
231 | - [eslint-config-next](https://www.npmjs.com/package/eslint-config-next): 14.2.3
232 | - [postcss](https://www.npmjs.com/package/postcss): ^8
233 | - [tailwindcss](https://www.npmjs.com/package/tailwindcss): ^3.4.1
234 | - [typescript](https://www.npmjs.com/package/typescript): ^5
235 |
236 | ## :books: Learn More
237 |
238 | To learn more about Next.js, take a look at the following resources:
239 |
240 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
241 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
242 |
243 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
244 |
245 | ## :page_with_curl: Deploy on Netlify
246 |
247 | The simplest way to deploy your React.js app is to use the [Netlify Platform](https://app.netlify.com/start) - a powerful platform for modern web projects.
248 |
249 | Explore the [Netlify deployment documentation](https://docs.netlify.com/site-deploys/create-deploys) for step-by-step instructions on deploying your React.js app on Netlify.
250 |
251 | Happy coding, and feel free to share your thoughts and improvements with the [Netlify community](https://community.netlify.com)!
252 |
253 | ## :star: Give A Star
254 |
255 | You can also give this repository a star to show more people and they can use this repository.
256 |
257 |
258 |
259 | (back to top)
260 |
--------------------------------------------------------------------------------
/public/icons/yoom-logo.svg:
--------------------------------------------------------------------------------
1 |
103 |
--------------------------------------------------------------------------------
/.github/images/stats.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/icons/checked.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------