diff --git a/package-lock.json b/package-lock.json index b10c65b..9a50d50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index fece97e..de0c9d6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/page.tsx b/src/app/page.tsx index cd08bf9..5792d71 100644 --- a/src/app/page.tsx +++ b/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() {
- - - + + +
- +
) -} - -const Feed = ({ - className, - variant = "col", - icon: Icon, - title -}: { - className?: string - variant?: "row" | "col" - title: string - icon: (props: React.HTMLAttributes) => React.ReactNode | JSX.Element -}) => { - return ( -
-
- - -
-
-
- - 1h ago - -

- Bitcoin (BTC) Price Prediction: When Will Bitcoin Reach $100,000? -

-
-
- - 1h ago - -

- Bitcoin (BTC) Price Prediction: When Will Bitcoin Reach $100,000? -

-
-
- - 1h ago - -

- Bitcoin (BTC) Price Prediction: When Will Bitcoin Reach $100,000? -

-
-
- - 1h ago - -

- Bitcoin (BTC) Price Prediction: When Will Bitcoin Reach $100,000? -

-
-
- - 1h ago - -

- Bitcoin (BTC) Price Prediction: When Will Bitcoin Reach $100,000? -

-
-
-
- ) -} - -const PaperIcon = ({ className }: { className?: string }) => { - return ( - - - - ) -} -const TopArrowLodashIcon = ({ className }: { className?: string }) => { - return ( - - - - - - ) -} -const TrendUpIcon = ({ className }: { className?: string }) => { - return ( - - - - - - - - - - - - ) -} +} \ No newline at end of file diff --git a/src/components/Feed.tsx b/src/components/Feed.tsx new file mode 100644 index 0000000..5e33fb5 --- /dev/null +++ b/src/components/Feed.tsx @@ -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>>(); + const [content, setContent] = useState['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 ( +
+
+ + +
+ + +
+ {content.map((item, index) => { + return ( +
+ + {formatDate(item.date)} + +

+ {item.content} +

+
+ ) + })} + {dataResponse?.next ? : null} +
+
+ + + +
+
+ ) +} + +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> = { + 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> { + 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 ( + + + + ) +} +const TopArrowLodashIcon = ({ className }: { className?: string }) => { + return ( + + + + + + ) +} +const TrendUpIcon = ({ className }: { className?: string }) => { + return ( + + + + + + + + + + + + ) +} + +const ICON_DICT = { + "paper-icon": PaperIcon, + "trend-up-icon": TrendUpIcon, + "top-arrow-icon": TopArrowLodashIcon +} as const diff --git a/src/utils.ts b/src/utils.ts index a6773ab..0e79ec0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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")