Skip to content

Commit

Permalink
feat: ✨ Navbar, local tunnel and clerk webhook.
Browse files Browse the repository at this point in the history
-Introduce navigation .
- Create navbar and components that compose it (browse _components).
- Add query-string and svix packages.
- Create tunnel with ngrok for stability and security reasons.
- Implement clerk webhook and it's route. - Add public routes to clerk middleware.
- Add images remote patterns to next.config.
- Update README.
- Add assets.

Navigation, full client life cycle authentication from login to logout.
  • Loading branch information
RicardoGEsteves committed Dec 13, 2023
1 parent 72ad71c commit 64daf53
Show file tree
Hide file tree
Showing 17 changed files with 466 additions and 17 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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

Expand Down
7 changes: 7 additions & 0 deletions app/(browse)/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function RootPage() {
return (
<div className="flex flex-col gap-y-4">
<h1 className="text-3xl font-bold">Home Page</h1>
</div>
);
}
37 changes: 37 additions & 0 deletions app/(browse)/_components/navbar/actions.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex items-center justify-end gap-x-2 ml-4 lg:ml-0">
{!user && (
<SignInButton>
<Button size="sm">Login</Button>
</SignInButton>
)}
{!!user && (
<div className="flex items-center gap-x-4">
<Button
size="sm"
variant="ghost"
className="text-muted-foreground"
asChild
>
<Link href={`/u/${user.username}`}>
<AppWindowIcon className="h-5 w-5 lg:mr-2" />
<span className="hidden lg:block">Dashboard</span>
</Link>
</Button>
<UserButton afterSignOutUrl="/" />
</div>
)}
</div>
);
};

export default Actions;
15 changes: 15 additions & 0 deletions app/(browse)/_components/navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Actions from "./actions";
import Logo from "./logo";
import Search from "./search";

const Navbar = () => {
return (
<nav className="fixed top-0 w-full h-20 z-[49] bg-background px-2 lg:px-4 flex justify-between items-center shadow-sm border-b">
<Logo />
<Search />
<Actions />
</nav>
);
};

export default Navbar;
36 changes: 36 additions & 0 deletions app/(browse)/_components/navbar/logo.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link href="/">
<div className="flex items-center gap-x-4 hover:opacity-75 transition">
<div className="rounded-full mr-12 shrink-0 lg:mr-0 lg:shrink">
<Image
src="/s3mer.svg"
alt="S3mer logo"
height={50}
width={50}
/>
</div>

<div className={cn("hidden lg:block", font.className)}>
<p className="text-lg font-semibold">S3MER</p>
<p className="text-xs text-muted-foreground">
Stream, Share, Engage!
</p>
</div>
</div>
</Link>
);
};

export default Logo;
64 changes: 64 additions & 0 deletions app/(browse)/_components/navbar/search.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLFormElement>) => {
e.preventDefault();

if (!value) return;

const url = qs.stringifyUrl(
{
url: "/search",
query: { term: value },
},
{ skipEmptyString: true }
);

router.push(url);
};

const onClear = () => {
setValue("");
};

return (
<form
className="relative w-full lg:w-[400px] flex items-center"
onSubmit={onSubmit}
>
<Input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Search"
className="rounded-r-none focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0"
/>

{value && (
<X
className="absolute top-2.5 right-14 h-5 w-5 text-muted-foreground cursor-pointer hover:opacity-75 transition"
onClick={onClear}
/>
)}
<Button
type="submit"
variant="secondary"
className="rounded-l-none"
>
<SearchIcon className="h-5 w-5 text-muted-foreground" />
</Button>
</form>
);
};

export default Search;
12 changes: 12 additions & 0 deletions app/(browse)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Navbar from "./_components/navbar";

const BrowseLayout = ({ children }: { children: React.ReactNode }) => {
return (
<>
<Navbar />
<div className="flex h-full pt-20">{children}</div>
</>
);
};

export default BrowseLayout;
94 changes: 94 additions & 0 deletions app/api/webhooks/clerk/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
Binary file modified app/favicon.ico
Binary file not shown.
10 changes: 0 additions & 10 deletions app/page.tsx

This file was deleted.

25 changes: 25 additions & 0 deletions components/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from "react"

import { cn } from "@/lib/utils"

export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"

export { Input }
10 changes: 9 additions & 1 deletion middleware.ts
Original file line number Diff line number Diff line change
@@ -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)(.*)"],
Expand Down
17 changes: 15 additions & 2 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 64daf53

Please sign in to comment.