diff --git a/README.md b/README.md index 921e647..c199610 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,11 @@ This repository hosts a cutting-edge livestream platform developed using a robus - **Frontend:** TypeScript, React, Next.js, TailwindCSS, Shadcn-ui - **Backend:** Prisma, MySQL, - **Real-time Communication:** Socket.io, WebRTC, WebSockets -- **Authentication & Security:** Clerk, JWT, Sonner, Svix +- **Authentication & Security:** Clerk, JWT, Sonner, Svix, ngrok - **State Management:** Zustand - **Media & Data Handling:** Tanstack/react-table, Uploadthing - **Streaming Protocols:** RTMP and WHIP connections -- **Additional Integrations:** Livekit, Webhooks +- **Additional Integrations:** Livekit, Webhooks, OBS ### Features & Capabilities: @@ -37,7 +37,7 @@ This project is continuously evolving, incorporating new technologies and enhanc #### Stack -`Typescript, React, Next.js, TailwindCSS, Shadcn-ui, Prisma, MySQL, Socket.io, WebRTC, WebSockets, Clerk, Livekit, Tanstack/react-table, Uploadthing, JWT, Sonner, Svix, Zustand, Webhooks, RTMP and WHIP connections, and more. ` +`Typescript, React, Next.js, TailwindCSS, Shadcn-ui, Prisma, MySQL, Socket.io, WebRTC, WebSockets, Clerk, Livekit, Tanstack/react-table, Uploadthing, JWT, Sonner, Svix, Ngrok, Zustand, Webhooks, RTMP and WHIP connections, and more. ` ## Getting Started diff --git a/app/(browse)/(home)/page.tsx b/app/(browse)/(home)/page.tsx new file mode 100644 index 0000000..1f32872 --- /dev/null +++ b/app/(browse)/(home)/page.tsx @@ -0,0 +1,7 @@ +export default function RootPage() { + return ( +
+

Home Page

+
+ ); +} diff --git a/app/(browse)/_components/navbar/actions.tsx b/app/(browse)/_components/navbar/actions.tsx new file mode 100644 index 0000000..93a92fc --- /dev/null +++ b/app/(browse)/_components/navbar/actions.tsx @@ -0,0 +1,37 @@ +import Link from "next/link"; +import { AppWindowIcon } from "lucide-react"; +import { SignInButton, UserButton, currentUser } from "@clerk/nextjs"; + +import { Button } from "@/components/ui/button"; + +const Actions = async () => { + const user = await currentUser(); + + return ( +
+ {!user && ( + + + + )} + {!!user && ( +
+ + +
+ )} +
+ ); +}; + +export default Actions; diff --git a/app/(browse)/_components/navbar/index.tsx b/app/(browse)/_components/navbar/index.tsx new file mode 100644 index 0000000..6bf831e --- /dev/null +++ b/app/(browse)/_components/navbar/index.tsx @@ -0,0 +1,15 @@ +import Actions from "./actions"; +import Logo from "./logo"; +import Search from "./search"; + +const Navbar = () => { + return ( + + ); +}; + +export default Navbar; diff --git a/app/(browse)/_components/navbar/logo.tsx b/app/(browse)/_components/navbar/logo.tsx new file mode 100644 index 0000000..e50fd05 --- /dev/null +++ b/app/(browse)/_components/navbar/logo.tsx @@ -0,0 +1,36 @@ +import Link from "next/link"; +import Image from "next/image"; +import { Poppins } from "next/font/google"; + +import { cn } from "@/lib/utils"; + +const font = Poppins({ + subsets: ["latin"], + weight: ["200", "300", "400", "500", "600", "700", "800"], +}); + +const Logo = () => { + return ( + +
+
+ S3mer logo +
+ +
+

S3MER

+

+ Stream, Share, Engage! +

+
+
+ + ); +}; + +export default Logo; diff --git a/app/(browse)/_components/navbar/search.tsx b/app/(browse)/_components/navbar/search.tsx new file mode 100644 index 0000000..8c579c4 --- /dev/null +++ b/app/(browse)/_components/navbar/search.tsx @@ -0,0 +1,64 @@ +"use client"; + +import qs from "query-string"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { SearchIcon, X } from "lucide-react"; + +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; + +const Search = () => { + const router = useRouter(); + const [value, setValue] = useState(""); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!value) return; + + const url = qs.stringifyUrl( + { + url: "/search", + query: { term: value }, + }, + { skipEmptyString: true } + ); + + router.push(url); + }; + + const onClear = () => { + setValue(""); + }; + + return ( +
+ setValue(e.target.value)} + placeholder="Search" + className="rounded-r-none focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0" + /> + + {value && ( + + )} + + + ); +}; + +export default Search; diff --git a/app/(browse)/layout.tsx b/app/(browse)/layout.tsx new file mode 100644 index 0000000..830a07a --- /dev/null +++ b/app/(browse)/layout.tsx @@ -0,0 +1,12 @@ +import Navbar from "./_components/navbar"; + +const BrowseLayout = ({ children }: { children: React.ReactNode }) => { + return ( + <> + +
{children}
+ + ); +}; + +export default BrowseLayout; diff --git a/app/api/webhooks/clerk/route.ts b/app/api/webhooks/clerk/route.ts new file mode 100644 index 0000000..1a7d0fa --- /dev/null +++ b/app/api/webhooks/clerk/route.ts @@ -0,0 +1,94 @@ +import { Webhook } from "svix"; +import { headers } from "next/headers"; +import { WebhookEvent } from "@clerk/nextjs/server"; + +import { db } from "@/lib/db"; +// import { resetIngresses } from "@/actions/ingress"; + +export async function POST(req: Request) { + // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook + const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET; + + if (!WEBHOOK_SECRET) { + throw new Error( + "Please add CLERK_WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local" + ); + } + + // Get the headers + const headerPayload = headers(); + const svix_id = headerPayload.get("svix-id"); + const svix_timestamp = headerPayload.get("svix-timestamp"); + const svix_signature = headerPayload.get("svix-signature"); + + // If there are no headers, error out + if (!svix_id || !svix_timestamp || !svix_signature) { + return new Response("Error occured -- no svix headers", { + status: 400, + }); + } + + // Get the body + const payload = await req.json(); + const body = JSON.stringify(payload); + + // Create a new Svix instance with your secret. + const wh = new Webhook(WEBHOOK_SECRET); + + let evt: WebhookEvent; + + // Verify the payload with the headers + try { + evt = wh.verify(body, { + "svix-id": svix_id, + "svix-timestamp": svix_timestamp, + "svix-signature": svix_signature, + }) as WebhookEvent; + } catch (err) { + console.error("Error verifying webhook:", err); + return new Response("Error occured", { + status: 400, + }); + } + + const eventType = evt.type; + + if (eventType === "user.created") { + await db.user.create({ + data: { + externalUserId: payload.data.id, + username: payload.data.username, + imageUrl: payload.data.image_url, + stream: { + create: { + name: `${payload.data.username}'s stream`, + }, + }, + }, + }); + } + + if (eventType === "user.updated") { + await db.user.update({ + where: { + externalUserId: payload.data.id, + }, + data: { + username: payload.data.username, + imageUrl: payload.data.image_url, + }, + }); + } + + if (eventType === "user.deleted") { + // await resetIngresses(payload.data.id); + + await db.user.delete({ + where: { + externalUserId: payload.data.id, + }, + }); + } + + return new Response("", { status: 200 }); +} diff --git a/app/favicon.ico b/app/favicon.ico index da1c37d..c41b1a6 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index 45fbac9..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { UserButton } from "@clerk/nextjs"; - -export default function RootPage() { - return ( -
-

Welcome to Root Page

- -
- ); -} diff --git a/components/ui/input.tsx b/components/ui/input.tsx new file mode 100644 index 0000000..677d05f --- /dev/null +++ b/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/middleware.ts b/middleware.ts index c01a173..7c8e8f3 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,6 +1,14 @@ import { authMiddleware } from "@clerk/nextjs"; -export default authMiddleware({}); +export default authMiddleware({ + publicRoutes: [ + "/", + "/api/webhooks(.*)", + "/api/uploadthing", + "/:username", + "/search", + ], +}); export const config = { matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], diff --git a/next.config.js b/next.config.js index 767719f..3d06ef0 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,17 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "utfs.io", + }, + { + protocol: "https", + hostname: "uploadthing.com", + }, + ], + }, +}; -module.exports = nextConfig +module.exports = nextConfig; diff --git a/package-lock.json b/package-lock.json index 65276b5..493b7f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,10 @@ "lucide-react": "^0.294.0", "next": "14.0.4", "next-themes": "^0.2.1", + "query-string": "^8.1.0", "react": "^18", "react-dom": "^18", + "svix": "^1.15.0", "tailwind-merge": "^2.1.0", "tailwindcss-animate": "^1.0.7" }, @@ -676,6 +678,11 @@ "integrity": "sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA==", "dev": true }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" + }, "node_modules/@swc/helpers": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", @@ -1600,6 +1607,14 @@ } } }, + "node_modules/decode-uri-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", + "engines": { + "node": ">=14.16" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1845,6 +1860,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2320,6 +2340,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -2351,6 +2376,17 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3507,6 +3543,25 @@ "tslib": "^2.0.3" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-fetch-native": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.0.1.tgz", @@ -4003,6 +4058,27 @@ "node": ">=6.0.0" } }, + "node_modules/query-string": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz", + "integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==", + "dependencies": { + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4120,6 +4196,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -4369,6 +4450,17 @@ "node": ">=0.10.0" } }, + "node_modules/split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -4560,6 +4652,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svix": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.15.0.tgz", + "integrity": "sha512-oV11/VIpD77QymPEIjGr8XvQwcJxPIRO8XVpWJb33ZX2qs1q7jYlVaSJ6ABYThKbmnxIGyJr5+RpchVOSE7pZg==", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "es6-promise": "^4.2.4", + "fast-sha256": "^1.3.0", + "svix-fetch": "^3.0.0", + "url-parse": "^1.4.3" + } + }, + "node_modules/svix-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/svix-fetch/-/svix-fetch-3.0.0.tgz", + "integrity": "sha512-rcADxEFhSqHbraZIsjyZNh4TF6V+koloX1OzZ+AQuObX9mZ2LIMhm1buZeuc5BIZPftZpJCMBsSiBaeszo9tRw==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "node_modules/swr": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz", @@ -4693,6 +4806,11 @@ "to-no-case": "^1.0.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -4888,6 +5006,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -4925,6 +5052,25 @@ "tslib": "^2.4.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.19", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.19.tgz", + "integrity": "sha512-d67JP4dHSbm2TrpFj8AbO8DnL1JXL5J9u0Kq2xW6d0TFDbCA3Muhdt8orXC22utleTVj7Prqt82baN6RBvnEgw==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index b857ef4..3d23983 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,10 @@ "lucide-react": "^0.294.0", "next": "14.0.4", "next-themes": "^0.2.1", + "query-string": "^8.1.0", "react": "^18", "react-dom": "^18", + "svix": "^1.15.0", "tailwind-merge": "^2.1.0", "tailwindcss-animate": "^1.0.7" }, diff --git a/public/s3mer.png b/public/s3mer.png index da1c37d..c41b1a6 100644 Binary files a/public/s3mer.png and b/public/s3mer.png differ diff --git a/public/s3mer.svg b/public/s3mer.svg index 6488fd2..2eb571a 100644 --- a/public/s3mer.svg +++ b/public/s3mer.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file