diff --git a/package-lock.json b/package-lock.json
index 9a50d50..81d1c3a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,11 +9,13 @@
"version": "0.1.0",
"dependencies": {
"@heroicons/react": "^2.0.18",
+ "@prisma/client": "^5.6.0",
"@radix-ui/react-scroll-area": "^1.0.5",
"date-fns": "^2.30.0",
"next": "14.0.2",
"react": "^18",
- "react-dom": "^18"
+ "react-dom": "^18",
+ "swr": "^2.2.4"
},
"devDependencies": {
"@types/node": "^20",
@@ -389,6 +391,31 @@
"node": ">= 8"
}
},
+ "node_modules/@prisma/client": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.6.0.tgz",
+ "integrity": "sha512-mUDefQFa1wWqk4+JhKPYq8BdVoFk9NFMBXUI8jAkBfQTtgx8WPx02U2HB/XbAz3GSUJpeJOKJQtNvaAIDs6sug==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@prisma/engines-version": "5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee"
+ },
+ "engines": {
+ "node": ">=16.13"
+ },
+ "peerDependencies": {
+ "prisma": "*"
+ },
+ "peerDependenciesMeta": {
+ "prisma": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@prisma/engines-version": {
+ "version": "5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee.tgz",
+ "integrity": "sha512-UoFgbV1awGL/3wXuUK3GDaX2SolqczeeJ5b4FVec9tzeGbSWJboPSbT0psSrmgYAKiKnkOPFSLlH6+b+IyOwAw=="
+ },
"node_modules/@radix-ui/number": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz",
@@ -4135,6 +4162,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/swr": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.4.tgz",
+ "integrity": "sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==",
+ "dependencies": {
+ "client-only": "^0.0.1",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "peerDependencies": {
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/tailwindcss": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
@@ -4426,6 +4465,14 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index de0c9d6..e51f6af 100644
--- a/package.json
+++ b/package.json
@@ -10,11 +10,13 @@
},
"dependencies": {
"@heroicons/react": "^2.0.18",
+ "@prisma/client": "^5.6.0",
"@radix-ui/react-scroll-area": "^1.0.5",
"date-fns": "^2.30.0",
"next": "14.0.2",
"react": "^18",
- "react-dom": "^18"
+ "react-dom": "^18",
+ "swr": "^2.2.4"
},
"devDependencies": {
"@types/node": "^20",
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 0000000..32acbca
--- /dev/null
+++ b/prisma/schema.prisma
@@ -0,0 +1,18 @@
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "sqlite"
+ url = "file:./dev.db"
+}
+
+model Article {
+ id Int @id @default(autoincrement())
+ title String
+ slug String
+ url String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ siteKey String
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index d141f6f..ff44935 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,22 +1,25 @@
-import {redirect} from "next/navigation"
-import { headers } from 'next/headers'
-import Feed from "@/components/Feed"
-import SearchBar from "@/components/SearchBar"
+import { redirect } from "next/navigation";
+import { headers } from "next/headers";
+import Feed from "@/components/Feed";
+import SearchBar from "@/components/SearchBar";
+import { fetchFeedData } from "@/lib/feed.ts";
type Props = {
- searchParams?: {
- q?: string | undefined
- };
-}
+ searchParams?: {
+ q?: string | undefined;
+ };
+};
-export default function Home({searchParams}: Props) {
- const headerList = headers()
- const referer = headerList.get("referer")
+export default async function Home({ searchParams }: Props) {
+ const headerList = headers();
+ const referer = headerList.get("referer");
- if(!referer && searchParams?.q) {
- redirect(`/search?q=${searchParams.q}`)
+ if (!referer && searchParams?.q) {
+ redirect(`/search?q=${searchParams.q}`);
}
-
+
+ const data = await fetchFeedData({});
+
return (
<>
@@ -24,26 +27,12 @@ export default function Home({searchParams}: Props) {
-
-
-
>
- )
-}
\ No newline at end of file
+ );
+}
diff --git a/src/components/Feed.tsx b/src/components/Feed.tsx
index bda4766..02aeb9c 100644
--- a/src/components/Feed.tsx
+++ b/src/components/Feed.tsx
@@ -1,57 +1,70 @@
-"use client"
+"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"
+import { formatDate } from "@/utils";
+import * as ScrollArea from "@radix-ui/react-scroll-area";
+import { useState, useEffect } from "react";
+import { Article } from "../lib/prisma.ts";
+import useSWR from "swr";
+import { ApiResponse, fetchFeedData } from "../lib/feed.ts";
+
+const fetcher = (url: string) => fetch(url).then((r) => r.json());
const Feed = ({
className,
variant = "col",
icon,
- title
+ title,
+ initialData,
}: {
- className?: string
- variant?: "row" | "col"
- title: string
- icon: keyof typeof ICON_DICT
+ className?: string;
+ variant?: "row" | "col";
+ title: string;
+ icon: keyof typeof ICON_DICT;
+ initialData: Article[];
}) => {
- const filters = ["latest", "day", "week", "month"] as const
- const [dataResponse, setDataResponse] = useState>>();
- const [content, setContent] = useState['data']>([])
+ const filters = ["latest", "day", "week", "month"] as const;
+ const [dataResponse, setDataResponse] =
+ useState>>();
+ const [content, setContent] = useState>(initialData);
const [selectedFilter, setSelectedFilter] =
- useState<(typeof filters)[number]>("latest")
+ useState<(typeof filters)[number]>("latest");
+ const [currentPage, setCurrentPage] = useState(0);
- const Icon = ICON_DICT[icon]
+ const Icon = ICON_DICT[icon];
- const fetchContent = useCallback(
- async (overwrite: boolean = false) => {
- const response = await fetchFeedData({
- filter: { timerange: selectedFilter },
- next: dataResponse?.next ?? undefined,
- current: dataResponse?.current,
- limit: 5
- })
- setDataResponse(response)
- setContent((current) => {
- if (overwrite) {
- return response.data
- }
- return [...current, ...response.data]
- })
- console.log("Fetched data")
+ const { data: swrData, error } = useSWR, any>(
+ `/api/feed?filter=${selectedFilter}&page=${currentPage}`,
+ fetcher,
+ {
+ revalidateOnFocus: false,
+ revalidateIfStale: false,
+ revalidateOnReconnect: false,
+ shouldRetryOnError: false,
+ fallbackData:
+ currentPage === 0 ? { data: initialData, current: 0 } : undefined, // Use initialData only for the first page
},
- [setContent, dataResponse, selectedFilter]
- )
-
- const handleFilterChange = (filter: (typeof filters)[number]) => {
- setSelectedFilter(filter)
- fetchContent(true)
- }
+ );
useEffect(() => {
- fetchContent()
- }, [])
+ if (swrData && currentPage !== 0) {
+ setContent((prevContent) => [...prevContent, ...swrData.data]);
+ } else if (swrData) {
+ setContent(swrData.data);
+ }
+ }, [swrData, currentPage]);
+
+ const handleFilterChange = (filter: (typeof filters)[number]) => {
+ setSelectedFilter(filter);
+ setCurrentPage(0); // Reset to the first page when the filter changes
+ };
+
+ const handleLoadMore = () => {
+ setCurrentPage((prevPage) => prevPage + 1); // Increment page number to fetch next set of data
+ };
+
+ if (error) {
+ return Failed to load
;
+ }
return (
@@ -78,7 +91,9 @@ const Feed = ({
-
+
{content.map((item, index) => {
@@ -88,71 +103,36 @@ const Feed = ({
className="flex bg-gray-800 flex-col justify-between w-full py-4 px-6 rounded"
>
- {formatDate(item.date)}
+ {formatDate(item.createdAt)}
- {item.content}
+ {item.title}
- )
+ );
})}
- {dataResponse?.next ?
: null}
+ {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
+export default Feed;
const PaperIcon = ({ className }: { className?: string }) => {
return (
@@ -169,8 +149,8 @@ const PaperIcon = ({ className }: { className?: string }) => {
fill="currentColor"
/>
- )
-}
+ );
+};
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
+ "top-arrow-icon": TopArrowLodashIcon,
+} as const;
diff --git a/src/lib/feed.ts b/src/lib/feed.ts
new file mode 100644
index 0000000..2cdbe0f
--- /dev/null
+++ b/src/lib/feed.ts
@@ -0,0 +1,56 @@
+import prisma, { Article } from "../lib/prisma.ts";
+export type ApiResponse> = {
+ data: T[];
+ current: number;
+ next?: number | null;
+ amount?: number;
+};
+export async function fetchFeedData({
+ filter,
+ limit = 5,
+ next = 5,
+ current = 0,
+}: {
+ filter?: { timerange?: "latest" | "day" | "week" | "month" };
+ next?: number;
+ limit?: number;
+ current?: number;
+}): Promise> {
+ let query = {};
+
+ if (filter?.timerange && filter.timerange !== "latest") {
+ const now = new Date();
+ const timeRanges = {
+ day: 1,
+ week: 7,
+ month: 30, // Approximation for a month, adjust as needed
+ };
+
+ const daysToSubtract = timeRanges[filter.timerange];
+ if (daysToSubtract !== undefined) {
+ const dateFrom = new Date(now.setDate(now.getDate() - daysToSubtract));
+ query = {
+ where: {
+ createdAt: {
+ gte: dateFrom,
+ },
+ },
+ };
+ }
+ }
+
+ const articles = await prisma.article.findMany({
+ ...query,
+ skip: current,
+ take: next,
+ });
+
+ const nextPointer = articles.length >= limit ? next + limit : null;
+
+ return {
+ data: articles,
+ current: next,
+ next: nextPointer,
+ amount: articles.length,
+ };
+}
diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts
new file mode 100644
index 0000000..5161e45
--- /dev/null
+++ b/src/lib/prisma.ts
@@ -0,0 +1,12 @@
+import { PrismaClient, Article } from "@prisma/client";
+
+const globalForPrisma = global as unknown as { prisma: PrismaClient };
+
+export const prisma = globalForPrisma.prisma || new PrismaClient();
+
+if (process.env.NODE_ENV !== "production") {
+ globalForPrisma.prisma = prisma;
+}
+
+export default prisma;
+export type { Article };
diff --git a/src/pages/api/feed.ts b/src/pages/api/feed.ts
new file mode 100644
index 0000000..f4038fe
--- /dev/null
+++ b/src/pages/api/feed.ts
@@ -0,0 +1,38 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import { fetchFeedData } from "@/lib/feed.ts";
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse,
+) {
+ const { filter, page = "0" } = req.query as any as {
+ filter: "latest" | "day" | "week" | "month";
+ page: string;
+ };
+
+ try {
+ // Define the limit of articles per page
+ const limit = 5;
+
+ // Calculate the `current` and `next` values based on the `page` parameter
+ const current = parseInt(page, 10) * limit;
+ const next = limit;
+
+ // Prepare the parameters for fetchFeedData
+ const queryParams = {
+ filter: filter ? { timerange: filter } : undefined,
+ next,
+ limit,
+ current,
+ };
+
+ // Fetch data using the fetchFeedData function
+ const dataResponse = await fetchFeedData(queryParams);
+
+ // Send the response back
+ res.status(200).json(dataResponse);
+ } catch (error) {
+ // Handle any errors
+ res.status(500).json({ error: "Internal Server Error" });
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
index e59724b..9423dac 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,27 +1,41 @@
{
- "compilerOptions": {
- "target": "es5",
- "lib": ["dom", "dom.iterable", "esnext"],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "preserve",
- "incremental": true,
- "plugins": [
- {
- "name": "next"
- }
+ "compilerOptions": {
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "allowImportingTsExtensions": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts"
],
- "paths": {
- "@/*": ["./src/*"]
- }
- },
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules"]
+ "exclude": [
+ "node_modules"
+ ]
}