Skip to content

Commit

Permalink
feat: Stream Info Card (#34)
Browse files Browse the repository at this point in the history
---

<details open="true"><summary>Generated summary (powered by <a href="https://app.graphite.dev">Graphite</a>)</summary>

> ## TL;DR
> This pull request includes various changes to the codebase. It adds a file router for handling file uploads, updates the StreamPlayer component to include an InfoCard component, and introduces an InfoModal component for editing stream information. It also includes updates to dependencies and the addition of new files.
> 
> ## What changed
> - Added a file router for handling file uploads
> - Updated the StreamPlayer component to include an InfoCard component
> - Introduced an InfoModal component for editing stream information
> - Added new files: "info-modal.tsx", "separator.tsx", "uploadthing.ts"
> 
> ## How to test
> 1. Test the file router by uploading different file types and checking if the permissions and file types are set correctly.
> 2. Test the StreamPlayer component by rendering it and verifying that the InfoCard component displays the stream information correctly.
> 3. Test the InfoModal component by rendering it and checking if the form for editing stream information works as expected.
> 4. Test the functionality of the InfoCard component by passing different props such as name, thumbnailUrl, hostIdentity, and viewerIdentity and verifying that the component renders correctly.
> 5. Test the InfoModal component by passing initialName and initialThumbnailUrl props and checking if the state variables are initialized correctly.
> 6. Test the conditional rendering by checking if the div element with specific styling or the UploadDropzone component is displayed based on the condition.
> 7. Test the form dialog by triggering the dialog box, entering values in the input fields, and submitting the form to update the stream information.
> 8. Test the Label and Separator components by rendering them and verifying that they apply the specified styles correctly.
> 9. Test the dependencies and peer dependencies by running the application and checking if all the required packages are installed and functioning correctly.
> 
> ## Why make this change
> - The file router is necessary for handling file uploads and setting permissions and file types for the file route.
> - The InfoCard component provides important information about the stream and enhances the functionality of the StreamPlayer component.
> - The InfoModal component allows users to edit stream information, improving the user experience.
> - The updates to dependencies ensure that the codebase is up to date and compatible with the latest versions of the required packages.
</details>
  • Loading branch information
Zaid-maker authored Jan 6, 2024
2 parents 69d5735 + 4d7a9c7 commit 1c015cb
Show file tree
Hide file tree
Showing 12 changed files with 463 additions and 22 deletions.
33 changes: 33 additions & 0 deletions app/api/uploadthing/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getSelf } from "@/lib/auth-service";
import { db } from "@/lib/db";
import { createUploadthing, type FileRouter } from "uploadthing/next";

const f = createUploadthing();

// FileRouter for your app, can contain multiple FileRoutes
export const ourFileRouter = {
// Define as many FileRoutes as you like, each with a unique routeSlug
thumbnailUploader: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } })
// Set permissions and file types for this FileRoute
.middleware(async () => {
// This code runs on your server before upload
const self = await getSelf();

return { user: self };
})
.onUploadComplete(async ({ metadata, file }) => {
await db.stream.update({
where: {
userId: metadata.user.id,
},
data: {
thumbnailUrl: file.url,
},
});

// !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
return { fileUrl: file.url };
}),
} satisfies FileRouter;

export type OurFileRouter = typeof ourFileRouter;
8 changes: 8 additions & 0 deletions app/api/uploadthing/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createNextRouteHandler } from "uploadthing/next";

import { ourFileRouter } from "./core";

// Export routes for Next App Router
export const { GET, POST } = createNextRouteHandler({
router: ourFileRouter,
});
7 changes: 7 additions & 0 deletions components/stream-player/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Chat, ChatSkeleton } from "./chat";
import { ChatToggle } from "./chat-toggle";
import { Video, VideoSkeleton } from "./video";
import { Header, HeaderSkeleton } from "./header";
import { InfoCard } from "./info-card";

interface StreamPlayerProps {
user: User & { stream: Stream | null };
Expand Down Expand Up @@ -53,6 +54,12 @@ export const StreamPlayer = ({
isFollowing={isFollowing}
name={stream.name}
/>
<InfoCard
hostIdentity={user.id}
viewerIdentity={identity}
name={stream.name}
thumbnailUrl={stream.thumbnailUrl}
/>
</div>
<div className={cn("col-span-1", collapsed && "hidden")}>
<Chat
Expand Down
62 changes: 62 additions & 0 deletions components/stream-player/info-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use client";

import { Pencil } from "lucide-react";
import React from "react";
import { Separator } from "../ui/separator";
import Image from "next/image";
import { InfoModal } from "./info-modal";

interface InfoCardProps {
name: string;
thumbnailUrl: string | null;
hostIdentity: string;
viewerIdentity: string;
}

export const InfoCard = ({
name,
thumbnailUrl,
hostIdentity,
viewerIdentity,
}: InfoCardProps) => {
return (
<div className="px-4">
<div className="rounded=xl bg-background">
<div className="flex items-center gap-x-2.5 p-4">
<div className="rounded-md bg-blue-600 p-2 h-auto w-auto">
<Pencil className="h-5 w-5" />
</div>
<div>
<h2 className="text-sm lg:text-lg font-semibold capitalize">
Edit your stream info
</h2>
<p className="text-muted-foreground text-xs lg:text-sm">
Maximize your visibility
</p>
</div>
<InfoModal initialName={name} initialThumbnailUrl={thumbnailUrl} />
</div>
<Separator />
<div className="p-4 lg:p-6 space-y-4">
<div>
<h3 className="text-sm text-muted-foreground mb-2">Name</h3>
<p className="text-sm font-semibold">{name}</p>
</div>
<div>
<h3 className="text-sm text-muted-foreground mb-2">Thumbnail</h3>
{thumbnailUrl && (
<div className="relative aspect-video rounded-md overflow-hidden w-[200px] border border-white/10">
<Image
fill
src={thumbnailUrl}
alt={name}
className="object-cover"
/>
</div>
)}
</div>
</div>
</div>
</div>
);
};
147 changes: 147 additions & 0 deletions components/stream-player/info-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"use client";

import { updateStream } from "@/actions/stream";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { UploadDropzone } from "@/lib/uploadthing";
import Image from "next/image";
import { useRouter } from "next/navigation";
import React, { ElementRef, useRef, useState, useTransition } from "react";
import { toast } from "sonner";
import { Hint } from "../hint";
import { Trash } from "lucide-react";

interface InfoModalProps {
initialName: string;
initialThumbnailUrl: string | null;
}

export const InfoModal = ({
initialName,
initialThumbnailUrl,
}: InfoModalProps) => {
const router = useRouter();
const closeRef = useRef<ElementRef<"button">>(null);

const [isPending, startTransition] = useTransition();
const [name, setName] = useState(initialName);
const [thumbnailUrl, setThumbnailUrl] = useState(initialThumbnailUrl);

const onRemove = () => {
startTransition(() => {
updateStream({ thumbnailUrl: null })
.then(() => {
toast.success("Thumbnail removed");
setThumbnailUrl("");
closeRef?.current?.click();
})
.catch(() => toast.error("Something went wrong!"));
});
};

const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

startTransition(() => {
updateStream({ name: name })
.then(() => {
toast.success("Stream settings updated");
closeRef?.current?.click();
})
.catch(() => toast.error("Something went wrong!"));
});
};

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};

return (
<Dialog>
<DialogTrigger asChild>
<Button variant="link" size="sm" className="ml-auto">
Edit
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Stream Info</DialogTitle>
</DialogHeader>
<form onSubmit={onSubmit} className="space-y-14">
<div className="space-y-2">
<Label>Name</Label>
<Input
disabled={isPending}
placeholder="Stream Name"
onChange={onChange}
value={name}
/>
</div>
<div className="space-y-2">
<Label>Thumbnail</Label>
{thumbnailUrl ? (
<div className="relative aspect-video rounded-xl overflow-hidden border border-white/10">
<div className="absolute top-2 right-2 z-[10]">
<Hint label="Remove thumbnail" asChild side="left">
<Button
type="button"
disabled={isPending}
onClick={onRemove}
className="h-auto w-auto p-1.5"
>
<Trash className="h-4 w-4" />
</Button>
</Hint>
</div>
<Image
alt="thumbnail"
src={thumbnailUrl}
className="object-cover"
fill
/>
</div>
) : (
<div className="rounded-xl border outline-dashed outline-muted">
<UploadDropzone
endpoint="thumbnailUploader"
appearance={{
label: {
color: "#FFFFFF",
},
allowedContent: {
color: "#FFFFFF",
},
}}
onClientUploadComplete={(res) => {
setThumbnailUrl(res?.[0]?.url);
router.refresh();
closeRef?.current?.click();
}}
/>
</div>
)}
</div>
<div className="flex justify-between">
<DialogClose ref={closeRef} asChild>
<Button type="button" variant="ghost">
Cancel
</Button>
</DialogClose>
<Button disabled={isPending} variant="primary" type="submit">
Save
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
};
26 changes: 26 additions & 0 deletions components/ui/label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client"

import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"

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

const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)

const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName

export { Label }
31 changes: 31 additions & 0 deletions components/ui/separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client"

import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"

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

const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName

export { Separator }
6 changes: 6 additions & 0 deletions lib/uploadthing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { generateComponents } from "@uploadthing/react";

import type { OurFileRouter } from "@/app/api/uploadthing/core";

export const { UploadButton, UploadDropzone, Uploader } =
generateComponents<OurFileRouter>();
2 changes: 1 addition & 1 deletion next.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ["utfs.io"],
remotePatterns: [{ protocol: "https", hostname: "utfs.io" }],
},
webpack: (config) => {
config.module.rules.push({
Expand Down
Loading

0 comments on commit 1c015cb

Please sign in to comment.