link cards updates, profile page with feed, auth and current user stuff
This commit is contained in:
parent
5bd643e672
commit
80b178306e
20
app/(app)/(profile)/[npub]/_components/Feed.tsx
Normal file
20
app/(app)/(profile)/[npub]/_components/Feed.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import Feed from "@/containers/Feed";
|
||||||
|
import Spinner from "@/components/spinner";
|
||||||
|
export default function ProfileFeed({ pubkey }: { pubkey: string }) {
|
||||||
|
return (
|
||||||
|
<div className="center w-full flex-col items-stretch space-y-6 text-primary">
|
||||||
|
<Feed
|
||||||
|
filter={{
|
||||||
|
authors: [pubkey],
|
||||||
|
kinds: [1],
|
||||||
|
}}
|
||||||
|
loader={() => (
|
||||||
|
<div className="center flex-col gap-y-4 pt-7 text-center">
|
||||||
|
<Spinner />
|
||||||
|
<p className="font-medium text-primary">Fetching notes...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -5,6 +5,10 @@ import { Button } from "@/components/ui/button";
|
|||||||
import SubscriptionCard from "@/components/SubscriptionCard";
|
import SubscriptionCard from "@/components/SubscriptionCard";
|
||||||
import { HiCheckBadge } from "react-icons/hi2";
|
import { HiCheckBadge } from "react-icons/hi2";
|
||||||
import Tabs from "@/components/Tabs";
|
import Tabs from "@/components/Tabs";
|
||||||
|
import useProfile from "@/lib/hooks/useProfile";
|
||||||
|
import { getTwoLetters, truncateText } from "@/lib/utils";
|
||||||
|
import ProfileFeed from "./_components/Feed";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
|
||||||
export default function ProfilePage({
|
export default function ProfilePage({
|
||||||
params: { npub },
|
params: { npub },
|
||||||
@ -14,6 +18,9 @@ export default function ProfilePage({
|
|||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const [activeTab, setActiveTab] = useState("feed");
|
const [activeTab, setActiveTab] = useState("feed");
|
||||||
|
const pubkey = nip19.decode(npub).data.toString();
|
||||||
|
const { profile } = useProfile(pubkey);
|
||||||
|
|
||||||
const demo = [
|
const demo = [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
@ -30,8 +37,9 @@ export default function ProfilePage({
|
|||||||
<div className="relative -mx-5 @container ">
|
<div className="relative -mx-5 @container ">
|
||||||
<div className="absolute top-0 h-[8rem] w-full" />
|
<div className="absolute top-0 h-[8rem] w-full" />
|
||||||
<div className="mx-auto max-w-5xl p-0">
|
<div className="mx-auto max-w-5xl p-0">
|
||||||
<div className="m-0 @4xl:pt-8 @5xl:px-5">
|
<div className="m-0 @5xl:px-5 @5xl:pt-8">
|
||||||
<div className="relative w-full overflow-hidden bg-gradient-to-b from-primary pb-[29%] @5xl:rounded-[20px]">
|
<div className="relative w-full overflow-hidden bg-gradient-to-b from-primary pb-[29%] @5xl:rounded-[20px]">
|
||||||
|
{!!profile.banner && (
|
||||||
<Image
|
<Image
|
||||||
className="absolute inset-0 h-full w-full object-cover align-middle"
|
className="absolute inset-0 h-full w-full object-cover align-middle"
|
||||||
src={
|
src={
|
||||||
@ -42,11 +50,13 @@ export default function ProfilePage({
|
|||||||
alt="banner"
|
alt="banner"
|
||||||
unoptimized
|
unoptimized
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative mx-auto mb-4 mt-[calc(-0.4375_*_4rem)] flex max-w-[800px] items-end justify-between gap-2 px-3 sm:mt-[calc(-0.4375_*_4.5rem)] sm:px-5 md:mt-[calc(-0.5625_*_5rem)] lg:mt-[calc(-0.5625_*_6rem)]">
|
<div className="relative mx-auto mb-4 mt-[calc(-0.4375_*_4rem)] flex max-w-[800px] items-end justify-between gap-2 px-3 sm:mt-[calc(-0.4375_*_4.5rem)] sm:px-5 md:mt-[calc(-0.5625_*_5rem)] lg:mt-[calc(-0.5625_*_6rem)]">
|
||||||
<div className="z-1 ml-[calc(-1_*_3px)] overflow-hidden rounded-[0.5rem] bg-background p-[3px] sm:ml-[calc(-1_*_4px)] sm:p-[4px] lg:ml-[calc(-1_*_6px)] lg:rounded-[1rem] lg:p-[6px]">
|
<div className="z-1 ml-[calc(-1_*_3px)] overflow-hidden rounded-[0.5rem] bg-background p-[3px] sm:ml-[calc(-1_*_4px)] sm:p-[4px] lg:ml-[calc(-1_*_6px)] lg:rounded-[1rem] lg:p-[6px]">
|
||||||
|
{profile.image ? (
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
"https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=2,background=white,quality=75,width=96,height=96/calendars/hw/70772773-6d97-4fbb-a076-fc4dee603080"
|
"https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=2,background=white,quality=75,width=96,height=96/calendars/hw/70772773-6d97-4fbb-a076-fc4dee603080"
|
||||||
@ -57,6 +67,14 @@ export default function ProfilePage({
|
|||||||
width={16}
|
width={16}
|
||||||
height={16}
|
height={16}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="center aspect-square w-[4rem] overflow-hidden rounded-[calc(0.5rem_-_3px)] bg-muted object-cover object-center text-primary @xl:text-2xl sm:w-[4.5rem] sm:rounded-[calc(0.5rem_-_4px)] md:w-[5rem] lg:w-[6rem] lg:rounded-[calc(1rem_-_6px)]">
|
||||||
|
{getTwoLetters({
|
||||||
|
npub,
|
||||||
|
profile,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button size={"sm"} className="rounded-sm px-5 sm:hidden">
|
<Button size={"sm"} className="rounded-sm px-5 sm:hidden">
|
||||||
Follow
|
Follow
|
||||||
@ -66,18 +84,27 @@ export default function ProfilePage({
|
|||||||
<div className="mx-auto max-w-[800px] space-y-1 px-4">
|
<div className="mx-auto max-w-[800px] space-y-1 px-4">
|
||||||
<div className="flex items-center gap-x-1.5 lg:gap-x-2.5">
|
<div className="flex items-center gap-x-1.5 lg:gap-x-2.5">
|
||||||
<h2 className="text-xl font-semibold sm:text-2xl lg:text-3xl">
|
<h2 className="text-xl font-semibold sm:text-2xl lg:text-3xl">
|
||||||
Zach Meyer
|
{profile.displayName ?? profile.name ?? truncateText(npub)}
|
||||||
</h2>
|
</h2>
|
||||||
|
{!!profile.nip05 && (
|
||||||
<HiCheckBadge className="h-5 w-5 text-primary lg:h-7 lg:w-7" />
|
<HiCheckBadge className="h-5 w-5 text-primary lg:h-7 lg:w-7" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-xs text-muted-foreground/80 md:text-sm">
|
<div className="flex items-center text-xs text-muted-foreground/80 md:text-sm">
|
||||||
<p>@zach</p> <div className="inline-flex px-1">·</div>
|
{!!profile.name && <p>{profile.name}</p>}
|
||||||
<p>zach@ordstr.com</p>
|
{!!profile.name && !!profile.nip05 && (
|
||||||
|
<>
|
||||||
|
<div className="inline-flex px-1">·</div>
|
||||||
|
<p>{profile.nip05}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-1 md:pt-2">
|
<div className="pt-1 md:pt-2">
|
||||||
|
{!!profile.about && (
|
||||||
<p className="line-clamp-3 text-xs text-muted-foreground md:text-sm">
|
<p className="line-clamp-3 text-xs text-muted-foreground md:text-sm">
|
||||||
This is my bio. You should check it out now.
|
{profile.about}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -107,6 +134,7 @@ export default function ProfilePage({
|
|||||||
setActiveTab={(t) => setActiveTab(t.name)}
|
setActiveTab={(t) => setActiveTab(t.name)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{activeTab === "feed" ? <ProfileFeed pubkey={pubkey} /> : ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import { UserMenu } from "./components/UserMenu";
|
|
||||||
import { Search } from "./components/Search";
|
import { Search } from "./components/Search";
|
||||||
import { Notifications } from "./components/Notifications";
|
import AuthActions from "./components/AuthActions";
|
||||||
import { MobileMenu } from "./components/MobileMenu";
|
|
||||||
import { Relays } from "./components/Relays";
|
|
||||||
|
|
||||||
import Logo from "@/assets/Logo";
|
import Logo from "@/assets/Logo";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
return (
|
return (
|
||||||
<header className="flex h-[var(--header-height)] shrink-0 grow-0 ">
|
<header className="flex h-[var(--header-height)] shrink-0 grow-0 ">
|
||||||
@ -21,9 +18,7 @@ export default function Header() {
|
|||||||
<Search />
|
<Search />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<Notifications />
|
<AuthActions />
|
||||||
<Relays />
|
|
||||||
<UserMenu />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
209
app/(app)/_layout/components/AuthActions.tsx
Normal file
209
app/(app)/_layout/components/AuthActions.tsx
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
"use client";
|
||||||
|
import Link from "next/link";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import useCurrentUser from "@/lib/hooks/useCurrentUser";
|
||||||
|
import { useModal } from "@/app/_providers/modal/provider";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { RiNotification4Line } from "react-icons/ri";
|
||||||
|
import { SiRelay } from "react-icons/si";
|
||||||
|
import { RELAYS } from "@/constants";
|
||||||
|
import StatusIndicator from "@/components/StatusIndicator";
|
||||||
|
import { type NDKUser } from "@nostr-dev-kit/ndk";
|
||||||
|
import { truncateText, getTwoLetters } from "@/lib/utils";
|
||||||
|
import { useNDK } from "@/app/_providers/ndk";
|
||||||
|
const LoginModal = dynamic(() => import("@/components/Modals/Login"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function AuthActions() {
|
||||||
|
const modal = useModal();
|
||||||
|
const { currentUser, logout } = useCurrentUser();
|
||||||
|
if (currentUser) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Notifications user={currentUser} />
|
||||||
|
<Relays />
|
||||||
|
<UserMenu user={currentUser} logout={logout} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={() => modal?.show(<LoginModal />)}
|
||||||
|
size={"sm"}
|
||||||
|
className="rounded-sm px-5"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Notifications({ user }: { user: NDKUser }) {
|
||||||
|
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() {
|
||||||
|
const { ndk } = useNDK();
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="center relative h-8 w-8 rounded-full bg-muted text-foreground"
|
||||||
|
>
|
||||||
|
<SiRelay className="h-[18px] w-[18px] text-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="z-header+ w-56" align="end" forceMount>
|
||||||
|
{ndk?.explicitRelayUrls?.map((r) => (
|
||||||
|
<DropdownMenuGroup key={r}>
|
||||||
|
<DropdownMenuItem className="flex items-center gap-x-2 overflow-hidden">
|
||||||
|
<StatusIndicator status="offline" />
|
||||||
|
<span className="w-full truncate">{r}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
Manage Relays
|
||||||
|
<DropdownMenuShortcut>⇧⌘M</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function UserMenu({
|
||||||
|
user,
|
||||||
|
logout,
|
||||||
|
}: {
|
||||||
|
user: NDKUser;
|
||||||
|
logout: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||||
|
<Avatar className="h-8 w-8">
|
||||||
|
<AvatarImage src={user.profile?.image} alt={user.npub} />
|
||||||
|
<AvatarFallback>{getTwoLetters(user)}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</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">
|
||||||
|
{user.profile?.nip05 ?? truncateText(user.npub)}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm font-medium leading-none">
|
||||||
|
{truncateText(user.npub)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Link
|
||||||
|
href={`/${user.npub}`}
|
||||||
|
className="flex w-full justify-between"
|
||||||
|
>
|
||||||
|
Profile
|
||||||
|
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{/* <DropdownMenuItem>
|
||||||
|
<Link href="/" className="flex w-full justify-between">
|
||||||
|
Billing
|
||||||
|
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
Settings
|
||||||
|
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>New Team</DropdownMenuItem> */}
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={logout} className="cursor-pointer">
|
||||||
|
Log out
|
||||||
|
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
@ -1,59 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { RiNotification4Line } from "react-icons/ri";
|
|
||||||
|
|
||||||
export function Notifications() {
|
|
||||||
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">
|
|
||||||
<p className="text-sm font-medium leading-none">shadcn</p>
|
|
||||||
<p className="text-xs leading-none text-muted-foreground">
|
|
||||||
m@example.com
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { SiRelay } from "react-icons/si";
|
|
||||||
import { RELAYS } from "@/constants";
|
|
||||||
export function Relays() {
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="center relative h-8 w-8 rounded-full bg-muted text-foreground"
|
|
||||||
>
|
|
||||||
<SiRelay className="h-[18px] w-[18px] text-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="z-header+ w-56" align="end" forceMount>
|
|
||||||
{RELAYS.map((r) => (
|
|
||||||
<DropdownMenuGroup key={r}>
|
|
||||||
<DropdownMenuItem>{r}</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem>
|
|
||||||
Manage Relays
|
|
||||||
<DropdownMenuShortcut>⇧⌘M</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
|
|
||||||
export function UserMenu() {
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
|
||||||
<Avatar className="h-8 w-8">
|
|
||||||
<AvatarImage src="/avatars/01.png" alt="@shadcn" />
|
|
||||||
<AvatarFallback>SC</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent className="z-header+ w-56" align="end" forceMount>
|
|
||||||
<DropdownMenuLabel className="font-normal">
|
|
||||||
<div className="flex flex-col space-y-1">
|
|
||||||
<p className="text-sm font-medium leading-none">shadcn</p>
|
|
||||||
<p className="text-xs leading-none text-muted-foreground">
|
|
||||||
m@example.com
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Link href="/npub" className="flex w-full justify-between">
|
|
||||||
Profile
|
|
||||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Link href="/" className="flex w-full justify-between">
|
|
||||||
Billing
|
|
||||||
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
Settings
|
|
||||||
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>New Team</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem>
|
|
||||||
Log out
|
|
||||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
@ -29,7 +29,7 @@ export default function Modal({
|
|||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setShowModal]
|
[setShowModal],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -86,7 +86,7 @@ export default function Modal({
|
|||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
<motion.div
|
<motion.div
|
||||||
key="desktop-backdrop"
|
key="desktop-backdrop"
|
||||||
className="fixed inset-0 z-modal- bg-background bg-opacity-10 backdrop-blur"
|
className="z-overlay fixed inset-0 bg-background bg-opacity-10 backdrop-blur"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
|
@ -27,7 +27,7 @@ export default function Leaflet({
|
|||||||
offset: { x: number; y: number };
|
offset: { x: number; y: number };
|
||||||
point: { x: number; y: number };
|
point: { x: number; y: number };
|
||||||
velocity: { x: number; y: number };
|
velocity: { x: number; y: number };
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
const offset = info.offset.y;
|
const offset = info.offset.y;
|
||||||
const velocity = info.velocity.y;
|
const velocity = info.velocity.y;
|
||||||
@ -45,7 +45,7 @@ export default function Leaflet({
|
|||||||
<motion.div
|
<motion.div
|
||||||
ref={leafletRef}
|
ref={leafletRef}
|
||||||
key="leaflet"
|
key="leaflet"
|
||||||
className="group fixed inset-x-0 bottom-0 z-modal max-h-[95vh] w-screen cursor-grab rounded-t-lg bg-background pb-5 active:cursor-grabbing sm:hidden standalone:pb-8"
|
className="standalone-pb-8 group fixed inset-x-0 bottom-0 z-modal max-h-[95vh] w-screen cursor-grab rounded-t-lg bg-background pb-5 active:cursor-grabbing sm:hidden"
|
||||||
initial={{ y: "100%" }}
|
initial={{ y: "100%" }}
|
||||||
animate={controls}
|
animate={controls}
|
||||||
exit={{ y: "100%" }}
|
exit={{ y: "100%" }}
|
||||||
@ -58,19 +58,19 @@ export default function Leaflet({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-t-4xl -mb-1 flex h-7 w-full items-center justify-center"
|
"rounded-t-4xl z-modal -mb-1 flex h-7 w-full items-center justify-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="-mr-1 h-1 w-6 rounded-full bg-[#636363] transition-all group-active:rotate-12" />
|
<div className="-mr-1 h-1 w-6 rounded-full bg-muted transition-all group-active:rotate-12" />
|
||||||
<div className="h-1 w-6 rounded-full bg-[#636363] transition-all group-active:-rotate-12" />
|
<div className="h-1 w-6 rounded-full bg-muted transition-all group-active:-rotate-12" />
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-[calc(95vh_-_28px)] w-full overflow-y-auto scrollbar-track-background scrollbar-thumb-gray-900">
|
<div className="scrollbar-muted-foreground max-h-[calc(95vh_-_28px)] w-full overflow-y-auto scrollbar-track-background">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div
|
<motion.div
|
||||||
key="leaflet-backdrop"
|
key="leaflet-backdrop"
|
||||||
className="fixed inset-0 z-modal- bg-background bg-opacity-40 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 }}
|
||||||
|
@ -65,6 +65,9 @@
|
|||||||
.bottom-tabs {
|
.bottom-tabs {
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
.standalone-pb-8 {
|
||||||
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
.standalone-hide {
|
.standalone-hide {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
import Container from "./components/Container";
|
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";
|
||||||
|
import { RenderText } from "../TextRendering";
|
||||||
export default function Kind1({}: Event) {
|
import { getTagsValues } from "@/lib/nostr/utils";
|
||||||
|
import LinkCard from "@/components/LinkCard";
|
||||||
|
export default function Kind1({ content, tags }: Event) {
|
||||||
|
const r = getTagsValues("r", tags);
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<CardTitle className="mb-1.5 line-clamp-2 text-lg font-semibold">
|
<CardDescription className="text-base text-foreground">
|
||||||
The start of the Nostr revolution
|
<RenderText text={content} />
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="line-clamp-4 text-sm">
|
|
||||||
This is the summary of this artilce. Let's hope that it is a good
|
|
||||||
article and that it will end up being worth reading. I don't want to
|
|
||||||
waste my time on some random other stuff.
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
{!!r.length && (
|
||||||
|
<div className="mt-1.5 flex flex-wrap">
|
||||||
|
{r.map((url) => (
|
||||||
|
<LinkCard key={url} url={url} className="max-w-[250px]" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,15 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { fetchMetadata } from "@/lib/fetchers/metadata";
|
import { fetchMetadata } from "@/lib/fetchers/metadata";
|
||||||
import { HiOutlineCheckBadge } from "react-icons/hi2";
|
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||||
|
|
||||||
type LinkCardProps = {
|
type LinkCardProps = {
|
||||||
url: string;
|
url: string;
|
||||||
@ -46,25 +44,29 @@ export default function LinkCard({
|
|||||||
if (metadata) {
|
if (metadata) {
|
||||||
return (
|
return (
|
||||||
<a href={url} target="_blank" rel="nonreferrer">
|
<a href={url} target="_blank" rel="nonreferrer">
|
||||||
<Card className="group">
|
<Card className={cn("group", className)}>
|
||||||
{metadata.image && (
|
{metadata.image && (
|
||||||
<div className="h-[150px] overflow-hidden rounded-t-md">
|
<div className="max-h-[100px] overflow-hidden rounded-t-md">
|
||||||
|
<AspectRatio ratio={16 / 9} className="bg-muted">
|
||||||
<Image
|
<Image
|
||||||
width={250}
|
width={250}
|
||||||
height={150}
|
height={100}
|
||||||
src={metadata.image}
|
src={metadata.image}
|
||||||
alt={metadata.title}
|
alt={metadata.title}
|
||||||
unoptimized
|
unoptimized
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-auto object-cover object-center transition-all group-hover:scale-105",
|
"aspect-video w-auto object-cover object-center align-middle transition-all group-hover:scale-105",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</AspectRatio>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="">
|
<div className="">
|
||||||
<CardHeader className="">
|
<CardHeader className="space-y-0 p-2">
|
||||||
<CardTitle className="line-clamp-2">{metadata.title}</CardTitle>
|
<CardTitle className="line-clamp-1 text-sm font-medium group-hover:underline">
|
||||||
<CardDescription className="line-clamp-3">
|
{metadata.title}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="line-clamp-2 text-[10px]">
|
||||||
{metadata.description}
|
{metadata.description}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
176
components/Modals/FormModal.tsx
Normal file
176
components/Modals/FormModal.tsx
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { type ReactNode } from "react";
|
||||||
|
import * as z from "zod";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import {
|
||||||
|
FieldErrors,
|
||||||
|
useForm,
|
||||||
|
FieldValues,
|
||||||
|
DefaultValues,
|
||||||
|
Path,
|
||||||
|
} from "react-hook-form";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import Template from "./Template";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
|
type FieldOptions =
|
||||||
|
| "toggle"
|
||||||
|
| "horizontal-tabs"
|
||||||
|
| "input"
|
||||||
|
| "text-area"
|
||||||
|
| "custom";
|
||||||
|
|
||||||
|
type DefaultFieldType<TSchema> = {
|
||||||
|
label: string;
|
||||||
|
slug: keyof z.infer<z.Schema<TSchema>> & string;
|
||||||
|
placeholder?: string;
|
||||||
|
description?: string;
|
||||||
|
lines?: number;
|
||||||
|
styles?: string;
|
||||||
|
value?: string | number | boolean;
|
||||||
|
custom?: ReactNode;
|
||||||
|
options?: { label: string; value: string; icon?: ReactNode }[];
|
||||||
|
};
|
||||||
|
type FieldType<TSchema> = DefaultFieldType<TSchema> &
|
||||||
|
(
|
||||||
|
| {
|
||||||
|
type: "select";
|
||||||
|
options: { label: string; value: string; icon?: ReactNode }[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: FieldOptions;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type FormModalProps<TSchema> = {
|
||||||
|
title: string;
|
||||||
|
fields: FieldType<TSchema>[];
|
||||||
|
errors?: FieldErrors;
|
||||||
|
isSubmitting?: boolean;
|
||||||
|
cta: {
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
defaultValues?: Partial<z.infer<z.Schema<TSchema>>>;
|
||||||
|
onSubmit: (props: z.infer<z.Schema<TSchema>>) => any;
|
||||||
|
formSchema: z.Schema<TSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FormModal<TSchema extends FieldValues>({
|
||||||
|
title,
|
||||||
|
fields,
|
||||||
|
cta,
|
||||||
|
errors,
|
||||||
|
isSubmitting,
|
||||||
|
formSchema,
|
||||||
|
defaultValues,
|
||||||
|
onSubmit,
|
||||||
|
}: FormModalProps<TSchema>) {
|
||||||
|
type FormDataType = z.infer<typeof formSchema>;
|
||||||
|
const form = useForm<FormDataType>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: defaultValues as DefaultValues<TSchema>,
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Template title={title} className="md:max-w-[400px]">
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
|
{fields.map(
|
||||||
|
({
|
||||||
|
label,
|
||||||
|
slug,
|
||||||
|
type,
|
||||||
|
placeholder,
|
||||||
|
description,
|
||||||
|
...fieldProps
|
||||||
|
}) => (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={slug as Path<TSchema>}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem
|
||||||
|
className={cn(
|
||||||
|
type === "toggle" &&
|
||||||
|
"flex items-center gap-x-3 space-y-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FormLabel>{label}</FormLabel>
|
||||||
|
{type === "input" ? (
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={placeholder} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
) : type === "text-area" ? (
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...field}
|
||||||
|
className="auto-sizing"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
) : type === "select" ? (
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={placeholder} />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent className="z-modal+">
|
||||||
|
{fieldProps.options?.map((o) => (
|
||||||
|
<SelectItem key={o.value} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : type === "toggle" ? (
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
) : (
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={placeholder} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
{!!description && (
|
||||||
|
<FormDescription>{description}</FormDescription>
|
||||||
|
)}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
<Button type="submit" className="w-full" loading={isSubmitting}>
|
||||||
|
{cta.text}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
89
components/Modals/Login.tsx
Normal file
89
components/Modals/Login.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import Template from "./Template";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useModal } from "@/app/_providers/modal/provider";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
// import { useKeys } from "@/app/_providers/keysProvider";
|
||||||
|
import { useNDK } from "@/app/_providers/ndk";
|
||||||
|
import useCurrentUser from "@/lib/hooks/useCurrentUser";
|
||||||
|
|
||||||
|
export default function LoginModal() {
|
||||||
|
const { loginWithNip07 } = useNDK();
|
||||||
|
const { loginWithPubkey } = useCurrentUser();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const modal = useModal();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const shouldReconnect = localStorage.getItem("shouldReconnect");
|
||||||
|
|
||||||
|
const getConnected = async (shouldReconnect: string) => {
|
||||||
|
let enabled: boolean | void = false;
|
||||||
|
|
||||||
|
if (typeof window.nostr === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldReconnect === "true") {
|
||||||
|
const user = await loginWithNip07();
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("NO auth");
|
||||||
|
}
|
||||||
|
console.log("LOGIN", user);
|
||||||
|
await loginWithPubkey(nip19.decode(user.npub).data.toString());
|
||||||
|
|
||||||
|
// keys?.setKeys({
|
||||||
|
// privkey: "",
|
||||||
|
// pubkey: ,
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.webln === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldReconnect === "true" && !window.webln.executing) {
|
||||||
|
try {
|
||||||
|
enabled = await window.webln.enable();
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shouldReconnect === "true") {
|
||||||
|
getConnected(shouldReconnect);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
setIsLoading(true);
|
||||||
|
if (typeof window.nostr !== "undefined") {
|
||||||
|
const user = await loginWithNip07();
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("NO auth");
|
||||||
|
}
|
||||||
|
console.log("LOGIN", user);
|
||||||
|
await loginWithPubkey(nip19.decode(user.npub).data.toString());
|
||||||
|
localStorage.setItem("shouldReconnect", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.webln !== "undefined") {
|
||||||
|
await window.webln.enable();
|
||||||
|
}
|
||||||
|
console.log("connected ");
|
||||||
|
setIsLoading(false);
|
||||||
|
modal?.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template title="Login" className="md:max-w-[400px]">
|
||||||
|
<div className="flex flex-col gap-y-5">
|
||||||
|
<Button onClick={() => void handleLogin()} loading={isLoading}>
|
||||||
|
Connect with extension
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
41
components/Modals/Template.tsx
Normal file
41
components/Modals/Template.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { HiX } from "react-icons/hi";
|
||||||
|
import { useModal } from "@/app/_providers/modal/provider";
|
||||||
|
|
||||||
|
type ModalProps = {
|
||||||
|
title: string;
|
||||||
|
} & React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
export default function Template({ title, children, className }: ModalProps) {
|
||||||
|
const modal = useModal();
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
modal?.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("relative w-full grow p-4 md:rounded-lg md:p-6", className)}
|
||||||
|
>
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontFamily: '"DM Sans", sans-serif',
|
||||||
|
}}
|
||||||
|
className="font-condensed text-xl font-semibold text-foreground"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="hidden text-muted-foreground transition-all hover:text-primary md:flex"
|
||||||
|
>
|
||||||
|
<HiX className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
19
components/StatusIndicator/index.tsx
Normal file
19
components/StatusIndicator/index.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const statuses = {
|
||||||
|
offline: "text-muted-foreground/20 bg-muted",
|
||||||
|
online: "text-green-400 bg-green-400/10",
|
||||||
|
error: "text-rose-400 bg-rose-400/10",
|
||||||
|
warning: "text-yellow-400 bg-yellow-400/10",
|
||||||
|
};
|
||||||
|
type StatusIndicatorProps = {
|
||||||
|
status: keyof typeof statuses;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function StatusIndicator({ status }: StatusIndicatorProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(statuses[status], "flex-none rounded-full p-1")}>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-current" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -20,6 +20,7 @@ export default function Tabs<T extends { name: string; label: string }>({
|
|||||||
>
|
>
|
||||||
{tabs.map((tab, idx) => (
|
{tabs.map((tab, idx) => (
|
||||||
<button
|
<button
|
||||||
|
key={tab.name}
|
||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => setActiveTab(tab)}
|
||||||
type="button"
|
type="button"
|
||||||
role="tab"
|
role="tab"
|
||||||
|
15
components/TextRendering/EventMention.tsx
Normal file
15
components/TextRendering/EventMention.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"use client";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
type EventMentionProps = {
|
||||||
|
mention: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EventMention({ mention }: EventMentionProps) {
|
||||||
|
// const { user } = useProfile(mention);
|
||||||
|
return (
|
||||||
|
<Link href={`/${mention}`}>
|
||||||
|
<span className="text-primary-foreground hover:underline">{`*\$${mention}`}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
35
components/TextRendering/ProfileMention.tsx
Normal file
35
components/TextRendering/ProfileMention.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
import { NOSTR_BECH32_REGEXP } from "@/lib/nostr/utils";
|
||||||
|
import { useNDK } from "@/app/_providers/ndk";
|
||||||
|
|
||||||
|
type ProfileMentionProps = {
|
||||||
|
mention: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getPubkey(mention: string) {
|
||||||
|
try {
|
||||||
|
return NOSTR_BECH32_REGEXP.test(mention)
|
||||||
|
? nip19.decode(mention).data.toString()
|
||||||
|
: mention;
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Error getting pubkey", err);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProfileMention({ mention }: ProfileMentionProps) {
|
||||||
|
const pubkey = getPubkey(mention);
|
||||||
|
// const { user } = useProfile(pubkey);
|
||||||
|
const { getProfile } = useNDK();
|
||||||
|
const profile = getProfile(mention);
|
||||||
|
return (
|
||||||
|
<Link href={`/${mention}`}>
|
||||||
|
<span className="text-primary-foreground hover:underline">{`@${
|
||||||
|
profile?.name ?? mention
|
||||||
|
}`}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
78
components/TextRendering/index.tsx
Normal file
78
components/TextRendering/index.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { cleanUrl } from "@/lib/utils";
|
||||||
|
import Link from "next/link";
|
||||||
|
import ProfileMention from "./ProfileMention";
|
||||||
|
import EventMention from "./EventMention";
|
||||||
|
|
||||||
|
const RenderText = ({ text }: { text?: string }) => {
|
||||||
|
if (!text) return null;
|
||||||
|
const Elements: JSX.Element[] = [];
|
||||||
|
const urlRegex =
|
||||||
|
/(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))/g;
|
||||||
|
const hashtagRegex = /#\b\w+\b/g;
|
||||||
|
const nostrPrefixRegex = /nostr:[a-z0-9]+/g;
|
||||||
|
// const usernameRegex = /(?:^|\s)\@(\w+)\b/g;
|
||||||
|
const combinedRegex = new RegExp(
|
||||||
|
`(${urlRegex.source}|${hashtagRegex.source}|${nostrPrefixRegex.source})`,
|
||||||
|
"g",
|
||||||
|
);
|
||||||
|
// Get Array of URLs
|
||||||
|
const specialValuesArray = text.match(combinedRegex);
|
||||||
|
const formattedText = text.replace(combinedRegex, "##[link]##");
|
||||||
|
|
||||||
|
const cleanTextArray = formattedText.split("##[link]##");
|
||||||
|
|
||||||
|
cleanTextArray.forEach((string, index) => {
|
||||||
|
const jsxElement = <span className="">{string}</span>;
|
||||||
|
Elements.push(jsxElement);
|
||||||
|
let specialElement;
|
||||||
|
if (specialValuesArray?.length && specialValuesArray.length > index) {
|
||||||
|
if (specialValuesArray[index]?.match(urlRegex)) {
|
||||||
|
specialElement = (
|
||||||
|
<a
|
||||||
|
className="text-primary-foreground hover:underline"
|
||||||
|
href={cleanUrl(specialValuesArray[index])}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{cleanUrl(specialValuesArray[index])}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
// specialElement = <ContentRendering url={specialValuesArray[index]} />;
|
||||||
|
// specialElement = <span>{cleanUrl(specialValuesArray[index])}</span>;
|
||||||
|
} else if (specialValuesArray[index]?.match(hashtagRegex)) {
|
||||||
|
specialElement = (
|
||||||
|
<Link href={`/?t=${specialValuesArray[index]?.substring(1)}`}>
|
||||||
|
<span className="break-words text-primary-foreground hover:underline">
|
||||||
|
{specialValuesArray[index]}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
// specialElement = <span className="">{specialValuesArray[index]}</span>;
|
||||||
|
} else if (specialValuesArray[index]?.match(nostrPrefixRegex)) {
|
||||||
|
const mention = specialValuesArray[index]?.split(":")[1];
|
||||||
|
if (mention.startsWith("nprofile") || mention.startsWith("npub")) {
|
||||||
|
specialElement = <ProfileMention mention={mention} />;
|
||||||
|
} else if (
|
||||||
|
mention.startsWith("nevent") ||
|
||||||
|
mention.startsWith("note") ||
|
||||||
|
mention.startsWith("naddr")
|
||||||
|
) {
|
||||||
|
specialElement = <EventMention mention={mention} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (specialElement) {
|
||||||
|
Elements.push(specialElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Elements.map((el, index) => (
|
||||||
|
<span key={index}>{el}</span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { RenderText };
|
@ -1,11 +1,11 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import Spinner from "../spinner";
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
"relative inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@ -31,27 +31,45 @@ const buttonVariants = cva(
|
|||||||
variant: "default",
|
variant: "default",
|
||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof buttonVariants> {
|
VariantProps<typeof buttonVariants> {
|
||||||
asChild?: boolean
|
asChild?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
(
|
||||||
const Comp = asChild ? Slot : "button"
|
{ className, variant, size, asChild = false, loading = false, ...props },
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="text-transparent">{props.children}</div>
|
||||||
|
<div className="center absolute inset-0">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
</Comp>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
Button.displayName = "Button"
|
Button.displayName = "Button";
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants };
|
||||||
|
32
containers/Feed/index.tsx
Normal file
32
containers/Feed/index.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
import KindCard from "@/components/KindCard";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import Spinner from "@/components/spinner";
|
||||||
|
import { Event } from "nostr-tools";
|
||||||
|
import useEvents from "@/lib/hooks/useEvents";
|
||||||
|
import { type NDKFilter } from "@nostr-dev-kit/ndk";
|
||||||
|
type FeedProps = {
|
||||||
|
filter?: NDKFilter;
|
||||||
|
className?: string;
|
||||||
|
loader?: () => JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Feed({ filter, className, loader: Loader }: FeedProps) {
|
||||||
|
const { events, isLoading } = useEvents({
|
||||||
|
filter: { ...filter },
|
||||||
|
});
|
||||||
|
if (isLoading) {
|
||||||
|
if (Loader) {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{events.map((e) => {
|
||||||
|
const event = e.rawEvent() as Event;
|
||||||
|
return <KindCard key={e.id} {...event} />;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
77
lib/hooks/useCurrentUser.ts
Normal file
77
lib/hooks/useCurrentUser.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import currentUserStore from "@/lib/stores/currentUser";
|
||||||
|
// import useEvents from "@/lib/hooks/useEvents";
|
||||||
|
import { UserSchema } from "@/types";
|
||||||
|
import { useNDK } from "@/app/_providers/ndk";
|
||||||
|
|
||||||
|
export default function useCurrentUser() {
|
||||||
|
const {
|
||||||
|
currentUser,
|
||||||
|
setCurrentUser,
|
||||||
|
setFollows,
|
||||||
|
updateCurrentUser,
|
||||||
|
follows,
|
||||||
|
} = currentUserStore();
|
||||||
|
const { loginWithNip07, getProfile, ndk } = useNDK();
|
||||||
|
|
||||||
|
// const {
|
||||||
|
// events: contactList,
|
||||||
|
// isLoading,
|
||||||
|
// onEvent,
|
||||||
|
// } = useEvents({
|
||||||
|
// filter: {
|
||||||
|
// kinds: [3],
|
||||||
|
// authors: [currentUser?.pubkey ?? ""],
|
||||||
|
// limit: 1,
|
||||||
|
// },
|
||||||
|
// enabled: !!currentUser,
|
||||||
|
// });
|
||||||
|
// onEvent((event) => {
|
||||||
|
// console.log("EVENT", event);
|
||||||
|
// const foundFollows = event.tags
|
||||||
|
// .filter(([key]) => key === "p")
|
||||||
|
// .map(([key, pubkey]) => pubkey);
|
||||||
|
// console.log("Found follows", foundFollows);
|
||||||
|
// if (follows.length !== foundFollows.length) {
|
||||||
|
// setFollows(follows);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
localStorage.removeItem("shouldReconnect");
|
||||||
|
setCurrentUser(null);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
function handleUpdateUser(userInfo: string) {
|
||||||
|
const parsedData = UserSchema.safeParse({
|
||||||
|
...currentUser,
|
||||||
|
...JSON.parse(userInfo),
|
||||||
|
});
|
||||||
|
if (parsedData.success) {
|
||||||
|
updateCurrentUser({
|
||||||
|
profile: {
|
||||||
|
...parsedData.data,
|
||||||
|
displayName: parsedData.data.display_name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginWithPubkey(pubkey: string) {
|
||||||
|
const user = ndk!.getUser({ hexpubkey: pubkey });
|
||||||
|
console.log("user", user);
|
||||||
|
await user.fetchProfile();
|
||||||
|
setCurrentUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentUser,
|
||||||
|
isLoading: false,
|
||||||
|
follows,
|
||||||
|
setCurrentUser,
|
||||||
|
logout,
|
||||||
|
updateUser: handleUpdateUser,
|
||||||
|
loginWithPubkey,
|
||||||
|
};
|
||||||
|
}
|
95
lib/hooks/useEvents.ts
Normal file
95
lib/hooks/useEvents.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useNDK } from "@/app/_providers/ndk";
|
||||||
|
import {
|
||||||
|
NDKEvent,
|
||||||
|
type NDKFilter,
|
||||||
|
type NDKSubscription,
|
||||||
|
} from "@nostr-dev-kit/ndk";
|
||||||
|
import { log } from "@/lib/utils";
|
||||||
|
import { uniqBy } from "ramda";
|
||||||
|
import { Event as NostrEvent } from "nostr-tools";
|
||||||
|
|
||||||
|
const debug = true;
|
||||||
|
type OnEventFunc = (event: NostrEvent) => void;
|
||||||
|
type OnDoneFunc = () => void;
|
||||||
|
type OnSubscribeFunc = (sub: NDKSubscription) => void;
|
||||||
|
type UseEventsProps = {
|
||||||
|
filter: NDKFilter;
|
||||||
|
enabled?: boolean;
|
||||||
|
eventFilter?: (e: NDKEvent) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useEvents({
|
||||||
|
filter,
|
||||||
|
enabled = true,
|
||||||
|
eventFilter = () => true,
|
||||||
|
}: UseEventsProps) {
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [sub, setSub] = useState<NDKSubscription | undefined>(undefined);
|
||||||
|
const [events, setEvents] = useState<NDKEvent[]>([]);
|
||||||
|
const [eventIds, setEventIds] = useState<Set<string>>(new Set());
|
||||||
|
let onEventCallback: null | OnEventFunc = null;
|
||||||
|
let onSubscribeCallback: null | OnSubscribeFunc = null;
|
||||||
|
let onDoneCallback: null | OnDoneFunc = null;
|
||||||
|
const { ndk } = useNDK();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !ndk) return;
|
||||||
|
void init();
|
||||||
|
return () => {
|
||||||
|
console.log("STOPPING", sub);
|
||||||
|
if (sub) {
|
||||||
|
sub.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [enabled, ndk]);
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
console.log("Running init");
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const sub = ndk!.subscribe(
|
||||||
|
{ limit: 50, ...filter },
|
||||||
|
{ closeOnEose: false },
|
||||||
|
);
|
||||||
|
setSub(sub);
|
||||||
|
onSubscribeCallback?.(sub);
|
||||||
|
sub.on("event", (e, r) => {
|
||||||
|
if (eventIds.has(e.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (eventFilter(e)) {
|
||||||
|
setEvents((prevEvents) => {
|
||||||
|
const events = uniqBy((a) => a.id, [...prevEvents, e]);
|
||||||
|
return events.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
});
|
||||||
|
setEventIds((prev) => prev.add(e.id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
log(debug, "error", `❌ nostr (${err})`);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
events,
|
||||||
|
onEvent: (_onEventCallback: OnEventFunc) => {
|
||||||
|
if (_onEventCallback) {
|
||||||
|
onEventCallback = _onEventCallback;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: (_onDoneCallback: OnDoneFunc) => {
|
||||||
|
if (_onDoneCallback) {
|
||||||
|
onDoneCallback = _onDoneCallback;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubscribe: (_onSubscribeCallback: OnSubscribeFunc) => {
|
||||||
|
if (_onSubscribeCallback) {
|
||||||
|
onSubscribeCallback = _onSubscribeCallback;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
22
lib/hooks/useProfile.ts
Normal file
22
lib/hooks/useProfile.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
import { NOSTR_BECH32_REGEXP } from "@/lib/nostr/utils";
|
||||||
|
import { useNDK } from "@/app/_providers/ndk";
|
||||||
|
import { type NDKUserProfile } from "@nostr-dev-kit/ndk";
|
||||||
|
|
||||||
|
export default function useProfile(key: string) {
|
||||||
|
const { ndk, getProfile } = useNDK();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ndk) return;
|
||||||
|
if (NOSTR_BECH32_REGEXP.test(key)) {
|
||||||
|
key = nip19.decode(key).data.toString();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
void ndk.getUser({ hexpubkey: key }).fetchProfile();
|
||||||
|
};
|
||||||
|
}, [key, ndk]);
|
||||||
|
|
||||||
|
return { profile: getProfile(key) };
|
||||||
|
}
|
28
lib/stores/currentUser.ts
Normal file
28
lib/stores/currentUser.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { type NDKUser } from "@nostr-dev-kit/ndk";
|
||||||
|
|
||||||
|
type Settings = {};
|
||||||
|
|
||||||
|
interface CurrentUserState {
|
||||||
|
currentUser: NDKUser | null;
|
||||||
|
follows: string[];
|
||||||
|
settings: Settings;
|
||||||
|
setCurrentUser: (user: NDKUser | null) => void;
|
||||||
|
updateCurrentUser: (user: Partial<NDKUser>) => void;
|
||||||
|
setFollows: (follows: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUserStore = create<CurrentUserState>()((set) => ({
|
||||||
|
currentUser: null,
|
||||||
|
follows: [],
|
||||||
|
settings: {},
|
||||||
|
setCurrentUser: (user) => set((state) => ({ ...state, currentUser: user })),
|
||||||
|
updateCurrentUser: (user) =>
|
||||||
|
set((state) => ({
|
||||||
|
...state,
|
||||||
|
currentUser: { ...state.currentUser, ...user } as NDKUser,
|
||||||
|
})),
|
||||||
|
setFollows: (follows) => set((state) => ({ ...state, follows: follows })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default currentUserStore;
|
@ -1,9 +1,8 @@
|
|||||||
import { type ClassValue, clsx } from "clsx"
|
import { type ClassValue, clsx } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatCount(count: number) {
|
export function formatCount(count: number) {
|
||||||
@ -27,6 +26,31 @@ 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 getTwoLetters(user: {
|
||||||
|
npub: string;
|
||||||
|
profile?: {
|
||||||
|
displayName?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
if (user.profile) {
|
||||||
|
if (user.profile.displayName) {
|
||||||
|
const firstLetter = user.profile.displayName.at(0);
|
||||||
|
const secondLetter =
|
||||||
|
user.profile.displayName.split(" ")[1].at(0) ??
|
||||||
|
user.profile.displayName.at(1) ??
|
||||||
|
"";
|
||||||
|
return firstLetter + secondLetter;
|
||||||
|
}
|
||||||
|
if (user.profile.name) {
|
||||||
|
const firstLetter = user.profile.name.at(0);
|
||||||
|
const secondLetter =
|
||||||
|
user.profile.name.split(" ")[1].at(0) ?? user.profile.name.at(1) ?? "";
|
||||||
|
return firstLetter + secondLetter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (user.npub.at(5) ?? "") + (user.npub.at(6) ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
export function removeDuplicates<T>(data: T[], key?: keyof T) {
|
export function removeDuplicates<T>(data: T[], key?: keyof T) {
|
||||||
if (key) {
|
if (key) {
|
||||||
|
@ -104,9 +104,6 @@ module.exports = {
|
|||||||
3: 3,
|
3: 3,
|
||||||
4: 4,
|
4: 4,
|
||||||
},
|
},
|
||||||
// screens: {
|
|
||||||
// standalone: { raw: "(display-mode: standalone)" },
|
|
||||||
// },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
import { WebLNProvider } from "webln";
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
webln?: WebLNProvider & {
|
||||||
|
executing?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
42
types/index.ts
Normal file
42
types/index.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
npub: string;
|
||||||
|
name?: string | undefined;
|
||||||
|
username?: string | undefined;
|
||||||
|
display_name?: string | undefined;
|
||||||
|
picture?: string | undefined;
|
||||||
|
banner?: string | undefined;
|
||||||
|
about?: string | undefined;
|
||||||
|
website?: string | undefined;
|
||||||
|
lud06?: string | undefined;
|
||||||
|
lud16?: string | undefined;
|
||||||
|
nip05?: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserSchema = z.object({
|
||||||
|
npub: z.string(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
username: z.string().optional(),
|
||||||
|
display_name: z.string().optional(),
|
||||||
|
picture: z.string().optional(),
|
||||||
|
banner: z.string().optional(),
|
||||||
|
about: z.string().optional(),
|
||||||
|
website: z.string().optional(),
|
||||||
|
lud06: z.string().optional(),
|
||||||
|
lud16: z.string().optional(),
|
||||||
|
nip05: z.string().optional(),
|
||||||
|
});
|
||||||
|
const EventSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
content: z.string(),
|
||||||
|
pubkey: z.string(),
|
||||||
|
tags: z.string().array().array(),
|
||||||
|
kind: z.number(),
|
||||||
|
created_at: z.number(),
|
||||||
|
sig: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export { UserSchema, EventSchema };
|
||||||
|
|
||||||
|
export type { User };
|
Loading…
x
Reference in New Issue
Block a user