better auth set up

This commit is contained in:
zmeyer44 2023-10-15 11:44:15 -04:00
parent 80b178306e
commit 36b76f943a
17 changed files with 272 additions and 91 deletions

View File

@ -111,7 +111,7 @@ export default function ProfilePage({
<div className="mx-auto max-w-[800px] space-y-6"> <div className="mx-auto max-w-[800px] space-y-6">
<div className="flex max-w-2xl flex-col gap-5"> <div className="flex max-w-2xl flex-col gap-5">
{demo.map((e) => ( {demo.map((e) => (
<SubscriptionCard {...e} /> <SubscriptionCard key={e.id} {...e} />
))} ))}
</div> </div>
<div className=""> <div className="">

View File

@ -4,7 +4,7 @@ export default function Keystone() {
return ( return (
<div className="center hidden sm:flex"> <div className="center hidden sm:flex">
<Link <Link
href="/" href="/app"
className="center fixed h-[var(--header-height)] w-[var(--sidebar-closed-width)] gap-x-3 border-r text-primary hover:text-primary/80 xl:w-[var(--sidebar-open-width)] xl:justify-start xl:pl-5" className="center fixed h-[var(--header-height)] w-[var(--sidebar-closed-width)] gap-x-3 border-r text-primary hover:text-primary/80 xl:w-[var(--sidebar-open-width)] xl:justify-start xl:pl-5"
> >
<Logo className="h-[30px] w-[30px]" /> <Logo className="h-[30px] w-[30px]" />

View File

@ -1,4 +1,5 @@
"use client"; "use client";
import { useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import useCurrentUser from "@/lib/hooks/useCurrentUser"; import useCurrentUser from "@/lib/hooks/useCurrentUser";
@ -17,18 +18,26 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { RiNotification4Line } from "react-icons/ri"; import { RiNotification4Line } from "react-icons/ri";
import { SiRelay } from "react-icons/si"; import { SiRelay } from "react-icons/si";
import { RELAYS } from "@/constants";
import StatusIndicator from "@/components/StatusIndicator"; import StatusIndicator from "@/components/StatusIndicator";
import { type NDKUser } from "@nostr-dev-kit/ndk"; import { type NDKUser } from "@nostr-dev-kit/ndk";
import { truncateText, getTwoLetters } from "@/lib/utils"; import { truncateText, getTwoLetters } from "@/lib/utils";
import { useNDK } from "@/app/_providers/ndk"; import { useNDK } from "@/app/_providers/ndk";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const LoginModal = dynamic(() => import("@/components/Modals/Login"), { const LoginModal = dynamic(() => import("@/components/Modals/Login"), {
ssr: false, ssr: false,
}); });
export default function AuthActions() { export default function AuthActions() {
const modal = useModal(); const modal = useModal();
const { currentUser, logout } = useCurrentUser(); const { currentUser, logout, attemptLogin } = useCurrentUser();
useEffect(() => {
attemptLogin();
}, []);
if (currentUser) { if (currentUser) {
return ( return (
<> <>
@ -53,8 +62,9 @@ export default function AuthActions() {
export function Notifications({ user }: { user: NDKUser }) { export function Notifications({ user }: { user: NDKUser }) {
return ( return (
<DropdownMenu> <TooltipProvider>
<DropdownMenuTrigger asChild> <Tooltip delayDuration={100}>
<TooltipTrigger>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@ -62,50 +72,67 @@ export function Notifications({ user }: { user: NDKUser }) {
> >
<RiNotification4Line className="h-[18px] w-[18px] text-foreground" /> <RiNotification4Line className="h-[18px] w-[18px] text-foreground" />
</Button> </Button>
</DropdownMenuTrigger> </TooltipTrigger>
<DropdownMenuContent className="z-header+ w-56" align="end" forceMount> <TooltipContent align="center">
<DropdownMenuLabel className="font-normal"> <p>Coming Soon</p>
<div className="flex flex-col space-y-1"> </TooltipContent>
{user.profile?.displayName || user.profile?.name ? ( </Tooltip>
<> </TooltipProvider>
<p className="text-sm font-medium leading-none">
{user.profile?.displayName ?? user.profile.name}
</p>
<p className="text-xs leading-none text-muted-foreground">
m@example.com
</p>
</>
) : (
<p className="text-sm font-medium leading-none">
{truncateText(user.npub)}
</p>
)}
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Billing
<DropdownMenuShortcut>B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>New Team</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
Log out
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
); );
// return (
// <DropdownMenu>
// <DropdownMenuTrigger asChild>
// <Button
// variant="ghost"
// size="icon"
// className="center relative h-8 w-8 rounded-full bg-muted text-foreground"
// >
// <RiNotification4Line className="h-[18px] w-[18px] text-foreground" />
// </Button>
// </DropdownMenuTrigger>
// <DropdownMenuContent className="z-header+ w-56" align="end" forceMount>
// <DropdownMenuLabel className="font-normal">
// <div className="flex flex-col space-y-1">
// {user.profile?.displayName || user.profile?.name ? (
// <>
// <p className="text-sm font-medium leading-none">
// {user.profile?.displayName ?? user.profile.name}
// </p>
// <p className="text-xs leading-none text-muted-foreground">
// m@example.com
// </p>
// </>
// ) : (
// <p className="text-sm font-medium leading-none">
// {truncateText(user.npub)}
// </p>
// )}
// </div>
// </DropdownMenuLabel>
// <DropdownMenuSeparator />
// <DropdownMenuGroup>
// <DropdownMenuItem>
// Profile
// <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
// </DropdownMenuItem>
// <DropdownMenuItem>
// Billing
// <DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
// </DropdownMenuItem>
// <DropdownMenuItem>
// Settings
// <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
// </DropdownMenuItem>
// <DropdownMenuItem>New Team</DropdownMenuItem>
// </DropdownMenuGroup>
// <DropdownMenuSeparator />
// <DropdownMenuItem>
// Log out
// <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
// </DropdownMenuItem>
// </DropdownMenuContent>
// </DropdownMenu>
// );
} }
export function Relays() { export function Relays() {
const { ndk } = useNDK(); const { ndk } = useNDK();

View File

@ -48,7 +48,10 @@ export default function HorizontalCarousel() {
return ( return (
<div className="scrollbar-thumb-rounded-full mr-auto flex min-w-0 max-w-full snap-x snap-mandatory overflow-x-auto pl-5 pr-[50vw] scrollbar-thin sm:pr-[200px]"> <div className="scrollbar-thumb-rounded-full mr-auto flex min-w-0 max-w-full snap-x snap-mandatory overflow-x-auto pl-5 pr-[50vw] scrollbar-thin sm:pr-[200px]">
{cards.map((creator, index) => ( {cards.map((creator, index) => (
<div className={cn("snap-start pl-2 sm:pl-5", index === 0 && "pl-5")}> <div
key={index}
className={cn("snap-start pl-2 sm:pl-5", index === 0 && "pl-5")}
>
<CreatorCard key={creator.displayName} {...creator} /> <CreatorCard key={creator.displayName} {...creator} />
</div> </div>
))} ))}

View File

@ -86,7 +86,7 @@ export default function Modal({
</FocusTrap> </FocusTrap>
<motion.div <motion.div
key="desktop-backdrop" key="desktop-backdrop"
className="z-overlay fixed inset-0 bg-background bg-opacity-10 backdrop-blur" className="z-overlay fixed inset-0 bg-background/40 backdrop-blur"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}

View File

@ -3,7 +3,6 @@ import { parse } from "node-html-parser";
import { z } from "zod"; import { z } from "zod";
import { validateUrl } from "@/lib/utils"; import { validateUrl } from "@/lib/utils";
type MetaData = { type MetaData = {
title: string; title: string;
description: string; description: string;
@ -81,6 +80,10 @@ async function handler(req: NextRequest) {
}); });
} catch (err) { } catch (err) {
console.log("Error", err); console.log("Error", err);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);
} }
} }

View File

@ -0,0 +1,60 @@
import Link from "next/link";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ReactNode } from "react";
type OptionLink = {
href: string;
type: "link";
};
type OptionButton = {
onClick: () => void;
type: "button";
};
type Option = {
label: string;
} & (OptionLink | OptionButton);
type DropDownMenuProps = {
options: Option[];
children: ReactNode;
};
export default function DropDownMenu({ children, options }: DropDownMenuProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent className="z-header+ w-56" align="end" forceMount>
<DropdownMenuGroup>
{options.map((o) => {
if (o.type === "button") {
return (
<DropdownMenuItem key={o.label}>
<button
onClick={o.onClick}
className="flex w-full justify-between"
>
{o.label}
</button>
</DropdownMenuItem>
);
} else {
return (
<DropdownMenuItem key={o.label}>
<Link href={o.href} className="flex w-full justify-between">
{o.label}
</Link>
</DropdownMenuItem>
);
}
})}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -4,10 +4,34 @@ import { type Event } from "nostr-tools";
import { RenderText } from "../TextRendering"; import { RenderText } from "../TextRendering";
import { getTagsValues } from "@/lib/nostr/utils"; import { getTagsValues } from "@/lib/nostr/utils";
import LinkCard from "@/components/LinkCard"; import LinkCard from "@/components/LinkCard";
export default function Kind1({ content, tags }: Event) { import { copyText } from "@/lib/utils";
import { nip19 } from "nostr-tools";
import { toast } from "sonner";
export default function Kind1(props: Event) {
const { content, pubkey, tags } = props;
const r = getTagsValues("r", tags); const r = getTagsValues("r", tags);
const npub = nip19.npubEncode(pubkey);
return ( return (
<Container> <Container
pubkey={pubkey}
actionOptions={[
{
label: "View profile",
href: `/${npub}`,
type: "link",
},
{
label: "Copy raw data",
type: "button",
onClick: () => {
void copyText(JSON.stringify(props));
toast.success("Copied Text!");
},
},
]}
>
<CardDescription className="text-base text-foreground"> <CardDescription className="text-base text-foreground">
<RenderText text={content} /> <RenderText text={content} />
</CardDescription> </CardDescription>

View File

@ -5,13 +5,13 @@ import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
import { type Event } from "nostr-tools"; import { type Event } from "nostr-tools";
import { removeDuplicates } from "@/lib/utils"; import { removeDuplicates } from "@/lib/utils";
export default function Kind30023({ content, tags }: Event) { export default function Kind30023({ content, pubkey, tags }: Event) {
const title = getTagValues("title", tags); const title = getTagValues("title", tags);
const summary = getTagValues("summary", tags); const summary = getTagValues("summary", tags);
const contentTags = removeDuplicates(getTagsValues("t", tags)); const contentTags = removeDuplicates(getTagsValues("t", tags));
return ( return (
<Container contentTags={contentTags}> <Container pubkey={pubkey} contentTags={contentTags}>
<CardTitle className="mb-1.5 line-clamp-2 text-lg font-semibold"> <CardTitle className="mb-1.5 line-clamp-2 text-lg font-semibold">
{title} {title}
</CardTitle> </CardTitle>

View File

@ -2,9 +2,9 @@ import Container from "./components/Container";
import { CardTitle, CardDescription } from "@/components/ui/card"; import { CardTitle, CardDescription } from "@/components/ui/card";
import { type Event } from "nostr-tools"; import { type Event } from "nostr-tools";
export default function Kind3745({}: Event) { export default function Kind3745({ pubkey }: Event) {
return ( return (
<Container> <Container pubkey={pubkey}>
<CardTitle className="mb-1.5 line-clamp-2 text-lg font-semibold"> <CardTitle className="mb-1.5 line-clamp-2 text-lg font-semibold">
The start of the Nostr revolution The start of the Nostr revolution
</CardTitle> </CardTitle>

View File

@ -9,23 +9,48 @@ import { ReactNode } from "react";
import ProfileHeader from "./ProfileHeader"; import ProfileHeader from "./ProfileHeader";
import Actions from "./Actions"; import Actions from "./Actions";
import Tags from "./Tags"; import Tags from "./Tags";
import { type Event } from "nostr-tools"; import DropDownMenu from "@/components/DropDownMenu";
type OptionLink = {
href: string;
type: "link";
};
type OptionButton = {
onClick: () => void;
type: "button";
};
type Option = {
label: string;
} & (OptionLink | OptionButton);
type CreatorCardProps = { type CreatorCardProps = {
pubkey: string;
contentTags?: string[]; contentTags?: string[];
children: ReactNode; children: ReactNode;
actionOptions?: Option[];
}; };
export default function Container({ children, contentTags }: CreatorCardProps) { export default function Container({
children,
contentTags,
pubkey,
actionOptions = [],
}: CreatorCardProps) {
return ( return (
<Card className="relative overflow-hidden"> <Card className="relative overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-4"> <CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-4">
<ProfileHeader /> <ProfileHeader pubkey={pubkey} />
<div className="-mr-1 flex items-center gap-x-1.5 text-xs text-muted-foreground"> <div className="-mr-1 flex items-center gap-x-1.5 text-xs text-muted-foreground">
{formatDate(new Date("10-5-23"), "MMM Do")} {formatDate(new Date("10-5-23"), "MMM Do")}
<Button size={"sm"} variant={"ghost"} className="center h-6 w-6 p-0"> <DropDownMenu options={actionOptions}>
<Button
size={"sm"}
variant={"ghost"}
className="center h-6 w-6 p-0"
>
<RiMoreFill className="h-4 w-4" /> <RiMoreFill className="h-4 w-4" />
</Button> </Button>
</DropDownMenu>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="px-4 pb-3"> <CardContent className="px-4 pb-3">

View File

@ -1,25 +1,30 @@
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar"; import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
import { HiCheckBadge } from "react-icons/hi2"; import { HiCheckBadge } from "react-icons/hi2";
import Link from "next/link";
import useProfile from "@/lib/hooks/useProfile";
import { nip19 } from "nostr-tools";
import { getTwoLetters, getNameToShow } from "@/lib/utils";
type ProfileHeaderProps = {}; type ProfileHeaderProps = {
export default function ProfileHeader({}: ProfileHeaderProps) { pubkey: string;
};
export default function ProfileHeader({ pubkey }: ProfileHeaderProps) {
const { profile } = useProfile(pubkey);
const npub = nip19.npubEncode(pubkey);
return ( return (
<div className="center gap-x-3"> <Link href={`/${npub}`} className="center group gap-x-3">
<Avatar className="center h-8 w-8 overflow-hidden rounded-sm bg-muted"> <Avatar className="center h-8 w-8 overflow-hidden rounded-sm bg-muted">
<AvatarImage <AvatarImage src={profile.image} alt={profile.displayName} />
src={ <AvatarFallback className="text-xs">
"https://images.unsplash.com/photo-1566492031773-4f4e44671857?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60" {getTwoLetters({ npub, profile })}
} </AvatarFallback>
alt="user"
/>
<AvatarFallback className="text-xs">SC</AvatarFallback>
</Avatar> </Avatar>
<div className="center 5 gap-1"> <div className="center 5 gap-1">
<span className="text-xs uppercase text-muted-foreground"> <span className="text-xs uppercase text-muted-foreground group-hover:underline">
Derek Seivers {getNameToShow({ npub, profile })}
</span> </span>
<HiCheckBadge className="h-4 w-4 text-primary" /> {!!profile.nip05 && <HiCheckBadge className="h-4 w-4 text-primary" />}
</div>
</div> </div>
</Link>
); );
} }

View File

@ -2,9 +2,9 @@ import Container from "./components/Container";
import { CardTitle, CardDescription } from "@/components/ui/card"; import { CardTitle, CardDescription } from "@/components/ui/card";
import { type Event } from "nostr-tools"; import { type Event } from "nostr-tools";
export default function KindDefault({}: Event) { export default function KindDefault({ pubkey }: Event) {
return ( return (
<Container> <Container pubkey={pubkey}>
<CardTitle className="mb-1.5 line-clamp-2 text-lg font-semibold"> <CardTitle className="mb-1.5 line-clamp-2 text-lg font-semibold">
The start of the Nostr revolution The start of the Nostr revolution
</CardTitle> </CardTitle>

View File

@ -10,7 +10,7 @@ import useCurrentUser from "@/lib/hooks/useCurrentUser";
export default function LoginModal() { export default function LoginModal() {
const { loginWithNip07 } = useNDK(); const { loginWithNip07 } = useNDK();
const { loginWithPubkey } = useCurrentUser(); const { loginWithPubkey, currentUser } = useCurrentUser();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const modal = useModal(); const modal = useModal();
@ -29,9 +29,7 @@ export default function LoginModal() {
if (!user) { if (!user) {
throw new Error("NO auth"); throw new Error("NO auth");
} }
console.log("LOGIN", user);
await loginWithPubkey(nip19.decode(user.npub).data.toString()); await loginWithPubkey(nip19.decode(user.npub).data.toString());
// keys?.setKeys({ // keys?.setKeys({
// privkey: "", // privkey: "",
// pubkey: , // pubkey: ,
@ -56,6 +54,11 @@ export default function LoginModal() {
getConnected(shouldReconnect); getConnected(shouldReconnect);
} }
}, []); }, []);
useEffect(() => {
if (currentUser) {
modal?.hide();
}
}, [currentUser]);
async function handleLogin() { async function handleLogin() {
setIsLoading(true); setIsLoading(true);

View File

@ -17,7 +17,10 @@ export default function Template({ title, children, className }: ModalProps) {
return ( return (
<div <div
className={cn("relative w-full grow p-4 md:rounded-lg md:p-6", className)} className={cn(
"relative w-full grow border bg-background p-4 shadow md:rounded-lg md:p-6",
className,
)}
> >
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h1 <h1

View File

@ -4,6 +4,7 @@ import currentUserStore from "@/lib/stores/currentUser";
// import useEvents from "@/lib/hooks/useEvents"; // import useEvents from "@/lib/hooks/useEvents";
import { UserSchema } from "@/types"; import { UserSchema } from "@/types";
import { useNDK } from "@/app/_providers/ndk"; import { useNDK } from "@/app/_providers/ndk";
import { nip19 } from "nostr-tools";
export default function useCurrentUser() { export default function useCurrentUser() {
const { const {
@ -38,6 +39,21 @@ export default function useCurrentUser() {
// } // }
// }); // });
async function attemptLogin() {
const shouldReconnect = localStorage.getItem("shouldReconnect");
if (!shouldReconnect || typeof window.nostr === "undefined") return;
const user = await loginWithNip07();
if (!user) {
throw new Error("NO auth");
}
console.log("LOGIN", user);
await loginWithPubkey(nip19.decode(user.npub).data.toString());
if (typeof window.webln !== "undefined") {
await window.webln.enable();
}
console.log("connected ");
}
function logout() { function logout() {
localStorage.removeItem("shouldReconnect"); localStorage.removeItem("shouldReconnect");
setCurrentUser(null); setCurrentUser(null);
@ -73,5 +89,6 @@ export default function useCurrentUser() {
logout, logout,
updateUser: handleUpdateUser, updateUser: handleUpdateUser,
loginWithPubkey, loginWithPubkey,
attemptLogin,
}; };
} }

View File

@ -26,6 +26,17 @@ export function truncateText(text: string, size?: number) {
let length = size ?? 5; let length = size ?? 5;
return text.slice(0, length) + "..." + text.slice(-length); return text.slice(0, length) + "..." + text.slice(-length);
} }
export function getNameToShow(user: {
npub: string;
profile?: {
displayName?: string;
name?: string;
};
}) {
return (
user.profile?.displayName ?? user.profile?.name ?? truncateText(user.npub)
);
}
export function getTwoLetters(user: { export function getTwoLetters(user: {
npub: string; npub: string;
profile?: { profile?: {