From 0be3e5386833c9461c7e55db5c55f29b6d7e55d0 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Thu, 17 Oct 2024 11:39:37 -0400 Subject: [PATCH 01/13] Add WIP news cards --- apps/web/ui/layout/sidebar/news.tsx | 53 ++++++++++++++++++++++ apps/web/ui/layout/sidebar/sidebar-nav.tsx | 2 + 2 files changed, 55 insertions(+) create mode 100644 apps/web/ui/layout/sidebar/news.tsx diff --git a/apps/web/ui/layout/sidebar/news.tsx b/apps/web/ui/layout/sidebar/news.tsx new file mode 100644 index 0000000000..919c1d0ad6 --- /dev/null +++ b/apps/web/ui/layout/sidebar/news.tsx @@ -0,0 +1,53 @@ +import { CSSProperties } from "react"; + +export function News() { + const cardCount = 5; + + return ( +
+
+ {[...Array(cardCount - 1)].map((_, idx) => ( +
+ +
+ ))} + +
+
+ ); +} + +function NewsCard({ + title, + description, +}: { + title: string; + description: string; +}) { + return ( +
+
+
+ + {title} + +

{description}

+
+
+ ); +} diff --git a/apps/web/ui/layout/sidebar/sidebar-nav.tsx b/apps/web/ui/layout/sidebar/sidebar-nav.tsx index f612b7db6e..6fcd644c61 100644 --- a/apps/web/ui/layout/sidebar/sidebar-nav.tsx +++ b/apps/web/ui/layout/sidebar/sidebar-nav.tsx @@ -13,6 +13,7 @@ import { } from "react"; import UserSurveyButton from "../user-survey"; import { ITEMS, type NavItem as NavItemType } from "./items"; +import { News } from "./news"; import { Usage } from "./usage"; import UserDropdown from "./user-dropdown"; import { WorkspaceDropdown } from "./workspace-dropdown"; @@ -97,6 +98,7 @@ export function SidebarNav({ toolContent }: { toolContent?: ReactNode }) {
+
From 65995e67e1ba815adec9c28cc3524b07474e8d39 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Thu, 17 Oct 2024 16:58:40 -0400 Subject: [PATCH 02/13] Update news card design + animations --- apps/web/ui/layout/sidebar/news.tsx | 69 ++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/apps/web/ui/layout/sidebar/news.tsx b/apps/web/ui/layout/sidebar/news.tsx index 919c1d0ad6..9264ca5892 100644 --- a/apps/web/ui/layout/sidebar/news.tsx +++ b/apps/web/ui/layout/sidebar/news.tsx @@ -1,18 +1,29 @@ +import { cn } from "@dub/utils"; import { CSSProperties } from "react"; +const OFFSET_FACTOR = 4; +const SCALE_FACTOR = 0.03; +const OPACITY_FACTOR = 0.1; + export function News() { const cardCount = 5; return ( -
-
- {[...Array(cardCount - 1)].map((_, idx) => ( +
+
+ {[...Array(cardCount)].map((_, idx) => (
3 + ? "opacity-0 group-hover:translate-y-[var(--y)] group-hover:opacity-[var(--opacity)]" + : "translate-y-[var(--y)] opacity-[var(--opacity)]", + )} style={ { - "--y": `-${(cardCount - 1 - idx) * 5}%`, - "--scale": 1 - 0.4 / idx / cardCount, + "--y": `-${(cardCount - (idx + 1)) * OFFSET_FACTOR}%`, + "--scale": 1 - (cardCount - (idx + 1)) * SCALE_FACTOR, + "--opacity": 1 - (cardCount - (idx + 1)) * OPACITY_FACTOR, } as CSSProperties } > @@ -20,13 +31,17 @@ export function News() { key={idx} title="Title Title" description="Description of the article that clicking this card will take you to" + hideContent={cardCount - idx > 2} + active={idx === cardCount - 1} />
))} - +
+ +
); @@ -35,18 +50,38 @@ export function News() { function NewsCard({ title, description, + hideContent, + active, }: { title: string; description: string; + hideContent?: boolean; + active?: boolean; }) { return ( -
-
-
- - {title} - -

{description}

+
+
+
+ + {title} + +

{description}

+
+
+
+
+ + +
+
); From ca1338ec473cceda8d49139b9bd550f051747630 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Fri, 18 Oct 2024 15:41:41 -0400 Subject: [PATCH 03/13] Add interactions + animations --- apps/web/ui/layout/sidebar/news.tsx | 107 ++++++++++++++++++--- apps/web/ui/layout/sidebar/sidebar-nav.tsx | 12 ++- 2 files changed, 103 insertions(+), 16 deletions(-) diff --git a/apps/web/ui/layout/sidebar/news.tsx b/apps/web/ui/layout/sidebar/news.tsx index 9264ca5892..5e8fb6a10b 100644 --- a/apps/web/ui/layout/sidebar/news.tsx +++ b/apps/web/ui/layout/sidebar/news.tsx @@ -1,22 +1,28 @@ import { cn } from "@dub/utils"; -import { CSSProperties } from "react"; +import Link from "next/link"; +import { CSSProperties, useRef, useState } from "react"; const OFFSET_FACTOR = 4; const SCALE_FACTOR = 0.03; const OPACITY_FACTOR = 0.1; export function News() { - const cardCount = 5; + const [cards, setCards] = useState([1, 2, 3, 4, 5, 6]); + const cardCount = cards.length; - return ( + return cards.length ? (
- {[...Array(cardCount)].map((_, idx) => ( + {cards.map((id, idx) => (
3 - ? "opacity-0 group-hover:translate-y-[var(--y)] group-hover:opacity-[var(--opacity)]" + ? [ + "opacity-0 group-hover:translate-y-[var(--y)] group-hover:opacity-[var(--opacity)]", + "group-has-[*[data-dragging=true]]:translate-y-[var(--y)] group-has-[*[data-dragging=true]]:opacity-[var(--opacity)]", + ] : "translate-y-[var(--y)] opacity-[var(--opacity)]", )} style={ @@ -26,6 +32,7 @@ export function News() { "--opacity": 1 - (cardCount - (idx + 1)) * OPACITY_FACTOR, } as CSSProperties } + aria-hidden={idx !== cardCount - 1} > 2} active={idx === cardCount - 1} + onDismiss={() => { + setCards((cards) => cards.filter((c) => c !== id)); + }} />
))} @@ -44,22 +54,89 @@ export function News() {
- ); + ) : null; } function NewsCard({ title, description, + onDismiss, hideContent, active, }: { title: string; description: string; + onDismiss?: () => void; hideContent?: boolean; active?: boolean; }) { + const ref = useRef(null); + const drag = useRef<{ start: number; delta: number }>({ start: 0, delta: 0 }); + const animation = useRef(); + const [dragging, setDragging] = useState(false); + + const onDragMove = (e: PointerEvent) => { + if (!ref.current) return; + const { clientX } = e; + const dx = clientX - drag.current.start; + drag.current.delta = dx; + ref.current.style.setProperty("--dx", dx.toString()); + }; + + const dismiss = () => { + if (!ref.current) return; + + // Dismiss card + animation.current = ref.current.animate( + { opacity: 0 }, + { duration: 150, easing: "ease-in-out", fill: "forwards" }, + ); + animation.current.onfinish = () => onDismiss?.(); + }; + + const onDragEnd = () => { + if (!ref.current) return; + document.removeEventListener("pointermove", onDragMove); + document.removeEventListener("pointerup", onDragEnd); + + const dx = drag.current.delta; + if (Math.abs(dx) > ref.current.clientWidth / 3) { + dismiss(); + return; + } + + setDragging(false); + + // Animate back to original position + animation.current = ref.current.animate( + { transform: "translateX(0)" }, + { duration: 150, easing: "ease-in-out" }, + ); + animation.current.onfinish = () => + ref.current?.style.setProperty("--dx", "0"); + }; + + const onPointerDown = (e: React.PointerEvent) => { + if (!ref.current || animation.current?.playState === "running") return; + document.addEventListener("pointermove", onDragMove); + document.addEventListener("pointerup", onDragEnd); + + setDragging(true); + drag.current.start = e.clientX; + drag.current.delta = 0; + ref.current.style.setProperty("--w", ref.current.clientWidth.toString()); + }; + return ( -
+
@@ -70,14 +147,22 @@ function NewsCard({
- -
diff --git a/apps/web/ui/layout/sidebar/sidebar-nav.tsx b/apps/web/ui/layout/sidebar/sidebar-nav.tsx index 6fcd644c61..6bc46cd8e0 100644 --- a/apps/web/ui/layout/sidebar/sidebar-nav.tsx +++ b/apps/web/ui/layout/sidebar/sidebar-nav.tsx @@ -35,7 +35,7 @@ export function SidebarNav({ toolContent }: { toolContent?: ReactNode }) { return ( -
-
+
{AREAS.map((area) => ( ))}
+
+ {area === "default" && } +
))}
-
- +
@@ -147,7 +149,7 @@ export function Area({ return (
Date: Fri, 18 Oct 2024 15:45:18 -0400 Subject: [PATCH 04/13] Add shadow --- apps/web/ui/layout/sidebar/news.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/ui/layout/sidebar/news.tsx b/apps/web/ui/layout/sidebar/news.tsx index 5e8fb6a10b..dab0b86e52 100644 --- a/apps/web/ui/layout/sidebar/news.tsx +++ b/apps/web/ui/layout/sidebar/news.tsx @@ -133,6 +133,7 @@ function NewsCard({ className={cn( "relative select-none gap-2 rounded-lg border border-neutral-200 bg-white p-3 text-[0.8125rem]", "translate-x-[calc(var(--dx)*1px)] rotate-[calc(var(--dx)*0.05deg)] opacity-[calc(1-max(var(--dx),-1*var(--dx))/var(--w)/2)]", + "transition-shadow data-[dragging=true]:shadow-[0_4px_12px_0_#0000000D]", )} data-dragging={dragging} onPointerDown={onPointerDown} From e5f1d5efba7154fc2c786fd898e6d24784c361ff Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Fri, 18 Oct 2024 16:52:30 -0400 Subject: [PATCH 05/13] Pull news articles from `dub.co` API --- .../web/app/app.dub.co/(dashboard)/layout.tsx | 5 +- apps/web/ui/layout/main-nav.tsx | 5 +- apps/web/ui/layout/sidebar/news-rsc.tsx | 29 +++++++++++ apps/web/ui/layout/sidebar/news.tsx | 48 +++++++++++++++---- apps/web/ui/layout/sidebar/sidebar-nav.tsx | 11 +++-- 5 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 apps/web/ui/layout/sidebar/news-rsc.tsx diff --git a/apps/web/app/app.dub.co/(dashboard)/layout.tsx b/apps/web/app/app.dub.co/(dashboard)/layout.tsx index b5361a442d..07b01ec73b 100644 --- a/apps/web/app/app.dub.co/(dashboard)/layout.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/layout.tsx @@ -1,5 +1,6 @@ import { MainNav } from "@/ui/layout/main-nav"; import { HelpButtonRSC } from "@/ui/layout/sidebar/help-button-rsc"; +import { NewsRSC } from "@/ui/layout/sidebar/news-rsc"; import Toolbar from "@/ui/layout/toolbar/toolbar"; import { constructMetadata } from "@dub/utils"; import { ReactNode } from "react"; @@ -12,7 +13,9 @@ export default async function Layout({ children }: { children: ReactNode }) { return (
- }>{children} + } newsContent={}> + {children} +
{/* */} diff --git a/apps/web/ui/layout/main-nav.tsx b/apps/web/ui/layout/main-nav.tsx index 132fa12ea1..5f52310f5b 100644 --- a/apps/web/ui/layout/main-nav.tsx +++ b/apps/web/ui/layout/main-nav.tsx @@ -27,7 +27,8 @@ export const SideNavContext = createContext({ export function MainNav({ children, toolContent, -}: PropsWithChildren<{ toolContent?: ReactNode }>) { + newsContent, +}: PropsWithChildren<{ toolContent?: ReactNode; newsContent?: ReactNode }>) { const pathname = usePathname(); const { isMobile } = useMediaQuery(); @@ -78,7 +79,7 @@ export function MainNav({ )} />
- +
diff --git a/apps/web/ui/layout/sidebar/news-rsc.tsx b/apps/web/ui/layout/sidebar/news-rsc.tsx new file mode 100644 index 0000000000..93fbf07f97 --- /dev/null +++ b/apps/web/ui/layout/sidebar/news-rsc.tsx @@ -0,0 +1,29 @@ +import { News, NewsArticle } from "./news"; + +export async function NewsRSC() { + const { latestNewsArticles } = await fetch( + // "https://dub.co/api/content?type=news", + "https://dub-site-git-news-api-dubinc.vercel.app/api/content?type=news", // TODO: Update to dub.co + { + next: { + revalidate: 60 * 60 * 24, // cache for 24 hours + }, + }, + ).then((res) => res.json()); + + return ( + [] + ) + .slice(0, 6) // Limit to 6 latest articles + .map((article) => ({ + ...article, + href: `https://dub.co/${article.type === "ChangelogPost" ? "changelog" : "blog"}/${article.slug}`, + }))} + /> + ); +} diff --git a/apps/web/ui/layout/sidebar/news.tsx b/apps/web/ui/layout/sidebar/news.tsx index dab0b86e52..b7e19f3c5a 100644 --- a/apps/web/ui/layout/sidebar/news.tsx +++ b/apps/web/ui/layout/sidebar/news.tsx @@ -1,21 +1,33 @@ +"use client"; + import { cn } from "@dub/utils"; +import Image from "next/image"; import Link from "next/link"; import { CSSProperties, useRef, useState } from "react"; +export interface NewsArticle { + slug: string; + type: string; + title: string; + summary: string; + image: string; + href: string; +} + const OFFSET_FACTOR = 4; const SCALE_FACTOR = 0.03; const OPACITY_FACTOR = 0.1; -export function News() { - const [cards, setCards] = useState([1, 2, 3, 4, 5, 6]); +export function News({ articles }: { articles: NewsArticle[] }) { + const [cards, setCards] = useState(articles); const cardCount = cards.length; return cards.length ? (
- {cards.map((id, idx) => ( + {cards.map(({ slug, type, title, summary, image, href }, idx) => (
3 @@ -36,12 +48,16 @@ export function News() { > 2} active={idx === cardCount - 1} onDismiss={() => { - setCards((cards) => cards.filter((c) => c !== id)); + setCards((cards) => + cards.filter((c) => c.slug !== slug || c.type !== type), + ); }} />
@@ -60,14 +76,18 @@ export function News() { function NewsCard({ title, description, + image, onDismiss, hideContent, + href, active, }: { title: string; description: string; + image?: string; onDismiss?: () => void; hideContent?: boolean; + href?: string; active?: boolean; }) { const ref = useRef(null); @@ -145,7 +165,16 @@ function NewsCard({

{description}

-
+
+ {image && ( + + )} +
Read more diff --git a/apps/web/ui/layout/sidebar/sidebar-nav.tsx b/apps/web/ui/layout/sidebar/sidebar-nav.tsx index 6bc46cd8e0..f78b4a19a8 100644 --- a/apps/web/ui/layout/sidebar/sidebar-nav.tsx +++ b/apps/web/ui/layout/sidebar/sidebar-nav.tsx @@ -13,14 +13,19 @@ import { } from "react"; import UserSurveyButton from "../user-survey"; import { ITEMS, type NavItem as NavItemType } from "./items"; -import { News } from "./news"; import { Usage } from "./usage"; import UserDropdown from "./user-dropdown"; import { WorkspaceDropdown } from "./workspace-dropdown"; const AREAS = ["userSettings", "workspaceSettings", "default"] as const; -export function SidebarNav({ toolContent }: { toolContent?: ReactNode }) { +export function SidebarNav({ + toolContent, + newsContent, +}: { + toolContent?: ReactNode; + newsContent?: ReactNode; +}) { const { slug } = useParams() as { slug?: string }; const { flags } = useWorkspace(); const pathname = usePathname(); @@ -94,7 +99,7 @@ export function SidebarNav({ toolContent }: { toolContent?: ReactNode }) { ))}
- {area === "default" && } + {area === "default" && newsContent}
))} From 7e00c714fd9bd8fc7e210234cc277aaa7df3fbeb Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Fri, 18 Oct 2024 16:54:05 -0400 Subject: [PATCH 06/13] Update news.tsx --- apps/web/ui/layout/sidebar/news.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/ui/layout/sidebar/news.tsx b/apps/web/ui/layout/sidebar/news.tsx index b7e19f3c5a..81cda3499b 100644 --- a/apps/web/ui/layout/sidebar/news.tsx +++ b/apps/web/ui/layout/sidebar/news.tsx @@ -23,8 +23,8 @@ export function News({ articles }: { articles: NewsArticle[] }) { const cardCount = cards.length; return cards.length ? ( -
-
+
+
{cards.map(({ slug, type, title, summary, image, href }, idx) => (
Date: Fri, 18 Oct 2024 14:47:18 -0700 Subject: [PATCH 07/13] getContentAPI --- apps/web/lib/fetchers.ts | 8 ++++++ .../web/ui/layout/sidebar/help-button-rsc.tsx | 10 ++----- apps/web/ui/layout/sidebar/news-rsc.tsx | 27 +++++-------------- apps/web/ui/layout/sidebar/news.tsx | 7 +++-- 4 files changed, 20 insertions(+), 32 deletions(-) diff --git a/apps/web/lib/fetchers.ts b/apps/web/lib/fetchers.ts index f6f9f4dd3e..d46b7b9501 100644 --- a/apps/web/lib/fetchers.ts +++ b/apps/web/lib/fetchers.ts @@ -2,6 +2,14 @@ import { prisma } from "@/lib/prisma"; import { cache } from "react"; import { getSession } from "./auth"; +export const getContentAPI = cache(async () => { + return await fetch("https://dub.co/api/content", { + next: { + revalidate: 60 * 60 * 24, // cache for 24 hours + }, + }).then((res) => res.json()); +}); + export const getDefaultWorkspace = cache(async () => { const session = await getSession(); if (!session) { diff --git a/apps/web/ui/layout/sidebar/help-button-rsc.tsx b/apps/web/ui/layout/sidebar/help-button-rsc.tsx index 315497d919..4ddcfb197e 100644 --- a/apps/web/ui/layout/sidebar/help-button-rsc.tsx +++ b/apps/web/ui/layout/sidebar/help-button-rsc.tsx @@ -1,14 +1,8 @@ +import { getContentAPI } from "@/lib/fetchers"; import { HelpButton } from "./help-button"; export async function HelpButtonRSC() { - const { popularHelpArticles, allHelpArticles } = await fetch( - "https://dub.co/api/content", - { - next: { - revalidate: 60 * 60 * 24, // cache for 24 hours - }, - }, - ).then((res) => res.json()); + const { popularHelpArticles, allHelpArticles } = await getContentAPI(); return ( res.json()); + const { latestNewsArticles } = await getContentAPI(); return ( [] - ) - .slice(0, 6) // Limit to 6 latest articles - .map((article) => ({ - ...article, - href: `https://dub.co/${article.type === "ChangelogPost" ? "changelog" : "blog"}/${article.slug}`, - }))} + articles={ + (Array.isArray(latestNewsArticles) + ? latestNewsArticles + : []) as NewsArticle[] + } /> ); } diff --git a/apps/web/ui/layout/sidebar/news.tsx b/apps/web/ui/layout/sidebar/news.tsx index 81cda3499b..993e18aa7d 100644 --- a/apps/web/ui/layout/sidebar/news.tsx +++ b/apps/web/ui/layout/sidebar/news.tsx @@ -11,7 +11,6 @@ export interface NewsArticle { title: string; summary: string; image: string; - href: string; } const OFFSET_FACTOR = 4; @@ -25,9 +24,9 @@ export function News({ articles }: { articles: NewsArticle[] }) { return cards.length ? (
- {cards.map(({ slug, type, title, summary, image, href }, idx) => ( + {cards.map(({ slug, type, title, summary, image }, idx) => (
3 @@ -51,7 +50,7 @@ export function News({ articles }: { articles: NewsArticle[] }) { title={title} description={summary} image={image} - href={href} + href={slug} hideContent={cardCount - idx > 2} active={idx === cardCount - 1} onDismiss={() => { From 626ed4fc3c52bc8552cb81dfd910db0a46c0c45f Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Fri, 18 Oct 2024 19:43:28 -0400 Subject: [PATCH 08/13] Prevent image drag --- apps/web/ui/layout/sidebar/news.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/ui/layout/sidebar/news.tsx b/apps/web/ui/layout/sidebar/news.tsx index 993e18aa7d..fb329c2088 100644 --- a/apps/web/ui/layout/sidebar/news.tsx +++ b/apps/web/ui/layout/sidebar/news.tsx @@ -171,6 +171,7 @@ function NewsCard({ alt="" fill className="rounded object-cover object-center" + draggable={false} /> )}
From 4eee62969c04d2e5f753d668d6782a633c634b2e Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Fri, 18 Oct 2024 19:44:37 -0400 Subject: [PATCH 09/13] Reverse order --- apps/web/ui/layout/sidebar/news.tsx | 76 +++++++++++++++-------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/apps/web/ui/layout/sidebar/news.tsx b/apps/web/ui/layout/sidebar/news.tsx index fb329c2088..4f754ce56d 100644 --- a/apps/web/ui/layout/sidebar/news.tsx +++ b/apps/web/ui/layout/sidebar/news.tsx @@ -24,43 +24,45 @@ export function News({ articles }: { articles: NewsArticle[] }) { return cards.length ? (
- {cards.map(({ slug, type, title, summary, image }, idx) => ( -
3 - ? [ - "opacity-0 group-hover:translate-y-[var(--y)] group-hover:opacity-[var(--opacity)]", - "group-has-[*[data-dragging=true]]:translate-y-[var(--y)] group-has-[*[data-dragging=true]]:opacity-[var(--opacity)]", - ] - : "translate-y-[var(--y)] opacity-[var(--opacity)]", - )} - style={ - { - "--y": `-${(cardCount - (idx + 1)) * OFFSET_FACTOR}%`, - "--scale": 1 - (cardCount - (idx + 1)) * SCALE_FACTOR, - "--opacity": 1 - (cardCount - (idx + 1)) * OPACITY_FACTOR, - } as CSSProperties - } - aria-hidden={idx !== cardCount - 1} - > - 2} - active={idx === cardCount - 1} - onDismiss={() => { - setCards((cards) => - cards.filter((c) => c.slug !== slug || c.type !== type), - ); - }} - /> -
- ))} + {cards + .toReversed() + .map(({ slug, type, title, summary, image }, idx) => ( +
3 + ? [ + "opacity-0 group-hover:translate-y-[var(--y)] group-hover:opacity-[var(--opacity)]", + "group-has-[*[data-dragging=true]]:translate-y-[var(--y)] group-has-[*[data-dragging=true]]:opacity-[var(--opacity)]", + ] + : "translate-y-[var(--y)] opacity-[var(--opacity)]", + )} + style={ + { + "--y": `-${(cardCount - (idx + 1)) * OFFSET_FACTOR}%`, + "--scale": 1 - (cardCount - (idx + 1)) * SCALE_FACTOR, + "--opacity": 1 - (cardCount - (idx + 1)) * OPACITY_FACTOR, + } as CSSProperties + } + aria-hidden={idx !== cardCount - 1} + > + 2} + active={idx === cardCount - 1} + onDismiss={() => { + setCards((cards) => + cards.filter((c) => c.slug !== slug || c.type !== type), + ); + }} + /> +
+ ))}
Date: Fri, 18 Oct 2024 19:50:36 -0400 Subject: [PATCH 10/13] Fix card sizing --- apps/web/ui/layout/sidebar/news.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/web/ui/layout/sidebar/news.tsx b/apps/web/ui/layout/sidebar/news.tsx index 4f754ce56d..ef1ecbca17 100644 --- a/apps/web/ui/layout/sidebar/news.tsx +++ b/apps/web/ui/layout/sidebar/news.tsx @@ -30,7 +30,7 @@ export function News({ articles }: { articles: NewsArticle[] }) {
3 ? [ "opacity-0 group-hover:translate-y-[var(--y)] group-hover:opacity-[var(--opacity)]", @@ -64,10 +64,7 @@ export function News({ articles }: { articles: NewsArticle[] }) {
))}
- +
@@ -164,7 +161,9 @@ function NewsCard({ {title} -

{description}

+

+ {description} +

{image && ( From 4cd7757a79eaa979f3b2f9c738b145d6bf628a21 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Mon, 21 Oct 2024 10:52:07 -0400 Subject: [PATCH 11/13] Persist dismissed cards --- apps/web/ui/layout/sidebar/news-rsc.tsx | 17 +++-- apps/web/ui/layout/sidebar/news.tsx | 85 +++++++++++++------------ 2 files changed, 55 insertions(+), 47 deletions(-) diff --git a/apps/web/ui/layout/sidebar/news-rsc.tsx b/apps/web/ui/layout/sidebar/news-rsc.tsx index 796746f9a3..09bb308de2 100644 --- a/apps/web/ui/layout/sidebar/news-rsc.tsx +++ b/apps/web/ui/layout/sidebar/news-rsc.tsx @@ -1,16 +1,19 @@ import { getContentAPI } from "@/lib/fetchers"; +import { ClientOnly } from "@dub/ui"; import { News, NewsArticle } from "./news"; export async function NewsRSC() { const { latestNewsArticles } = await getContentAPI(); return ( - + + + ); } diff --git a/apps/web/ui/layout/sidebar/news.tsx b/apps/web/ui/layout/sidebar/news.tsx index ef1ecbca17..59351af23b 100644 --- a/apps/web/ui/layout/sidebar/news.tsx +++ b/apps/web/ui/layout/sidebar/news.tsx @@ -1,5 +1,6 @@ "use client"; +import { useLocalStorage } from "@dub/ui"; import { cn } from "@dub/utils"; import Image from "next/image"; import Link from "next/link"; @@ -7,7 +8,6 @@ import { CSSProperties, useRef, useState } from "react"; export interface NewsArticle { slug: string; - type: string; title: string; summary: string; image: string; @@ -18,51 +18,56 @@ const SCALE_FACTOR = 0.03; const OPACITY_FACTOR = 0.1; export function News({ articles }: { articles: NewsArticle[] }) { - const [cards, setCards] = useState(articles); + const [dismissedNews, setDismissedNews] = useLocalStorage( + "dismissed-news", + [], + ); + + const cards = articles.filter(({ slug }) => !dismissedNews.includes(slug)); const cardCount = cards.length; return cards.length ? (
- {cards - .toReversed() - .map(({ slug, type, title, summary, image }, idx) => ( -
3 - ? [ - "opacity-0 group-hover:translate-y-[var(--y)] group-hover:opacity-[var(--opacity)]", - "group-has-[*[data-dragging=true]]:translate-y-[var(--y)] group-has-[*[data-dragging=true]]:opacity-[var(--opacity)]", - ] - : "translate-y-[var(--y)] opacity-[var(--opacity)]", - )} - style={ - { - "--y": `-${(cardCount - (idx + 1)) * OFFSET_FACTOR}%`, - "--scale": 1 - (cardCount - (idx + 1)) * SCALE_FACTOR, - "--opacity": 1 - (cardCount - (idx + 1)) * OPACITY_FACTOR, - } as CSSProperties + {cards.toReversed().map(({ slug, title, summary, image }, idx) => ( +
3 + ? [ + "opacity-0 group-hover:translate-y-[var(--y)] group-hover:opacity-[var(--opacity)]", + "group-has-[*[data-dragging=true]]:translate-y-[var(--y)] group-has-[*[data-dragging=true]]:opacity-[var(--opacity)]", + ] + : "translate-y-[var(--y)] opacity-[var(--opacity)]", + )} + style={ + { + "--y": `-${(cardCount - (idx + 1)) * OFFSET_FACTOR}%`, + "--scale": 1 - (cardCount - (idx + 1)) * SCALE_FACTOR, + "--opacity": + // Hide cards that are too far down (will show top 6) + cardCount - (idx + 1) >= 6 + ? 0 + : 1 - (cardCount - (idx + 1)) * OPACITY_FACTOR, + } as CSSProperties + } + aria-hidden={idx !== cardCount - 1} + > + 2} + active={idx === cardCount - 1} + onDismiss={ + () => setDismissedNews([slug, ...dismissedNews.slice(0, 50)]) // Limit to keep storage size low } - aria-hidden={idx !== cardCount - 1} - > - 2} - active={idx === cardCount - 1} - onDismiss={() => { - setCards((cards) => - cards.filter((c) => c.slug !== slug || c.type !== type), - ); - }} - /> -
- ))} + /> +
+ ))}
From 023c2e0ec9d29be66c0d77c599cbba76e81f5d94 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Mon, 21 Oct 2024 11:24:32 -0400 Subject: [PATCH 12/13] Improve animations --- apps/web/ui/layout/sidebar/news.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/web/ui/layout/sidebar/news.tsx b/apps/web/ui/layout/sidebar/news.tsx index 59351af23b..9412c47ba2 100644 --- a/apps/web/ui/layout/sidebar/news.tsx +++ b/apps/web/ui/layout/sidebar/news.tsx @@ -109,9 +109,12 @@ function NewsCard({ const dismiss = () => { if (!ref.current) return; + const cardWidth = ref.current.getBoundingClientRect().width; + const translateX = Math.sign(drag.current.delta) * cardWidth; + // Dismiss card animation.current = ref.current.animate( - { opacity: 0 }, + { opacity: 0, transform: `translateX(${translateX}px)` }, { duration: 150, easing: "ease-in-out", fill: "forwards" }, ); animation.current.onfinish = () => onDismiss?.(); @@ -140,7 +143,8 @@ function NewsCard({ }; const onPointerDown = (e: React.PointerEvent) => { - if (!ref.current || animation.current?.playState === "running") return; + if (!active || !ref.current || animation.current?.playState === "running") + return; document.addEventListener("pointermove", onDragMove); document.addEventListener("pointerup", onDragEnd); From 171073b95fa413df6a40235351c31bc72e7c6fd2 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Mon, 21 Oct 2024 14:44:38 -0400 Subject: [PATCH 13/13] Update properties and improve touch interactions --- apps/web/ui/layout/sidebar/news.tsx | 81 ++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/apps/web/ui/layout/sidebar/news.tsx b/apps/web/ui/layout/sidebar/news.tsx index 9412c47ba2..be77ab1771 100644 --- a/apps/web/ui/layout/sidebar/news.tsx +++ b/apps/web/ui/layout/sidebar/news.tsx @@ -1,13 +1,13 @@ "use client"; -import { useLocalStorage } from "@dub/ui"; +import { useLocalStorage, useMediaQuery } from "@dub/ui"; import { cn } from "@dub/utils"; import Image from "next/image"; import Link from "next/link"; import { CSSProperties, useRef, useState } from "react"; export interface NewsArticle { - slug: string; + href: string; title: string; summary: string; image: string; @@ -23,21 +23,21 @@ export function News({ articles }: { articles: NewsArticle[] }) { [], ); - const cards = articles.filter(({ slug }) => !dismissedNews.includes(slug)); + const cards = articles.filter(({ href }) => !dismissedNews.includes(href)); const cardCount = cards.length; return cards.length ? (
- {cards.toReversed().map(({ slug, title, summary, image }, idx) => ( + {cards.toReversed().map(({ href, title, summary, image }, idx) => (
3 ? [ - "opacity-0 group-hover:translate-y-[var(--y)] group-hover:opacity-[var(--opacity)]", - "group-has-[*[data-dragging=true]]:translate-y-[var(--y)] group-has-[*[data-dragging=true]]:opacity-[var(--opacity)]", + "opacity-0 sm:group-hover:translate-y-[var(--y)] sm:group-hover:opacity-[var(--opacity)]", + "sm:group-has-[*[data-dragging=true]]:translate-y-[var(--y)] sm:group-has-[*[data-dragging=true]]:opacity-[var(--opacity)]", ] : "translate-y-[var(--y)] opacity-[var(--opacity)]", )} @@ -59,16 +59,16 @@ export function News({ articles }: { articles: NewsArticle[] }) { title={title} description={summary} image={image} - href={slug} + href={href} hideContent={cardCount - idx > 2} active={idx === cardCount - 1} onDismiss={ - () => setDismissedNews([slug, ...dismissedNews.slice(0, 50)]) // Limit to keep storage size low + () => setDismissedNews([href, ...dismissedNews.slice(0, 50)]) // Limit to keep storage size low } />
))} -
+
@@ -93,8 +93,20 @@ function NewsCard({ href?: string; active?: boolean; }) { + const { isMobile } = useMediaQuery(); + const ref = useRef(null); - const drag = useRef<{ start: number; delta: number }>({ start: 0, delta: 0 }); + const drag = useRef<{ + start: number; + delta: number; + startTime: number; + maxDelta: number; + }>({ + start: 0, + delta: 0, + startTime: 0, + maxDelta: 0, + }); const animation = useRef(); const [dragging, setDragging] = useState(false); @@ -103,6 +115,7 @@ function NewsCard({ const { clientX } = e; const dx = clientX - drag.current.start; drag.current.delta = dx; + drag.current.maxDelta = Math.max(drag.current.maxDelta, Math.abs(dx)); ref.current.style.setProperty("--dx", dx.toString()); }; @@ -120,19 +133,17 @@ function NewsCard({ animation.current.onfinish = () => onDismiss?.(); }; - const onDragEnd = () => { + const stopDragging = (cancelled: boolean) => { if (!ref.current) return; - document.removeEventListener("pointermove", onDragMove); - document.removeEventListener("pointerup", onDragEnd); + unbindListeners(); + setDragging(false); const dx = drag.current.delta; - if (Math.abs(dx) > ref.current.clientWidth / 3) { + if (Math.abs(dx) > ref.current.clientWidth / (cancelled ? 2 : 3)) { dismiss(); return; } - setDragging(false); - // Animate back to original position animation.current = ref.current.animate( { transform: "translateX(0)" }, @@ -140,20 +151,49 @@ function NewsCard({ ); animation.current.onfinish = () => ref.current?.style.setProperty("--dx", "0"); + + drag.current = { start: 0, delta: 0, startTime: 0, maxDelta: 0 }; }; + const onDragEnd = () => stopDragging(false); + const onDragCancel = () => stopDragging(true); + const onPointerDown = (e: React.PointerEvent) => { if (!active || !ref.current || animation.current?.playState === "running") return; - document.addEventListener("pointermove", onDragMove); - document.addEventListener("pointerup", onDragEnd); + bindListeners(); setDragging(true); drag.current.start = e.clientX; + drag.current.startTime = Date.now(); drag.current.delta = 0; ref.current.style.setProperty("--w", ref.current.clientWidth.toString()); }; + const onClick = () => { + if (!ref.current) return; + if ( + isMobile && + drag.current.maxDelta < ref.current.clientWidth / 10 && + (!drag.current.startTime || Date.now() - drag.current.startTime < 250) + ) { + // Touch user didn't drag far or for long, open the link + window.open(href, "_blank"); + } + }; + + const bindListeners = () => { + document.addEventListener("pointermove", onDragMove); + document.addEventListener("pointerup", onDragEnd); + document.addEventListener("pointercancel", onDragCancel); + }; + + const unbindListeners = () => { + document.removeEventListener("pointermove", onDragMove); + document.removeEventListener("pointerup", onDragEnd); + document.removeEventListener("pointercancel", onDragCancel); + }; + return (
@@ -188,7 +229,7 @@ function NewsCard({