feat: fix feeds and add dynamic fetching

This commit is contained in:
Juan Di Toro 2023-11-22 12:27:11 +01:00
parent a7d124073f
commit f44a5a35df
5 changed files with 471 additions and 175 deletions

208
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@radix-ui/react-scroll-area": "^1.0.5",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"next": "14.0.2", "next": "14.0.2",
"react": "^18", "react": "^18",
@ -388,6 +389,203 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@radix-ui/number": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz",
"integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==",
"dependencies": {
"@babel/runtime": "^7.13.10"
}
},
"node_modules/@radix-ui/primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
"integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==",
"dependencies": {
"@babel/runtime": "^7.13.10"
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
"integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz",
"integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz",
"integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.5.tgz",
"integrity": "sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/number": "1.0.1",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@rushstack/eslint-patch": { "node_modules/@rushstack/eslint-patch": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz",
@ -421,13 +619,13 @@
"version": "15.7.10", "version": "15.7.10",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz",
"integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==", "integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==",
"dev": true "devOptional": true
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.2.37", "version": "18.2.37",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.37.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.37.tgz",
"integrity": "sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==", "integrity": "sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -438,7 +636,7 @@
"version": "18.2.15", "version": "18.2.15",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.15.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.15.tgz",
"integrity": "sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==", "integrity": "sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/react": "*" "@types/react": "*"
} }
@ -447,7 +645,7 @@
"version": "0.16.6", "version": "0.16.6",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.6.tgz", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.6.tgz",
"integrity": "sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==", "integrity": "sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==",
"dev": true "devOptional": true
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "6.10.0", "version": "6.10.0",
@ -1124,7 +1322,7 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
"dev": true "devOptional": true
}, },
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",

View File

@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@radix-ui/react-scroll-area": "^1.0.5",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"next": "14.0.2", "next": "14.0.2",
"react": "^18", "react": "^18",

View File

@ -1,9 +1,5 @@
import Feed from "@/components/Feed"
import SearchBar from "@/components/SearchBar" import SearchBar from "@/components/SearchBar"
import Image from "next/image"
const newsItems = [
{ id: 123, timestamp: Date.now(), title: "Test", description: "Well hello" }
]
export default function Home() { export default function Home() {
return ( return (
@ -11,171 +7,28 @@ export default function Home() {
<SearchBar /> <SearchBar />
<div className="space-y-8 w-full my-10"> <div className="space-y-8 w-full my-10">
<div className="flex flex-row flex-wrap justify-between w-full"> <div className="flex flex-row flex-wrap justify-between w-full">
<Feed title="Latest from the community" icon={PaperIcon} className="w-[calc(33%-16px)] max-w-md" /> <Feed
<Feed title="Rising Posts" icon={TrendUpIcon} className="w-[calc(33%-16px)] max-w-md" /> title="Latest from the community"
<Feed title="Top Posts" icon={TopArrowLodashIcon} className="w-[calc(33%-16px)] max-w-md" /> icon={'paper-icon'}
className="w-[calc(33%-16px)] max-w-md"
/>
<Feed
title="Rising Posts"
icon={'trend-up-icon'}
className="w-[calc(33%-16px)] max-w-md"
/>
<Feed
title="Top Posts"
icon={'top-arrow-icon'}
className="w-[calc(33%-16px)] max-w-md"
/>
</div> </div>
<Feed title="Another heading" icon={TrendUpIcon} className="w-full" variant="row" /> <Feed
title="Another heading"
icon={'trend-up-icon'}
className="w-full"
/>
</div> </div>
</> </>
) )
} }
const Feed = ({
className,
variant = "col",
icon: Icon,
title
}: {
className?: string
variant?: "row" | "col"
title: string
icon: (props: React.HTMLAttributes<SVGAElement>) => React.ReactNode | JSX.Element
}) => {
return (
<section className={`flex flex-col space-y-6 ${className}`}>
<header className="flex flex-row space-x-3 items-start">
<Icon className="text-primary mt-1" />
<nav>
<h3 className="text-primary text-xl">{title}</h3>
<ul className="text-gray-400 text-sm list-none [&>li:hover]:cursor-pointer [&>li:hover]:text-white">
<li className="text-white inline after:content-['/'] after:mx-1 after:text-gray-400">
latest
</li>
<li className="text-current inline after:content-['/'] after:mx-1 after:text-gray-400">
day
</li>
<li className="text-current inline after:content-['/'] after:mx-1 after:text-gray-400">
week
</li>
<li className="text-current inline">month</li>
</ul>
</nav>
</header>
<div className={`w-full flex gap-4 max-h-[400px] flex-${variant} ${variant === 'col' ? "overflow-y-auto" : "overflow-x-auto"}`}>
<article className="flex bg-gray-800 flex-col justify-between w-full py-4 px-6 rounded">
<span className="inline-block text-gray-500 w-full flex-1">
1h ago
</span>
<p className="inline-block text-white w-[25ch] flex-auto">
Bitcoin (BTC) Price Prediction: When Will Bitcoin Reach $100,000?
</p>
</article>
<article className="flex bg-gray-800 flex-col justify-between w-full py-4 px-6 rounded">
<span className="inline-block text-gray-500 w-full flex-1">
1h ago
</span>
<p className="inline-block text-white w-[25ch] flex-auto">
Bitcoin (BTC) Price Prediction: When Will Bitcoin Reach $100,000?
</p>
</article>
<article className="flex bg-gray-800 flex-col justify-between w-full py-4 px-6 rounded">
<span className="inline-block text-gray-500 w-full flex-1">
1h ago
</span>
<p className="inline-block text-white w-[25ch] flex-auto">
Bitcoin (BTC) Price Prediction: When Will Bitcoin Reach $100,000?
</p>
</article>
<article className="flex bg-gray-800 flex-col justify-between w-full py-4 px-6 rounded">
<span className="inline-block text-gray-500 w-full flex-1">
1h ago
</span>
<p className="inline-block text-white w-[25ch] flex-auto">
Bitcoin (BTC) Price Prediction: When Will Bitcoin Reach $100,000?
</p>
</article>
<article className="flex bg-gray-800 flex-col justify-between w-full py-4 px-6 rounded">
<span className="inline-block text-gray-500 w-full flex-1">
1h ago
</span>
<p className="inline-block text-white w-[25ch] flex-auto">
Bitcoin (BTC) Price Prediction: When Will Bitcoin Reach $100,000?
</p>
</article>
</div>
</section>
)
}
const PaperIcon = ({ className }: { className?: string }) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
className={className}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.75 0.75V23.25H20.25V0.75H3.75ZM6.46729 2.95093H9.23364C9.64819 2.95093 9.98364 3.28674 9.98364 3.70093C9.98364 4.11511 9.64819 4.45093 9.23364 4.45093H6.46729C6.05273 4.45093 5.71729 4.11511 5.71729 3.70093C5.71729 3.28674 6.05273 2.95093 6.46729 2.95093ZM17.5327 16.9918H6.46729C6.05273 16.9918 5.71729 16.656 5.71729 16.2418C5.71729 15.8276 6.05273 15.4918 6.46729 15.4918H17.5327C17.9473 15.4918 18.2827 15.8276 18.2827 16.2418C18.2827 16.656 17.9473 16.9918 17.5327 16.9918ZM17.5327 14.1639H6.46729C6.05273 14.1639 5.71729 13.8281 5.71729 13.4139C5.71729 12.9998 6.05273 12.6639 6.46729 12.6639H17.5327C17.9473 12.6639 18.2827 12.9998 18.2827 13.4139C18.2827 13.8281 17.9473 14.1639 17.5327 14.1639ZM17.5327 11.3361H6.46729C6.05273 11.3361 5.71729 11.0002 5.71729 10.5861C5.71729 10.1719 6.05273 9.83606 6.46729 9.83606H17.5327C17.9473 9.83606 18.2827 10.1719 18.2827 10.5861C18.2827 11.0002 17.9473 11.3361 17.5327 11.3361ZM17.5327 8.50818H9.23364C8.81909 8.50818 8.48364 8.17236 8.48364 7.75818C8.48364 7.34399 8.81909 7.00818 9.23364 7.00818H17.5327C17.9473 7.00818 18.2827 7.34399 18.2827 7.75818C18.2827 8.17236 17.9473 8.50818 17.5327 8.50818Z"
fill="currentColor"
/>
</svg>
)
}
const TopArrowLodashIcon = ({ className }: { className?: string }) => {
return (
<svg
className={className}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.0001 4.97949L16.7458 9.72518H7.25439L12.0001 4.97949Z"
fill="currentColor"
/>
<path
d="M12.7896 22.0004V8.23828H11.2106V22.0004H12.7896Z"
fill="currentColor"
/>
<path
d="M16.7456 3.57846V1.99951L7.25423 1.99951V3.57846L16.7456 3.57846Z"
fill="currentColor"
/>
</svg>
)
}
const TrendUpIcon = ({ className }: { className?: string }) => {
return (
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
className={className}
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_61_503)">
<path
d="M23.0769 6L13.5769 15.5L8.5769 10.5L1.0769 18"
stroke="#ACF9C0"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M17.0769 6H23.0769V12"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_61_503">
<rect
width="24"
height="24"
fill="white"
transform="translate(0.0769043)"
/>
</clipPath>
</defs>
</svg>
)
}

243
src/components/Feed.tsx Normal file
View File

@ -0,0 +1,243 @@
"use client"
import { formatDate } from "@/utils"
import * as ScrollArea from "@radix-ui/react-scroll-area"
import { sub } from "date-fns"
import { useState, useCallback, useEffect } from "react"
const Feed = ({
className,
variant = "col",
icon,
title
}: {
className?: string
variant?: "row" | "col"
title: string
icon: keyof typeof ICON_DICT
}) => {
const filters = ["latest", "day", "week", "month"] as const
const [dataResponse, setDataResponse] = useState<Awaited<ReturnType<typeof fetchFeedData>>>();
const [content, setContent] = useState<NonNullable<typeof dataResponse>['data']>([])
const [selectedFilter, setSelectedFilter] =
useState<(typeof filters)[number]>("latest")
const Icon = ICON_DICT[icon]
const fetchContent = useCallback(
async (overwrite: boolean = false) => {
const response = await fetchFeedData({
filter: { timerange: selectedFilter },
next: dataResponse?.next,
current: dataResponse?.current,
limit: 5
})
setDataResponse(response)
setContent((current) => {
if (overwrite) {
return response.data
}
return [...current, ...response.data]
})
console.log("Fetched data")
},
[setContent, dataResponse, selectedFilter]
)
const handleFilterChange = (filter: (typeof filters)[number]) => {
setSelectedFilter(filter)
fetchContent(true)
}
useEffect(() => {
fetchContent()
}, [])
return (
<section className={`w-full h-full space-y-6 ${className}`}>
<header className="flex flex-row space-x-3 items-start">
<Icon className="text-primary mt-1" />
<nav>
<h3 className="text-primary text-xl">{title}</h3>
<ul className="text-gray-400 text-sm list-none [&>li:hover]:text-white">
{filters.map((filter, index) => (
<li
key={index}
className={`inline cursor-pointer ${
index === filters.length - 1
? ""
: "after:content-['/'] after:mx-1 after:text-gray-400"
} ${
filter === selectedFilter ? "text-white" : "text-gray-400"
}`}
onClick={() => handleFilterChange(filter)}
>
{filter}
</li>
))}
</ul>
</nav>
</header>
<ScrollArea.Root className={`overflow-hidden w-full h-[400px] rounded-md`}>
<ScrollArea.Viewport className="w-full h-full">
<div className={`flex gap-4 flex-${variant}`}>
{content.map((item, index) => {
return (
<article
key={index}
className="flex bg-gray-800 flex-col justify-between w-full py-4 px-6 rounded"
>
<span className="inline-block text-gray-500 w-full flex-1">
{formatDate(item.date)}
</span>
<p className="inline-block text-white w-[25ch] flex-auto">
{item.content}
</p>
</article>
)
})}
{dataResponse?.next ? <button
className="bg-gray-600 text-gray-300 rounded-md p-2 px-4"
onClick={() => fetchContent()}
>
Fetch more
</button> : null}
</div>
</ScrollArea.Viewport>
<ScrollArea.ScrollAreaScrollbar orientation="vertical" className="flex h-full select-none touch-none p-0.5 bg-gray-500 transition-colors duration-[160ms] ease-out hover:bg-gray-700 data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:h-2.5">
<ScrollArea.ScrollAreaThumb className="flex-1 bg-gray-400 rounded-[10px] relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.ScrollAreaScrollbar>
</ScrollArea.Root>
</section>
)
}
const data = Array.from({ length: 20 }, (_, i) => ({
id: i,
date: sub(new Date(), { days: i }).toISOString(),
content: `Content ${i}`
}))
// Filter data by timerange and pagination
// Randomly sort the data
const randomlySortedData = data.sort(() => Math.random() - 0.5)
type ApiResponse<T = Record<string, any>> = {
data: T[],
current: number,
next?: number | null,
amount?: number
}
async function fetchFeedData({
filter,
limit = 5,
next = 5,
current = 0
}: {
filter?: { timerange?: "latest" | "day" | "week" | "month" }
next?: number
limit?: number
current?: number
}): Promise<ApiResponse<typeof data[number]>> {
const data = filter?.timerange
? randomlySortedData.filter(() => Math.random() > 0.5)
: randomlySortedData
const sliced = data.slice(current, next)
const nextPointer = sliced.length >= limit ? next + limit : null
return {
data: sliced,
current: next,
next: nextPointer,
amount: sliced.length
}
}
export default Feed
const PaperIcon = ({ className }: { className?: string }) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
className={className}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.75 0.75V23.25H20.25V0.75H3.75ZM6.46729 2.95093H9.23364C9.64819 2.95093 9.98364 3.28674 9.98364 3.70093C9.98364 4.11511 9.64819 4.45093 9.23364 4.45093H6.46729C6.05273 4.45093 5.71729 4.11511 5.71729 3.70093C5.71729 3.28674 6.05273 2.95093 6.46729 2.95093ZM17.5327 16.9918H6.46729C6.05273 16.9918 5.71729 16.656 5.71729 16.2418C5.71729 15.8276 6.05273 15.4918 6.46729 15.4918H17.5327C17.9473 15.4918 18.2827 15.8276 18.2827 16.2418C18.2827 16.656 17.9473 16.9918 17.5327 16.9918ZM17.5327 14.1639H6.46729C6.05273 14.1639 5.71729 13.8281 5.71729 13.4139C5.71729 12.9998 6.05273 12.6639 6.46729 12.6639H17.5327C17.9473 12.6639 18.2827 12.9998 18.2827 13.4139C18.2827 13.8281 17.9473 14.1639 17.5327 14.1639ZM17.5327 11.3361H6.46729C6.05273 11.3361 5.71729 11.0002 5.71729 10.5861C5.71729 10.1719 6.05273 9.83606 6.46729 9.83606H17.5327C17.9473 9.83606 18.2827 10.1719 18.2827 10.5861C18.2827 11.0002 17.9473 11.3361 17.5327 11.3361ZM17.5327 8.50818H9.23364C8.81909 8.50818 8.48364 8.17236 8.48364 7.75818C8.48364 7.34399 8.81909 7.00818 9.23364 7.00818H17.5327C17.9473 7.00818 18.2827 7.34399 18.2827 7.75818C18.2827 8.17236 17.9473 8.50818 17.5327 8.50818Z"
fill="currentColor"
/>
</svg>
)
}
const TopArrowLodashIcon = ({ className }: { className?: string }) => {
return (
<svg
className={className}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.0001 4.97949L16.7458 9.72518H7.25439L12.0001 4.97949Z"
fill="currentColor"
/>
<path
d="M12.7896 22.0004V8.23828H11.2106V22.0004H12.7896Z"
fill="currentColor"
/>
<path
d="M16.7456 3.57846V1.99951L7.25423 1.99951V3.57846L16.7456 3.57846Z"
fill="currentColor"
/>
</svg>
)
}
const TrendUpIcon = ({ className }: { className?: string }) => {
return (
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
className={className}
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_61_503)">
<path
d="M23.0769 6L13.5769 15.5L8.5769 10.5L1.0769 18"
stroke="#ACF9C0"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M17.0769 6H23.0769V12"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_61_503">
<rect
width="24"
height="24"
fill="white"
transform="translate(0.0769043)"
/>
</clipPath>
</defs>
</svg>
)
}
const ICON_DICT = {
"paper-icon": PaperIcon,
"trend-up-icon": TrendUpIcon,
"top-arrow-icon": TopArrowLodashIcon
} as const

View File

@ -2,7 +2,8 @@ import { formatDistanceToNow } from "date-fns"
// Utility function to format dates // Utility function to format dates
export const formatDate = (date: string | Date) => { export const formatDate = (date: string | Date) => {
const distance = formatDistanceToNow(new Date(date), { addSuffix: true }) const _date = new Date(date)
const distance = formatDistanceToNow(_date, { addSuffix: true })
return distance return distance
.replace(/less than a minute?/, "<1m") .replace(/less than a minute?/, "<1m")
.replace(/ minutes?/, "m") .replace(/ minutes?/, "m")