updated event feed
This commit is contained in:
parent
b84845eade
commit
c583eb9a6d
@ -22,6 +22,7 @@ import {
|
||||
import dynamic from "next/dynamic";
|
||||
import { useModal } from "@/app/_providers/modal/provider";
|
||||
import { IconType } from "react-icons";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const ZapPickerModal = dynamic(() => import("@/components/Modals/ZapPicker"), {
|
||||
ssr: false,
|
||||
@ -42,7 +43,6 @@ type NavigationElement = {
|
||||
name: string;
|
||||
label: string;
|
||||
icon: IconType;
|
||||
current: boolean;
|
||||
active: boolean;
|
||||
} & (NavigationLink | NavigationButton);
|
||||
const flockstrEvent = {
|
||||
@ -61,6 +61,7 @@ const flockstrEvent = {
|
||||
|
||||
export default function Sidebar() {
|
||||
const modal = useModal();
|
||||
const pathname = usePathname();
|
||||
|
||||
const navigation: NavigationElement[] = [
|
||||
{
|
||||
@ -69,16 +70,14 @@ export default function Sidebar() {
|
||||
label: "Home",
|
||||
icon: RiHome6Fill,
|
||||
type: "link",
|
||||
current: true,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
href: "",
|
||||
href: "/explore",
|
||||
name: "explore",
|
||||
label: "Explore",
|
||||
icon: RiCompassLine,
|
||||
type: "link",
|
||||
current: false,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
@ -87,7 +86,6 @@ export default function Sidebar() {
|
||||
label: "Messages",
|
||||
icon: RiQuestionAnswerLine,
|
||||
type: "link",
|
||||
current: false,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
@ -96,13 +94,12 @@ export default function Sidebar() {
|
||||
label: "Zap Flockstr",
|
||||
icon: HiOutlineLightningBolt,
|
||||
type: "button",
|
||||
current: false,
|
||||
active: true,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<nav className="z-header- hidden h-[calc(100svh_-_var(--header-height))] w-[var(--sidebar-closed-width)] flex-col sm:flex">
|
||||
<div className="fixed bottom-0 flex h-[calc(100svh_-_var(--header-height))] w-[var(--sidebar-closed-width)] flex-col border-r xl:w-[var(--sidebar-open-width)]">
|
||||
<div className="fixed bottom-0 flex h-[calc(100svh_-_var(--header-height))] w-[var(--sidebar-closed-width)] flex-col border-r xl:w-[var(--sidebar-open-width)]">
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex flex-col items-stretch gap-y-2 p-4">
|
||||
{navigation.map((item) => {
|
||||
@ -114,7 +111,7 @@ export default function Sidebar() {
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"center group relative min-h-[48px] min-w-[48px] rounded-lg hover:bg-muted xl:justify-start xl:gap-x-4 xl:p-2.5",
|
||||
item.current
|
||||
pathname === item.href
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
@ -136,7 +133,7 @@ export default function Sidebar() {
|
||||
<div
|
||||
className={cn(
|
||||
"center group relative min-h-[48px] min-w-[48px] rounded-lg hover:bg-muted xl:justify-start xl:gap-x-4 xl:p-2.5",
|
||||
item.current
|
||||
false
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
@ -165,7 +162,7 @@ export default function Sidebar() {
|
||||
onClick={item.onClick}
|
||||
className={cn(
|
||||
"center group relative min-h-[48px] min-w-[48px] rounded-lg hover:bg-muted xl:justify-start xl:gap-x-4 xl:p-2.5",
|
||||
item.current
|
||||
false
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
@ -187,7 +184,7 @@ export default function Sidebar() {
|
||||
<div
|
||||
className={cn(
|
||||
"center group relative min-h-[48px] min-w-[48px] rounded-lg hover:bg-muted xl:justify-start xl:gap-x-4 xl:p-2.5",
|
||||
item.current
|
||||
false
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
|
@ -20,7 +20,10 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
{/* Sidebar */}
|
||||
<Sidebar />
|
||||
<div className="relative flex flex-1 shrink-0 grow justify-center sm:w-[calc(100vw_-_var(--sidebar-closed-width))] xl:w-[calc(100vw_-_var(--sidebar-open-width))]">
|
||||
<div className="flex-1 overflow-x-hidden pb-5">{children}</div>
|
||||
<div className="w-[100vw] flex-1 pb-5 sm:w-[calc(100vw_-_var(--sidebar-closed-width))] xl:w-[calc(100vw_-_var(--sidebar-open-width))]">
|
||||
{/* <div className="flex-1 overflow-y-auto overflow-x-hidden pb-5"> */}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{/* Mobile Banner */}
|
||||
<MobileBanner />
|
||||
|
99
app/(app)/explore/_components/CalendarSection.tsx
Normal file
99
app/(app)/explore/_components/CalendarSection.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { CardLoading } from "@/components/Cards/CalendarEvent";
|
||||
import { formatDate, fromUnix, relativeTime } from "@/lib/utils/dates";
|
||||
|
||||
import useCurrentUser from "@/lib/hooks/useCurrentUser";
|
||||
import { DUMMY_1 } from "@/constants";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { getTagValues } from "@/lib/nostr/utils";
|
||||
import LargeFeedCard from "@/components/Cards/CalendarEvent/LargeFeedCard";
|
||||
import { cn } from "@/lib/utils";
|
||||
type CalendarSectionProps = {
|
||||
events: NDKEvent[];
|
||||
};
|
||||
|
||||
export default function CalendarSection({ events }: CalendarSectionProps) {
|
||||
const { currentUser } = useCurrentUser();
|
||||
const firstEvent = events.at(0);
|
||||
if (!firstEvent) return null;
|
||||
const startDateUnix = getTagValues("start", firstEvent?.tags);
|
||||
if (!startDateUnix) return null;
|
||||
const startDate = fromUnix(parseInt(startDateUnix));
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full items-start gap-x-3 @container">
|
||||
{/* Date Indicator */}
|
||||
<div className="sticky top-[calc(var(--header-height)_+_28px)] hidden w-[230px] shrink-0 md:block">
|
||||
<CalendarIcon date={startDate} />
|
||||
</div>
|
||||
{/* Date Indicator Mobile */}
|
||||
<div className="absolute inset-y-0 right-0 z-50 md:hidden">
|
||||
<div className="sticky top-[calc(var(--header-height)_+_14px)] shrink-0">
|
||||
<CalendarIconOpacity date={startDate} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Events */}
|
||||
<div className="flex-1 space-y-4 max-md:pt-[60px]">
|
||||
{events.map((e) => (
|
||||
<LargeFeedCard key={e.id} event={e} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function CalendarIcon({ date }: { date: Date }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex overflow-hidden rounded-md border bg-background shadow",
|
||||
)}
|
||||
>
|
||||
<div className="w-fit shrink-0 bg-primary p-1.5 px-3 text-primary-foreground @xl:p-2 @xl:px-3.5">
|
||||
{/* <span className="text-2xl font-bold">24</span> */}
|
||||
<span className="font-semibold">{formatDate(date, "ddd")}</span>
|
||||
</div>
|
||||
<div className="center flex whitespace-nowrap px-3 text-sm font-medium text-muted-foreground @lg:text-base @xl:px-3.5">
|
||||
<div className="">
|
||||
{formatDate(date, "MMMM Do")}
|
||||
<span className="-mt-2 block text-[10px] font-normal">
|
||||
{relativeTime(date)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function CalendarIconOpacity({ date }: { date: Date }) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [top, setTop] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Add a scroll event listener to the window
|
||||
const handleScroll = () => {
|
||||
if (ref.current) {
|
||||
// Get the position of the div relative to the viewport
|
||||
const divRect = ref.current.getBoundingClientRect();
|
||||
// Change the opacity when the div reaches the top of the screen
|
||||
if (divRect.top <= 110) {
|
||||
setTop(true);
|
||||
} else {
|
||||
setTop(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
// Remove the scroll event listener when the component unmounts
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div ref={ref} className={cn(top && "opacity-50")}>
|
||||
<CalendarIcon date={date} />
|
||||
</div>
|
||||
);
|
||||
}
|
58
app/(app)/explore/page.tsx
Normal file
58
app/(app)/explore/page.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
import { NDKEvent, type NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import CalendarSection from "./_components/CalendarSection";
|
||||
import useEvents from "@/lib/hooks/useEvents";
|
||||
import { getTagValues } from "@/lib/nostr/utils";
|
||||
import { fromUnix, daysOffset } from "@/lib/utils/dates";
|
||||
export default function Page() {
|
||||
const { events } = useEvents({
|
||||
filter: {
|
||||
kinds: [31923 as NDKKind],
|
||||
limit: 100,
|
||||
},
|
||||
});
|
||||
const eventsByDay = groupEventsByDay(events);
|
||||
|
||||
return (
|
||||
<div className="relative flex-col px-5 pt-5 sm:pt-7">
|
||||
<div className="mx-auto max-w-[900px] space-y-4">
|
||||
{eventsByDay.map((e) => (
|
||||
<CalendarSection events={e} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function groupEventsByDay(events: NDKEvent[]) {
|
||||
const eventDays: Record<string, NDKEvent[]> = {};
|
||||
for (const event of events) {
|
||||
const eventStartTime = getTagValues("start", event.tags);
|
||||
if (!eventStartTime) continue;
|
||||
const startDate = fromUnix(parseInt(eventStartTime));
|
||||
const daysAway = daysOffset(startDate);
|
||||
if (daysAway < 1) continue;
|
||||
if (eventDays[`${daysAway}`]) {
|
||||
eventDays[`${daysAway}`]!.push(event);
|
||||
} else {
|
||||
eventDays[`${daysAway}`] = [event];
|
||||
}
|
||||
}
|
||||
const groupedArray = Object.entries(eventDays)
|
||||
.sort(([aKey], [bKey, test]) => {
|
||||
console.log("test", test);
|
||||
const aDay = parseInt(aKey);
|
||||
|
||||
const bDay = parseInt(bKey);
|
||||
if (aDay > bDay) {
|
||||
return 1;
|
||||
} else if (aDay < bDay) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map(([_, events]) => events);
|
||||
console.log("object", eventDays);
|
||||
console.log("returing", groupedArray);
|
||||
return groupedArray;
|
||||
}
|
89
components/Cards/CalendarEvent/LargeFeedCard.tsx
Normal file
89
components/Cards/CalendarEvent/LargeFeedCard.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { formatDate, fromUnix } from "@/lib/utils/dates";
|
||||
import { BANNER } from "@/constants";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import SmallProfileLine from "@/components/ProfileContainers/SmallProfileLine";
|
||||
import AvatarStack from "@/components/ProfileContainers/AvatarStack";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { getTagValues, getTagAllValues } from "@/lib/nostr/utils";
|
||||
import { HiOutlineMapPin, HiOutlineUserCircle } from "react-icons/hi2";
|
||||
|
||||
type LargeFeedCardProps = {
|
||||
event: NDKEvent;
|
||||
};
|
||||
|
||||
export default function LargeFeedCard({ event }: LargeFeedCardProps) {
|
||||
const { tags, pubkey, content } = event;
|
||||
const image = getTagValues("image", tags);
|
||||
const users = getTagAllValues("p", tags);
|
||||
|
||||
return (
|
||||
<Card className="relative flex justify-between gap-x-4 rounded-[1rem] bg-muted p-[0.5rem]">
|
||||
<CardHeader className="w-3/5 justify-between p-0 pr-5">
|
||||
<div className="">
|
||||
<div className="flex">
|
||||
<SmallProfileLine pubkey={pubkey} />
|
||||
</div>
|
||||
<div className="pl-3">
|
||||
<CardTitle className="mt-3 line-clamp-2 text-xl leading-6">
|
||||
{getTagValues("name", tags)}
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-2 line-clamp-4 text-[13px] leading-5">
|
||||
{content}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4 pl-2">
|
||||
{!!users.length && (
|
||||
<div className="flex shrink-0 items-center gap-x-2">
|
||||
<HiOutlineUserCircle className="h-5 w-5 text-primary" />
|
||||
<AvatarStack
|
||||
pubkeys={users.slice(0, 4)}
|
||||
className="z-0 h-6 w-6 text-[9px]"
|
||||
remaining={users.length - 4 > 2 ? users.length - 4 : 0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-x-2">
|
||||
<HiOutlineMapPin className="h-5 w-5 shrink-0 text-primary" />
|
||||
<p className="line-clamp-1 text-xs text-muted-foreground">
|
||||
124 Main street, first ave, NY NY, 118034. Across the street from
|
||||
too long
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-col justify-end self-start pl-2 pt-2">
|
||||
<div className="flex w-3/4 items-center justify-stretch gap-3">
|
||||
<Button className="flex-1">RSVP</Button>
|
||||
<Button variant={"outline"} className="flex-1">
|
||||
Share
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<div className="absolute inset-y-[0.5rem] right-[0.5rem] w-2/5">
|
||||
{image ? (
|
||||
<Image
|
||||
src={image}
|
||||
unoptimized
|
||||
alt="Image"
|
||||
fill
|
||||
className={cn(
|
||||
"h-full max-h-[150px] rounded-[0.5rem] object-cover object-center max-sm:max-h-[100px]",
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className=""></div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -129,7 +129,7 @@ export default function CalendarEventCard({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function CardLoading({ className }: { className: string }) {
|
||||
export function CardLoading({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
57
components/ProfileContainers/AvatarStack.tsx
Normal file
57
components/ProfileContainers/AvatarStack.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
import { cn, formatCount, getTwoLetters } from "@/lib/utils";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import useProfile from "@/lib/hooks/useProfile";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
type AvatarStackProps = {
|
||||
pubkeys: string[];
|
||||
remaining?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const zIndexes = ["z-50", "z-40", "z-30", "z-20", "z-10", "z-0"];
|
||||
|
||||
export default function AvatarStack({
|
||||
pubkeys,
|
||||
className,
|
||||
remaining,
|
||||
}: AvatarStackProps) {
|
||||
return (
|
||||
<div className="isolate flex -space-x-2 overflow-hidden py-[2px]">
|
||||
{pubkeys.map((p, idx) => (
|
||||
<User key={p} pubkey={p} className={cn(zIndexes[idx], className)} />
|
||||
))}
|
||||
{!!remaining && (
|
||||
<Avatar
|
||||
className={cn(
|
||||
"relative inline-block h-8 w-8 rounded-full text-xs ring-2 ring-background",
|
||||
className,
|
||||
"z-0",
|
||||
)}
|
||||
>
|
||||
<AvatarFallback className="bg-muted font-semibold text-primary">{`+${formatCount(
|
||||
remaining,
|
||||
)}`}</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function User({ pubkey, className }: { pubkey: string; className: string }) {
|
||||
const { profile } = useProfile(pubkey);
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
className={cn(
|
||||
"relative inline-block h-8 w-8 rounded-full ring-2 ring-background",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<AvatarImage src={profile?.image} />
|
||||
<AvatarFallback>{getTwoLetters({ npub, profile })}</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
}
|
@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { RiCloseFill } from "react-icons/ri";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { formatDate } from "@/lib/utils/dates";
|
||||
import { formatDate, fromUnix } from "@/lib/utils/dates";
|
||||
import Actions from "./Actions";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { getTagAllValues, getTagValues } from "@/lib/nostr/utils";
|
||||
@ -67,7 +67,7 @@ export default function ArticlePage({ event }: ArticleProps) {
|
||||
</div>
|
||||
<div className="h-[20px] w-full"></div>
|
||||
<div className="vmax-h-[calc(100vh_-_100px)] overflow-y-auto">
|
||||
<article className="prose dark:prose-invert prose-zinc relative mx-auto max-w-3xl pt-7">
|
||||
<article className="prose prose-zinc relative mx-auto max-w-3xl pt-7 dark:prose-invert">
|
||||
<div className="">
|
||||
<div className="flex items-center justify-between gap-1 lg:mb-2">
|
||||
{tags.map((t) => (
|
||||
@ -79,7 +79,7 @@ export default function ArticlePage({ event }: ArticleProps) {
|
||||
<div className="center text-xs text-muted-foreground/50">
|
||||
{!!createdAt && (
|
||||
<span className="mr-2.5">
|
||||
{formatDate(new Date(createdAt * 1000), "MMMM Do, YYYY")}
|
||||
{formatDate(fromUnix(createdAt), "MMMM Do, YYYY")}
|
||||
</span>
|
||||
)}
|
||||
<span className="h-3 w-[1px] rounded-full bg-muted-foreground/50"></span>
|
||||
|
@ -60,21 +60,33 @@ export function relativeTime(timestamp: Date) {
|
||||
dayjs.updateLocale("en", {
|
||||
relativeTime: {
|
||||
future: "in %s",
|
||||
past: "%s ago",
|
||||
s: "%s seconds",
|
||||
m: "1 min",
|
||||
mm: "%d mins",
|
||||
h: "1 hour",
|
||||
past: "now",
|
||||
s: "now",
|
||||
m: "a minute",
|
||||
mm: "%d minutes",
|
||||
h: "an hour",
|
||||
hh: "%d hours",
|
||||
d: "1 day",
|
||||
d: "a day",
|
||||
dd: "%d days",
|
||||
y: "1 year",
|
||||
M: "a month",
|
||||
MM: "%d months",
|
||||
y: "a year",
|
||||
yy: "%d years",
|
||||
},
|
||||
});
|
||||
dayjs.extend(relative, config);
|
||||
return dayjs(timestamp).fromNow();
|
||||
}
|
||||
export function daysOffset(targetDate: Date) {
|
||||
const currentDate = new Date();
|
||||
|
||||
// Calculate the time difference in milliseconds
|
||||
const timeDifference = targetDate.getTime() - currentDate.getTime();
|
||||
// Convert the time difference to days
|
||||
const daysDifference = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||||
|
||||
return daysDifference;
|
||||
}
|
||||
export function formatDate(timestamp: Date, format?: string) {
|
||||
dayjs.extend(advancedFormat);
|
||||
dayjs.extend(timezone);
|
||||
@ -92,6 +104,9 @@ export function convertToTimezoneDate(inputDate: Date, _timezone: string) {
|
||||
return dayjs(inputDate).tz(_timezone).toDate();
|
||||
}
|
||||
|
||||
export function fromUnix(unix: number) {
|
||||
return new Date(unix * 1000);
|
||||
}
|
||||
export function addMinutesToDate(inputDate: Date, minutesToAdd: number) {
|
||||
if (!(inputDate instanceof Date)) {
|
||||
throw new Error("Invalid date input");
|
||||
@ -112,21 +127,6 @@ export function addMinutesToDate(inputDate: Date, minutesToAdd: number) {
|
||||
export function toUnix(inputDate: Date) {
|
||||
return dayjs(inputDate).unix();
|
||||
}
|
||||
function timezoneDiff(ianatz: string) {
|
||||
const date = new Date();
|
||||
// suppose the date is 12:00 UTC
|
||||
var invdate = new Date(
|
||||
date.toLocaleString("en-US", {
|
||||
timeZone: ianatz,
|
||||
}),
|
||||
);
|
||||
|
||||
// then invdate will be 07:00 in Toronto
|
||||
// and the diff is 5 hours
|
||||
var diff = date.getTime() - invdate.getTime();
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
export function convertToTimezone(inputDate: Date, targetTimezone: string) {
|
||||
if (!(inputDate instanceof Date)) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user