Merge branch 'develop' of git.lumeweb.com:LumeWeb/web3.news into develop
This commit is contained in:
commit
1ff20dcf73
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.0.18",
|
"@heroicons/react": "^2.0.18",
|
||||||
|
"@prisma/client": "^5.6.0",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
|
@ -17,6 +18,7 @@
|
||||||
"next": "14.0.2",
|
"next": "14.0.2",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"swr": "^2.2.4",
|
||||||
"tailwind-merge": "^2.0.0"
|
"tailwind-merge": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -427,6 +429,31 @@
|
||||||
"node": ">= 8"
|
"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": {
|
"node_modules/@radix-ui/number": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz",
|
||||||
|
@ -4642,6 +4669,18 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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/tailwind-merge": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.0.0.tgz",
|
||||||
|
@ -4986,6 +5025,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.0.18",
|
"@heroicons/react": "^2.0.18",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@prisma/client": "^5.6.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
|
@ -18,7 +19,8 @@
|
||||||
"next": "14.0.2",
|
"next": "14.0.2",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"tailwind-merge": "^2.0.0"
|
"tailwind-merge": "^2.0.0",
|
||||||
|
"swr": "^2.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -3,80 +3,74 @@
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--primary: 136, 87%, 83%;
|
--primary: 136, 87%, 83%;
|
||||||
--primary-foreground: black;
|
--primary-foreground: black;
|
||||||
|
|
||||||
--secondary: 169 46% 37%;
|
--secondary: 169 46% 37%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--muted: 0 0% 32%;
|
--muted: 0 0% 32%;
|
||||||
--muted-foreground: 0 0% 32%;
|
--muted-foreground: 0 0% 32%;
|
||||||
|
|
||||||
--accent: 210 40% 96.1%;
|
--accent: 210 40% 96.1%;
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--border: 0 0% 32%;
|
--border: 0 0% 32%;
|
||||||
--input: 0 0% 32%;
|
--input: 0 0% 32%;
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 222.2 84% 4.9%;
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
--card: 222.2 84% 4.9%;
|
--card: 222.2 84% 4.9%;
|
||||||
--card-foreground: 210 40% 98%;
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--popover: 222.2 84% 4.9%;
|
--popover: 222.2 84% 4.9%;
|
||||||
--popover-foreground: 210 40% 98%;
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--primary: 210 40% 98%;
|
--primary: 210 40% 98%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--muted: 217.2 32.6% 17.5%;
|
--muted: 217.2 32.6% 17.5%;
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
--accent: 217.2 32.6% 17.5%;
|
--accent: 217.2 32.6% 17.5%;
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--border: 217.2 32.6% 17.5%;
|
--border: 217.2 32.6% 17.5%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--input: 217.2 32.6% 17.5%;
|
||||||
--ring: 212.7 26.8% 83.9%;
|
--ring: 212.7 26.8% 83.9%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
main astro-island {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,25 @@
|
||||||
import {redirect} from "next/navigation"
|
import { redirect } from "next/navigation";
|
||||||
import { headers } from 'next/headers'
|
import { headers } from "next/headers";
|
||||||
import Feed from "@/components/Feed"
|
import Feed from "@/components/Feed";
|
||||||
import SearchBar from "@/components/SearchBar"
|
import SearchBar from "@/components/SearchBar";
|
||||||
|
import { fetchFeedData } from "@/lib/feed.ts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
searchParams?: {
|
searchParams?: {
|
||||||
q?: string | undefined
|
q?: string | undefined;
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function Home({searchParams}: Props) {
|
export default async function Home({ searchParams }: Props) {
|
||||||
const headerList = headers()
|
const headerList = headers();
|
||||||
const referer = headerList.get("referer")
|
const referer = headerList.get("referer");
|
||||||
|
|
||||||
if(!referer && searchParams?.q) {
|
if (!referer && searchParams?.q) {
|
||||||
redirect(`/search?q=${searchParams.q}`)
|
redirect(`/search?q=${searchParams.q}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await fetchFeedData({});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
|
@ -24,26 +27,12 @@ export default function Home({searchParams}: Props) {
|
||||||
<div className="flex flex-row flex-wrap justify-between w-full">
|
<div className="flex flex-row flex-wrap justify-between w-full">
|
||||||
<Feed
|
<Feed
|
||||||
title="Latest from the community"
|
title="Latest from the community"
|
||||||
icon={'paper-icon'}
|
icon={"paper-icon"}
|
||||||
className="w-[calc(33%-20px)] max-w-md"
|
|
||||||
/>
|
|
||||||
<Feed
|
|
||||||
title="Rising Posts"
|
|
||||||
icon={'trend-up-icon'}
|
|
||||||
className="w-[calc(33%-20px)] max-w-md"
|
|
||||||
/>
|
|
||||||
<Feed
|
|
||||||
title="Top Posts"
|
|
||||||
icon={'top-arrow-icon'}
|
|
||||||
className="w-[calc(33%-20px)] max-w-md"
|
className="w-[calc(33%-20px)] max-w-md"
|
||||||
|
initialData={data.data}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Feed
|
|
||||||
title="Another heading"
|
|
||||||
icon={'trend-up-icon'}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
|
@ -1,57 +1,70 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { formatDate } from "@/utils"
|
import { formatDate } from "@/utils";
|
||||||
import * as ScrollArea from "@radix-ui/react-scroll-area"
|
import * as ScrollArea from "@radix-ui/react-scroll-area";
|
||||||
import { sub } from "date-fns"
|
import { useState, useEffect } from "react";
|
||||||
import { useState, useCallback, 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 = ({
|
const Feed = ({
|
||||||
className,
|
className,
|
||||||
variant = "col",
|
variant = "col",
|
||||||
icon,
|
icon,
|
||||||
title
|
title,
|
||||||
|
initialData,
|
||||||
}: {
|
}: {
|
||||||
className?: string
|
className?: string;
|
||||||
variant?: "row" | "col"
|
variant?: "row" | "col";
|
||||||
title: string
|
title: string;
|
||||||
icon: keyof typeof ICON_DICT
|
icon: keyof typeof ICON_DICT;
|
||||||
|
initialData: Article[];
|
||||||
}) => {
|
}) => {
|
||||||
const filters = ["latest", "day", "week", "month"] as const
|
const filters = ["latest", "day", "week", "month"] as const;
|
||||||
const [dataResponse, setDataResponse] = useState<Awaited<ReturnType<typeof fetchFeedData>>>();
|
const [dataResponse, setDataResponse] =
|
||||||
const [content, setContent] = useState<NonNullable<typeof dataResponse>['data']>([])
|
useState<Awaited<ReturnType<typeof fetchFeedData>>>();
|
||||||
|
const [content, setContent] = useState<NonNullable<Article[]>>(initialData);
|
||||||
const [selectedFilter, setSelectedFilter] =
|
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(
|
const { data: swrData, error } = useSWR<ApiResponse<Article>, any>(
|
||||||
async (overwrite: boolean = false) => {
|
`/api/feed?filter=${selectedFilter}&page=${currentPage}`,
|
||||||
const response = await fetchFeedData({
|
fetcher,
|
||||||
filter: { timerange: selectedFilter },
|
{
|
||||||
next: dataResponse?.next ?? undefined,
|
revalidateOnFocus: false,
|
||||||
current: dataResponse?.current,
|
revalidateIfStale: false,
|
||||||
limit: 5
|
revalidateOnReconnect: false,
|
||||||
})
|
shouldRetryOnError: false,
|
||||||
setDataResponse(response)
|
fallbackData:
|
||||||
setContent((current) => {
|
currentPage === 0 ? { data: initialData, current: 0 } : undefined, // Use initialData only for the first page
|
||||||
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(() => {
|
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 <div>Failed to load</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`w-full h-full space-y-6 ${className}`}>
|
<section className={`w-full h-full space-y-6 ${className}`}>
|
||||||
|
@ -78,7 +91,9 @@ const Feed = ({
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<ScrollArea.Root className={`overflow-hidden w-full h-[400px] rounded-md`}>
|
<ScrollArea.Root
|
||||||
|
className={`overflow-hidden w-full h-[400px] rounded-md`}
|
||||||
|
>
|
||||||
<ScrollArea.Viewport className="w-full h-full">
|
<ScrollArea.Viewport className="w-full h-full">
|
||||||
<div className={`flex gap-4 flex-${variant}`}>
|
<div className={`flex gap-4 flex-${variant}`}>
|
||||||
{content.map((item, index) => {
|
{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"
|
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">
|
<span className="inline-block text-gray-500 w-full flex-1">
|
||||||
{formatDate(item.date)}
|
{formatDate(item.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
<p className="inline-block text-white w-[25ch] flex-auto">
|
<p className="inline-block text-white w-[25ch] flex-auto">
|
||||||
{item.content}
|
{item.title}
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
{dataResponse?.next ? <button
|
{dataResponse?.next ? (
|
||||||
className="bg-gray-600 text-gray-300 rounded-md p-2 px-4"
|
<button
|
||||||
onClick={() => fetchContent()}
|
className="bg-gray-600 text-gray-300 rounded-md p-2 px-4"
|
||||||
>
|
onClick={handleLoadMore}
|
||||||
Fetch more
|
>
|
||||||
</button> : null}
|
Fetch more
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea.Viewport>
|
</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.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.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.ScrollAreaScrollbar>
|
||||||
</ScrollArea.Root>
|
</ScrollArea.Root>
|
||||||
</section>
|
</section>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const data = Array.from({ length: 20 }, (_, i) => ({
|
export default Feed;
|
||||||
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 }) => {
|
const PaperIcon = ({ className }: { className?: string }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -169,8 +149,8 @@ const PaperIcon = ({ className }: { className?: string }) => {
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
const TopArrowLodashIcon = ({ className }: { className?: string }) => {
|
const TopArrowLodashIcon = ({ className }: { className?: string }) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
@ -194,8 +174,8 @@ const TopArrowLodashIcon = ({ className }: { className?: string }) => {
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
const TrendUpIcon = ({ className }: { className?: string }) => {
|
const TrendUpIcon = ({ className }: { className?: string }) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
@ -233,11 +213,11 @@ const TrendUpIcon = ({ className }: { className?: string }) => {
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const ICON_DICT = {
|
const ICON_DICT = {
|
||||||
"paper-icon": PaperIcon,
|
"paper-icon": PaperIcon,
|
||||||
"trend-up-icon": TrendUpIcon,
|
"trend-up-icon": TrendUpIcon,
|
||||||
"top-arrow-icon": TopArrowLodashIcon
|
"top-arrow-icon": TopArrowLodashIcon,
|
||||||
} as const
|
} as const;
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import prisma, { Article } from "../lib/prisma.ts";
|
||||||
|
export type ApiResponse<T = Record<string, any>> = {
|
||||||
|
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<ApiResponse<Article>> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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 };
|
|
@ -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" });
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +1,41 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
"allowJs": true,
|
"dom",
|
||||||
"skipLibCheck": true,
|
"dom.iterable",
|
||||||
"strict": true,
|
"esnext"
|
||||||
"noEmit": true,
|
],
|
||||||
"esModuleInterop": true,
|
"allowJs": true,
|
||||||
"module": "esnext",
|
"allowImportingTsExtensions": true,
|
||||||
"moduleResolution": "bundler",
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true,
|
"strict": true,
|
||||||
"isolatedModules": true,
|
"noEmit": true,
|
||||||
"jsx": "preserve",
|
"esModuleInterop": true,
|
||||||
"incremental": true,
|
"module": "esnext",
|
||||||
"plugins": [
|
"moduleResolution": "bundler",
|
||||||
{
|
"resolveJsonModule": true,
|
||||||
"name": "next"
|
"isolatedModules": true,
|
||||||
}
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"paths": {
|
"exclude": [
|
||||||
"@/*": ["./src/*"]
|
"node_modules"
|
||||||
}
|
]
|
||||||
},
|
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
||||||
"exclude": ["node_modules"]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue