From a36b526312769819bf24bd16e1dc75322ee25656 Mon Sep 17 00:00:00 2001 From: Aamir Azad <82281117+aamirazad@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:36:09 +0530 Subject: [PATCH] feat: user profile dropdown & settings (#12) --- next.config.js | 19 ++- package.json | 1 + pnpm-lock.yaml | 20 ++- src/app/actions.ts | 12 +- src/app/layout.tsx | 7 +- src/app/setup/forms.tsx | 291 -------------------------------------- src/app/setup/page.tsx | 291 +++++++++++++++++++++++++++++++++++++- src/components/topnav.tsx | 52 ++++++- 8 files changed, 380 insertions(+), 313 deletions(-) delete mode 100644 src/app/setup/forms.tsx diff --git a/next.config.js b/next.config.js index 41cc5e8..07d270c 100644 --- a/next.config.js +++ b/next.config.js @@ -6,12 +6,19 @@ await import("./src/env.js"); /** @type {import("next").NextConfig} */ const config = { - typescript: { - ignoreBuildErrors: true, - }, - eslint: { - ignoreDuringBuilds: true, - }, + typescript: { + ignoreBuildErrors: true, + }, + eslint: { + ignoreDuringBuilds: true, + }, + images: { + remotePatterns: [ + { + hostname: "img.clerk.com", + }, + ], + }, }; export default config; diff --git a/package.json b/package.json index 9363e31..29957de 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@clerk/nextjs": "^5.1.5", + "@clerk/themes": "^2.1.9", "@hookform/resolvers": "^3.6.0", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ba4ae9..fbce1ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@clerk/nextjs': specifier: ^5.1.5 version: 5.1.5(next@14.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/themes': + specifier: ^2.1.9 + version: 2.1.9 '@hookform/resolvers': specifier: ^3.6.0 version: 3.6.0(react-hook-form@7.51.5(react@18.3.1)) @@ -180,6 +183,10 @@ packages: react-dom: optional: true + '@clerk/themes@2.1.9': + resolution: {integrity: sha512-MAWwVSYlm+hsGlR7bxkY8Wo7eOeOk6qYjeHxNPbKKLM52+/dioOTsHOnhRck2qzFUquBmQ93nHGzvsvQSoCGaQ==} + engines: {node: '>=18.17.0'} + '@clerk/types@4.6.0': resolution: {integrity: sha512-kowqVGqLfu0Zl2Pteum70MfkGHqBUoHHeR+u2+yWVl1lKHLCiyY1u8ntYBEIolAylBaQNDuRzxyMIDPSxjPE8g==} engines: {node: '>=18.17.0'} @@ -2863,6 +2870,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@clerk/themes@2.1.9': + dependencies: + '@clerk/types': 4.6.0 + tslib: 2.4.1 + '@clerk/types@4.6.0': dependencies: csstype: 3.1.1 @@ -4017,7 +4029,7 @@ snapshots: dot-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.4.1 + tslib: 2.6.3 drizzle-kit@0.22.7: dependencies: @@ -4762,7 +4774,7 @@ snapshots: lower-case@2.0.2: dependencies: - tslib: 2.4.1 + tslib: 2.6.3 lru-cache@10.2.2: {} @@ -4842,7 +4854,7 @@ snapshots: no-case@3.0.4: dependencies: lower-case: 2.0.2 - tslib: 2.4.1 + tslib: 2.6.3 node-gyp-build@4.8.1: {} @@ -5211,7 +5223,7 @@ snapshots: snake-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.4.1 + tslib: 2.6.3 snakecase-keys@5.4.4: dependencies: diff --git a/src/app/actions.ts b/src/app/actions.ts index 4323746..6a31c59 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -2,6 +2,7 @@ import { db } from "@/server/db"; import { users } from "@/server/db/schema"; +import { currentUser } from "@clerk/nextjs/server"; export async function setFullUserName(name: string, userId: string) { try { @@ -22,7 +23,7 @@ export async function setPaperlessURL(url: string, userId: string) { .onConflictDoUpdate({ target: users.userId, set: { paperlessURL: url } }); } catch { throw new Error("Database error"); - } + } } export async function setPaperlessToken(token: string, userId: string) { @@ -30,8 +31,11 @@ export async function setPaperlessToken(token: string, userId: string) { await db .insert(users) .values({ paperlessToken: token, userId: userId }) - .onConflictDoUpdate({ target: users.userId, set: { paperlessToken: token } }); + .onConflictDoUpdate({ + target: users.userId, + set: { paperlessToken: token }, + }); } catch { throw new Error("Database error"); - } -} \ No newline at end of file + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 14ade83..3bc9df7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,7 @@ import { TopNav } from "@/components/topnav"; import { ClerkProvider } from "@clerk/nextjs"; import { ThemeProvider } from "@/components/theme-provider"; import { cn } from "@/lib/utils"; +import { dark } from "@clerk/themes"; export const metadata = { title: "Homelab Connector", @@ -18,7 +19,11 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + >; -}) { - const { user, isLoaded } = useUser(); - const pathname = usePathname(); - const formSchema = z.object({ - FullName: z.string().min(1, { - message: "Required.", - }), - }); - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - FullName: "", - }, - }); - - if (!isLoaded) { - return Loading...; - } - - if (!user) { - return redirect("/sign-in?redirect=" + pathname); - } - - async function onSubmit(values: z.infer) { - setActiveTab((prevTab) => prevTab + 1); // Increment activeTab - try { - await setFullUserName(values["FullName"], user!.id); - // Operation succeeded, show success toast - toast("Your name preferences was saved"); - // Optionally, move to a new tab or take another action to indicate success - } catch { - // Operation failed, show error toast - toast("Uh oh! Something went wrong.", { - description: "Your name preferences were not saved.", - action: { - label: "Go back", - onClick: () => setActiveTab(0), // Go back to try again - }, - }); - } - } - - return ( -
- - ( - - Full Name - - - - - - )} - /> - - - - ); -} - -function PaperlessURL({ - setActiveTab, -}: { - setActiveTab: Dispatch>; -}) { - const { user, isLoaded } = useUser(); - const pathname = usePathname(); - const formSchema = z.object({ - URL: z.string(), - }); - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - URL: "", - }, - }); - - if (!isLoaded) { - return Loading...; - } - - if (!user) { - return redirect("/sign-in?redirect=" + pathname); - } - - async function onSubmit(values: z.infer) { - if (values["URL"] == "") { - setActiveTab((prevTab) => prevTab + 2); // Skip api key form - } else { - setActiveTab((prevTab) => prevTab + 1); // Increment activeTab - } - try { - await setPaperlessURL(values["URL"], user!.id); - // Operation succeeded, show success toast - toast("Your paperless URL preferences was saved"); - // Optionally, move to a new tab or take another action to indicate success - } catch { - // Operation failed, show error toast - toast("Uh oh! Something went wrong.", { - description: "Your Paperless URL preferences were not saved.", - action: { - label: "Go back", - onClick: () => setActiveTab(1), // Go back to try again - }, - }); - } - } - - return ( -
- - ( - - Paperless URL - - - - Leave empty to disable - - )} - /> - - - - ); -} - -function PaperlessToken({ - setActiveTab, -}: { - setActiveTab: Dispatch>; -}) { - const { user, isLoaded } = useUser(); - const pathname = usePathname(); - const formSchema = z.object({ - token: z.string(), - }); - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - token: "", - }, - }); - - if (!isLoaded) { - return Loading...; - } - - if (!user) { - return redirect("/sign-in?redirect=" + pathname); - } - - async function onSubmit(values: z.infer) { - setActiveTab((prevTab) => prevTab + 1); // Increment activeTab - - try { - await setPaperlessToken(values["token"], user!.id); - // Operation succeeded, show success toast - toast("Your paperless token preferences was saved"); - } catch { - // Operation failed, show error toast - toast("Uh oh! Something went wrong.", { - description: "Your Paperless token preferences were not saved.", - action: { - label: "Go back", - onClick: () => setActiveTab(1), // Go back to try again - }, - }); - } - } - - return ( -
- - ( - - Paperless API Token - - - - - You can create (or re-create) an API token by opening the "My - Profile" link in the user dropdown found in the web UI and - clicking the circular arrow button. - - - )} - /> - - - - ); -} - -function Done() { - setTimeout(function () { - redirect("/"); - }, 3000); - return <>All done! Redirecting home..; -} - -interface ProgressIndicatorProps { - activeTab: number; - totalTabs: number; - setActiveTab: (tabIndex: number) => void; -} - -const ProgressIndicator: React.FC = ({ - activeTab, - totalTabs, - setActiveTab, -}) => { - return ( -
- {Array.from({ length: totalTabs - 1 }, (_, index) => ( - setActiveTab(index)} - key={index} - className={`mx-1.5 inline-block h-3 w-3 cursor-pointer rounded-full transition delay-75 hover:scale-125 hover:bg-blue-300 ${ - index === activeTab ? "bg-blue-500" : "bg-gray-300" - }`} - > - ))} -
- ); -}; - -export default function Forms() { - const [activeTab, setActiveTab] = useState(0); - - const formElements = [ - , - , - , - , - ]; - return ( - <> - {formElements[activeTab]} - - - - ); -} diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index 5c6bb67..c4d174d 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -1,9 +1,290 @@ -import Forms from "./forms"; +"use client"; -export default async function userSetup() { +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { Dispatch, SetStateAction, useState } from "react"; +import { useUser } from "@clerk/nextjs"; +import { redirect, usePathname } from "next/navigation"; +import LoadingSpinner from "@/components/loading-spinner"; +import { + setFullUserName, + setPaperlessToken, + setPaperlessURL, +} from "../actions"; +import { Toaster } from "@/components/ui/sonner"; +import { toast } from "sonner"; + +function FullName({ + setActiveTab, +}: { + setActiveTab: Dispatch>; +}) { + const { user, isLoaded } = useUser(); + const pathname = usePathname(); + const formSchema = z.object({ + FullName: z.string().min(1, { + message: "Required.", + }), + }); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + FullName: "", + }, + }); + + if (!isLoaded) { + return Loading...; + } + + if (!user) { + return redirect("/sign-in?redirect=" + pathname); + } + + async function onSubmit(values: z.infer) { + setActiveTab((prevTab) => prevTab + 1); // Increment activeTab + try { + await setFullUserName(values["FullName"], user!.id); + // Operation succeeded, show success toast + toast("Your name preferences was saved"); + // Optionally, move to a new tab or take another action to indicate success + } catch { + // Operation failed, show error toast + toast("Uh oh! Something went wrong.", { + description: "Your name preferences were not saved.", + action: { + label: "Go back", + onClick: () => setActiveTab(0), // Go back to try again + }, + }); + } + } + + return ( +
+ + ( + + Full Name + + + + + + )} + /> + + + + ); +} + +function PaperlessURL({ + setActiveTab, +}: { + setActiveTab: Dispatch>; +}) { + const { user, isLoaded } = useUser(); + const pathname = usePathname(); + const formSchema = z.object({ + URL: z.string(), + }); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + URL: "", + }, + }); + + if (!isLoaded) { + return Loading...; + } + + if (!user) { + return redirect("/sign-in?redirect=" + pathname); + } + + async function onSubmit(values: z.infer) { + if (values["URL"] == "") { + setActiveTab((prevTab) => prevTab + 2); // Skip api key form + } else { + setActiveTab((prevTab) => prevTab + 1); // Increment activeTab + } + try { + await setPaperlessURL(values["URL"], user!.id); + // Operation succeeded, show success toast + toast("Your paperless URL preferences was saved"); + // Optionally, move to a new tab or take another action to indicate success + } catch { + // Operation failed, show error toast + toast("Uh oh! Something went wrong.", { + description: "Your Paperless URL preferences were not saved.", + action: { + label: "Go back", + onClick: () => setActiveTab(1), // Go back to try again + }, + }); + } + } + + return ( +
+ + ( + + Paperless URL + + + + Leave empty to disable + + )} + /> + + + + ); +} + +function PaperlessToken({ + setActiveTab, +}: { + setActiveTab: Dispatch>; +}) { + const { user, isLoaded } = useUser(); + const pathname = usePathname(); + const formSchema = z.object({ + token: z.string(), + }); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + token: "", + }, + }); + + if (!isLoaded) { + return Loading...; + } + + if (!user) { + return redirect("/sign-in?redirect=" + pathname); + } + + async function onSubmit(values: z.infer) { + setActiveTab((prevTab) => prevTab + 1); // Increment activeTab + + try { + await setPaperlessToken(values["token"], user!.id); + // Operation succeeded, show success toast + toast("Your paperless token preferences was saved"); + } catch { + // Operation failed, show error toast + toast("Uh oh! Something went wrong.", { + description: "Your Paperless token preferences were not saved.", + action: { + label: "Go back", + onClick: () => setActiveTab(1), // Go back to try again + }, + }); + } + } + + return ( +
+ + ( + + Paperless API Token + + + + + You can create (or re-create) an API token by opening the "My + Profile" link in the user dropdown found in the web UI and + clicking the circular arrow button. + + + )} + /> + + + + ); +} + +function Done() { + setTimeout(function () { + redirect("/"); + }, 3000); + return <>All done! Redirecting home..; +} + +interface ProgressIndicatorProps { + activeTab: number; + totalTabs: number; + setActiveTab: (tabIndex: number) => void; +} + +const ProgressIndicator: React.FC = ({ + activeTab, + totalTabs, + setActiveTab, +}) => { + return ( +
+ {Array.from({ length: totalTabs - 1 }, (_, index) => ( + setActiveTab(index)} + key={index} + className={`mx-1.5 inline-block h-3 w-3 cursor-pointer rounded-full transition delay-75 hover:scale-125 hover:bg-blue-300 ${ + index === activeTab ? "bg-blue-500" : "bg-gray-300" + }`} + > + ))} +
+ ); +}; + +export default function SetupPage() { + const [activeTab, setActiveTab] = useState(0); + + const formElements = [ + , + , + , + , + ]; return ( -
- -
+ <> + {formElements[activeTab]} + + + ); } diff --git a/src/components/topnav.tsx b/src/components/topnav.tsx index d13f416..acba025 100644 --- a/src/components/topnav.tsx +++ b/src/components/topnav.tsx @@ -1,11 +1,59 @@ "use client"; -import { SignInButton, SignedIn, SignedOut, UserButton } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useClerk } from "@clerk/nextjs"; import Link from "next/link"; import { Button, buttonVariants } from "@/components/ui/button"; import Tooltip from "@/components/tooltip"; import { ModeToggle } from "@/components/mode-toggle"; import { Separator } from "@/components/ui/separator"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import Image from "next/image"; +import { useUser } from "@clerk/nextjs"; +import { User } from "lucide-react"; +import { useRouter } from "next/navigation"; + +function UserSettings() { + const { user } = useUser(); + const { openUserProfile } = useClerk(); + const router = useRouter(); + + return ( + + + {user ? ( + Avatar + ) : ( + + )} + + + My Account + + openUserProfile()}> + Manage Account + + router.push("/settings")}> + Settings + + + Logout + + + ); +} export function TopNav() { return ( @@ -46,7 +94,7 @@ export function TopNav() { - +