mirror of
https://github.com/RSSNext/Folo.git
synced 2026-10-29 12:12:59 +08:00
feat(ssr): optimize entries styles
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
import type { EntriesPreview } from "@client/query/entries"
|
||||
import type { Feed } from "@client/query/feed"
|
||||
import { TitleMarquee } from "@follow/components/ui/marquee/index.jsx"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import dayjs from "dayjs"
|
||||
import type { FC } from "react"
|
||||
|
||||
import { TeleportalTakeOff } from "../layout/main/teleportal"
|
||||
import { FeedIcon } from "../ui/feed-icon"
|
||||
import { LazyImage } from "../ui/image"
|
||||
|
||||
@@ -14,41 +12,22 @@ export const GridList: FC<{
|
||||
feed?: Feed
|
||||
}> = ({ entries, feed }) => {
|
||||
return (
|
||||
<TeleportalTakeOff>
|
||||
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 lg:px-3">
|
||||
{entries.map((entry) => (
|
||||
<Wrapper key={entry.id} href={entry.url || entry.authorUrl || ""}>
|
||||
<div className="relative -mx-1.5 -mt-1.5">
|
||||
<LazyImage
|
||||
src={entry.media?.[0]!.url}
|
||||
className="aspect-video h-auto w-full shrink-0 rounded-md object-cover"
|
||||
/>
|
||||
</div>
|
||||
<GridItemFooter feed={feed} entryId={entry.id} entryPreview={entry} />
|
||||
</Wrapper>
|
||||
))}
|
||||
</div>
|
||||
</TeleportalTakeOff>
|
||||
)
|
||||
}
|
||||
|
||||
const Wrapper: FC<{
|
||||
children: React.ReactNode
|
||||
href: string
|
||||
wrapperClassName?: string
|
||||
}> = ({ children, href, wrapperClassName }) => {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className={cn(
|
||||
"hover:bg-material-medium overflow-hidden rounded-md p-1.5 duration-200",
|
||||
wrapperClassName,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 lg:px-3">
|
||||
{entries.map((entry) => (
|
||||
<div
|
||||
className="hover:bg-material-medium overflow-hidden rounded-md p-1.5 duration-200"
|
||||
key={entry.id}
|
||||
>
|
||||
<div className="relative -mx-1.5 -mt-1.5">
|
||||
<LazyImage
|
||||
src={entry.media?.[0]!.url}
|
||||
className="aspect-video h-auto w-full shrink-0 rounded-md object-cover"
|
||||
/>
|
||||
</div>
|
||||
<GridItemFooter feed={feed} entryId={entry.id} entryPreview={entry} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ const NormalList: FC<{
|
||||
return (
|
||||
<>
|
||||
{entries?.map((entry) => (
|
||||
<a className="relative" href={entry.url || void 0} target="_blank" key={entry.id}>
|
||||
<div className="relative cursor-default" key={entry.id}>
|
||||
<NormalListItem
|
||||
withDetails
|
||||
entryId={entry.id}
|
||||
@@ -61,7 +61,7 @@ const NormalList: FC<{
|
||||
feedId: feed?.feed.id || entry.feeds?.id!,
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -23,57 +23,42 @@ function NormalListItemImpl({
|
||||
const displayTime = entry.entries.publishedAt
|
||||
|
||||
return (
|
||||
<MagneticHoverEffect className={"group relative flex gap-2 py-4 pl-3 pr-2"}>
|
||||
<MagneticHoverEffect className={"group relative mx-auto flex max-w-3xl gap-2 py-4 pl-3 pr-2"}>
|
||||
<FeedIcon feed={feed} fallback entry={entry.entries} />
|
||||
<div className={"-mt-0.5 flex-1 text-sm leading-tight"}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-1 text-[10px] font-bold",
|
||||
"text-text-secondary",
|
||||
entry.collections && "text-zinc-600 dark:text-zinc-500",
|
||||
)}
|
||||
>
|
||||
<div className={"-mt-0.5 flex-1 text-base leading-tight"}>
|
||||
<div className={cn("flex gap-1 text-xs font-bold", "text-text-secondary")}>
|
||||
<EllipsisHorizontalTextWithTooltip className="truncate">
|
||||
{feed?.title}
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
<span>·</span>
|
||||
<span className="shrink-0">{!!displayTime && <RelativeTime date={displayTime} />}</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"text-text relative my-0.5 line-clamp-2 break-words",
|
||||
!!entry.collections && "pr-5",
|
||||
entry.entries.title ? withDetails && "font-medium" : "text-[13px]",
|
||||
)}
|
||||
>
|
||||
<div className={cn("text-text relative my-0.5 line-clamp-1 break-words font-medium")}>
|
||||
{entry.entries.title}
|
||||
|
||||
{/* {!!entry.collections && <StarIcon className="absolute right-0 top-0" />} */}
|
||||
</div>
|
||||
{withDetails && (
|
||||
<div className="flex gap-2">
|
||||
<div className={cn("grow text-[13px]", "text-text-secondary line-clamp-5")}>
|
||||
<div className={cn("grow text-sm", "text-text-secondary line-clamp-3")}>
|
||||
{entry.entries.description}
|
||||
</div>
|
||||
|
||||
{entry.entries.media?.[0] && (
|
||||
<div className="relative size-24 shrink-0 overflow-hidden rounded">
|
||||
<LazyImage
|
||||
proxy={{
|
||||
width: 160,
|
||||
height: 160,
|
||||
}}
|
||||
className="overflow-hidden rounded"
|
||||
src={entry.entries.media[0].url}
|
||||
height={entry.entries.media[0].height}
|
||||
width={entry.entries.media[0].width}
|
||||
blurhash={entry.entries.media[0].blurhash}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{entry.entries.media?.[0] && (
|
||||
<div className="relative size-24 shrink-0 overflow-hidden rounded">
|
||||
<LazyImage
|
||||
proxy={{
|
||||
width: 160,
|
||||
height: 160,
|
||||
}}
|
||||
className="overflow-hidden rounded-lg"
|
||||
src={entry.entries.media[0].url}
|
||||
height={entry.entries.media[0].height}
|
||||
width={entry.entries.media[0].width}
|
||||
blurhash={entry.entries.media[0].blurhash}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</MagneticHoverEffect>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { TeleportalTakeOff } from "@client/components/layout/main/teleportal"
|
||||
import { LazyImage } from "@client/components/ui/image"
|
||||
import { getPreferredTitle } from "@client/lib/helper"
|
||||
import type { EntriesPreview } from "@client/query/entries"
|
||||
@@ -75,7 +74,7 @@ const breakpoints = {
|
||||
// 48rem => 48 * 16= 768
|
||||
768: 3,
|
||||
// 72rem => 72 * 16= 1152
|
||||
1152: 4,
|
||||
976: 4,
|
||||
// 80rem => 80 * 16= 1280
|
||||
1280: 5,
|
||||
1536: 6,
|
||||
@@ -173,33 +172,31 @@ export const PictureList: FC<{
|
||||
|
||||
return (
|
||||
<PhotoProvider>
|
||||
<TeleportalTakeOff>
|
||||
<div className="relative flex min-w-0 px-2 lg:px-6">
|
||||
<MemoedDangerousHTMLStyle>{inlineStyle}</MemoedDangerousHTMLStyle>
|
||||
<div className="flex w-full flex-wrap" ref={containerRef}>
|
||||
{isInitLayout && (
|
||||
<MasonryItemWidthContext value={currentItemWidth}>
|
||||
{/* eslint-disable-next-line @eslint-react/no-context-provider */}
|
||||
<MasonryItemsAspectRatioContext.Provider value={masonryItemsRadio}>
|
||||
<MasonryItemsAspectRatioSetterContext value={setMasonryItemsRadio}>
|
||||
<div className="relative w-full">
|
||||
<Masonry
|
||||
items={items}
|
||||
columnGutter={yGutter}
|
||||
columnWidth={currentItemWidth}
|
||||
columnCount={currentColumn}
|
||||
overscanBy={2}
|
||||
render={render}
|
||||
itemKey={itemKey}
|
||||
/>
|
||||
</div>
|
||||
</MasonryItemsAspectRatioSetterContext>
|
||||
</MasonryItemsAspectRatioContext.Provider>
|
||||
</MasonryItemWidthContext>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative flex min-w-0 px-2 lg:px-6">
|
||||
<MemoedDangerousHTMLStyle>{inlineStyle}</MemoedDangerousHTMLStyle>
|
||||
<div className="flex w-full flex-wrap" ref={containerRef}>
|
||||
{isInitLayout && (
|
||||
<MasonryItemWidthContext value={currentItemWidth}>
|
||||
{/* eslint-disable-next-line @eslint-react/no-context-provider */}
|
||||
<MasonryItemsAspectRatioContext.Provider value={masonryItemsRadio}>
|
||||
<MasonryItemsAspectRatioSetterContext value={setMasonryItemsRadio}>
|
||||
<div className="relative w-full">
|
||||
<Masonry
|
||||
items={items}
|
||||
columnGutter={yGutter}
|
||||
columnWidth={currentItemWidth}
|
||||
columnCount={currentColumn}
|
||||
overscanBy={2}
|
||||
render={render}
|
||||
itemKey={itemKey}
|
||||
/>
|
||||
</div>
|
||||
</MasonryItemsAspectRatioSetterContext>
|
||||
</MasonryItemsAspectRatioContext.Provider>
|
||||
</MasonryItemWidthContext>
|
||||
)}
|
||||
</div>
|
||||
</TeleportalTakeOff>
|
||||
</div>
|
||||
</PhotoProvider>
|
||||
)
|
||||
}
|
||||
@@ -226,7 +223,7 @@ const render: React.ComponentType<
|
||||
ratio={data.height && data.width ? data.height / data.width : undefined}
|
||||
>
|
||||
<div
|
||||
className="overflow-hidden rounded-md"
|
||||
className="size-full overflow-hidden rounded-md"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { atom, useAtomValue, useSetAtom } from "jotai"
|
||||
import type { FC, PropsWithChildren } from "react"
|
||||
import { createPortal } from "react-dom"
|
||||
|
||||
const portalAtoms = atom(null as HTMLElement | null)
|
||||
export const TeleportalDestination = () => {
|
||||
const setPortal = useSetAtom(portalAtoms)
|
||||
|
||||
return <div className="flex flex-col" ref={setPortal} />
|
||||
}
|
||||
|
||||
export const TeleportalTakeOff: FC<PropsWithChildren> = ({ children }) => {
|
||||
const portal = useAtomValue(portalAtoms)
|
||||
|
||||
if (!portal) return null
|
||||
|
||||
return createPortal(children, portal)
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export const LazyImage = ({
|
||||
src={nextSrc}
|
||||
height={height}
|
||||
width={width}
|
||||
className={cn("size-full", className)}
|
||||
className={cn("size-full object-cover", className)}
|
||||
onClick={onClick}
|
||||
tabIndex={1}
|
||||
/>
|
||||
|
||||
@@ -100,7 +100,7 @@ export function Component() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn("w-full pb-12 pt-8", "flex max-w-3xl flex-col gap-2")}>
|
||||
<div className={cn("w-full pb-12 pt-8", "flex flex-col gap-2")}>
|
||||
{entries.isLoading && !entries.data ? (
|
||||
<LoadingCircle size="large" className="center mt-12" />
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Header } from "@client/components/layout/header"
|
||||
import { TeleportalDestination } from "@client/components/layout/main/teleportal"
|
||||
import { MemoedDangerousHTMLStyle } from "@follow/components/common/MemoedDangerousHTMLStyle.jsx"
|
||||
import { PoweredByFooter } from "@follow/components/common/PoweredByFooter.jsx"
|
||||
import { Outlet } from "react-router"
|
||||
@@ -16,7 +15,6 @@ export const Component = () => {
|
||||
<main className="relative mx-auto w-full max-w-[var(--container-max-width)] flex-1">
|
||||
<Outlet />
|
||||
</main>
|
||||
<TeleportalDestination />
|
||||
<PoweredByFooter />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import * as React from "react"
|
||||
import { useCallback, useImperativeHandle, useRef } from "react"
|
||||
import { useImperativeHandle, useRef } from "react"
|
||||
|
||||
type MagneticHoverEffectProps<T extends React.ElementType> = {
|
||||
as?: T
|
||||
@@ -22,47 +22,13 @@ export const MagneticHoverEffect = <T extends React.ElementType = "div">({
|
||||
return itemRef.current as unknown as T
|
||||
})
|
||||
|
||||
const handleMouseEnter = (e: React.MouseEvent<HTMLElement>) => {
|
||||
if (!itemRef.current) return
|
||||
const rect = itemRef.current.getBoundingClientRect()
|
||||
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 100
|
||||
const y = ((e.clientY - rect.top) / rect.height) * 100
|
||||
|
||||
itemRef.current.style.transition = "transform 0.2s cubic-bezier(0.33, 1, 0.68, 1)"
|
||||
itemRef.current.style.setProperty("--origin-x", `${x}%`)
|
||||
itemRef.current.style.setProperty("--origin-y", `${y}%`)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!itemRef.current) return
|
||||
itemRef.current.style.transform = "translate(0px, 0px)"
|
||||
itemRef.current.style.transition = "transform 0.4s cubic-bezier(0.33, 1, 0.68, 1)"
|
||||
}
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
if (!itemRef.current) return
|
||||
const rect = itemRef.current.getBoundingClientRect()
|
||||
|
||||
const centerX = rect.left + rect.width / 2
|
||||
const centerY = rect.top + rect.height / 2
|
||||
|
||||
const distanceX = (e.clientX - centerX) * 0.05
|
||||
const distanceY = (e.clientY - centerY) * 0.05
|
||||
|
||||
itemRef.current.style.transform = `translate(${distanceX}px, ${distanceY}px)`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={itemRef as any}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseMove={handleMouseMove}
|
||||
{...rest}
|
||||
className={cn(
|
||||
"relative",
|
||||
"inline-block transition-transform duration-200 ease-out will-change-transform",
|
||||
"inline-block duration-200 ease-out",
|
||||
"hover:before:bg-material-ultra-thick",
|
||||
"before:backdrop-blur-background before:absolute before:-inset-x-2 before:inset-y-0 before:z-[-1] before:scale-0 before:rounded-xl before:opacity-0 before:transition-all before:duration-200 before:[transform-origin:var(--origin-x)_var(--origin-y)] hover:before:scale-100 hover:before:opacity-100",
|
||||
rest.className,
|
||||
|
||||
Reference in New Issue
Block a user