Skip to content

Commit

Permalink
Sharing chat UI functionality (#38)
Browse files Browse the repository at this point in the history
* install dialog for sharing

* Add share chat component

* Add chat context to root layout

* add copy to clipboard for share component

* Share CRUD

* Fix max chat readers limit

* Fixes

* Fix share dialog auto grow

* Fix lint and react warning

* Text link copied button

* Last touches to the share form
  • Loading branch information
cristiandouce authored Oct 7, 2024
1 parent 6639d2c commit d7f71d3
Show file tree
Hide file tree
Showing 14 changed files with 1,202 additions and 399 deletions.
9 changes: 8 additions & 1 deletion app/chat/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import { generateId } from "ai";
import { useActions, useUIState } from "ai/rsc";
import Link from "next/link";
import { use, useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { useChat } from "@/components/chat/context";
import {
ArrowUpIcon,
ChevronRightIcon,
Expand Down Expand Up @@ -42,6 +44,7 @@ export default function Chat({ params }: { params: { id: string } }) {
const { continueConversation } = useActions();
const { user } = useUser();
const { scrollRef, messagesRef, visibilityRef } = useScrollAnchor();
const { setChatId } = useChat();

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
Expand Down Expand Up @@ -80,6 +83,11 @@ export default function Chat({ params }: { params: { id: string } }) {
]);
};

useEffect(() => {
setChatId(params.id);
return () => setChatId();
}, [params.id, setChatId]);

return (
<main
className="flex overflow-hidden h-full mx-auto pt-4"
Expand Down Expand Up @@ -127,7 +135,6 @@ export default function Chat({ params }: { params: { id: string } }) {
)}
<div ref={visibilityRef} className="w-full h-px" />
</div>

{conversation.length === 0 && (
<div className="flex flex-col gap-80 max-w-4xl mx-auto w-full mb-5 mt-auto">
<div className="min-w-0 min-h-0 w-full flex flex-col items-center gap-6">
Expand Down
11 changes: 8 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import "./globals.css";

import { Inter } from "next/font/google";

import { Header } from "@/components/header";
import { ChatProvider } from "@/components/chat/context";
import { Header } from "@/components/chat/header";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/toaster";
import { cn } from "@/lib/utils";

const inter = Inter({ subsets: ["latin"] });
Expand All @@ -29,8 +31,11 @@ export default async function RootLayout({
enableSystem
disableTransitionOnChange
>
<Header />
{children}
<Toaster />
<ChatProvider>
<Header />
{children}
</ChatProvider>
</ThemeProvider>
</body>
</html>
Expand Down
197 changes: 77 additions & 120 deletions components/auth0/connected-accounts.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
"use client";

import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import React, { useCallback, useEffect, useState } from "react";

import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Toaster } from "@/components/ui/toaster";
import { useToast } from "@/components/ui/use-toast";

function Spinner() {
Expand Down Expand Up @@ -71,13 +64,10 @@ export default function ConnectedAccounts({
const { toast } = useToast();
const router = useRouter();

const [currentConnectedAccounts, setCurrentConnectedAccounts] =
useState(connectedAccounts);
const [currentConnectedAccounts, setCurrentConnectedAccounts] = useState(connectedAccounts);
const [fetching, setFetching] = useState(false);
const [isLinkingAccount, setIsLinkingAccount] = useState<string | null>(null);
const [isUnlinkingAccount, setIsUnlinkingAccount] = useState<string | null>(
null
);
const [isUnlinkingAccount, setIsUnlinkingAccount] = useState<string | null>(null);

const handleFetchConnectedAccounts = useCallback(
async function handleFetchSessions() {
Expand Down Expand Up @@ -130,125 +120,92 @@ export default function ConnectedAccounts({

return toast({
title: "Info",
description:
"There was a problem unlinking the account. Try again later.",
description: "There was a problem unlinking the account. Try again later.",
});
}

router.push(
`/api/auth/login?returnTo=${encodeURIComponent(
"/profile#connected-accounts"
)}`
);
router.push(`/api/auth/login?returnTo=${encodeURIComponent("/profile#connected-accounts")}`);
};

return (
<>
<Toaster />
<Card>
<CardHeader className="p-4 md:p-6">
<CardTitle className="text-lg font-normal"></CardTitle>
<CardDescription>
Connect your social accounts to access their information.
</CardDescription>
</CardHeader>

<CardContent className="grid gap-6 p-4 pt-0 md:p-6 md:pt-0">
{fetching && (
<div className="flex w-full items-center justify-left">
<Spinner />
<span className="text-sm text-muted-foreground">
Retrieving your connected accounts...
</span>
</div>
)}

{!currentConnectedAccounts && !fetching && (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between space-x-2">
<Label className="flex flex-col space-y-2">
<p className="font-normal leading-snug text-muted-foreground max-w-fit">
There was a problem fetching your connected accounts. Try
again later.
</p>
</Label>
</div>
<Card>
<CardHeader className="p-4 md:p-6">
<CardTitle className="text-lg font-normal"></CardTitle>
<CardDescription>Connect your social accounts to access their information.</CardDescription>
</CardHeader>

<CardContent className="grid gap-6 p-4 pt-0 md:p-6 md:pt-0">
{fetching && (
<div className="flex w-full items-center justify-left">
<Spinner />
<span className="text-sm text-muted-foreground">Retrieving your connected accounts...</span>
</div>
)}

{!currentConnectedAccounts && !fetching && (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between space-x-2">
<Label className="flex flex-col space-y-2">
<p className="font-normal leading-snug text-muted-foreground max-w-fit">
There was a problem fetching your connected accounts. Try again later.
</p>
</Label>
</div>
)}

{!fetching &&
currentConnectedAccounts &&
availableAccounts.map(
(
{ connection, displayName, description, api }: Account,
idx: number
) => {
const isConnected = currentConnectedAccounts.some(
(cca) => cca.connection === connection
);

const isMainConnection =
connection === currentConnectedAccounts[0]?.connection;

return (
<div
key={`connection-${idx}-${connection}`}
className="flex flex-col gap-6"
>
{idx > 0 && <Separator />}
<div
key={connection}
className="flex flex-col md:flex-row items-center justify-between md:space-x-2 space-y-6 md:space-y-0"
>
<Label className="flex flex-col space-y-1">
<span className="leading-6">{displayName}</span>
{description && (
<p className="font-normal leading-snug text-muted-foreground max-w-fit">
{description}
</p>
)}
</Label>
<div className="flex space-x-24 items-center justify-end md:min-w-24">
{isConnected ? (
<>
{onUnlink && (
<Button
className="h-fit min-w-24"
variant="outline"
onClick={handleUnlinkAccount(connection)}
disabled={
isUnlinkingAccount === connection ||
isMainConnection
}
>
{isUnlinkingAccount === connection && (
<Spinner />
)}
Disconnect
</Button>
)}
</>
) : (
</div>
)}

{!fetching &&
currentConnectedAccounts &&
availableAccounts.map(({ connection, displayName, description, api }: Account, idx: number) => {
const isConnected = currentConnectedAccounts.some((cca) => cca.connection === connection);

const isMainConnection = connection === currentConnectedAccounts[0]?.connection;

return (
<div key={`connection-${idx}-${connection}`} className="flex flex-col gap-6">
{idx > 0 && <Separator />}
<div
key={connection}
className="flex flex-col md:flex-row items-center justify-between md:space-x-2 space-y-6 md:space-y-0"
>
<Label className="flex flex-col space-y-1">
<span className="leading-6">{displayName}</span>
{description && (
<p className="font-normal leading-snug text-muted-foreground max-w-fit">{description}</p>
)}
</Label>
<div className="flex space-x-24 items-center justify-end md:min-w-24">
{isConnected ? (
<>
{onUnlink && (
<Button
className="h-fit min-w-24"
variant="outline"
disabled={
!allowLink || isLinkingAccount === connection
}
onClick={handleLinkAccount(connection, api)}
onClick={handleUnlinkAccount(connection)}
disabled={isUnlinkingAccount === connection || isMainConnection}
>
{isLinkingAccount === connection && <Spinner />}
Connect
{isUnlinkingAccount === connection && <Spinner />}
Disconnect
</Button>
)}
</div>
</div>
</>
) : (
<Button
className="h-fit min-w-24"
variant="outline"
disabled={!allowLink || isLinkingAccount === connection}
onClick={handleLinkAccount(connection, api)}
>
{isLinkingAccount === connection && <Spinner />}
Connect
</Button>
)}
</div>
);
}
)}
</CardContent>
</Card>
</>
</div>
</div>
);
})}
</CardContent>
</Card>
);
}
34 changes: 34 additions & 0 deletions components/chat/context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import React, { createContext, ReactNode, useContext, useState } from "react";

interface ChatContextProps {
chatId?: string;
setChatId: (id?: string) => void;
}

const ChatContext = createContext<ChatContextProps | undefined>(undefined);

export const ChatProvider = ({
chatId: initialChatId,
children,
}: {
chatId?: string;
children: ReactNode;
}) => {
const [chatId, setChatId] = useState(initialChatId);

return (
<ChatContext.Provider value={{ chatId, setChatId }}>
{children}
</ChatContext.Provider>
);
};

export const useChat = () => {
const context = useContext(ChatContext);
if (!context) {
throw new Error("useChat must be used within a ChatProvider");
}
return context;
};
59 changes: 59 additions & 0 deletions components/chat/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Link from "next/link";

import { ArrowRightIcon, GHIcon, IconAuth0 } from "@/components/icons";
import { getSession } from "@auth0/nextjs-auth0";

import UserButton from "../auth0/user-button";
import { DropdownMenu, DropdownMenuGroup, DropdownMenuItem, DropdownMenuShortcut } from "../ui/dropdown-menu";
import { ShareConversation } from "./share";

export async function Header() {
const session = await getSession();
const user = session?.user!;

return (
<header className="sticky top-0 z-50 flex items-center justify-between w-full px-6 py-3 h-14 shrink-0 bg-background backdrop-blur-xl">
<div className="flex items-center gap-6">
<span className="inline-flex items-center home-links whitespace-nowrap">
<Link href="https://lab.auth0.com" rel="noopener" target="_blank">
<IconAuth0 className="w-5 h-5 sm:h-6 sm:w-6" />
</Link>
</span>
<Link
href="#"
target="_blank"
rel="noopener noreferrer"
className="hover:text-black transition-all duration-300 text-sm font-light text-slate-500 flex items-center gap-1"
>
Learn about Auth for GenAI <ArrowRightIcon />
</Link>
</div>
<div className="flex items-center justify-end gap-6">
<div className="flex items-center justify-end gap-6">
<ShareConversation user={user} />

<Link
href="https://github.com/auth0-lab/market0"
rel="noopener noreferrer"
target="_blank"
className="bg-white text-slate-500 border border-slate-500 flex gap-2 items-center px-3 py-2 rounded-md text-sm hover:bg-gray-100 hover:text-black transition-colors duration-300"
>
<GHIcon /> GitHub
</Link>
<UserButton user={user}>
<DropdownMenu>
<DropdownMenuGroup>
<DropdownMenuItem>
<Link href="/profile" className="flex gap-2 items-center">
Profile
</Link>
<DropdownMenuShortcut>⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenu>
</UserButton>
</div>
</div>
</header>
);
}
Loading

0 comments on commit d7f71d3

Please sign in to comment.