Merge branch 'develop' of git.lumeweb.com:LumeWeb/web3.news into develop

This commit is contained in:
Juan Di Toro 2023-12-05 15:04:48 +01:00
commit 1ff20dcf73
10 changed files with 364 additions and 214 deletions

47
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@heroicons/react": "^2.0.18",
"@prisma/client": "^5.6.0",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
@ -17,6 +18,7 @@
"next": "14.0.2",
"react": "^18",
"react-dom": "^18",
"swr": "^2.2.4",
"tailwind-merge": "^2.0.0"
},
"devDependencies": {
@ -427,6 +429,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",
@ -4642,6 +4669,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/tailwind-merge": {
"version": "2.0.0",
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -11,6 +11,7 @@
"dependencies": {
"@heroicons/react": "^2.0.18",
"@radix-ui/react-icons": "^1.3.0",
"@prisma/client": "^5.6.0",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"clsx": "^2.0.0",
@ -18,7 +19,8 @@
"next": "14.0.2",
"react": "^18",
"react-dom": "^18",
"tailwind-merge": "^2.0.0"
"tailwind-merge": "^2.0.0",
"swr": "^2.2.4"
},
"devDependencies": {
"@types/node": "^20",

18
prisma/schema.prisma Normal file
View File

@ -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
}

View File

@ -3,80 +3,74 @@
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 136, 87%, 83%;
--primary-foreground: black;
--primary: 136, 87%, 83%;
--primary-foreground: black;
--secondary: 169 46% 37%;
--secondary-foreground: 222.2 47.4% 11.2%;
--secondary: 169 46% 37%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 0 0% 32%;
--muted-foreground: 0 0% 32%;
--muted: 0 0% 32%;
--muted-foreground: 0 0% 32%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 0 0% 32%;
--input: 0 0% 32%;
--ring: 222.2 84% 4.9%;
--border: 0 0% 32%;
--input: 0 0% 32%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
main astro-island {
display: flex;
flex-direction: column;
height: 100vh;
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -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 (
<>
<SearchBar />
@ -24,26 +27,12 @@ export default function Home({searchParams}: Props) {
<div className="flex flex-row flex-wrap justify-between w-full">
<Feed
title="Latest from the community"
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'}
icon={"paper-icon"}
className="w-[calc(33%-20px)] max-w-md"
initialData={data.data}
/>
</div>
<Feed
title="Another heading"
icon={'trend-up-icon'}
className="w-full"
/>
</div>
</>
)
}
);
}

View File

@ -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<Awaited<ReturnType<typeof fetchFeedData>>>();
const [content, setContent] = useState<NonNullable<typeof dataResponse>['data']>([])
const filters = ["latest", "day", "week", "month"] as const;
const [dataResponse, setDataResponse] =
useState<Awaited<ReturnType<typeof fetchFeedData>>>();
const [content, setContent] = useState<NonNullable<Article[]>>(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<ApiResponse<Article>, 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 <div>Failed to load</div>;
}
return (
<section className={`w-full h-full space-y-6 ${className}`}>
@ -78,7 +91,9 @@ const Feed = ({
</ul>
</nav>
</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">
<div className={`flex gap-4 flex-${variant}`}>
{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"
>
<span className="inline-block text-gray-500 w-full flex-1">
{formatDate(item.date)}
{formatDate(item.createdAt)}
</span>
<p className="inline-block text-white w-[25ch] flex-auto">
{item.content}
{item.title}
</p>
</article>
)
);
})}
{dataResponse?.next ? <button
className="bg-gray-600 text-gray-300 rounded-md p-2 px-4"
onClick={() => fetchContent()}
>
Fetch more
</button> : null}
{dataResponse?.next ? (
<button
className="bg-gray-600 text-gray-300 rounded-md p-2 px-4"
onClick={handleLoadMore}
>
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.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
export default Feed;
const PaperIcon = ({ className }: { className?: string }) => {
return (
@ -169,8 +149,8 @@ const PaperIcon = ({ className }: { className?: string }) => {
fill="currentColor"
/>
</svg>
)
}
);
};
const TopArrowLodashIcon = ({ className }: { className?: string }) => {
return (
<svg
@ -194,8 +174,8 @@ const TopArrowLodashIcon = ({ className }: { className?: string }) => {
fill="currentColor"
/>
</svg>
)
}
);
};
const TrendUpIcon = ({ className }: { className?: string }) => {
return (
<svg
@ -233,11 +213,11 @@ const TrendUpIcon = ({ className }: { className?: string }) => {
</clipPath>
</defs>
</svg>
)
}
);
};
const ICON_DICT = {
"paper-icon": PaperIcon,
"trend-up-icon": TrendUpIcon,
"top-arrow-icon": TopArrowLodashIcon
} as const
"top-arrow-icon": TopArrowLodashIcon,
} as const;

56
src/lib/feed.ts Normal file
View File

@ -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,
};
}

12
src/lib/prisma.ts Normal file
View File

@ -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 };

38
src/pages/api/feed.ts Normal file
View File

@ -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" });
}
}

View File

@ -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"
]
}