updated event feed

This commit is contained in:
zmeyer44 2023-10-27 13:10:07 -04:00
parent b84845eade
commit c583eb9a6d
9 changed files with 341 additions and 38 deletions

View File

@ -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",
)}

View File

@ -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 />

View 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>
);
}

View 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;
}

View 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>
);
}

View File

@ -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(

View 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>
);
}

View File

@ -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>

View File

@ -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)) {