feat: fix feeds and add dynamic fetching
This commit is contained in:
parent
a7d124073f
commit
f44a5a35df
|
@ -9,6 +9,7 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"date-fns": "^2.30.0",
|
||||
"next": "14.0.2",
|
||||
"react": "^18",
|
||||
|
@ -388,6 +389,203 @@
|
|||
"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": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz",
|
||||
|
@ -421,13 +619,13 @@
|
|||
"version": "15.7.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz",
|
||||
"integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.37.tgz",
|
||||
"integrity": "sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
|
@ -438,7 +636,7 @@
|
|||
"version": "18.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.15.tgz",
|
||||
"integrity": "sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
|
@ -447,7 +645,7 @@
|
|||
"version": "0.16.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.6.tgz",
|
||||
"integrity": "sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "6.10.0",
|
||||
|
@ -1124,7 +1322,7 @@
|
|||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
|
||||
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"date-fns": "^2.30.0",
|
||||
"next": "14.0.2",
|
||||
"react": "^18",
|
||||
|
|
191
src/app/page.tsx
191
src/app/page.tsx
|
@ -1,9 +1,5 @@
|
|||
import Feed from "@/components/Feed"
|
||||
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() {
|
||||
return (
|
||||
|
@ -11,171 +7,28 @@ export default function Home() {
|
|||
<SearchBar />
|
||||
<div className="space-y-8 w-full my-10">
|
||||
<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 title="Rising Posts" icon={TrendUpIcon} className="w-[calc(33%-16px)] max-w-md" />
|
||||
<Feed title="Top Posts" icon={TopArrowLodashIcon} className="w-[calc(33%-16px)] max-w-md" />
|
||||
<Feed
|
||||
title="Latest from the community"
|
||||
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>
|
||||
<Feed title="Another heading" icon={TrendUpIcon} className="w-full" variant="row" />
|
||||
<Feed
|
||||
title="Another heading"
|
||||
icon={'trend-up-icon'}
|
||||
className="w-full"
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -2,7 +2,8 @@ import { formatDistanceToNow } from "date-fns"
|
|||
|
||||
// Utility function to format dates
|
||||
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
|
||||
.replace(/less than a minute?/, "<1m")
|
||||
.replace(/ minutes?/, "m")
|
||||
|
|
Loading…
Reference in New Issue