diff --git a/app/discover/PubListing.tsx b/app/discover/PubListing.tsx new file mode 100644 index 00000000..c2638591 --- /dev/null +++ b/app/discover/PubListing.tsx @@ -0,0 +1,95 @@ +"use client"; +import { AtUri } from "@atproto/syntax"; +import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider"; +import { BaseThemeProvider } from "components/ThemeManager/ThemeProvider"; +import { PubLeafletPublication, PubLeafletThemeColor } from "lexicons/api"; +import { blobRefToSrc } from "src/utils/blobRefToSrc"; +import { Json } from "supabase/database.types"; + +export const PubListing = (props: { + record: Json; + uri: string; + documents_in_publications: { + indexed_at: string; + documents: { data: Json } | null; + }[]; +}) => { + let record = props.record as PubLeafletPublication.Record; + let theme = usePubTheme(record); + let backgroundImage = record?.theme?.backgroundImage?.image?.ref + ? blobRefToSrc( + record?.theme?.backgroundImage?.image?.ref, + new AtUri(props.uri).host, + ) + : null; + + let backgroundImageRepeat = record?.theme?.backgroundImage?.repeat; + let backgroundImageSize = record?.theme?.backgroundImage?.width || 500; + if (!record) return null; + return ( + + +
+ {!record?.icon ? record.name.slice(0, 1).toLocaleUpperCase() : null} +
+
+

{record.name}

+

{record.description}

+
+

+ Updated {timeAgo(props.documents_in_publications[0].indexed_at)} +

+
+
+
+
+ ); +}; + +function timeAgo(timestamp: string): string { + const now = new Date(); + const date = new Date(timestamp); + const diffMs = now.getTime() - date.getTime(); + const diffSeconds = Math.floor(diffMs / 1000); + const diffMinutes = Math.floor(diffSeconds / 60); + const diffHours = Math.floor(diffMinutes / 60); + const diffDays = Math.floor(diffHours / 24); + const diffYears = Math.floor(diffDays / 365); + + if (diffYears > 0) { + return `${diffYears} year${diffYears === 1 ? "" : "s"} ago`; + } else if (diffDays > 0) { + return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`; + } else if (diffHours > 0) { + return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`; + } else if (diffMinutes > 0) { + return `${diffMinutes} minute${diffMinutes === 1 ? "" : "s"} ago`; + } else { + return "just now"; + } +} diff --git a/app/discover/SortButtons.tsx b/app/discover/SortButtons.tsx new file mode 100644 index 00000000..3b90228d --- /dev/null +++ b/app/discover/SortButtons.tsx @@ -0,0 +1,97 @@ +"use client"; +import Link from "next/link"; +import { useState } from "react"; +import { theme } from "tailwind.config"; + +export default function SortButtons(props: { order: string }) { + const [selected, setSelected] = useState<"recentlyUpdated" | "popular">( + "recentlyUpdated", + ); + + return ( +
+ + + Recently Updated + + + + + Popular + +
+ ); +} + +const SortButton = (props: { + children: React.ReactNode; + selected: boolean; +}) => { + return ( +
+ + {props.selected && ( + <> +
+ +
+
+ +
+
+ +
+ + )} +
+ ); +}; + +const GlitterBig = () => { + return ( + + + + ); +}; + +const GlitterSmall = () => { + return ( + + + + ); +}; diff --git a/app/discover/SortedPublicationList.tsx b/app/discover/SortedPublicationList.tsx new file mode 100644 index 00000000..438dcf01 --- /dev/null +++ b/app/discover/SortedPublicationList.tsx @@ -0,0 +1,149 @@ +"use client"; +import Link from "next/link"; +import { useState } from "react"; +import { theme } from "tailwind.config"; +import { PublicationsList } from "./page"; +import { PubListing } from "./PubListing"; + +export function SortedPublicationList(props: { + publications: PublicationsList; + order: string; +}) { + let [order, setOrder] = useState(props.order); + return ( +
+ { + const url = new URL(window.location.href); + url.searchParams.set("order", o); + window.history.pushState({}, "", url); + setOrder(o); + }} + /> +
+ {props.publications + ?.filter((pub) => pub.documents_in_publications.length > 0) + ?.sort((a, b) => { + if (order === "popular") { + console.log("sorting by popularity"); + return ( + b.publication_subscriptions[0].count - + a.publication_subscriptions[0].count + ); + } + const aDate = new Date( + a.documents_in_publications[0]?.indexed_at || 0, + ); + const bDate = new Date( + b.documents_in_publications[0]?.indexed_at || 0, + ); + return bDate.getTime() - aDate.getTime(); + }) + .map((pub) => )} +
+
+ ); +} + +export default function SortButtons(props: { + order: string; + setOrder: (order: string) => void; +}) { + const [selected, setSelected] = useState<"recentlyUpdated" | "popular">( + "recentlyUpdated", + ); + + return ( +
+ props.setOrder("recentlyUpdated")} + > + Recently Updated + + + props.setOrder("popular")} + > + Popular + +
+ ); +} + +const SortButton = (props: { + children: React.ReactNode; + onClick: () => void; + selected: boolean; +}) => { + return ( +
+ + {props.selected && ( + <> +
+ +
+
+ +
+
+ +
+ + )} +
+ ); +}; + +const GlitterBig = () => { + return ( + + + + ); +}; + +const GlitterSmall = () => { + return ( + + + + ); +}; diff --git a/app/discover/page.tsx b/app/discover/page.tsx new file mode 100644 index 00000000..16eca8c6 --- /dev/null +++ b/app/discover/page.tsx @@ -0,0 +1,42 @@ +import { supabaseServerClient } from "supabase/serverClient"; +import Link from "next/link"; +import { SortedPublicationList } from "./SortedPublicationList"; + +export type PublicationsList = Awaited>; +async function getPublications() { + let { data: publications, error } = await supabaseServerClient + .from("publications") + .select( + "*, documents_in_publications(*, documents(*)), publication_subscriptions(count)", + ) + .or( + "record->preferences->showInDiscover.is.null,record->preferences->>showInDiscover.eq.true", + ) + .order("indexed_at", { + referencedTable: "documents_in_publications", + ascending: false, + }) + .limit(1, { referencedTable: "documents_in_publications" }); + return publications; +} +export default async function Discover(props: { + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +}) { + let order = ((await props.searchParams).order as string) || "recentlyUpdated"; + let publications = await getPublications(); + + return ( +
+
+
+

Discover

+

+ Explore publications on Leaflet ✨ Or{" "} + make your own! +

+
+ +
+
+ ); +} diff --git a/app/lish/Subscribe.tsx b/app/lish/Subscribe.tsx index e9e1b5d0..e5cee604 100644 --- a/app/lish/Subscribe.tsx +++ b/app/lish/Subscribe.tsx @@ -311,7 +311,7 @@ let BlueskySubscribeButton = (props: { > {isClient && ( { let [formState, setFormState] = useState<"normal" | "loading">("normal"); let [nameValue, setNameValue] = useState(""); let [descriptionValue, setDescriptionValue] = useState(""); + let [showInDiscover, setShowInDiscover] = useState(true); let [logoFile, setLogoFile] = useState(null); let [logoPreview, setLogoPreview] = useState(null); let [domainValue, setDomainValue] = useState(""); @@ -45,6 +47,7 @@ export const CreatePubForm = () => { description: descriptionValue, iconFile: logoFile, subdomain: domainValue, + preferences: { showInDiscover }, }); // Show a spinner while this is happening! Maybe a progress bar? setTimeout(() => { @@ -117,6 +120,24 @@ export const CreatePubForm = () => { domainState={domainState} setDomainState={setDomainState} /> +
+ setShowInDiscover(e.target.checked)} + > +
+

+ Show In{" "} + + Discover + +

+

+ This publication will appear on our public Discover page +

+
+
+
{ let { data: pubData } = usePublicationData(); @@ -24,6 +26,11 @@ export const EditPubForm = () => { let [formState, setFormState] = useState<"normal" | "loading">("normal"); let [nameValue, setNameValue] = useState(record?.name || ""); + let [showInDiscover, setShowInDiscover] = useState( + record?.preferences?.showInDiscover === undefined + ? true + : record.preferences.showInDiscover, + ); let [descriptionValue, setDescriptionValue] = useState( record?.description || "", ); @@ -53,6 +60,9 @@ export const EditPubForm = () => { name: nameValue, description: descriptionValue, iconFile: iconFile, + preferences: { + showInDiscover: showInDiscover, + }, }); toast({ type: "success", content: "Updated!" }); setFormState("normal"); @@ -95,6 +105,7 @@ export const EditPubForm = () => { }} />
+