diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..b9467d1 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,61 @@ +name: Docker Build and Push + +on: + workflow_dispatch: + push: + branches: + - main + - master + +env: + REGISTRY_NAME: ghcr.io + IMAGE_NAME: lumina + +jobs: + # ci: + # name: CI + # uses: ./.github/workflows/ci.yml + + build_and_push: + # needs: [ci] + name: Build and Push + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Collecting Metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY_NAME }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + + - name: Building And Pushing Image + id: docker_build + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm64 + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 83fb74e..c42e560 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine as builder +FROM node:21-alpine3.18 as builder WORKDIR /app COPY lumina/package.json lumina/package-lock.json ./ @@ -6,7 +6,7 @@ RUN npm ci COPY ./lumina/ . RUN npm run build -FROM node:18-alpine as runner +FROM node:21-alpine3.18 as runner WORKDIR /app COPY --from=builder /app/package.json . COPY --from=builder /app/package-lock.json . diff --git a/README.md b/README.md index 7850f12..b602fcb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ -# lumina -A Nostr Client for scrolling through Images on nostr +# lumina.rocks + +A social media for images and pictures 📸 + +## Docker +### Quickstart +`docker run --rm -it -p 3000:3000 ghcr.io/mroxso/lumina-rocks-website:latest` \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index 2cf66fa..40d6f4c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,7 +1,7 @@ version: '3' services: - lumina: + lumina-rocks-website: build: . - # image: ghcr.io/lumina-rocks/lumina:latest + # image: ghcr.io/mroxso/lumina-rocks-website:latest ports: - "8080:3000" \ No newline at end of file diff --git a/kustomize/base/deployment.yaml b/kustomize/base/deployment.yaml new file mode 100644 index 0000000..5d43bf4 --- /dev/null +++ b/kustomize/base/deployment.yaml @@ -0,0 +1,19 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lumina-deployment +spec: + replicas: 1 + selector: + matchLabels: + app: lumina + template: + metadata: + labels: + app: lumina + spec: + containers: + - name: lumina + image: ghcr.io/lumina-rocks/lumina:latest + ports: + - containerPort: 80 diff --git a/kustomize/base/kustomization.yaml b/kustomize/base/kustomization.yaml new file mode 100644 index 0000000..6d1374a --- /dev/null +++ b/kustomize/base/kustomization.yaml @@ -0,0 +1,3 @@ +resources: + - deployment.yaml + - service.yaml diff --git a/kustomize/base/service.yaml b/kustomize/base/service.yaml new file mode 100644 index 0000000..62907a4 --- /dev/null +++ b/kustomize/base/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: my-app +spec: + selector: + app: my-app + ports: + - protocol: TCP + port: 80 + targetPort: 80 diff --git a/kustomize/environments/beta/kustomization.yaml b/kustomize/environments/beta/kustomization.yaml new file mode 100644 index 0000000..fb7dc89 --- /dev/null +++ b/kustomize/environments/beta/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - ../../base diff --git a/kustomize/environments/prod/kustomization.yaml b/kustomize/environments/prod/kustomization.yaml new file mode 100644 index 0000000..fb7dc89 --- /dev/null +++ b/kustomize/environments/prod/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - ../../base diff --git a/lumina/app/dashboard/[pubkey]/page.tsx b/lumina/app/dashboard/[pubkey]/page.tsx new file mode 100644 index 0000000..fdc92fb --- /dev/null +++ b/lumina/app/dashboard/[pubkey]/page.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useParams } from 'next/navigation' +import { nip19 } from "nostr-tools"; +import { NostrProvider } from "nostr-react"; +import Statistics from '@/components/dashboard/Statistics'; + +const DashboardPage: React.FC= ({ }) => { + + const params = useParams() + let pubkey = params.pubkey + // check if pubkey contains "npub" + // if so, then we need to convert it to a pubkey + if (pubkey.includes("npub")) { + // convert npub to pubkey + pubkey = nip19.decode(pubkey.toString()).data.toString() + } + + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + + return ( + <> + + + + + ); +} + +export default DashboardPage; \ No newline at end of file diff --git a/lumina/app/feed/page.tsx b/lumina/app/feed/page.tsx new file mode 100644 index 0000000..9862352 --- /dev/null +++ b/lumina/app/feed/page.tsx @@ -0,0 +1,61 @@ +'use client'; + +import Head from "next/head"; +import ProfileInfoCard from "@/components/ProfileInfoCard"; +import ProfileFeed from "@/components/ProfileFeed"; +import { useParams } from 'next/navigation' +import { nip19 } from "nostr-tools"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { SectionIcon, GridIcon } from '@radix-ui/react-icons' +import TagFeed from "@/components/TagFeed"; +import { NostrProvider } from "nostr-react"; +import FollowerFeed from "@/components/FollowerFeed"; +import ProfileQuickViewFeed from "@/components/ProfileQuickViewFeed"; +import FollowerQuickViewFeed from "@/components/FollowerQuickViewFeed"; + +export default function FeedPage() { + + let pubkey = null; + if (typeof window !== 'undefined') { + pubkey = window.localStorage.getItem('pubkey'); + } + + // check if pubkey contains "npub" + // if so, then we need to convert it to a pubkey + // if (pubkey.includes("npub")) { + // // convert npub to pubkey + // pubkey = nip19.decode(pubkey.toString()).data.toString() + // } + + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + + return ( + <> + + + LUMINA.rocks - {pubkey} + + + + +
+

Follower Feed

+ + + + + + + + + + + + +
+
+ + ); +} diff --git a/lumina/app/global/page.tsx b/lumina/app/global/page.tsx new file mode 100644 index 0000000..597578b --- /dev/null +++ b/lumina/app/global/page.tsx @@ -0,0 +1,32 @@ +"use client"; + +import GlobalFeed from "@/components/GlobalFeed"; +import { NostrProvider } from "nostr-react"; + +export default function Home() { + + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + + return ( + //
+ //
+ // + // + // + // + // + // Home + // + // + // + // + // + +
+ +
+
+ ); +} diff --git a/lumina/app/icon.tsx b/lumina/app/icon.tsx new file mode 100644 index 0000000..6e49223 --- /dev/null +++ b/lumina/app/icon.tsx @@ -0,0 +1,40 @@ +import { ImageResponse } from 'next/og' + +// Route segment config +export const runtime = 'edge' + +// Image metadata +export const size = { + width: 32, + height: 32, +} +export const contentType = 'image/png' + +// Image generation +export default function Icon() { + return new ImageResponse( + ( + // ImageResponse JSX element +
+ L +
+ ), + // ImageResponse options + { + // For convenience, we can re-use the exported icons size metadata + // config to also set the ImageResponse's width and height. + ...size, + } + ) +} \ No newline at end of file diff --git a/lumina/app/layout.tsx b/lumina/app/layout.tsx index ccf0699..7b3fe80 100644 --- a/lumina/app/layout.tsx +++ b/lumina/app/layout.tsx @@ -1,17 +1,21 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import { Metadata } from "next"; import "./globals.css"; +import { NostrProvider } from "nostr-react"; +import Head from "next/head"; import { ThemeProvider } from "@/components/theme-provider"; -import { DropdownThemeMode } from "@/components/DropdownThemeMode"; -import { Navigation } from "@/components/Navigation"; - -const inter = Inter({ subsets: ["latin"] }); +import { TopNavigation } from "@/components/headerComponents/TopNavigation"; +import BottomBar from "@/components/BottomBar"; +import { Inter } from "next/font/google"; +import { Toaster } from "@/components/ui/toaster" export const metadata: Metadata = { title: "LUMINA", - description: "LUMINA.rocks", + description: "An effortless, enjoyable, and innovative way to capture, enhance, and share moments with everyone, decentralized and boundless.", + manifest: "/manifest.json", }; +const inter = Inter({ subsets: ["latin"] }); + export default function RootLayout({ children, }: Readonly<{ @@ -19,6 +23,10 @@ export default function RootLayout({ }>) { return ( + + + + - - {children} + + +
+ {children} +
+
diff --git a/lumina/app/login/page.tsx b/lumina/app/login/page.tsx new file mode 100644 index 0000000..fabcf76 --- /dev/null +++ b/lumina/app/login/page.tsx @@ -0,0 +1,20 @@ +'use client'; + +import Head from "next/head"; +import { LoginForm } from "@/components/LoginForm"; + +export default function LoginPage() { + return ( + <> + + LUMINA.rocks - Login + + + + +
+ +
+ + ); +} diff --git a/lumina/app/note/[id]/page.tsx b/lumina/app/note/[id]/page.tsx new file mode 100644 index 0000000..cbdc230 --- /dev/null +++ b/lumina/app/note/[id]/page.tsx @@ -0,0 +1,39 @@ +'use client'; + +import Head from "next/head"; +import { useParams } from 'next/navigation' +import NotePageComponent from "@/components/NotePageComponent"; +import { nip19 } from "nostr-tools"; +import { NostrProvider } from "nostr-react"; + +export default function NotePage() { + + const params = useParams() + let id = params.id + + if (id.includes("note1")) { + id = nip19.decode(id.toString()).data.toString() + } + + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + + return ( + <> + + + LUMINA.rocks - {id} + + + + +
+
+ +
+
+
+ + ); +} diff --git a/lumina/app/notifications/page.tsx b/lumina/app/notifications/page.tsx new file mode 100644 index 0000000..05bd74a --- /dev/null +++ b/lumina/app/notifications/page.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { nip19 } from "nostr-tools"; +import { NostrProvider } from "nostr-react"; +import Notifications from '@/components/Notifications'; + +const NotificationsPage: React.FC= ({ }) => { + let pubkey = ''; + + if (typeof window !== 'undefined') { + pubkey = window.localStorage.getItem("pubkey") ?? ''; + } + + // check if pubkey contains "npub" + // if so, then we need to convert it to a pubkey + if (pubkey.includes("npub")) { + // convert npub to pubkey + pubkey = nip19.decode(pubkey.toString()).data.toString() + } + + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + + return ( + <> + + + + + ); +} + +export default NotificationsPage; \ No newline at end of file diff --git a/lumina/app/onboarding/createProfile/page.tsx b/lumina/app/onboarding/createProfile/page.tsx new file mode 100644 index 0000000..7bc560c --- /dev/null +++ b/lumina/app/onboarding/createProfile/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { UpdateProfileForm } from "@/components/UpdateProfileForm"; +import { NostrProvider } from "nostr-react"; + + +export default function OnboardingCreateProfile() { + + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + + return ( + <> + +
+

Step 2: Create Profile

+ +
+
+ + ); +} diff --git a/lumina/app/onboarding/page.tsx b/lumina/app/onboarding/page.tsx new file mode 100644 index 0000000..1c7e644 --- /dev/null +++ b/lumina/app/onboarding/page.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { CreateSecretKeyForm } from "@/components/onboarding/createSecretKeyForm"; +import { Button } from "@/components/ui/button"; +import { NostrProvider } from "nostr-react"; + + +export default function OnboardingHome() { + + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + + return ( + <> + +
+

Step 1: Create your secret key

+ +
+
+ + ); +} diff --git a/lumina/app/page.tsx b/lumina/app/page.tsx index 04900de..e3587b4 100644 --- a/lumina/app/page.tsx +++ b/lumina/app/page.tsx @@ -1,36 +1,26 @@ "use client"; -import { Button } from "@/components/ui/button"; -import Image from "next/image"; -import Link from "next/link"; -import { NavigationMenu, NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu"; -import GlobalFeed from "@/components/GlobalFeed"; +import { Search } from "@/components/Search"; +import { TrendingAccounts } from "@/components/TrendingAccounts"; +import { TrendingImages } from "@/components/TrendingImages"; import { NostrProvider } from "nostr-react"; -const relayUrls = [ - "wss://relay.damus.io", - "wss://relay.nostr.band", -]; export default function Home() { + + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + return ( - //
- //
- // - // - // - // - // - // Home - // - // - // - // - // - -
- -
-
+ <> + +
+ +
+ {/* */} + +
+ ); } diff --git a/lumina/app/profile/[pubkey]/page.tsx b/lumina/app/profile/[pubkey]/page.tsx index afb9a33..91b628e 100644 --- a/lumina/app/profile/[pubkey]/page.tsx +++ b/lumina/app/profile/[pubkey]/page.tsx @@ -1,20 +1,16 @@ 'use client'; -import Head from "next/head"; -import { NostrProvider } from "nostr-react"; import ProfileInfoCard from "@/components/ProfileInfoCard"; import ProfileFeed from "@/components/ProfileFeed"; import { useParams } from 'next/navigation' import { nip19 } from "nostr-tools"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { SectionIcon, GridIcon } from '@radix-ui/react-icons' +import ProfileQuickViewFeed from "@/components/ProfileQuickViewFeed"; +import ProfileTextFeed from "@/components/ProfileTextFeed"; +import { NostrProvider } from "nostr-react"; -const relayUrls = [ - "wss://relay.damus.io", - "wss://relay.nostr.band", -]; - -export default function Home() { +export default function ProfilePage() { const params = useParams() let pubkey = params.pubkey @@ -25,28 +21,32 @@ export default function Home() { pubkey = nip19.decode(pubkey.toString()).data.toString() } + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + return ( <> - - - LUMINA.rocks - {pubkey} - - - - -
-
+ +
+
- - - + + + + Notes + + + - QuickView coming soon. + + +
diff --git a/lumina/app/profile/settings/page.tsx b/lumina/app/profile/settings/page.tsx new file mode 100644 index 0000000..3fc67d0 --- /dev/null +++ b/lumina/app/profile/settings/page.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { useParams } from 'next/navigation' +import { nip19 } from "nostr-tools"; +import { NostrProvider } from "nostr-react"; +import { UpdateProfileForm } from "@/components/UpdateProfileForm"; + +export default function ProfileSettingsPage() { + + let pubkey = null; + if (typeof window !== 'undefined') { + pubkey = window.localStorage.getItem('pubkey'); + } + // check if pubkey is not null and contains "npub" + // if so, then we need to convert it to a pubkey + if (pubkey && pubkey.includes("npub")) { + // convert npub to pubkey + pubkey = nip19.decode(pubkey.toString()).data.toString() + } + + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + + return ( + <> + +
+ +
+
+ + ); +} diff --git a/lumina/app/reel/page.tsx b/lumina/app/reel/page.tsx new file mode 100644 index 0000000..313a8cd --- /dev/null +++ b/lumina/app/reel/page.tsx @@ -0,0 +1,31 @@ +"use client"; + +import ReelFeed from "@/components/ReelFeed" +import { NostrProvider } from "nostr-react"; + +export default function ReelPage() { + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + + return ( + //
+ //
+ // + // + // + // + // + // Home + // + // + // + // + // + +
+ +
+
+ ); +} diff --git a/lumina/app/search/[searchTag]/page.tsx b/lumina/app/search/[searchTag]/page.tsx new file mode 100644 index 0000000..6d420b2 --- /dev/null +++ b/lumina/app/search/[searchTag]/page.tsx @@ -0,0 +1,57 @@ +'use client'; + +import Head from "next/head"; +import ProfileInfoCard from "@/components/ProfileInfoCard"; +import ProfileFeed from "@/components/ProfileFeed"; +import { useParams } from 'next/navigation' +import { nip19 } from "nostr-tools"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { SectionIcon, GridIcon } from '@radix-ui/react-icons' +import TagFeed from "@/components/TagFeed"; +import { NostrProvider } from "nostr-react"; +import FollowerFeed from "@/components/FollowerFeed"; +import ProfileQuickViewFeed from "@/components/ProfileQuickViewFeed"; +import FollowerQuickViewFeed from "@/components/FollowerQuickViewFeed"; +import SearchProfilesBox from "@/components/searchComponents/SearchProfilesBox"; +import SearchNotesBox from "@/components/searchComponents/SearchNotesBox"; + +export default function SearchPage() { + + let pubkey = null; + if (typeof window !== 'undefined') { + pubkey = window.localStorage.getItem('pubkey'); + } + + const params = useParams() + let searchTag = params.searchTag + + // check if pubkey contains "npub" + // if so, then we need to convert it to a pubkey + // if (pubkey.includes("npub")) { + // // convert npub to pubkey + // pubkey = nip19.decode(pubkey.toString()).data.toString() + // } + + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + + return ( + <> + + + LUMINA.rocks + + + + +
+
+ + +
+
+
+ + ); +} diff --git a/lumina/app/search/page.tsx b/lumina/app/search/page.tsx new file mode 100644 index 0000000..aad638c --- /dev/null +++ b/lumina/app/search/page.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { Search } from "@/components/Search"; +import { TrendingAccounts } from "@/components/TrendingAccounts"; +import { TrendingImages } from "@/components/TrendingImages"; +import { NostrProvider } from "nostr-react"; + + +export default function SearchMainPage() { + + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + + return ( + <> + +
+ +
+
+ + ); +} diff --git a/lumina/app/tag/[tag]/page.tsx b/lumina/app/tag/[tag]/page.tsx index 6a5512e..c163188 100644 --- a/lumina/app/tag/[tag]/page.tsx +++ b/lumina/app/tag/[tag]/page.tsx @@ -1,7 +1,6 @@ 'use client'; import Head from "next/head"; -import { NostrProvider } from "nostr-react"; import ProfileInfoCard from "@/components/ProfileInfoCard"; import ProfileFeed from "@/components/ProfileFeed"; import { useParams } from 'next/navigation' @@ -9,11 +8,7 @@ import { nip19 } from "nostr-tools"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { SectionIcon, GridIcon } from '@radix-ui/react-icons' import TagFeed from "@/components/TagFeed"; - -const relayUrls = [ - "wss://relay.damus.io", - "wss://relay.nostr.band", -]; +import { NostrProvider } from "nostr-react"; export default function Home() { @@ -26,9 +21,13 @@ export default function Home() { // pubkey = nip19.decode(pubkey.toString()).data.toString() // } + const relayUrls = [ + "wss://relay.lumina.rocks", + ]; + return ( <> - + LUMINA.rocks - {tag} diff --git a/lumina/bun.lockb b/lumina/bun.lockb index 03a51a8..a8ca6e2 100755 Binary files a/lumina/bun.lockb and b/lumina/bun.lockb differ diff --git a/lumina/components/BottomBar.tsx b/lumina/components/BottomBar.tsx new file mode 100644 index 0000000..b5654d1 --- /dev/null +++ b/lumina/components/BottomBar.tsx @@ -0,0 +1,55 @@ +"use client"; + +/** + * v0 by Vercel. + * @see https://v0.dev/t/mwaJmHMv0vd + * Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app + */ +import { BellIcon, GlobeIcon, HomeIcon, RowsIcon } from "@radix-ui/react-icons" +import Link from "next/link" +import { JSX, SVGProps, useEffect, useState } from "react" +import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar" +import { useRouter, usePathname } from 'next/navigation' +import { SearchIcon } from "lucide-react"; + +export default function BottomBar() { + + const router = useRouter(); + const [pubkey, setPubkey] = useState(null); + + useEffect(() => { + return setPubkey(window.localStorage.getItem('pubkey') ?? null); + }, []); + + const pathname = usePathname(); + const isActive = (path: string, currentPath: string) => currentPath === path ? 'text-purple-500' : ''; + + return ( + + ) +} \ No newline at end of file diff --git a/lumina/components/CommentCard.tsx b/lumina/components/CommentCard.tsx new file mode 100644 index 0000000..86c766c --- /dev/null +++ b/lumina/components/CommentCard.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { useProfile } from "nostr-react"; +import { + nip19, +} from "nostr-tools"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, +} from "@/components/ui/carousel" +import ReactionButton from '@/components/ReactionButton'; +import { Avatar, AvatarImage } from '@/components/ui/avatar'; +import ViewRawButton from '@/components/ViewRawButton'; +import ViewNoteButton from './ViewNoteButton'; +import Link from 'next/link'; + +interface CommentCardProps { + pubkey: string; + text: string; + eventId: string; + tags: string[][]; + event: any; +} + +const NoteCard: React.FC = ({ pubkey, text, eventId, tags, event }) => { + const { data: userData } = useProfile({ + pubkey, + }); + + const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || nip19.npubEncode(pubkey); + const imageSrc = text.match(/https?:\/\/[^ ]*\.(png|jpg|gif)/g); + const textWithoutImage = text.replace(/https?:\/\/.*\.(?:png|jpg|gif)/g, ''); + const createdAt = new Date(event.created_at * 1000); + const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; + const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; + + return ( + <> + + + + + + + +
+ + + + {title} +
+
+ +

{title}

+
+
+
+ +
+
+ +
+ { +
+ {imageSrc && imageSrc.length > 1 ? ( + + + {imageSrc.map((src, index) => ( + + + + ))} + + + + + ) : ( + imageSrc ? : "" + )} +
+ } +
+
+ {textWithoutImage} +
+
+
+
+
+ +
+ +
+
+ + {createdAt.toLocaleString()} + +
+ + ); +} + +export default NoteCard; \ No newline at end of file diff --git a/lumina/components/CommentsCompontent.tsx b/lumina/components/CommentsCompontent.tsx new file mode 100644 index 0000000..8f913a2 --- /dev/null +++ b/lumina/components/CommentsCompontent.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { useNostrEvents } from "nostr-react"; +import { + nip19, +} from "nostr-tools"; +import CommentCard from '@/components/CommentCard'; + +interface CommentsCompontentProps { + pubkey: string; + event: any; +} + +const CommentsCompontent: React.FC = ({ pubkey, event }) => { + + const { events } = useNostrEvents({ + filter: { + kinds: [1], + '#e': [event.id], + }, + }); + + return ( + <> +

Comments

+ {events.map((event) => ( +
+ +
+ ))} + + ); +} + +export default CommentsCompontent; \ No newline at end of file diff --git a/lumina/components/FollowButton.tsx b/lumina/components/FollowButton.tsx new file mode 100644 index 0000000..a70001c --- /dev/null +++ b/lumina/components/FollowButton.tsx @@ -0,0 +1,97 @@ + +import React, { useEffect, useState } from 'react'; +import { Button } from './ui/button'; +import { useNostr, useNostrEvents } from 'nostr-react'; +import { finalizeEvent } from 'nostr-tools'; +import { sign } from 'crypto'; +import { SignalMedium } from 'lucide-react'; + +interface FollowButtonProps { + pubkey: string; + userPubkey: string; +} + +const FollowButton: React.FC = ({ pubkey, userPubkey }) => { + // const { publish } = useNostr(); + const [isFollowing, setIsFollowing] = useState(false); + + let storedPubkey: string | null = null; + let storedNsec: string | null = null; + let isLoggedIn = false; + if (typeof window !== 'undefined') { + storedPubkey = window.localStorage.getItem('pubkey'); + storedNsec = window.localStorage.getItem('nsec'); + isLoggedIn = storedPubkey !== null; + } + + const { events } = useNostrEvents({ + filter: { + kinds: [3], + authors: [userPubkey], + limit: 1, + }, + }); + + let followingPubkeys = events.flatMap((event) => event.tags.map(tag => tag[1])); + // filter out all null or undefined + followingPubkeys = followingPubkeys.filter((tag) => tag); + + + useEffect(() => { + if (followingPubkeys.includes(pubkey)) { + setIsFollowing(true); + } + }, [followingPubkeys, isFollowing, setIsFollowing]); + + const handleFollow = async () => { + // if (isLoggedIn) { + + // let eventTemplate = { + // kind: 3, + // created_at: Math.floor(Date.now() / 1000), + // tags: [followingPubkeys], + // content: '', + // } + + // console.log(eventTemplate); + + // if (isFollowing) { + // eventTemplate.tags = eventTemplate.tags.filter(tag => tag[1] !== pubkey); + // } else { + // eventTemplate.tags[0].push(pubkey); + // } + + // console.log(eventTemplate); + + // let signedEvent = null; + // if (storedNsec != null) { + // // TODO: Sign Nostr Event with nsec + // const nsecArray = storedNsec ? new TextEncoder().encode(storedNsec) : new Uint8Array(); + // signedEvent = finalizeEvent(eventTemplate, nsecArray); + // console.log(signedEvent); + // } else if (storedPubkey != null) { + // // TODO: Request Extension to sign Nostr Event + // console.log('Requesting Extension to sign Nostr Event..'); + // try { + // signedEvent = await window.nostr.signEvent(eventTemplate); + // } catch (error) { + // console.error('Nostr Extension not found or aborted.'); + // } + // } + + // if (signedEvent !== null) { + // console.log(signedEvent); + // publish(signedEvent); + // setIsFollowing(!isFollowing); + // } + // } + }; + + return ( + + ); +}; + +export default FollowButton; \ No newline at end of file diff --git a/lumina/components/FollowerFeed.tsx b/lumina/components/FollowerFeed.tsx new file mode 100644 index 0000000..4a36966 --- /dev/null +++ b/lumina/components/FollowerFeed.tsx @@ -0,0 +1,54 @@ +import { useRef } from "react"; +import { useNostrEvents, dateToUnix } from "nostr-react"; +import NoteCard from './NoteCard'; + +interface FollowerFeedProps { + pubkey: string; +} + +const FollowerFeed: React.FC = ({ pubkey }) => { + const now = useRef(new Date()); // Make sure current time isn't re-rendered + + const { events: following, isLoading: followingLoading } = useNostrEvents({ + filter: { + kinds: [3], + authors: [pubkey], + limit: 1, + }, + }); + // let followingPubkeys = following.map((event) => event.tags[event.tags.length - 1][1]); + // let followingPubkeys = following.flatMap((event) => event.tags.map(tag => tag[1])).slice(0, 50); + let followingPubkeys = following.flatMap((event) => event.tags.map(tag => tag[1])).slice(0, 500); + + const { events } = useNostrEvents({ + filter: { + // since: dateToUnix(now.current), // all new events from now + // since: 0, + limit: 1000, + kinds: [1], + authors: followingPubkeys, + }, + }); + + // const filteredEvents = events.filter((event) => event.content.includes(".jpg")); + // filter events with regex that checks for png, jpg, or gif + let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif|mp4|webm)/g)?.[0]); + + // now filter all events with a tag[0] == t and tag[1] == nsfw + filteredEvents = filteredEvents.filter((event) => event.tags.map((tag) => tag[0] == "t" && tag[1] == "nsfw")); + // filter out all replies + filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' })); + + return ( + <> + {filteredEvents.map((event) => ( + //

{event.pubkey} posted: {event.content}

+
+ +
+ ))} + + ); +} + +export default FollowerFeed; \ No newline at end of file diff --git a/lumina/components/FollowerQuickViewFeed.tsx b/lumina/components/FollowerQuickViewFeed.tsx new file mode 100644 index 0000000..8f4c002 --- /dev/null +++ b/lumina/components/FollowerQuickViewFeed.tsx @@ -0,0 +1,73 @@ +import { useRef } from "react"; +import { useNostrEvents, dateToUnix } from "nostr-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import QuickViewNoteCard from "./QuickViewNoteCard"; + +interface FollowerQuickViewFeedProps { + pubkey: string; +} + +const FollowerQuickViewFeed: React.FC = ({ pubkey }) => { + const now = useRef(new Date()); // Make sure current time isn't re-rendered + + const { events: following, isLoading: followingLoading } = useNostrEvents({ + filter: { + kinds: [3], + authors: [pubkey], + limit: 1, + }, + }); + // let followingPubkeys = following.map((event) => event.tags[event.tags.length - 1][1]); + // let followingPubkeys = following.flatMap((event) => event.tags.map(tag => tag[1])).slice(0, 50); + let followingPubkeys = following.flatMap((event) => event.tags.map(tag => tag[1])).slice(0, 500); + + const { events } = useNostrEvents({ + filter: { + // since: dateToUnix(now.current), // all new events from now + // since: 0, + limit: 1000, + kinds: [1], + authors: followingPubkeys, + }, + }); + + let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif)/g)?.[0]); + // filter out all replies (tag[0] == e) + filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' })); + + return ( + <> +
+ {filteredEvents.length === 0 ? ( + <> +
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ + ) : (filteredEvents.map((event) => ( + + )))} +
+ + ); +} + +export default FollowerQuickViewFeed; \ No newline at end of file diff --git a/lumina/components/GlobalFeed.tsx b/lumina/components/GlobalFeed.tsx index c4b376a..e09a0fa 100644 --- a/lumina/components/GlobalFeed.tsx +++ b/lumina/components/GlobalFeed.tsx @@ -30,7 +30,7 @@ const GlobalFeed: React.FC = () => { {filteredEvents.map((event) => ( //

{event.pubkey} posted: {event.content}

- +
))} diff --git a/lumina/components/LoginForm.tsx b/lumina/components/LoginForm.tsx new file mode 100644 index 0000000..9033685 --- /dev/null +++ b/lumina/components/LoginForm.tsx @@ -0,0 +1,189 @@ +declare global { + interface Window { + nostr: any; + } +} + +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion" +import { useEffect, useRef } from "react" +import { getPublicKey, generateSecretKey, nip19 } from 'nostr-tools' +import { InfoIcon } from "lucide-react"; +import Link from "next/link"; +import { bytesToHex, hexToBytes } from '@noble/hashes/utils' + +export function LoginForm() { + + let publicKey = useRef(null); + let nsecInput = useRef(null); + let npubInput = useRef(null); + + useEffect(() => { + // handle Amber Login Response + const urlParams = new URLSearchParams(window.location.search); + const amberResponse = urlParams.get('amberResponse'); + if (amberResponse !== null) { + localStorage.setItem("pubkey", nip19.decode(amberResponse).data.toString()); + localStorage.setItem("loginType", "amber"); + window.location.href = `/profile/${amberResponse}`; + } + }, []); + + + const handleAmber = async () => { + const hostname = window.location.host; + console.log(hostname); + if (!hostname) { + throw new Error("Hostname is null or undefined"); + } + const intent = `intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;S.callbackUrl=http://${hostname}/login?amberResponse=;end`; + window.location.href = intent; + } + + const handleExtensionLogin = async () => { + // eslint-disable-next-line + if (window.nostr !== undefined) { + publicKey.current = await window.nostr.getPublicKey() + console.log("Logged in with pubkey: ", publicKey.current); + if (publicKey.current !== null) { + localStorage.setItem("pubkey", publicKey.current); + localStorage.setItem("loginType", "extension"); + // window.location.reload(); + window.location.href = `/profile/${nip19.npubEncode(publicKey.current)}`; + } + } + }; + + // const handleNsecSignUp = async () => { + // let nsec = generateSecretKey(); + // console.log('nsec: ' + nsec); + + // let nsecHex = bytesToHex(nsec); + // console.log('bytesToHex nsec: ' + nsecHex); + + // let pubkey = getPublicKey(nsec); + // console.log('pubkey: ' + pubkey); + + // localStorage.setItem("nsec", nsecHex); + // localStorage.setItem("pubkey", pubkey); + // localStorage.setItem("loginType", "raw_nsec") + // window.location.href = `/profile/${nip19.npubEncode(pubkey)}`; + // }; + + const handleNsecLogin = async () => { + if (nsecInput.current !== null) { + try { + let input = nsecInput.current.value; + if(input.includes("nsec")) { + input = bytesToHex(nip19.decode(input).data as Uint8Array); + console.log('decoded nsec: ' + input); + } + let nsecBytes = hexToBytes(input); + let nsecHex = bytesToHex(nsecBytes); + let pubkey = getPublicKey(nsecBytes); + + localStorage.setItem("nsec", nsecHex); + localStorage.setItem("pubkey", pubkey); + localStorage.setItem("loginType", "raw_nsec") + + window.location.href = `/profile/${nip19.npubEncode(pubkey)}`; + } catch (e) { + console.error(e); + } + } + }; + + const handleNpubLogin = async () => { + if (npubInput.current !== null) { + try { + let input = npubInput.current.value; + let npub = null; + let pubkey = null; + if(input.startsWith("npub1")) { + npub = input; + pubkey = nip19.decode(input).data.toString(); + } else { + pubkey = input; + npub = nip19.npubEncode(input); + } + + localStorage.setItem("pubkey", pubkey); + localStorage.setItem("loginType", "readOnly_npub") + + window.location.href = `/profile/${npub}`; + } catch (e) { + console.error(e); + } + } + }; + + + return ( + + + Login to Lumina + + Login to your account either with a nostr extension or with your nsec. + + + +
+ + + + +
+
+ + + + +
+
+ or + + + Login with npub (read-only) + +
+ + + +
+
+
+
+ or + + + Login with nsec (not recommended) + +
+ + + +
+
+
+
+
+ + +
+ ) +} \ No newline at end of file diff --git a/lumina/components/Navigation.tsx b/lumina/components/Navigation.tsx deleted file mode 100644 index fc3f125..0000000 --- a/lumina/components/Navigation.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client" - -import * as React from "react" -import { MoonIcon, SunIcon } from "@radix-ui/react-icons" -import { DropdownThemeMode } from "@/components/DropdownThemeMode" - -export function Navigation() { - - return ( -
-
- -
-
- ) -} diff --git a/lumina/components/NoteCard.tsx b/lumina/components/NoteCard.tsx index 3671082..3ba7913 100644 --- a/lumina/components/NoteCard.tsx +++ b/lumina/components/NoteCard.tsx @@ -1,14 +1,11 @@ import React from 'react'; -// import Button from 'react-bootstrap/Button'; -import { Button } from '@/components/ui/button'; -import { useNostrEvents, useProfile } from "nostr-react"; +import { useProfile } from "nostr-react"; import { nip19, } from "nostr-tools"; import { Card, CardContent, - CardDescription, CardFooter, CardHeader, CardTitle, @@ -27,113 +24,135 @@ import { CarouselPrevious, } from "@/components/ui/carousel" import ReactionButton from '@/components/ReactionButton'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Avatar, AvatarImage } from '@/components/ui/avatar'; import ViewRawButton from '@/components/ViewRawButton'; -import Image from 'next/image'; +import ViewNoteButton from './ViewNoteButton'; +import Link from 'next/link'; +import ViewCopyButton from './ViewCopyButton'; +import { Event as NostrEvent } from "nostr-tools"; +import ZapButton from './ZapButton'; interface NoteCardProps { pubkey: string; text: string; eventId: string; tags: string[][]; - event: any; + event: NostrEvent; + showViewNoteCardButton: boolean; } -const NoteCard: React.FC = ({ pubkey, text, eventId, tags, event }) => { +const NoteCard: React.FC = ({ pubkey, text, eventId, tags, event, showViewNoteCardButton }) => { const { data: userData } = useProfile({ pubkey, }); const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || nip19.npubEncode(pubkey); - // const imageSrc = text.match(/https?:\/\/.*\.(?:png|jpg|gif)/g)?.[0]; - const imageSrc = text.match(/https?:\/\/.*\.(?:png|jpg|gif)/g)?.[0].split(' '); - const textWithoutImage = text.replace(/https?:\/\/.*\.(?:png|jpg|gif)/g, ''); - // const textWithoutImage = text.replace(/https?:\/\/.*\.(?:png|jpg|gif)(\?.*)?/g, ''); + // text = text.replaceAll('\n', '
'); + text = text.replaceAll('\n', ' '); + const imageSrc = text.match(/https?:\/\/[^ ]*\.(png|jpg|gif)/g); + const videoSrc = text.match(/https?:\/\/[^ ]*\.(mp4|webm|mov)/g); + const textWithoutImage = text.replace(/https?:\/\/.*\.(?:png|jpg|gif|mp4|webm|mov)/g, ''); const createdAt = new Date(event.created_at * 1000); const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; - // const profileImageSrc = userData?.picture || "https://via.placeholder.com/150"; const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; return ( <> - - - - - {/* - - */} - {/* {title} */} - - - - - - - - -

{title}

-
-
-
-
-
- {/* Card Description */} -
- -
- { - // imageSrc ? imageSrc.map((src, index) => ) : "" -
- {imageSrc && imageSrc.length > 1 ? ( - - - {imageSrc.map((src, index) => ( - - - {/* {textWithoutImage} */} - - ))} - - - - - ) : ( - imageSrc ? : "" - // imageSrc ? {textWithoutImage} : "" - )} + + + + + + + +
+ + + + {title} +
+
+ +

{title}

+
+
+
+ +
+
+ +
+ { +
+
+ {imageSrc && imageSrc.length > 1 ? ( + + + {imageSrc.map((src, index) => ( + + + + ))} + + + + + ) : ( + imageSrc ? : "" + )} +
+
+ {videoSrc && videoSrc.length > 1 ? ( + + + {videoSrc.map((src, index) => ( + + + ))} + + + + + ) : ( + videoSrc ?
+
+ } +
+
+ {textWithoutImage} +
- } -
- {textWithoutImage} -
-
-
- - -
- - - {createdAt.toLocaleString()} - - +
+
+
+ + + {showViewNoteCardButton && } +
+
+ + +
+
+ + + {createdAt.toLocaleString()} + + ); } diff --git a/lumina/components/NotePageComponent.tsx b/lumina/components/NotePageComponent.tsx new file mode 100644 index 0000000..7cfa1d6 --- /dev/null +++ b/lumina/components/NotePageComponent.tsx @@ -0,0 +1,41 @@ +import { useRef } from "react"; +import { useNostrEvents } from "nostr-react"; +import NoteCard from '@/components/NoteCard'; +import CommentsCompontent from "@/components/CommentsCompontent"; + +interface NotePageComponentProps { + id: string; +} + +const NotePageComponent: React.FC = ({ id }) => { + const now = useRef(new Date()); // Make sure current time isn't re-rendered + + const { events } = useNostrEvents({ + filter: { + // since: dateToUnix(now.current), // all new events from now + // since: 0, + ids: [id], + limit: 1, + kinds: [1], + }, + }); + + // filter out all events that also have another e tag with another id + const filteredEvents = events.filter((event) => { return event.tags.filter((tag) => { return tag[0] === '#e' && tag[1] !== id }).length === 0 }); + + return ( + <> + {events.map((event) => ( + //

{event.pubkey} posted: {event.content}

+
+ +
+ +
+
+ ))} + + ); +} + +export default NotePageComponent; \ No newline at end of file diff --git a/lumina/components/Notification.tsx b/lumina/components/Notification.tsx new file mode 100644 index 0000000..3351f41 --- /dev/null +++ b/lumina/components/Notification.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { useNostrEvents, useProfile } from "nostr-react"; +import { Card, CardHeader, CardTitle, CardContent, CardFooter, CardDescription } from '@/components/ui/card'; +import { + NostrEvent, + Event, + nip19, +} from "nostr-tools"; +import { Avatar, AvatarImage } from './ui/avatar'; +import Link from 'next/link'; + +interface NotificationProps { + event: NostrEvent; +} + +const Notification: React.FC = ({ event }) => { + let sender = event.pubkey; + let sats = 0; + let reactedToId = ''; + + const { data: userData, isLoading: userDataLoading } = useProfile({ + pubkey: sender, + }); + + if (!event) { + return null; + } + + if (event.kind === 9735) { + for (let tag of event.tags) { + if (tag[0] === 'P') { + sender = tag[1]; + } + if (tag[0] === 'bolt11') { + let bolt11decoded = require('light-bolt11-decoder').decode(tag[1]); + for (let field of bolt11decoded.sections) { + if (field.name === 'amount') { + sats = field.value / 1000; + } + } + } + } + } + + if (event.kind === 7) { + for (let tag of event.tags) { + if (tag[0] === 'e') { + reactedToId = tag[1]; + } + } + } + + let name = userData?.name ?? nip19.npubEncode(event.pubkey).slice(0, 8) + ':' + nip19.npubEncode(event.pubkey).slice(-3); + let createdAt = new Date(event.created_at * 1000); + + return ( + <> +
+ {/* ZAP */} + {event.kind === 9735 && ( +
+

{sats} sats ⚡️

+
+ + + +
+
+

{name} zapped you

+

{createdAt.toLocaleDateString() + ' ' + createdAt.toLocaleTimeString()}

+
+
+ )} + {/* FOLLOW */} + {event.kind === 3 && ( +
+

{event.content}

+
+ + + +
+
+

{name} started following you

+

{createdAt.toLocaleDateString() + ' ' + createdAt.toLocaleTimeString()}

+
+
+ )} + {/* REACTION */} + {event.kind === 7 && ( + +
+

{event.content}

+
+ + + +
+
+

{name} reacted to you

+

{createdAt.toLocaleDateString() + ' ' + createdAt.toLocaleTimeString()}

+
+
+ + )} +
+
+ + ); +} + +export default Notification; \ No newline at end of file diff --git a/lumina/components/Notifications.tsx b/lumina/components/Notifications.tsx new file mode 100644 index 0000000..4920166 --- /dev/null +++ b/lumina/components/Notifications.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { useNostrEvents, useProfile } from "nostr-react"; +import { Card, CardHeader, CardTitle, CardContent, CardFooter, CardDescription } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { AvatarImage } from '@radix-ui/react-avatar'; +import { Avatar } from '@/components/ui/avatar'; +import NIP05 from '@/components/nip05'; +import { + nip19, +} from "nostr-tools"; +import Notification from './Notification'; + +interface NotificationsProps { + pubkey: string; +} + +const Notifications: React.FC = ({ pubkey }) => { + const { data: userData, isLoading: userDataLoading } = useProfile({ + pubkey, + }); + + + // const { events: followers, isLoading: followersLoading } = useNostrEvents({ + // filter: { + // kinds: [3], + // '#p': [pubkey], + // limit: 50, + // }, + // }); + + const { events: zaps, isLoading: zapsLoading } = useNostrEvents({ + filter: { + kinds: [9735], + '#p': [pubkey], + limit: 20, + }, + }); + + const { events: reactions, isLoading: reactionsLoading } = useNostrEvents({ + filter: { + kinds: [7], + '#p': [pubkey], + limit: 20, + }, + }); + + // const { events: following, isLoading: followingLoading } = useNostrEvents({ + // filter: { + // kinds: [3], + // authors: [pubkey], + // limit: 1, + // }, + // }); + + // filter for only new followings (latest in a users followers list) + // const filteredFollowers = followers.filter(follower => { + // const lastPTag = follower.tags[follower.tags.length - 1]; + // if (lastPTag[0] === "p" && lastPTag[1] === pubkey.toString()) { + // // console.log(follower.tags[follower.tags.length - 1]); + // return true; + // } + // }); + + // let allNotifications = [...filteredFollowers, ...zaps].sort((a, b) => b.created_at - a.created_at); + let allNotifications = [...zaps, ...reactions].sort((a, b) => b.created_at - a.created_at); + + return ( + <> +
+ {/* */} + + + Notifications + + + {allNotifications.map((notification, index) => ( + + ))} + + +
+ + ); +} + +export default Notifications; \ No newline at end of file diff --git a/lumina/components/ProfileFeed.tsx b/lumina/components/ProfileFeed.tsx index b3cedcf..182e6f4 100644 --- a/lumina/components/ProfileFeed.tsx +++ b/lumina/components/ProfileFeed.tsx @@ -20,7 +20,7 @@ const ProfileFeed: React.FC = ({ pubkey }) => { }, }); - let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif)/g)?.[0]); + let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif|mp4|webm|mov)/g)?.[0]); // filter out all replies (tag[0] == e) filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' })); @@ -40,7 +40,7 @@ const ProfileFeed: React.FC = ({ pubkey }) => { //

{event.pubkey} posted: {event.content}

//
- +
)))} diff --git a/lumina/components/ProfileInfoCard.tsx b/lumina/components/ProfileInfoCard.tsx index 5ef2bef..3032fa5 100644 --- a/lumina/components/ProfileInfoCard.tsx +++ b/lumina/components/ProfileInfoCard.tsx @@ -1,35 +1,148 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { useProfile } from "nostr-react"; -import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card'; -import { Skeleton } from './ui/skeleton'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { AvatarImage } from '@radix-ui/react-avatar'; +import { Avatar } from '@/components/ui/avatar'; +import NIP05 from '@/components/nip05'; +import { nip19 } from "nostr-tools"; +import Link from 'next/link'; +import { Button } from './ui/button'; +import { ImStatsDots } from "react-icons/im"; +import FollowButton from './FollowButton'; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" +import { Input } from './ui/input'; +import { Share1Icon } from '@radix-ui/react-icons'; +import { toast } from './ui/use-toast'; interface ProfileInfoCardProps { pubkey: string; } -const ProfileInfoCard: React.FC = ({ pubkey }) => { - const { data: userData } = useProfile({ - pubkey, - }); +const ProfileInfoCard: React.FC = React.memo(({ pubkey }) => { - const title = userData?.username || userData?.display_name || userData?.name || userData?.npub; + let userPubkey = ''; + let host = ''; + if (typeof window !== 'undefined') { + userPubkey = window.localStorage.getItem('pubkey') ?? ''; + host = window.location.host; + } + + const { data: userData, isLoading } = useProfile({ pubkey }); + + const npubShortened = useMemo(() => { + let encoded = nip19.npubEncode(pubkey); + let parts = encoded.split('npub'); + return 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3); + }, [pubkey]); + + const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened; const description = userData?.about?.replace(/(?:\r\n|\r|\n)/g, '
'); + const nip05 = userData?.nip05; + + const handleCopyLink = async () => { + try { + await navigator.clipboard.writeText(host+"/profile/"+nip19.npubEncode(pubkey)); + toast({ + description: 'URL copied to clipboard', + title: 'Copied' + }); + } catch (err) { + toast({ + description: 'Error copying URL to clipboard', + title: 'Error', + variant: 'destructive' + }); + } + }; + + const handleCopyPublicKey = async () => { + try { + await navigator.clipboard.writeText(nip19.npubEncode(pubkey)); + toast({ + description: 'PublicKey copied to clipboard', + title: 'Copied' + }); + } catch (err) { + toast({ + description: 'Error copying PublicKey to clipboard', + title: 'Error', + variant: 'destructive' + }); + } + }; + return ( - <> -

{title}

-
- {description ? ( - -
-
- ) : ( -
- +
+ + + + +
+ + + + {title} +
+ +
+
+ +
+ {/* */} +
+ +
+ + + + + + + + + + Share this Profile + Share this Profile with others. + +
+ {/*

URL

*/} +
+ + +
+
+ + +
+
+ + + + + +
+
+
+
- )} -
- + + +
+ + +
); -} +}); + +ProfileInfoCard.displayName = 'ProfileInfoCard'; export default ProfileInfoCard; \ No newline at end of file diff --git a/lumina/components/ProfileQuickViewFeed.tsx b/lumina/components/ProfileQuickViewFeed.tsx new file mode 100644 index 0000000..678a4c6 --- /dev/null +++ b/lumina/components/ProfileQuickViewFeed.tsx @@ -0,0 +1,63 @@ +import { useRef, useState } from "react"; +import { useNostrEvents, dateToUnix } from "nostr-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import QuickViewNoteCard from "./QuickViewNoteCard"; +import { Button } from "@/components/ui/button"; + +interface ProfileQuickViewFeedProps { + pubkey: string; +} + +const ProfileQuickViewFeed: React.FC = ({ pubkey }) => { + const now = useRef(new Date()); // Make sure current time isn't re-rendered + const [limit, setLimit] = useState(100); + + const { isLoading ,events } = useNostrEvents({ + filter: { + authors: [pubkey], + limit: limit, + kinds: [1], + }, + }); + + let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif|mp4|webm|mov)/g)?.[0]); + // filter out all replies (tag[0] == e) + filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' })); + + const loadMore = () => { + setLimit(limit => limit + 50); + } + + return ( + <> +
+ {filteredEvents.length === 0 && isLoading ? ( + <> +
+ +
+
+ +
+
+ +
+ + ) : ( + <> + {filteredEvents.map((event) => ( + + ))} + + )} +
+ {!isLoading ? ( +
+ +
+ ) : null} + + ); +} + +export default ProfileQuickViewFeed; \ No newline at end of file diff --git a/lumina/components/ProfileTextFeed.tsx b/lumina/components/ProfileTextFeed.tsx new file mode 100644 index 0000000..56f6b71 --- /dev/null +++ b/lumina/components/ProfileTextFeed.tsx @@ -0,0 +1,51 @@ +import { useRef } from "react"; +import { useNostrEvents, dateToUnix } from "nostr-react"; +import NoteCard from '@/components/NoteCard'; +import { Skeleton } from "@/components/ui/skeleton"; + +interface ProfileTextFeedProps { + pubkey: string; +} + +const ProfileTextFeed: React.FC = ({ pubkey }) => { + const now = useRef(new Date()); // Make sure current time isn't re-rendered + + const { events, isLoading } = useNostrEvents({ + filter: { + // since: dateToUnix(now.current), // all new events from now + authors: [pubkey], + // since: 0, + // limit: 10, + kinds: [1], + }, + }); + + // filter out all images since we only want text messages + let filteredEvents = events.filter((event) => !event.content.match(/https?:\/\/.*\.(?:png|jpg|gif)/g)?.[0]); + // filter out all replies (tag[0] == e) + filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' })); + + return ( + <> + {/*

Profile Feed

*/} + + {filteredEvents.length === 0 && isLoading ? ( +
+ +
+ + +
+
+ ) : (filteredEvents.map((event) => ( + //

{event.pubkey} posted: {event.content}

+ // +
+ +
+ )))} + + ); +} + +export default ProfileTextFeed; \ No newline at end of file diff --git a/lumina/components/QuickViewNoteCard.tsx b/lumina/components/QuickViewNoteCard.tsx new file mode 100644 index 0000000..33e15db --- /dev/null +++ b/lumina/components/QuickViewNoteCard.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { useProfile } from "nostr-react"; +import { + nip19, +} from "nostr-tools"; +import { + Card, + SmallCardContent, +} from "@/components/ui/card" +import Image from 'next/image'; +import Link from 'next/link'; +import { PlayIcon, StackIcon, VideoIcon } from '@radix-ui/react-icons'; + +interface NoteCardProps { + pubkey: string; + text: string; + eventId: string; + tags: string[][]; + event: any; + linkToNote: boolean; +} + +const QuickViewNoteCard: React.FC = ({ pubkey, text, eventId, tags, event, linkToNote }) => { + const { data: userData } = useProfile({ + pubkey, + }); + + const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || nip19.npubEncode(pubkey); + text = text.replaceAll('\n', ' '); + const imageSrc = text.match(/https?:\/\/[^ ]*\.(png|jpg|gif)/g); + const videoSrc = text.match(/https?:\/\/[^ ]*\.(mp4|webm|mov)/g); + const textWithoutImage = text.replace(/https?:\/\/.*\.(?:png|jpg|gif|mp4|webm|mov)/g, ''); + const createdAt = new Date(event.created_at * 1000); + const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; + const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; + const encodedNoteId = nip19.noteEncode(event.id) + + const card = ( + + +
+
+ {imageSrc && imageSrc.length > 1 && !videoSrc ? ( +
+
+ +
+ {text} +
+ ) : imageSrc && imageSrc.length > 0 ? ( +
+ {videoSrc && videoSrc.length > 0 && +
+ +
+ } + {text} +
+ ) : videoSrc && videoSrc.length > 0 ? ( +
+
+ +
+
+ ) : null} +
+
+
+
+ ); + + return ( + <> + {linkToNote ? ( + + {card} + + ) : ( + card + )} + + ); +} + +export default QuickViewNoteCard; \ No newline at end of file diff --git a/lumina/components/ReactionButton.tsx b/lumina/components/ReactionButton.tsx index e1b79fd..16565e8 100644 --- a/lumina/components/ReactionButton.tsx +++ b/lumina/components/ReactionButton.tsx @@ -4,12 +4,24 @@ import { type Event as NostrEvent, getEventHash, getPublicKey, - getSignature, + finalizeEvent, } from "nostr-tools"; import { Button } from "@/components/ui/button"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" +import { ReloadIcon } from "@radix-ui/react-icons"; +import ReactionButtonReactionList from "./ReactionButtonReactionList"; -export default function ReactionButton(event: any) { - const { events } = useNostrEvents({ +export default function ReactionButton({ event }: { event: any }) { + const { events, isLoading } = useNostrEvents({ filter: { // since: dateToUnix(now.current), // all new events from now // since: 0, @@ -19,40 +31,73 @@ export default function ReactionButton(event: any) { }, }); - const { publish } = useNostr(); + // filter out all events that also have another e tag with another id + // this will filter out likes that are made on comments and not on the note itself + const filteredEvents = events.filter((event) => { return event.tags.filter((tag) => { return tag[0] === '#e' && tag[1] !== event.id }).length === 0 }); - const onPost = async () => { - const privKey = prompt("Paste your private key:"); + // const { publish } = useNostr(); - if (!privKey) { - alert("no private key provided"); - return; - } + // const onPost = async () => { + // const privKey = prompt("Paste your private key:"); - const message = prompt("Enter the message you want to send:"); + // if (!privKey) { + // alert("no private key provided"); + // return; + // } - if (!message) { - alert("no message provided"); - return; - } + // const message = prompt("Enter the message you want to send:"); - const event: NostrEvent = { - content: message, - kind: 1, - tags: [], - created_at: dateToUnix(), - pubkey: getPublicKey(privKey), - id: "", - sig: "" - }; + // if (!message) { + // alert("no message provided"); + // return; + // } - event.id = getEventHash(event); - event.sig = getSignature(event, privKey); + // const event: NostrEvent = { + // content: message, + // kind: 1, + // tags: [], + // created_at: dateToUnix(), + // pubkey: getPublicKey(privKey), + // id: "", + // sig: "" + // }; - publish(event); - }; + // event.id = getEventHash(event); + // event.sig = getSignature(event, privKey); + + // publish(event); + // }; return ( - + + + {/* */} + {isLoading ? ( + + ) : ( + + )} + + + + Reactions + + {/* TODO: Create Reaction Event on Click */} +
+ + + +
+
+ + + + + + +
+
+ + // ); } \ No newline at end of file diff --git a/lumina/components/ReactionButtonReactionList.tsx b/lumina/components/ReactionButtonReactionList.tsx new file mode 100644 index 0000000..13d0a7f --- /dev/null +++ b/lumina/components/ReactionButtonReactionList.tsx @@ -0,0 +1,12 @@ +import { ScrollArea } from "@/components/ui/scroll-area" +import ReactionButtonReactionListItem from "./ReactionButtonReactionListItem"; + +export default function ReactionButtonReactionList({ filteredEvents }: { filteredEvents: any }) { + return ( + + {filteredEvents.map((event: any) => ( + + ))} + + ); +} \ No newline at end of file diff --git a/lumina/components/ReactionButtonReactionListItem.tsx b/lumina/components/ReactionButtonReactionListItem.tsx new file mode 100644 index 0000000..6dff4cc --- /dev/null +++ b/lumina/components/ReactionButtonReactionListItem.tsx @@ -0,0 +1,43 @@ +import Link from "next/link"; +import { useNostr, dateToUnix, useNostrEvents, useProfile } from "nostr-react"; + +import { + type Event as NostrEvent, + getEventHash, + getPublicKey, + finalizeEvent, + nip19, +} from "nostr-tools"; +import { Avatar, AvatarImage } from "@/components/ui/avatar"; + +export default function ReactionButtonReactionListItem({ event }: { event: NostrEvent }) { + + let pubkey = event.pubkey; + + const { data: userData } = useProfile({ + pubkey, + }); + + const title = userData?.username || userData?.display_name || userData?.name || nip19.npubEncode(pubkey).slice(0, 8) + ':' + nip19.npubEncode(pubkey).slice(-3);; + const createdAt = new Date(event.created_at * 1000); + const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; + const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; + const content = event.content; + + console.log("event", event.content); + + return ( + +
+
+ {/* */} + + + + {title} + {content} +
+
+ + ); +} \ No newline at end of file diff --git a/lumina/components/ReelFeed.tsx b/lumina/components/ReelFeed.tsx new file mode 100644 index 0000000..7cf8f9f --- /dev/null +++ b/lumina/components/ReelFeed.tsx @@ -0,0 +1,40 @@ +import { useRef } from "react"; +import { useNostrEvents, dateToUnix } from "nostr-react"; +import NoteCard from './NoteCard'; + +const ReelFeed: React.FC = () => { + const now = useRef(new Date()); // Make sure current time isn't re-rendered + + const { events } = useNostrEvents({ + filter: { + // since: dateToUnix(now.current), // all new events from now + // since: 0, + // limit: 100, + kinds: [1063], + }, + }); + + // const filteredEvents = events.filter((event) => event.content.includes(".jpg")); + // filter events with regex that checks for png, jpg, or gif + let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif)/g)?.[0]); + + // now filter all events with a tag[0] == t and tag[1] == nsfw + // filteredEvents = filteredEvents.filter((event) => event.tags.map((tag) => tag[0] == "t" && tag[1] == "nsfw")); + filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 't' && tag[1] == 'nsfw'})); + // filter out all replies + filteredEvents = filteredEvents.filter((event) => !event.tags.some((tag) => { return tag[0] == 'e' })); + + return ( + <> +

Reel Feed

+ {filteredEvents.map((event) => ( + //

{event.pubkey} posted: {event.content}

+
+ +
+ ))} + + ); +} + +export default ReelFeed; \ No newline at end of file diff --git a/lumina/components/Search.tsx b/lumina/components/Search.tsx new file mode 100644 index 0000000..82a2c9e --- /dev/null +++ b/lumina/components/Search.tsx @@ -0,0 +1,62 @@ +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { queryProfile } from "nostr-tools/nip05" +import { nip19 } from "nostr-tools" +import { useState } from 'react'; +import { ReloadIcon } from "@radix-ui/react-icons"; +// import { useRouter } from 'next/router'; +import { useRouter } from 'next/navigation'; + +export function Search() { + const router = useRouter(); + + const [inputValue, setInputValue] = useState(''); + const [isLoading, setIsLoading] = useState(false); // Neuer Zustand für das Laden + + const calculateAndRedirect = async () => { + setIsLoading(true); + + if (inputValue.startsWith('npub')) { // npub Search + // window.location.href = `/profile/${inputValue}`; + router.push(`/profile/${inputValue}`); + } else if (inputValue.startsWith('#')) { // Hashtag Search + // window.location.href = `/tag/${inputValue.replaceAll('#', '')}`; + router.push(`/tag/${inputValue.replaceAll('#', '')}`); + } else if(inputValue.includes('@')) { // NIP-05 Search + // if inputValue starts with @, then add a "_" at the beginning + if(inputValue.startsWith('@')) { + setInputValue('_' + inputValue); + } + + let profile = await queryProfile(inputValue); + if(profile?.pubkey !== undefined) { // Only redirect if profile is found + router.push(`/profile/${nip19.npubEncode(profile?.pubkey)}`); + } + } else { + router.push(`/search/${inputValue}`); + } + setIsLoading(false); + } + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + calculateAndRedirect(); + } + } + + return ( +
+ setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + /> + {/* */} + +
+ ) +} \ No newline at end of file diff --git a/lumina/components/TagFeed.tsx b/lumina/components/TagFeed.tsx index ddcf488..2d38764 100644 --- a/lumina/components/TagFeed.tsx +++ b/lumina/components/TagFeed.tsx @@ -21,7 +21,7 @@ const TagFeed: React.FC = ({tag}) => { // const filteredEvents = events.filter((event) => event.content.includes(".jpg")); // filter events with regex that checks for png, jpg, or gif - let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif)/g)?.[0]); + let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif|mp4|webm)/g)?.[0]); // now filter all events with a tag[0] == t and tag[1] == nsfw filteredEvents = filteredEvents.filter((event) => event.tags.map((tag) => tag[0] == "t" && tag[1] == "nsfw")); @@ -34,7 +34,7 @@ const TagFeed: React.FC = ({tag}) => { {filteredEvents.map((event) => ( //

{event.pubkey} posted: {event.content}

- +
))} diff --git a/lumina/components/TrendingAccount.tsx b/lumina/components/TrendingAccount.tsx new file mode 100644 index 0000000..9d844a8 --- /dev/null +++ b/lumina/components/TrendingAccount.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { useProfile } from "nostr-react"; +import { + nip19, +} from "nostr-tools"; +import { + Card, + CardHeader, + CardTitle, + SmallCardContent, +} from "@/components/ui/card" +import { Avatar, AvatarImage } from '@/components/ui/avatar'; +import Link from 'next/link'; + +interface TrendingAccountProps { + pubkey: string; +} + +const TrendingAccount: React.FC = ({ pubkey }) => { + const { data: userData } = useProfile({ + pubkey, + }); + + const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || nip19.npubEncode(pubkey); + const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; + const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; + + return ( + <> + + + + +
+ + + + {/* {title.substring(0, 12)} */} + {title} +
+ +
+
+ +
+
+
+
+
+
+ + ); +} + +export default TrendingAccount; \ No newline at end of file diff --git a/lumina/components/TrendingAccounts.tsx b/lumina/components/TrendingAccounts.tsx index d64ad5d..bcc48a2 100644 --- a/lumina/components/TrendingAccounts.tsx +++ b/lumina/components/TrendingAccounts.tsx @@ -1,39 +1,26 @@ -import { useRef } from "react"; -import { useNostrEvents, dateToUnix } from "nostr-react"; -import NoteCard from './NoteCard'; +import React, { useState, useEffect } from 'react'; +import TrendingAccount from '@/components/TrendingAccount'; -const TrendingAccounts: React.FC = () => { - const now = useRef(new Date()); // Make sure current time isn't re-rendered +export function TrendingAccounts() { + const [profiles, setProfiles] = useState([]); - const { events } = useNostrEvents({ - filter: { - // since: dateToUnix(now.current), // all new events from now - // since: 0, - limit: 100, - kinds: [1], - }, - }); + useEffect(() => { + fetch('https://api.nostr.band/v0/trending/profiles') + .then(res => res.json()) + .then(data => setProfiles(data.profiles)) + .catch(error => { + console.error('Error calling trending profiles:', error); + }); + }, []); - // const filteredEvents = events.filter((event) => event.content.includes(".jpg")); - // filter events with regex that checks for png, jpg, or gif - let filteredEvents = events.filter((event) => event.content.match(/https?:\/\/.*\.(?:png|jpg|gif)/g)?.[0]); - - // now filter all events with a tag[0] == t and tag[1] == nsfw - filteredEvents = filteredEvents.filter((event) => event.tags.map((tag) => tag[0] == "t" && tag[1] == "nsfw")); - // filter out all replies - filteredEvents = filteredEvents.filter((event) => event.tags.map((tag) => tag[0] == "e")); - - return ( - <> -

Global Feed

- {filteredEvents.map((event) => ( - //

{event.pubkey} posted: {event.content}

-
- + return ( +
+

Trending Accounts

+
+ {profiles && profiles.length > 0 && profiles.slice(0,4).map((profile, index) => ( + + ))} +
- ))} - - ); -} - -export default TrendingAccounts; \ No newline at end of file + ); +} \ No newline at end of file diff --git a/lumina/components/TrendingImage.tsx b/lumina/components/TrendingImage.tsx new file mode 100644 index 0000000..ecea5cc --- /dev/null +++ b/lumina/components/TrendingImage.tsx @@ -0,0 +1,84 @@ +import React, { useMemo } from 'react'; +import { useNostr, useNostrEvents, useProfile } from "nostr-react"; +import { + nip19, +} from "nostr-tools"; +import { + Card, + CardHeader, + CardTitle, + SmallCardContent, +} from "@/components/ui/card" +import Image from 'next/image'; +import Link from 'next/link'; +import { Avatar } from './ui/avatar'; +import { AvatarImage } from '@radix-ui/react-avatar'; + +interface TrendingImageProps { + eventId: string; + pubkey: string; +} + +const TrendingImage: React.FC = ({ eventId, pubkey }) => { + const { data: userData } = useProfile({ + pubkey, + }); + + const { events } = useNostrEvents({ + filter: { + kinds: [1], + ids: [eventId] + }, + }); + + const npubShortened = useMemo(() => { + let encoded = nip19.npubEncode(pubkey); + let parts = encoded.split('npub'); + return 'npub' + parts[1].slice(0, 4) + ':' + parts[1].slice(-3); + }, [pubkey]); + + let text = events && events.length > 0 ? events[0].content : ''; + const createdAt = events && events.length > 0 ? new Date(events[0].created_at * 1000) : new Date(); + const title = userData?.username || userData?.display_name || userData?.name || userData?.npub || npubShortened; + text = text.replaceAll('\n', ' '); + const imageSrc = text.match(/https?:\/\/[^ ]*\.(png|jpg|gif)/g); + const textWithoutImage = text.replace(/https?:\/\/.*\.(?:png|jpg|gif)/g, ''); + const hrefProfile = `/profile/${nip19.npubEncode(pubkey)}`; + const profileImageSrc = userData?.picture || "https://robohash.org/" + pubkey; + + return ( + <> + + + +
+ + + + {title} +
+
+
+ +
+
+ {imageSrc && imageSrc.length > 0 && ( +
+ + {text} + +
+ // {text} + //
+ // {text} + //
+ )} +
+
+
+
+ + ); +} + +export default TrendingImage; \ No newline at end of file diff --git a/lumina/components/TrendingImages.tsx b/lumina/components/TrendingImages.tsx new file mode 100644 index 0000000..10c09c7 --- /dev/null +++ b/lumina/components/TrendingImages.tsx @@ -0,0 +1,27 @@ +import React, { useState, useEffect } from 'react'; +import TrendingImage from './TrendingImage'; + +export function TrendingImages() { + const [profiles, setProfiles] = useState([]); + + useEffect(() => { + fetch('https://api.nostr.band/v0/trending/images') + .then(res => res.json()) + .then(data => setProfiles(data.images)) + .catch(error => { + console.error('Error calling trending profiles:', error); + }); + }, []); + + return ( +
+

Currently Trending

+
+ {profiles && profiles.length > 0 && profiles.map((profile, index) => ( + //

{profile.id}

+ + ))} +
+
+ ); +} \ No newline at end of file diff --git a/lumina/components/UpdateProfileForm.tsx b/lumina/components/UpdateProfileForm.tsx new file mode 100644 index 0000000..57c5e56 --- /dev/null +++ b/lumina/components/UpdateProfileForm.tsx @@ -0,0 +1,102 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { generateSecretKey, getPublicKey } from 'nostr-tools/pure' +import { nip19 } from "nostr-tools" +import { Label } from "./ui/label" +import { Textarea } from "@/components/ui/textarea" +import { finalizeEvent, verifyEvent } from 'nostr-tools/pure' +import { bytesToHex, hexToBytes } from '@noble/hashes/utils' +import { useNostr, useProfile } from 'nostr-react'; + +export function UpdateProfileForm() { + + const { publish } = useNostr(); + + let npub = ''; + let pubkey = ''; + let nsec: Uint8Array; + + if (typeof window !== 'undefined') { + pubkey = window.localStorage.getItem("pubkey") ?? ''; + const nsecHex = window.localStorage.getItem("nsec"); + + if (pubkey && pubkey.length > 0) { + npub = nip19.npubEncode(pubkey); + } + + if (nsecHex && nsecHex.length > 0) { + nsec = hexToBytes(nsecHex); + } + } + + let { data: userData } = useProfile({ + pubkey, + }); + + const [username, setUsername] = useState(userData?.name); + const [displayName, setDisplayName] = useState(userData?.display_name); + const [bio, setBio] = useState(userData?.about); + + const handleUsernameChange = (event: React.ChangeEvent) => { + setUsername(event.target.value); + }; + const handleDisplayNameChange = (event: React.ChangeEvent) => { + setDisplayName(event.target.value); + }; + const handleBioChange = (event: React.ChangeEvent) => { + setBio(event.target.value); + }; + + async function handleProfileUpdate() { + const username = (document.getElementById('username') as HTMLInputElement).value; + const bio = (document.getElementById('bio') as HTMLInputElement).value; + const displayname = (document.getElementById('displayname') as HTMLInputElement).value; + + if (nsec) { + let event = finalizeEvent({ + kind: 0, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: `{"name": "${username}", "about": "${bio}"}`, + }, nsec); + + let isGood = verifyEvent(event); + + // console.log('isGood: ' + isGood); + // console.log(event); + + if (isGood) { + publish(event); + window.location.href = `/profile/${npub}`; + } + } + } + + return ( +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + {/* */} +