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
+
+ 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 (
+
+ );
+};
+
+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