├── .env.example ├── .eslintrc.cjs ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── components.json ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── prettier.config.cjs ├── prisma ├── migrations │ ├── 20230606005725_init │ │ └── migration.sql │ ├── 20230624043330_call_name_change │ │ └── migration.sql │ ├── 20230624043916_call_name_change │ │ └── migration.sql │ ├── 20230624223235_schema_change │ │ └── migration.sql │ ├── 20230629195519_make_user_id_nullable │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── features-image.png ├── hero-image.png ├── site.webmanifest └── web-shot.png ├── src ├── app │ ├── (auth) │ │ ├── login │ │ │ └── page.tsx │ │ └── register │ │ │ └── page.tsx │ ├── (call) │ │ └── call │ │ │ └── [[...slug]] │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ ├── (calls) │ │ └── calls │ │ │ ├── history │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ ├── (home) │ │ ├── layout.tsx │ │ └── page.tsx │ ├── (preview) │ │ └── preview │ │ │ └── [[...slug]] │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── call │ │ │ ├── code │ │ │ │ └── route.ts │ │ │ ├── create │ │ │ │ └── route.ts │ │ │ ├── delete │ │ │ │ └── route.ts │ │ │ ├── join │ │ │ │ └── route.ts │ │ │ └── leave │ │ │ │ └── route.ts │ │ └── sendEmail │ │ │ └── route.ts │ ├── layout.tsx │ └── sitemap.ts ├── components │ ├── call │ │ ├── call-footer.tsx │ │ ├── call-history-per-page-dropdown.tsx │ │ ├── conference.tsx │ │ ├── create-call-card.tsx │ │ ├── delete-call-actions.tsx │ │ ├── invite-participants-dialog.tsx │ │ ├── join-call-dialog.tsx │ │ ├── pagination.tsx │ │ └── rejoin-call.tsx │ ├── delete-action-alert.tsx │ ├── email-template.tsx │ ├── layout │ │ ├── card-shell.tsx │ │ ├── footer.tsx │ │ ├── full-nav.tsx │ │ └── user-account-dropdown.tsx │ ├── mode-toggle.tsx │ ├── room-provider.tsx │ ├── social-auth-form.tsx │ ├── theme-provider.tsx │ ├── ui │ │ ├── alert-dialog.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── icons.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── peer.tsx │ │ ├── select.tsx │ │ ├── table.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── use-toast.ts │ │ └── video.tsx │ ├── user-avatar-label-group.tsx │ └── user-avatar.tsx ├── config │ └── site-config.ts ├── context │ └── call-id-context.tsx ├── env.mjs ├── hooks │ └── use-copy.tsx ├── lib │ ├── date.ts │ ├── extract-id.ts │ ├── session.ts │ └── utils.ts ├── middleware.ts ├── schemas │ ├── call.ts │ ├── invite.ts │ └── join.ts ├── server │ ├── auth.ts │ ├── db.ts │ └── management-token.ts ├── styles │ └── globals.css ├── types │ ├── next-auth.d.ts │ └── types.ts └── utils │ └── absoluteUrl.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Since the ".env" file is gitignored, you can use the ".env.example" file to 2 | # build a new ".env" file when you clone the repo. Keep this file up-to-date 3 | # when you add new variables to `.env`. 4 | 5 | # This file will be committed to version control, so make sure not to have any 6 | # secrets in it. If you are cloning this repo, create a copy of this file named 7 | # ".env" and populate it with your secrets. 8 | 9 | # When adding additional environment variables, the schema in "/src/env.mjs" 10 | # should be updated accordingly. 11 | 12 | # Next Auth 13 | # You can generate a new secret on the command line with: 14 | # openssl rand -base64 32 15 | # https://next-auth.js.org/configuration/options#secret 16 | # NEXTAUTH_SECRET="" 17 | NEXTAUTH_URL="http://localhost:3000" 18 | 19 | # Next Auth Github Provider 20 | # Your GitHub OAuth credentials. 21 | GITHUB_CLIENT_ID="your-github-client-id" 22 | GITHUB_CLIENT_SECRET="your-github-client-secret" 23 | 24 | # Next Auth Google Provider 25 | # Your Google OAuth credentials. 26 | GOOGLE_CLIENT_ID="your-google-client-id" 27 | GOOGLE_CLIENT_SECRET="your-google-client-secret" 28 | 29 | # Next Auth Discord Provider 30 | # Your Discord OAuth credentials. 31 | DISCORD_CLIENT_ID="your-discord-client-id" 32 | DISCORD_CLIENT_SECRET="your-discord-client-secret" 33 | 34 | # The base URL of your app. 35 | NEXT_PUBLIC_APP_URL="http://localhost:3000" 36 | 37 | # Your database connection string. 38 | DATABASE_URL="your-database-url" 39 | 40 | # Your Resend API key. 41 | RESEND_API_KEY="your-resend-api-key" 42 | 43 | # 100ms access credentials 44 | # The endpoint for the 100ms API. 45 | TOKEN_ENDPOINT="https://api.100ms.live/v2" 46 | 47 | # Your 100ms template ID. 48 | TEMPLATE_ID="your-template-id" 49 | 50 | # Your 100ms access key. 51 | ACCESS_KEY="your-access-key" 52 | 53 | # Your 100ms app secret. 54 | APP_SECRET="your-app-secret" 55 | 56 | 57 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const path = require("path"); 3 | 4 | /** @type {import("eslint").Linter.Config} */ 5 | const config = { 6 | overrides: [ 7 | { 8 | extends: [ 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | ], 11 | files: ["*.ts", "*.tsx"], 12 | parserOptions: { 13 | project: path.join(__dirname, "tsconfig.json"), 14 | }, 15 | }, 16 | ], 17 | parser: "@typescript-eslint/parser", 18 | parserOptions: { 19 | project: path.join(__dirname, "tsconfig.json"), 20 | }, 21 | plugins: ["@typescript-eslint"], 22 | extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], 23 | rules: { 24 | "@typescript-eslint/consistent-type-imports": [ 25 | "warn", 26 | { 27 | prefer: "type-imports", 28 | fixStyle: "inline-type-imports", 29 | }, 30 | ], 31 | "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], 32 | }, 33 | }; 34 | 35 | module.exports = config; 36 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | next-env.d.ts 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # local env files 34 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 35 | .env 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | . 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | . Translations are available at 128 | . -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Thank you for investing your time in contributing to our project! Any contribution you make will be reflected on [skateshop](<[skateshop.sadmn.com](https://github.com/sadmann7/skateshop)>) :sparkles:. 4 | 5 | Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. 6 | 7 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 8 | 9 | ## New contributor guide 10 | 11 | To get an overview of the project, read the [README](README.md). Here are some resources to help you get started with open source contributions: 12 | 13 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 14 | - [Understanding the GitHub flow](https://guides.github.com/introduction/flow/) 15 | 16 | - [GitHub Help Documentation](https://help.github.com/) 17 | - [GitHub Flavored Markdown](https://guides.github.com/features/mastering-markdown/) 18 | 19 | ## Getting started 20 | 21 | ### Fork the repository 22 | 23 | Fork the project [on GitHub](https://github.com/sadmann7/skateshop) 24 | 25 | ### Clone the project 26 | 27 | Clone your fork locally. Do not clone the original repository unless you plan to become a long-term contributor and have been given permission to do so. 28 | 29 | ```shell 30 | git clone https://github.com/sadmann7/skateshop 31 | cd skateshop 32 | ``` 33 | 34 | ### Install dependencies 35 | 36 | Install the project dependencies: 37 | 38 | ```shell 39 | pnpm install 40 | ``` 41 | 42 | ### Create a branch 43 | 44 | Create and check out your feature branch: 45 | 46 | ```shell 47 | git checkout -b my-new-feature 48 | ``` 49 | 50 | ### Make changes locally 51 | 52 | Make your changes to the codebase. See the [development guide](contributing/development.md) for more information. 53 | 54 | ### Commit your changes 55 | 56 | Commit your changes: 57 | 58 | ```shell 59 | git commit -m 'Add some feature' 60 | ``` 61 | 62 | ### Push your changes 63 | 64 | Push your changes to your fork: 65 | 66 | ```shell 67 | git push -u origin my-new-feature 68 | ``` 69 | 70 | ### Create a pull request 71 | 72 | When you're finished with the changes, create a pull request, also known as a PR. 73 | 74 | - Fill the "Ready for review" template so that we can review your PR. This template helps reviewers understand your changes as well as the purpose of your pull request. 75 | 76 | ### Issues 77 | 78 | #### Create a new issue 79 | 80 | If you spot a problem in the codebase that you believe needs to be fixed, or you have an idea for a new feature, take a look at the [Issues](https://github.com/sadmann7/skateshop/issues). 81 | 82 | If you can't find an open issue addressing the problem, [open a new one](https://github.com/sadmann7/skateshop/issues/new). Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring. 83 | 84 | #### Solve an issue 85 | 86 | Scan through our [existing issues](https://github.com/sadmann7/skateshop/issues) to find one that interests you. You can narrow down the search using `labels` and `projects` to find issues that need attention. 87 | 88 | Then, fork the repository, create a branch, and make your changes. 89 | 90 | Finally, open a pull request with the changes. 91 | 92 | ### Your PR is merged 93 | 94 | Congratulations :tada::tada: The GitHub team thanks you :sparkles:. 95 | 96 | Once your PR is merged, your contributions will be publicly visible on the [skateshop](https://github.com/sadmann7/skateshop). 97 | 98 | ### Credits 99 | 100 | This Contributing Guide is adapted from [GitHub docs contributing guide](https://github.com/github/docs/blob/main/CONTRIBUTING.md?plain=1). -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2023 Jaleel Bennett 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Callsquare 2 | 3 | > **Warning** 4 | > This is a work-in-progress and not the finished product. 5 | > 6 | > I work on this project fir an hour or so whenever I have free time during the night after work. Feel free to leave feature suggestions and/or contribute to the project. 7 | 8 | [![Callsquare](./public/web-shot.png)](https://callsquare.jaleelbennett.com/) 9 | 10 | ## About this project 11 | 12 | This project was conceived as an experiment to explore the capabilities of Next.js 13. The primary objective was to build a modern application that incorporates a variety of features including, but not limited to, authentication, API route handlers, middleware, and server components. 13 | 14 | The application is designed to simulate a video call platform, providing a practical context in which to apply and test these features. It leverages the power of Next.js 13 to handle server-side operations, routing, and server side rendering. 15 | 16 | Key features of the application include: 17 | 18 | - User Authentication: Ensuring secure access to the application and protecting user data. 19 | - API Routes: Facilitating communication between the client and server, enabling data exchange for various application features. 20 | - Middleware: Managing the sequence of functions that process requests and responses. 21 | - Server Components: Utilizing Next.js 13's server components to deliver an optimized user experience. 22 | T 23 | his project is an ongoing experiment, with plans for further exploration and expansion of its capabilities. 24 | 25 | ## Known Issues 26 | 27 | A list of things not working right now: 28 | 29 | 1. Share screen functionality is not working. 30 | 2. Invite email is not working. (Using React Email and Nodmailer, email sending works, but Resend with React Email throws this error when sending: "The gmail.com domain is not verified. Please, add and verify your domain on Resend". The doamin has been verified and an api key generated for it. I can't seem to find any documentation or issues online that address this.) 31 | 32 | ## Tech Stack 33 | 34 | - [Next.js](https://nextjs.org) 35 | - [NextAuth.js](https://next-auth.js.org) 36 | - [Prisma](https://prisma.io) 37 | - [Tailwind CSS](https://tailwindcss.com) 38 | - [100ms](https://100ms.live) 39 | - [Shadcn UI](https://ui.shadcn.com) 40 | - [React Email](https://react.email/) 41 | - [Resend](https://resend.com/) 42 | 43 | ## Features to be implemented 44 | 45 | - [ ] Invite email with **React Email** and **Resend** 46 | - [x] Scheduling calls 47 | - [ ] Screen Annotations 48 | - [x] Deleting calls records 49 | - [ ] In call chat 50 | - [ ] Call recording 51 | - [x] View call details 52 | - [x] Rejoiing calls 53 | - [ ] Call transcription 54 | - [ ] Call limits 55 | - [ ] Screen sharing 56 | 57 | ## Installation 58 | 59 | ### 1. Clone the repository 60 | 61 | ```bash 62 | git clone https://github.com/JaleelB/callsquare 63 | ``` 64 | 65 | ### 2. Install dependencies 66 | 67 | ```bash 68 | pnpm i 69 | ``` 70 | 71 | ### 3. Create a `.env` file 72 | 73 | Create a `.env.local` file in the root directory and add the environment variables as shown in the `.env.example` file. 74 | 75 | ## How do I deploy this? 76 | 77 | Follow the deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information 78 | 79 | ## License 80 | 81 | Licensed under the MIT License. Check the [LICENSE](./LICENSE) file for details. 82 | 83 | ## Contributing 84 | 85 | Contributions are welcome! Please open an issue if you have any questions or suggestions. Your contributions are welcomed and will be acknowledged. 86 | 87 | See the [contributing guide](./CONTRIBUTING.md) for more information. 88 | -------------------------------------------------------------------------------- /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": "src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "~/components", 14 | "utils": "~/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful 3 | * for Docker builds. 4 | */ 5 | await import("./src/env.mjs"); 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | reactStrictMode: true, 10 | 11 | /** 12 | * If you have `experimental: { appDir: true }` set, then you must comment the below `i18n` config 13 | * out. 14 | * 15 | * @see https://github.com/vercel/next.js/issues/41980 16 | */ 17 | // i18n: { 18 | // locales: ["en"], 19 | // defaultLocale: "en", 20 | // }, 21 | images: { 22 | domains: [ 23 | "avatars.githubusercontent.com", 24 | "lh3.googleusercontent.com", 25 | "images.pexels.com", 26 | ], 27 | }, 28 | }; 29 | export default config; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "callsquare", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "lint": "next lint", 9 | "start": "next start", 10 | "postinstall": "prisma generate" 11 | }, 12 | "dependencies": { 13 | "@100mslive/react-icons": "^0.8.6", 14 | "@100mslive/react-sdk": "^0.8.6", 15 | "@100mslive/react-ui": "^0.8.8", 16 | "@hookform/resolvers": "^3.1.0", 17 | "@next-auth/prisma-adapter": "^1.0.7", 18 | "@prisma/client": "^5.0.0", 19 | "@radix-ui/react-alert-dialog": "^1.0.4", 20 | "@radix-ui/react-checkbox": "^1.0.4", 21 | "@radix-ui/react-collapsible": "^1.0.3", 22 | "@radix-ui/react-dialog": "^1.0.4", 23 | "@radix-ui/react-dropdown-menu": "^2.0.5", 24 | "@radix-ui/react-icons": "^1.3.0", 25 | "@radix-ui/react-label": "^2.0.2", 26 | "@radix-ui/react-select": "^1.2.2", 27 | "@radix-ui/react-slot": "^1.0.2", 28 | "@radix-ui/react-toast": "^1.1.4", 29 | "@react-email/components": "0.0.17", 30 | "@react-email/render": "^0.0.7", 31 | "@t3-oss/env-nextjs": "^0.3.1", 32 | "@tanstack/react-query": "^4.29.7", 33 | "@tanstack/react-query-devtools": "^4.29.12", 34 | "@types/jsonwebtoken": "^9.0.2", 35 | "@types/uuid": "^9.0.2", 36 | "@types/uuid4": "^2.0.0", 37 | "@vercel/analytics": "^1.0.1", 38 | "class-variance-authority": "^0.6.1", 39 | "clsx": "^1.2.1", 40 | "js-cookie": "^3.0.5", 41 | "jsonwebtoken": "^9.0.0", 42 | "lucide-react": "^0.260.0", 43 | "next": "14.0.1", 44 | "next-auth": "^4.22.1", 45 | "next-themes": "^0.2.1", 46 | "nodemailer": "^6.9.3", 47 | "prisma": "^5.0.0", 48 | "react": "18.2.0", 49 | "react-dom": "18.2.0", 50 | "react-email": "^2.1.2", 51 | "react-hook-form": "^7.44.3", 52 | "resend": "^3.2.0", 53 | "superjson": "1.12.2", 54 | "tailwind-merge": "^1.13.2", 55 | "tailwindcss-animate": "^1.0.6", 56 | "uuid4": "^2.0.3", 57 | "uuidv4": "^6.2.13", 58 | "vercel": "^31.0.4", 59 | "zod": "^3.21.4" 60 | }, 61 | "devDependencies": { 62 | "@types/eslint": "^8.37.0", 63 | "@types/js-cookie": "^3.0.3", 64 | "@types/node": "^18.16.0", 65 | "@types/nodemailer": "^6.4.8", 66 | "@types/prettier": "^2.7.2", 67 | "@types/react": "^18.2.6", 68 | "@types/react-dom": "^18.2.4", 69 | "@typescript-eslint/eslint-plugin": "^5.59.6", 70 | "@typescript-eslint/parser": "^5.59.6", 71 | "autoprefixer": "^10.4.14", 72 | "eslint": "^8.40.0", 73 | "eslint-config-next": "^14.0.1", 74 | "postcss": "^8.4.21", 75 | "prettier": "^2.8.8", 76 | "prettier-plugin-tailwindcss": "^0.2.8", 77 | "tailwindcss": "^3.3.0", 78 | "typescript": "^5.0.4" 79 | }, 80 | "ct3aMetadata": { 81 | "initVersion": "7.13.1" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | const config = { 3 | plugins: [require.resolve("prettier-plugin-tailwindcss")], 4 | }; 5 | 6 | module.exports = config; 7 | -------------------------------------------------------------------------------- /prisma/migrations/20230606005725_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `Account` ( 3 | `id` VARCHAR(191) NOT NULL, 4 | `userId` VARCHAR(191) NOT NULL, 5 | `type` VARCHAR(191) NOT NULL, 6 | `provider` VARCHAR(191) NOT NULL, 7 | `providerAccountId` VARCHAR(191) NOT NULL, 8 | `refresh_token` TEXT NULL, 9 | `access_token` TEXT NULL, 10 | `expires_at` INTEGER NULL, 11 | `token_type` VARCHAR(191) NULL, 12 | `scope` VARCHAR(191) NULL, 13 | `id_token` TEXT NULL, 14 | `session_state` VARCHAR(191) NULL, 15 | 16 | UNIQUE INDEX `Account_provider_providerAccountId_key`(`provider`, `providerAccountId`), 17 | PRIMARY KEY (`id`) 18 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 19 | 20 | -- CreateTable 21 | CREATE TABLE `Session` ( 22 | `id` VARCHAR(191) NOT NULL, 23 | `sessionToken` VARCHAR(191) NOT NULL, 24 | `userId` VARCHAR(191) NOT NULL, 25 | `expires` DATETIME(3) NOT NULL, 26 | 27 | UNIQUE INDEX `Session_sessionToken_key`(`sessionToken`), 28 | PRIMARY KEY (`id`) 29 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 30 | 31 | -- CreateTable 32 | CREATE TABLE `User` ( 33 | `id` VARCHAR(191) NOT NULL, 34 | `name` VARCHAR(191) NULL, 35 | `email` VARCHAR(191) NULL, 36 | `emailVerified` DATETIME(3) NULL, 37 | `image` VARCHAR(191) NULL, 38 | 39 | UNIQUE INDEX `User_email_key`(`email`), 40 | PRIMARY KEY (`id`) 41 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 42 | 43 | -- CreateTable 44 | CREATE TABLE `VerificationToken` ( 45 | `identifier` VARCHAR(191) NOT NULL, 46 | `token` VARCHAR(191) NOT NULL, 47 | `expires` DATETIME(3) NOT NULL, 48 | 49 | UNIQUE INDEX `VerificationToken_token_key`(`token`), 50 | UNIQUE INDEX `VerificationToken_identifier_token_key`(`identifier`, `token`) 51 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 52 | 53 | -- CreateTable 54 | CREATE TABLE `Call` ( 55 | `id` VARCHAR(191) NOT NULL, 56 | `userId` VARCHAR(191) NOT NULL, 57 | `title` VARCHAR(191) NULL, 58 | `status` VARCHAR(191) NULL, 59 | `startTime` DATETIME(3) NOT NULL, 60 | `endTime` DATETIME(3) NOT NULL, 61 | `duration` INTEGER NULL, 62 | `maxParticipants` INTEGER NULL, 63 | `inviteLink` VARCHAR(191) NULL, 64 | 65 | PRIMARY KEY (`id`) 66 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 67 | 68 | -- CreateTable 69 | CREATE TABLE `Participant` ( 70 | `id` VARCHAR(191) NOT NULL, 71 | `callId` VARCHAR(191) NOT NULL, 72 | `userId` VARCHAR(191) NULL, 73 | `name` VARCHAR(191) NOT NULL, 74 | `email` VARCHAR(191) NULL, 75 | `role` VARCHAR(191) NOT NULL, 76 | `status` VARCHAR(191) NOT NULL, 77 | 78 | PRIMARY KEY (`id`) 79 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 80 | 81 | -- AddForeignKey 82 | ALTER TABLE `Account` ADD CONSTRAINT `Account_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 83 | 84 | -- AddForeignKey 85 | ALTER TABLE `Session` ADD CONSTRAINT `Session_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 86 | 87 | -- AddForeignKey 88 | ALTER TABLE `Call` ADD CONSTRAINT `Call_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 89 | 90 | -- AddForeignKey 91 | ALTER TABLE `Participant` ADD CONSTRAINT `Participant_callId_fkey` FOREIGN KEY (`callId`) REFERENCES `Call`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 92 | 93 | -- AddForeignKey 94 | ALTER TABLE `Participant` ADD CONSTRAINT `Participant_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 95 | -------------------------------------------------------------------------------- /prisma/migrations/20230624043330_call_name_change/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `callId` on the `Participant` table. All the data in the column will be lost. 5 | - Added the required column `name` to the `Call` table without a default value. This is not possible if the table is not empty. 6 | - Added the required column `callName` to the `Participant` table without a default value. This is not possible if the table is not empty. 7 | - Made the column `userId` on table `Participant` required. This step will fail if there are existing NULL values in that column. 8 | - Made the column `email` on table `Participant` required. This step will fail if there are existing NULL values in that column. 9 | 10 | */ 11 | -- DropForeignKey 12 | ALTER TABLE `Participant` DROP FOREIGN KEY `Participant_callId_fkey`; 13 | 14 | -- DropForeignKey 15 | ALTER TABLE `Participant` DROP FOREIGN KEY `Participant_userId_fkey`; 16 | 17 | -- AlterTable 18 | ALTER TABLE `Call` ADD COLUMN `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 19 | ADD COLUMN `name` VARCHAR(191) NOT NULL, 20 | MODIFY `endTime` DATETIME(3) NULL; 21 | 22 | -- AlterTable 23 | ALTER TABLE `Participant` DROP COLUMN `callId`, 24 | ADD COLUMN `callName` VARCHAR(191) NOT NULL, 25 | ADD COLUMN `endTime` DATETIME(3) NULL, 26 | ADD COLUMN `startTime` DATETIME(3) NULL, 27 | MODIFY `userId` VARCHAR(191) NOT NULL, 28 | MODIFY `email` VARCHAR(191) NOT NULL; 29 | 30 | -- AddForeignKey 31 | ALTER TABLE `Participant` ADD CONSTRAINT `Participant_callName_fkey` FOREIGN KEY (`callName`) REFERENCES `Call`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 32 | 33 | -- AddForeignKey 34 | ALTER TABLE `Participant` ADD CONSTRAINT `Participant_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 35 | -------------------------------------------------------------------------------- /prisma/migrations/20230624043916_call_name_change/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `callId` to the `Participant` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE `Participant` DROP FOREIGN KEY `Participant_callName_fkey`; 9 | 10 | -- AlterTable 11 | ALTER TABLE `Participant` ADD COLUMN `callId` VARCHAR(191) NOT NULL; 12 | 13 | -- AddForeignKey 14 | ALTER TABLE `Participant` ADD CONSTRAINT `Participant_callId_fkey` FOREIGN KEY (`callId`) REFERENCES `Call`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20230624223235_schema_change/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX `Participant_callName_fkey` ON `Participant`; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20230629195519_make_user_id_nullable/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE `Participant` MODIFY `userId` VARCHAR(191) NULL, 3 | MODIFY `email` VARCHAR(191) NULL; 4 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "mysql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | // Necessary for Next auth 11 | model Account { 12 | id String @id @default(cuid()) 13 | userId String 14 | type String 15 | provider String 16 | providerAccountId String 17 | refresh_token String? @db.Text 18 | access_token String? @db.Text 19 | expires_at Int? 20 | token_type String? 21 | scope String? 22 | id_token String? @db.Text 23 | session_state String? 24 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 25 | 26 | @@unique([provider, providerAccountId]) 27 | } 28 | 29 | model Session { 30 | id String @id @default(cuid()) 31 | sessionToken String @unique 32 | userId String 33 | expires DateTime 34 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 35 | } 36 | 37 | model User { 38 | id String @id @default(cuid()) 39 | name String? 40 | email String? @unique 41 | emailVerified DateTime? 42 | image String? 43 | accounts Account[] 44 | sessions Session[] 45 | calls Call[] 46 | participants Participant[] 47 | } 48 | 49 | model VerificationToken { 50 | identifier String 51 | token String @unique 52 | expires DateTime 53 | 54 | @@unique([identifier, token]) 55 | } 56 | 57 | model Call { 58 | id String @id @default(cuid()) 59 | name String 60 | userId String 61 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 62 | title String? 63 | status String? 64 | startTime DateTime 65 | endTime DateTime? 66 | duration Int? 67 | maxParticipants Int? 68 | inviteLink String? 69 | participants Participant[] 70 | createdAt DateTime @default(now()) 71 | } 72 | 73 | model Participant { 74 | id String @id @default(cuid()) 75 | callId String 76 | callName String 77 | call Call @relation(fields: [callId], references: [id], onDelete: Cascade) 78 | userId String? 79 | user User? @relation(fields: [userId], references: [id], onDelete: Cascade) 80 | name String 81 | email String? 82 | role String 83 | status String 84 | startTime DateTime? 85 | endTime DateTime? 86 | } -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaleelB/callsquare/b18c01175253eaf006fba2269f0fc6bd7739db60/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaleelB/callsquare/b18c01175253eaf006fba2269f0fc6bd7739db60/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaleelB/callsquare/b18c01175253eaf006fba2269f0fc6bd7739db60/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaleelB/callsquare/b18c01175253eaf006fba2269f0fc6bd7739db60/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaleelB/callsquare/b18c01175253eaf006fba2269f0fc6bd7739db60/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaleelB/callsquare/b18c01175253eaf006fba2269f0fc6bd7739db60/public/favicon.ico -------------------------------------------------------------------------------- /public/features-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaleelB/callsquare/b18c01175253eaf006fba2269f0fc6bd7739db60/public/features-image.png -------------------------------------------------------------------------------- /public/hero-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaleelB/callsquare/b18c01175253eaf006fba2269f0fc6bd7739db60/public/hero-image.png -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /public/web-shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaleelB/callsquare/b18c01175253eaf006fba2269f0fc6bd7739db60/public/web-shot.png -------------------------------------------------------------------------------- /src/app/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from "next"; 2 | import Link from "next/link"; 3 | import { Icons } from "~/components/ui/icons"; 4 | import SocialAuthForm from "~/components/social-auth-form"; 5 | 6 | export const metadata: Metadata = { 7 | title: "CallSquare - Connect with Ease", 8 | description: 9 | "Sign in to CallSquare and start connecting with your friends, family, and colleagues through seamless video calls.", 10 | }; 11 | 12 | export default function LoginPage() { 13 | return ( 14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 |

22 | Connect with Callsquare 23 |

24 |

25 | Welcome back! Sign in to your Callsquare account 26 |

27 |
28 | 29 |

30 | 34 | Don't have an account? Sign Up 35 | 36 |

37 |
38 |
39 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/app/(auth)/register/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from "next"; 2 | import Link from "next/link"; 3 | import { Icons } from "~/components/ui/icons"; 4 | import SocialAuthForm from "~/components/social-auth-form"; 5 | 6 | export const metadata: Metadata = { 7 | title: "CallSquare - Sign Up", 8 | description: 9 | "Create your CallSquare account today and start connecting with friends, family, and colleagues through seamless video calls.", 10 | }; 11 | 12 | export default function RegisterPage() { 13 | return ( 14 |
15 |
21 |
22 |
23 |
24 | 25 | 26 | 27 |

28 | Let's get started 29 |

30 |

31 | Create your Callsquare account 32 |

33 |
34 | 35 |

36 | 40 | Already have an account? Sign In 41 | 42 |

43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/app/(call)/call/[[...slug]]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getCurrentUser } from "~/lib/session"; 2 | import { redirect } from "next/navigation"; 3 | import { cookies } from "next/headers"; 4 | 5 | export default async function CallLayout({ 6 | children, 7 | params 8 | }: { 9 | children: React.ReactNode 10 | params: { 11 | slug: string 12 | } 13 | }) { 14 | 15 | const user = await getCurrentUser(); 16 | const unAuthorizedUserName = cookies().get("username"); 17 | 18 | if (!user && !unAuthorizedUserName) { 19 | redirect(`/preview/${params.slug}`) 20 | } 21 | 22 | return ( 23 |
24 |
25 | {children} 26 |
27 |
28 | ); 29 | } -------------------------------------------------------------------------------- /src/app/(call)/call/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { selectIsConnectedToRoom, useHMSActions, useHMSStore } from "@100mslive/react-sdk"; 3 | import Cookies from 'js-cookie'; 4 | import React from "react"; 5 | import CallFooter from "~/components/call/call-footer"; 6 | import Conference from "~/components/call/conference"; 7 | import { useParams, useRouter } from "next/navigation"; 8 | import { getSession } from "next-auth/react"; 9 | import { type RoomCodeResponse } from "~/types/types"; 10 | import { extractId } from "~/lib/extract-id"; 11 | import { useToast } from "~/components/ui/use-toast"; 12 | 13 | 14 | export default function CallPage(){ 15 | 16 | const params = useParams(); 17 | const router = useRouter() 18 | const isConnected = useHMSStore(selectIsConnectedToRoom); 19 | const hmsActions = useHMSActions(); 20 | const { toast } = useToast() 21 | const actions = useHMSActions(); 22 | const roomName = Cookies.get("room-name"); 23 | const roomId = Cookies.get("room-id"); 24 | const unAuthUsername = Cookies.get("username"); 25 | 26 | const joinCall = React.useCallback(async () => { 27 | 28 | if (!roomId) { 29 | console.error("Room id is not defined"); 30 | return; 31 | } 32 | 33 | try { 34 | const roomCodeResponse = await fetch(`/api/call/code`, { 35 | method: "POST", 36 | headers: { 37 | "Content-Type": "application/json", 38 | }, 39 | body: JSON.stringify({ 40 | callName: roomName ? roomName : params.slug, 41 | }), 42 | }) 43 | 44 | if(roomCodeResponse?.ok){ 45 | 46 | // use room code to fetch auth token 47 | const codeResponse: RoomCodeResponse = await roomCodeResponse.json() as RoomCodeResponse; 48 | const roomCode = codeResponse.code; 49 | const authToken = await hmsActions.getAuthTokenByRoomCode({ roomCode }) 50 | const session = await getSession(); 51 | 52 | if(session && session.user.name){ 53 | const userName = session.user.name; 54 | await hmsActions.join({ userName, authToken }); 55 | } else if(!session && unAuthUsername){ 56 | await hmsActions.join({ userName: unAuthUsername, authToken }); 57 | } 58 | else { 59 | console.error("Session or user name is not defined"); 60 | toast({ 61 | title: "Something went wrong.", 62 | description: "This call cannot joined. Please try again.", 63 | variant: "destructive", 64 | }); 65 | router.replace("/calls"); 66 | } 67 | } else { 68 | throw new Error("Room code response not OK"); 69 | } 70 | 71 | } catch (error) { 72 | console.error(error) 73 | toast({ 74 | title: "Something went wrong.", 75 | description: "This call cannot be joined. Please try again.", 76 | variant: "destructive", 77 | }) 78 | router.replace("/calls"); 79 | } 80 | 81 | }, [hmsActions, toast, params.slug, router, roomName, roomId, unAuthUsername]); 82 | 83 | const leaveCall = React.useCallback(async () => { 84 | 85 | const response = await fetch(`/api/call/leave`, { 86 | method: "PATCH", 87 | headers: { 88 | "Content-Type": "application/json", 89 | }, 90 | body: JSON.stringify({ 91 | callName: roomName ? roomName : extractId(params.slug as string), 92 | roomId: roomId, 93 | }), 94 | }) 95 | 96 | if(!response.ok){ 97 | toast({ 98 | title: "Something went wrong.", 99 | description: "Your call cannot be left. Please try again.", 100 | variant: "destructive", 101 | }) 102 | } 103 | 104 | await actions.leave(); 105 | 106 | }, [roomName, params.slug, roomId, actions, toast]); 107 | 108 | React.useEffect(() => { 109 | void joinCall(); 110 | }, [joinCall]); 111 | 112 | React.useEffect(() => { 113 | window.onunload = () => { 114 | if (isConnected) { 115 | void leaveCall(); 116 | } 117 | }; 118 | 119 | }, [isConnected, leaveCall]); 120 | 121 | 122 | return( 123 |
124 | 125 | 126 |
127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /src/app/(calls)/calls/history/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function HistoryLayout({ 2 | children 3 | }: { 4 | children: React.ReactNode 5 | }) { 6 | 7 | return ( 8 |
9 | {children} 10 |
11 | ); 12 | } -------------------------------------------------------------------------------- /src/app/(calls)/calls/history/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function CallHistoryLoading() { 2 | return ( 3 |
4 |

5 | Call history 6 |

7 |

8 | Review your past interactions and revisit meaningful moments 9 |

10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/(calls)/calls/history/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import PerPageDropdown from "~/components/call/call-history-per-page-dropdown"; 3 | import DeleteCallActions from "~/components/call/delete-call-actions"; 4 | import CallHistoryPagination from "~/components/call/pagination"; 5 | import { 6 | Table, 7 | TableHeader, 8 | TableRow, 9 | TableHead, 10 | TableBody, 11 | TableCell 12 | } from "~/components/ui/table"; 13 | import { getCurrentUser } from "~/lib/session"; 14 | import { cn } from "~/lib/utils"; 15 | import { prisma } from "~/server/db"; 16 | 17 | export default async function HistoryPage({ 18 | searchParams 19 | }: { 20 | searchParams: { 21 | page: string, 22 | per_page: string, 23 | } 24 | }){ 25 | 26 | const user = await getCurrentUser(); 27 | 28 | if(searchParams.page === undefined || searchParams.per_page === undefined){ 29 | redirect("/calls/history?page=1&per_page=10") 30 | } 31 | 32 | const { page, per_page } = searchParams; 33 | 34 | if (!user) { 35 | redirect("/login") 36 | } 37 | 38 | const calls = await prisma.call.findMany({ 39 | where: { 40 | userId: user.id, 41 | }, 42 | include: { 43 | participants: true, 44 | }, 45 | orderBy: { 46 | startTime: 'desc', 47 | }, 48 | skip: (parseInt(page) - 1) * parseInt(per_page), 49 | take: parseInt((per_page), 10), 50 | }); 51 | 52 | const totalCalls = await prisma.call.count({ 53 | where: { 54 | userId: user.id, 55 | }, 56 | }); 57 | 58 | 59 | const perPageOptions = [10, 20, 30]; 60 | const selectedPerPage = parseInt(per_page, 10); 61 | const totalPages = Math.ceil(totalCalls / parseInt(per_page, 10)); 62 | 63 | 64 | return ( 65 |
66 |

Call history

67 |

Review your past interactions and revisit meaningful moments

68 | 69 |
70 | 71 | 72 | 73 | Call name 74 | Date 75 | Start time 76 | End time 77 | 78 | 79 | 80 | 81 | { 82 | calls.length !== 0 ? ( 83 | calls.map((call) => ( 84 | 85 | {call.title} 86 | {new Date(call.startTime).toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: '2-digit' })} 87 | {new Date(call.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} 88 | {call.endTime ? new Date(call.endTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : 'null'} 89 | 90 | 91 | 92 | 93 | )) 94 | ) : ( 95 | 96 | 97 | No results. 98 | 99 | 100 | ) 101 | } 102 | 103 |
104 |
105 |
106 |
107 | 113 | Page {page} 114 | 119 |
120 |
121 | ) 122 | } -------------------------------------------------------------------------------- /src/app/(calls)/calls/layout.tsx: -------------------------------------------------------------------------------- 1 | import SiteFooter from "~/components/layout/footer"; 2 | import FullNav from "~/components/layout/full-nav"; 3 | import UserAccountDropdown from "~/components/layout/user-account-dropdown"; 4 | import { getCurrentUser } from "~/lib/session"; 5 | import { notFound } from "next/navigation"; 6 | import CallIdProvider from "~/context/call-id-context"; 7 | 8 | export default async function CallsHomeLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | const user = await getCurrentUser(); 14 | 15 | if (!user) { 16 | notFound(); 17 | } 18 | 19 | return ( 20 |
21 | 22 | 29 | 30 |
31 | {children} 32 |
33 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/app/(calls)/calls/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from "next"; 2 | import Link from "next/link"; 3 | import { Badge } from "~/components/ui/badge"; 4 | import { Icons } from "~/components/ui/icons"; 5 | import { formatDate } from "~/lib/date"; 6 | import { getCurrentUser } from "~/lib/session"; 7 | import JoinCallDialog from "~/components/call/join-call-dialog"; 8 | import InviteParticipantsDialog from "~/components/call/invite-participants-dialog"; 9 | import { type CardProps } from "~/components/layout/card-shell"; 10 | import CreateCallCard from "~/components/call/create-call-card"; 11 | import { Button } from "~/components/ui/button"; 12 | 13 | export const metadata: Metadata = { 14 | title: "CallSquare - Calls Hub", 15 | description: 16 | "Access your CallSquare Calls Hub to manage and join your video calls seamlessly.", 17 | }; 18 | 19 | const cardsData: CardProps[] = [ 20 | { 21 | title: "Create a call", 22 | description: 23 | "Create a call and invite others to join in conversation, discussion, or collaboration.", 24 | icon: , 25 | buttonText: "Create", 26 | loadingIcon: , 27 | buttonIcon: , 28 | }, 29 | { 30 | title: "Join a call", 31 | description: 32 | "Join a call by to participate in a conversation, discussion, or collaboration.", 33 | icon: , 34 | buttonText: "Join", 35 | loadingIcon: , 36 | buttonIcon: , 37 | }, 38 | { 39 | title: "Invite Participants", 40 | description: 41 | "Invite your friends or participants to join your call and engage in a conversation.", 42 | icon: , 43 | loadingIcon: , 44 | buttonText: "Invite", 45 | buttonIcon: , 46 | }, 47 | ]; 48 | 49 | export default async function CallsPage() { 50 | const user = await getCurrentUser(); 51 | 52 | return ( 53 |
54 |
55 |
56 |
57 | {formatDate(new Date())} 58 |

59 | {`Welcome ${user?.name as string}`} 60 |

61 |
62 |
63 |
64 |
65 |
66 |
67 | 68 | 69 | 70 |
71 | 78 | 86 | 87 |
88 |
89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import SiteFooter from "~/components/layout/footer"; 3 | import FullNav from "~/components/layout/full-nav"; 4 | import { Button } from "~/components/ui/button"; 5 | 6 | export default function HomePageLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | return ( 12 |
13 | 14 |
15 | 16 | 23 | 24 | 25 | 31 | 32 |
33 |
34 |
{children}
35 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import { Badge, badgeVariants } from "~/components/ui/badge"; 5 | import { Icons } from "~/components/ui/icons"; 6 | import { Button } from "~/components/ui/button"; 7 | import { siteConfig } from "~/config/site-config"; 8 | 9 | type Tool = { 10 | name: string; 11 | icon: React.ReactNode; 12 | }; 13 | 14 | type Features = { 15 | description: string; 16 | } & Tool; 17 | 18 | const tools: Tool[] = [ 19 | { 20 | name: "Next.js 13", 21 | icon: ( 22 | 23 | 24 | 25 | ), 26 | }, 27 | { 28 | name: "React 18", 29 | icon: ( 30 | 31 | 32 | 33 | ), 34 | }, 35 | { 36 | name: "Next Auth", 37 | icon: ( 38 | 45 | 46 | 47 | ), 48 | }, 49 | { 50 | name: "Tailwind CSS", 51 | icon: ( 52 | 53 | 54 | 55 | ), 56 | }, 57 | { 58 | name: "Radix UI", 59 | icon: ( 60 | 65 | 66 | 67 | ), 68 | }, 69 | { 70 | name: "Prisma", 71 | icon: ( 72 | 77 | 82 | 83 | ), 84 | }, 85 | ]; 86 | 87 | const features: Features[] = [ 88 | { 89 | name: "Partcipent Invites", 90 | description: 91 | "Invite participants to your meeting via an invite link or an invite email.", 92 | icon: ( 93 | 98 | 104 | 105 | ), 106 | }, 107 | { 108 | name: "Screen Sharing", 109 | description: "Share your screen with other participants in your call.", 110 | icon: ( 111 | 116 | 122 | 128 | 129 | ), 130 | }, 131 | { 132 | name: "Accessing call history", 133 | description: 134 | "Keep track of your past interactions with the call history feature.", 135 | icon: ( 136 | 141 | 147 | 153 | 154 | ), 155 | }, 156 | ]; 157 | 158 | export default function IndexPage() { 159 | return ( 160 | <> 161 |
162 |
163 |
164 | 170 | Find the project on Github 171 | 172 | 173 |

174 | Video calls made possible with Next.js 13 175 |

176 |

177 | {siteConfig.description} 178 |

179 | 180 | 183 | 184 |
185 | Hero Image 191 |
192 |
193 | 194 |
195 |
196 |
197 |

198 | This project was built using the following technologies 199 |

200 |
201 | 202 |
203 | {tools.map((tool, index) => ( 204 |
205 | {tool.icon} 206 |

207 | {tool.name} 208 |

209 |
210 | ))} 211 |
212 |
213 |
214 | 215 |
216 |
217 |
218 | Features 219 |
220 |

221 | Features for Enhanced Communication 222 |

223 |

224 | This project offers a range of features designed to facilitate 225 | smooth and efficient communication. From high-definition video 226 | calls to screen sharing and call history, 227 |

228 |
229 |
230 |
231 | Hero Image 238 |
239 | {features.map((feature, index) => ( 240 |
244 | {feature.icon} 245 |
246 |

{feature.name}

247 |

248 | {feature.description} 249 |

250 |
251 |
252 | ))} 253 |
254 |
255 |
256 |
257 | 258 |
259 |
260 |
261 |

262 | This is an open source project 263 |

264 |

265 | Callsquare is open source. Check out the GitHub repository to get 266 | started. 267 |

268 |
269 | 270 | 275 | 279 | 280 |
281 |
282 | 283 | ); 284 | } 285 | -------------------------------------------------------------------------------- /src/app/(preview)/preview/[[...slug]]/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import FullNav from "~/components/layout/full-nav"; 3 | import { Button } from "~/components/ui/button"; 4 | 5 | export default function CallPreviewLayout({ 6 | children, 7 | }: { 8 | children: React.ReactNode; 9 | }) { 10 | return ( 11 |
12 |
13 | 14 | 15 | 22 | 23 | 24 |
25 |
26 | {children} 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/app/(preview)/preview/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-misused-promises */ 2 | "use client"; 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { useForm } from "react-hook-form"; 5 | import { type z } from "zod"; 6 | import { previewJoinSchema } from "~/schemas/join"; 7 | import { useParams, useRouter } from "next/navigation"; 8 | import React from "react"; 9 | import Cookies from "js-cookie"; 10 | import { Button } from "~/components/ui/button"; 11 | import { Input } from "~/components/ui/input"; 12 | import { useToast } from "~/components/ui/use-toast"; 13 | import { Icons } from "~/components/ui/icons"; 14 | import Link from "next/link"; 15 | 16 | type FormData = z.infer 17 | 18 | export default function CallPreviewPage(){ 19 | 20 | const router = useRouter(); 21 | const { 22 | register, 23 | handleSubmit, 24 | formState: { errors } 25 | } = useForm({ 26 | resolver: zodResolver(previewJoinSchema) 27 | }); 28 | const params = useParams(); 29 | const { toast } = useToast(); 30 | 31 | 32 | async function joinCall(data: FormData){ 33 | 34 | try { 35 | 36 | const joinResponse = await fetch(`/api/call/join`, { 37 | method: "POST", 38 | headers: { 39 | "Content-Type": "application/json", 40 | }, 41 | body: JSON.stringify({ 42 | callName: params.slug, 43 | username: data.name, 44 | }), 45 | }); 46 | 47 | 48 | if (!joinResponse.ok) { 49 | throw new Error('Join response not OK'); 50 | } 51 | 52 | Cookies.set("username", data.name); 53 | router.replace(`/call/${params.slug as string}`); 54 | 55 | } catch (error) { 56 | console.error('Error during fetch:', error); 57 | toast({ 58 | title: "Something went wrong.", 59 | description: "This call cannot be joined. Please try again.", 60 | variant: "destructive", 61 | }); 62 | } 63 | 64 | } 65 | 66 | 67 | return( 68 |
69 |
70 |
71 | 72 | 73 | 74 |

Ready to join?

75 |

Enter you name to join the video call

76 |
77 |
78 | 83 | {errors.name && typeof errors.name.message === 'string' &&

{errors.name.message}

} 84 | 92 |
93 |
94 |
95 | ) 96 | } 97 | 98 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from "~/server/auth"; 2 | import NextAuth from "next-auth"; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 5 | const handler = NextAuth(authOptions); 6 | export { handler as GET, handler as POST }; 7 | -------------------------------------------------------------------------------- /src/app/api/call/code/route.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "next-auth/next" 2 | import { z } from "zod" 3 | import { env } from "~/env.mjs" 4 | import { authOptions } from "~/server/auth" 5 | import { prisma } from "~/server/db" 6 | import { generateManagementToken } from "~/server/management-token" 7 | 8 | const roomCodeSchema = z.object({ 9 | callName: z.string(), 10 | }) 11 | 12 | interface RoomCodeBody { 13 | callName: string; 14 | } 15 | 16 | type RoomCode = { 17 | code: string; 18 | }; 19 | 20 | export async function POST(req: Request) { 21 | 22 | try { 23 | const session = await getServerSession(authOptions) 24 | 25 | let userId; 26 | if (session) { 27 | const { user } = session 28 | if (user && user.id) { 29 | userId = user.id; 30 | } 31 | } 32 | 33 | const json: RoomCodeBody = await req.json() as RoomCodeBody; 34 | const body = roomCodeSchema.parse(json) 35 | 36 | const call = await prisma.call.findFirst({ 37 | where: { status: 'created', name: body.callName }, 38 | }); 39 | 40 | if (!call || call.status === 'ended') { 41 | return new Response("Not Found", { status: 404 }) 42 | } 43 | 44 | let role = "guest"; 45 | if (userId) { 46 | const participant = await prisma.participant.findUnique({ 47 | where: { id: userId }, 48 | }); 49 | if (participant) { 50 | role = participant.role; 51 | } 52 | } 53 | 54 | const roomId = call.id; 55 | 56 | const token = await generateManagementToken(); 57 | const response = await fetch(`${env.TOKEN_ENDPOINT}/room-codes/room/${roomId}/role/${role}`, { 58 | method: 'POST', 59 | headers: { 60 | 'Authorization': `Bearer ${token}`, 61 | 'Content-Type': 'application/json' 62 | }, 63 | }); 64 | 65 | if (!response.ok) { 66 | throw new Error(`HTTP error! status: ${response.status}`); 67 | } 68 | 69 | const { code }: RoomCode = await response.json() as RoomCode; 70 | return new Response(JSON.stringify({ code })); 71 | 72 | } catch (error) { 73 | console.error(error) 74 | return new Response(null, { status: 500 }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/api/call/create/route.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "next-auth/next" 2 | import { z } from "zod" 3 | import { env } from "~/env.mjs" 4 | import { authOptions } from "~/server/auth" 5 | import { prisma } from "~/server/db" 6 | import { cookies } from 'next/headers' 7 | import { generateManagementToken } from "~/server/management-token" 8 | 9 | const callCreateSchema = z.object({ 10 | callName: z.string().uuid(), 11 | }) 12 | 13 | interface CallCreateBody { 14 | callName: string; 15 | } 16 | 17 | type Room = { 18 | id: string; 19 | }; 20 | 21 | export async function POST(req: Request) { 22 | 23 | try { 24 | const session = await getServerSession(authOptions) 25 | 26 | if (!session) { 27 | return new Response("Unauthorized", { status: 403 }) 28 | } 29 | 30 | const { user } = session 31 | if (!user || !user.id || !user.name || !user.email) { 32 | throw new Error('You must be logged in to create a call'); 33 | } 34 | 35 | const json: CallCreateBody = await req.json() as CallCreateBody; 36 | const body = callCreateSchema.parse(json) 37 | 38 | const roomId = await createRoom(body.callName); 39 | const existingCall = await prisma.call.findUnique({ 40 | where: { id: roomId }, 41 | }); 42 | 43 | if (existingCall) { 44 | throw new Error('A call with this ID already exists'); 45 | } 46 | 47 | const newCall = await prisma.call.create({ 48 | data: { 49 | id: roomId, 50 | name: body.callName, 51 | userId: user.id, 52 | title: user.name + "'s Call", 53 | startTime: new Date(), 54 | status: 'created', 55 | endTime: null, 56 | }, 57 | }); 58 | 59 | if (!newCall) { 60 | throw new Error('Error creating call'); 61 | } 62 | 63 | const inviteLink = `${env.NEXT_PUBLIC_APP_URL}/call/${newCall.name}`; 64 | 65 | // Update the call with the invite link 66 | await prisma.call.update({ 67 | where: { id: newCall.id }, 68 | data: { inviteLink }, 69 | }); 70 | 71 | 72 | await prisma.participant.create({ 73 | data: { 74 | userId: user.id, 75 | name: user.name, 76 | email: user.email, 77 | callName: newCall.name, 78 | role: 'host', 79 | status: 'joined', 80 | callId: newCall.id, 81 | startTime: new Date(), 82 | }, 83 | }); 84 | 85 | //store room code in session 86 | cookies().set('room-id', newCall.id) 87 | cookies().set('room-name', newCall.name) 88 | 89 | return new Response(JSON.stringify({ success: true })); 90 | 91 | } catch (error) { 92 | console.log(error) 93 | return new Response(null, { status: 500 }) 94 | } 95 | 96 | } 97 | 98 | async function createRoom(name: string){ 99 | 100 | const managementToken = await generateManagementToken(); 101 | 102 | const response = await fetch(`${env.TOKEN_ENDPOINT}/rooms`, { 103 | method: 'POST', 104 | headers: { 105 | 'Authorization': `Bearer ${managementToken}`, 106 | 'Content-Type': 'application/json' 107 | }, 108 | body: JSON.stringify({ 109 | name: name, 110 | template_id: env.TEMPLATE_ID, 111 | region: 'us' 112 | }) 113 | }); 114 | 115 | if (!response.ok) { 116 | throw new Error(`HTTP error! status: ${response.status}`); 117 | } 118 | 119 | const { id }: Room = await response.json() as Room; 120 | return id; 121 | } 122 | 123 | -------------------------------------------------------------------------------- /src/app/api/call/delete/route.ts: -------------------------------------------------------------------------------- 1 | import { revalidatePath } from "next/cache"; 2 | import { NextResponse } from "next/server"; 3 | import { z } from "zod"; 4 | import { getCurrentUser } from "~/lib/session"; 5 | import { prisma } from "~/server/db"; 6 | 7 | const deleteSchema = z.object({ 8 | callId: z.string().min(8), 9 | path: z.string().min(2), 10 | }); 11 | 12 | interface DeleteCallBody { 13 | callId: string; 14 | path: string; 15 | } 16 | 17 | export async function POST (req: Request) { 18 | 19 | const user = await getCurrentUser(); 20 | 21 | if (!user) { 22 | return new Response("Unauthorized", { status: 403 }) 23 | } 24 | 25 | const json: DeleteCallBody = await req.json() as DeleteCallBody; 26 | const { callId, path } = deleteSchema.parse(json) 27 | 28 | try { 29 | await prisma.call.delete({ 30 | where: { 31 | id: callId, 32 | userId: user.id, 33 | }, 34 | }); 35 | 36 | revalidatePath(path); 37 | return NextResponse.json({success: true }); 38 | 39 | } catch (error) { 40 | return NextResponse.json({ success: false, error: "Call could not be created." }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/api/call/join/route.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "next-auth/next" 2 | import { cookies } from "next/headers" 3 | import { z } from "zod" 4 | import { authOptions } from "~/server/auth" 5 | import { prisma } from "~/server/db" 6 | 7 | const joinCallSchema = z.object({ 8 | username: z.string().optional(), 9 | callName: z.string().uuid(), 10 | audio: z.boolean().optional(), 11 | video: z.boolean().optional(), 12 | }) 13 | 14 | interface JoinCallBody { 15 | callName: string; 16 | username?: string; 17 | audio?: boolean; 18 | video?: boolean; 19 | } 20 | 21 | export async function POST(req: Request) { 22 | 23 | try { 24 | const session = await getServerSession(authOptions) 25 | 26 | let userId, userName, userEmail; 27 | if (session) { 28 | const { user } = session 29 | if (user && user.id && user.name && user.email) { 30 | userId = user.id; 31 | userName = user.name; 32 | userEmail = user.email; 33 | } 34 | } 35 | 36 | const json: JoinCallBody = await req.json() as JoinCallBody; 37 | const body = joinCallSchema.parse(json) 38 | 39 | const call = await prisma.call.findFirst({ 40 | where: { status: 'created', name: body.callName }, 41 | }); 42 | 43 | if (!call || call.status === 'ended') { 44 | return new Response("Not Found", { status: 404 }) 45 | } 46 | 47 | let participants; 48 | if (userId) { 49 | participants = await prisma.participant.findMany({ 50 | where: { userId: userId, callId: call.id }, 51 | }); 52 | } 53 | 54 | let participant = participants ? participants[0] : null; 55 | 56 | if (!participant) { 57 | participant = await prisma.participant.create({ 58 | data: { 59 | callName: call.name, 60 | userId: userId || null, 61 | email: userEmail || null, 62 | name: body.username || userName || "Guest", 63 | role: "guest", 64 | status: 'joined', 65 | callId: call.id, 66 | startTime: new Date() 67 | }, 68 | }); 69 | } else { 70 | participant = await prisma.participant.update({ 71 | // where: { id: userId }, 72 | where: { id: participant.id }, 73 | data: { 74 | callName: call.name, 75 | status: 'joined', 76 | startTime: new Date() 77 | }, 78 | }); 79 | } 80 | 81 | cookies().set('room-id', call.id) 82 | cookies().set('room-name', call.name) 83 | 84 | return new Response(JSON.stringify(participant)) 85 | 86 | } catch (error) { 87 | console.log(error) 88 | return new Response(null, { status: 500 }) 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/app/api/call/leave/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "~/server/db"; 2 | import { getServerSession } from "next-auth"; 3 | import { authOptions } from "~/server/auth"; 4 | import { z } from "zod"; 5 | import { env } from "~/env.mjs"; 6 | import { generateManagementToken } from "~/server/management-token"; 7 | 8 | const leaveCallSchema = z.object({ 9 | callName: z.string().uuid(), 10 | roomId: z.string().min(8), 11 | userName: z.string().min(1).optional(), 12 | }) 13 | 14 | interface leaveCallBody { 15 | callName: string; 16 | roomId: string 17 | userName?: string; 18 | } 19 | 20 | export async function PATCH(req: Request) { 21 | 22 | try{ 23 | const session = await getServerSession(authOptions) 24 | const json: leaveCallBody = await req.json() as leaveCallBody; 25 | const body = leaveCallSchema.parse(json) 26 | let participant; 27 | 28 | if (session) { 29 | const { user } = session 30 | participant = await prisma.participant.findFirst({ 31 | where: { 32 | userId: user.id, 33 | callName: body.callName, 34 | }, 35 | }); 36 | } else { 37 | 38 | participant = await prisma.participant.findFirst({ 39 | where: { 40 | name: body.userName, 41 | callName: body.callName, 42 | }, 43 | }); 44 | } 45 | 46 | if (!participant) { 47 | throw new Error('Participant not found'); 48 | } 49 | 50 | const endTime = new Date(); 51 | const updatedParticipant = await prisma.participant.update({ 52 | where: { 53 | id: participant.id, 54 | }, 55 | data: { 56 | status: 'left', 57 | endTime 58 | }, 59 | }); 60 | 61 | // Check if there are any other participants in the call 62 | const otherParticipants = await prisma.participant.findMany({ 63 | where: { 64 | callName: updatedParticipant.callName, 65 | status: 'joined', 66 | }, 67 | }); 68 | 69 | if (otherParticipants.length === 0) { 70 | 71 | const managementToken = await generateManagementToken(); 72 | const response = await fetch(`${env.TOKEN_ENDPOINT}/active-rooms/${body.roomId}/end-room`, { 73 | method: 'POST', 74 | headers: { 75 | 'Authorization': `Bearer ${managementToken}`, 76 | 'Content-Type': 'application/json' 77 | }, 78 | body: JSON.stringify({ 79 | lock: true 80 | }) 81 | }) 82 | 83 | if(response?.ok){ 84 | await prisma.call.update({ 85 | where: { id: body.roomId }, 86 | data: { 87 | status: 'ended', 88 | endTime, 89 | duration: endTime.getTime() - (participant?.startTime as Date).getTime() 90 | }, 91 | }); 92 | } 93 | } 94 | 95 | return new Response(JSON.stringify(updatedParticipant)) 96 | 97 | } 98 | catch (error) { 99 | console.log(error) 100 | return new Response(null, { status: 500 }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/app/api/sendEmail/route.ts: -------------------------------------------------------------------------------- 1 | import { Resend } from "resend"; 2 | import { env } from "~/env.mjs"; 3 | import { type EmailProps } from "~/types/types"; 4 | import { z } from "zod"; 5 | import { InviteEmail } from "~/components/email-template"; 6 | import { type ErrorResponse } from "resend"; 7 | 8 | const resend = new Resend(env.RESEND_API_KEY); 9 | 10 | const emailSchema = z.object({ 11 | recipient: z.string(), 12 | link: z.string(), 13 | recipientUsername: z.string(), 14 | senderImage: z.string(), 15 | invitedByUsername: z.string(), 16 | invitedByEmail: z.string(), 17 | }); 18 | 19 | export async function POST(req: Request) { 20 | const json: EmailProps = (await req.json()) as EmailProps; 21 | const body = emailSchema.parse(json); 22 | 23 | try { 24 | const { error } = await resend.emails.send({ 25 | from: `Callsquare <${body.invitedByEmail}>`, 26 | to: body.recipient, 27 | subject: "Invitation to join call on Callsquare", 28 | react: InviteEmail({ 29 | recipientUsername: body.recipientUsername, 30 | senderImage: body.senderImage, 31 | invitedByUsername: body.invitedByUsername, 32 | invitedByEmail: body.invitedByEmail, 33 | inviteLink: body.link, 34 | }), 35 | text: "Invitation to join call on Callsquare", 36 | }); 37 | 38 | if (error) { 39 | console.log(error); 40 | return new Response(error.message, { status: 500 }); 41 | } 42 | 43 | return new Response(null, { status: 200 }); 44 | } catch (error) { 45 | const resendError = error as ErrorResponse; 46 | 47 | if (resendError?.message) { 48 | return new Response(resendError?.message, { status: 429 }); 49 | } 50 | 51 | if (error instanceof Error) { 52 | return new Response(error.message, { status: 500 }); 53 | } 54 | 55 | return new Response("Something went wrong", { status: 500 }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import RoomProvider from "~/components/room-provider"; 3 | import CallIdProvider from "~/context/call-id-context"; 4 | import { Toaster } from "~/components/ui/toaster"; 5 | import { siteConfig } from "~/config/site-config"; 6 | import { Inter } from "next/font/google"; 7 | import { ThemeProvider } from "~/components/theme-provider"; 8 | import Script from "next/script"; 9 | 10 | const inter = Inter({ subsets: ["latin"] }); 11 | 12 | export const metadata = { 13 | title: { 14 | default: siteConfig.name, 15 | template: "%s | " + siteConfig.name, 16 | }, 17 | description: siteConfig.description, 18 | keywords: [ 19 | "Video Conferencing", 20 | "Virtual Collaboration", 21 | "Web Meetings", 22 | "Video Chat", 23 | "Next.js", 24 | "React", 25 | "Tailwind CSS", 26 | "Server Components", 27 | "Group Video Calls", 28 | ], 29 | authors: [ 30 | { 31 | name: "jaleelb", 32 | url: "https://jaleelbennett.com", 33 | }, 34 | ], 35 | creator: "jaleelb", 36 | themeColor: [ 37 | { media: "(prefers-color-scheme: light)", color: "white" }, 38 | { media: "(prefers-color-scheme: dark)", color: "black" }, 39 | ], 40 | openGraph: { 41 | type: "website", 42 | locale: "en_US", 43 | url: siteConfig.url, 44 | title: siteConfig.name, 45 | description: siteConfig.description, 46 | siteName: siteConfig.name, 47 | images: [ 48 | { 49 | url: `${siteConfig.url}/web-shot.png`, 50 | width: 1200, 51 | height: 715, 52 | alt: "Callsquare", 53 | }, 54 | ], 55 | }, 56 | twitter: { 57 | card: "summary_large_image", 58 | title: siteConfig.name, 59 | description: siteConfig.description, 60 | images: [`${siteConfig.url}/web-shot.png`], 61 | creator: "@jal_eelll", 62 | }, 63 | icons: { 64 | icon: "/favicon.ico", 65 | shortcut: "/favicon-16x16.png", 66 | apple: "/apple-touch-icon.png", 67 | }, 68 | manifest: `${siteConfig.url}/site.webmanifest`, 69 | }; 70 | 71 | export default function RootLayout({ 72 | children, 73 | }: { 74 | children: React.ReactNode; 75 | }) { 76 | return ( 77 | 78 | 79 | 85 | 86 | 87 | {children} 88 | 89 | 90 | 91 | 92 |