From 7d21c9126a9d78ee4236d80c495d4585b62f7da8 Mon Sep 17 00:00:00 2001 From: Ricardo Esteves Date: Fri, 15 Dec 2023 18:01:17 +0000 Subject: [PATCH] feat: :sparkles: Stream and chat features. - Create stream functionality. - Implement chat feature. - Create chat page. - Create stream service. - Create stream actions. - Create loading skeletons for chat page. Users can now define their chat settings. --- actions/stream.ts | 47 +++++++++++++++++ .../chat/_components/toggle-card.tsx | 51 +++++++++++++++++++ app/(dashboard)/u/[username]/chat/loading.tsx | 18 +++++++ app/(dashboard)/u/[username]/chat/page.tsx | 40 +++++++++++++++ components/ui/switch.tsx | 29 +++++++++++ lib/stream-service.ts | 9 ++++ package-lock.json | 47 +++++++++++++++++ package.json | 1 + 8 files changed, 242 insertions(+) create mode 100644 actions/stream.ts create mode 100644 app/(dashboard)/u/[username]/chat/_components/toggle-card.tsx create mode 100644 app/(dashboard)/u/[username]/chat/loading.tsx create mode 100644 app/(dashboard)/u/[username]/chat/page.tsx create mode 100644 components/ui/switch.tsx create mode 100644 lib/stream-service.ts diff --git a/actions/stream.ts b/actions/stream.ts new file mode 100644 index 0000000..46a238b --- /dev/null +++ b/actions/stream.ts @@ -0,0 +1,47 @@ +"use server"; + +import { Stream } from "@prisma/client"; +import { revalidatePath } from "next/cache"; + +import { db } from "@/lib/db"; +import { getSelf } from "@/lib/auth-service"; + +export const updateStream = async (values: Partial) => { + try { + const self = await getSelf(); + const selfStream = await db.stream.findUnique({ + where: { + userId: self.id, + }, + }); + + if (!selfStream) { + throw new Error("Stream not found"); + } + + const validData = { + thumbnailUrl: values.thumbnailUrl, + name: values.name, + isChatEnabled: values.isChatEnabled, + isChatFollowersOnly: values.isChatFollowersOnly, + isChatDelayed: values.isChatDelayed, + }; + + const stream = await db.stream.update({ + where: { + id: selfStream.id, + }, + data: { + ...validData, + }, + }); + + revalidatePath(`/u/${self.username}/chat`); + revalidatePath(`/u/${self.username}`); + revalidatePath(`/${self.username}`); + + return stream; + } catch { + throw new Error("Internal Error"); + } +}; diff --git a/app/(dashboard)/u/[username]/chat/_components/toggle-card.tsx b/app/(dashboard)/u/[username]/chat/_components/toggle-card.tsx new file mode 100644 index 0000000..e0d8c23 --- /dev/null +++ b/app/(dashboard)/u/[username]/chat/_components/toggle-card.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { toast } from "sonner"; +import { useTransition } from "react"; + +import { Switch } from "@/components/ui/switch"; +import { updateStream } from "@/actions/stream"; +import { Skeleton } from "@/components/ui/skeleton"; + +type FieldTypes = "isChatEnabled" | "isChatDelayed" | "isChatFollowersOnly"; + +interface ToggleCardProps { + label: string; + value: boolean; + field: FieldTypes; +} + +const ToggleCard = ({ label, value = false, field }: ToggleCardProps) => { + const [isPending, startTransition] = useTransition(); + + const onChange = () => { + startTransition(() => { + updateStream({ [field]: !value }) + .then(() => toast.success("Chat settings updated!")) + .catch(() => toast.error("Something went wrong")); + }); + }; + + return ( +
+
+

{label}

+
+ + {value ? "On" : "Off"} + +
+
+
+ ); +}; + +export default ToggleCard; + +export const ToggleCardSkeleton = () => { + return ; +}; diff --git a/app/(dashboard)/u/[username]/chat/loading.tsx b/app/(dashboard)/u/[username]/chat/loading.tsx new file mode 100644 index 0000000..563c39f --- /dev/null +++ b/app/(dashboard)/u/[username]/chat/loading.tsx @@ -0,0 +1,18 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +import { ToggleCardSkeleton } from "./_components/toggle-card"; + +const ChatLoading = () => { + return ( +
+ +
+ + + +
+
+ ); +}; + +export default ChatLoading; diff --git a/app/(dashboard)/u/[username]/chat/page.tsx b/app/(dashboard)/u/[username]/chat/page.tsx new file mode 100644 index 0000000..981226a --- /dev/null +++ b/app/(dashboard)/u/[username]/chat/page.tsx @@ -0,0 +1,40 @@ +import { getSelf } from "@/lib/auth-service"; +import { getStreamByUserId } from "@/lib/stream-service"; + +import ToggleCard from "./_components/toggle-card"; + +const ChatPage = async () => { + const self = await getSelf(); + const stream = await getStreamByUserId(self.id); + + if (!stream) { + throw new Error("Stream not found"); + } + + return ( +
+
+

Settings

+
+
+ + + +
+
+ ); +}; + +export default ChatPage; diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx new file mode 100644 index 0000000..bc69cf2 --- /dev/null +++ b/components/ui/switch.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/lib/stream-service.ts b/lib/stream-service.ts new file mode 100644 index 0000000..7962f19 --- /dev/null +++ b/lib/stream-service.ts @@ -0,0 +1,9 @@ +import { db } from "@/lib/db"; + +export const getStreamByUserId = async (userId: string) => { + const stream = await db.stream.findUnique({ + where: { userId }, + }); + + return stream; +}; diff --git a/package-lock.json b/package-lock.json index ad45eca..acec5c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@prisma/client": "^5.7.0", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", @@ -932,6 +933,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz", + "integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-previous": "1.0.1", + "@radix-ui/react-use-size": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", @@ -1036,6 +1066,23 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", + "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", diff --git a/package.json b/package.json index 7f4fec1..4c76506 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@prisma/client": "^5.7.0", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0",