refactor: migrate to remix

This commit is contained in:
Derrick Hammer 2023-12-17 22:18:17 -05:00
parent f6e627e045
commit 2d339f2ebe
Signed by: pcfreak30
GPG Key ID: C997C339BE476FF2
41 changed files with 8501 additions and 778 deletions

View File

@ -3,9 +3,9 @@
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 { Article } from "@/lib/prisma";
import useSWR from "swr";
import { ApiResponse } from "../lib/feed.ts";
import { ApiResponse } from "@/lib/feed";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
@ -42,7 +42,7 @@ const Feed = ({
currentPage === 0
? { data: initialData, current: 0, next: 5 }
: undefined, // Use initialData only for the first page
},
}
);
useEffect(() => {

View File

@ -0,0 +1,20 @@
import { Link } from "@remix-run/react";
import React from "react";
type Props = {};
const Footer = ({}: Props) => {
return (
<div className="w-full mt-5 flex flex-row items-center justify-center text-gray-400">
<Link to="/about" className="hover:text-white hover:underline">
About Web3.news
</Link>
<div className="h-7 w-[1px] bg-current mx-4" />
<Link to="/donate" className="hover:text-white hover:underline">
Contribute to the cause
</Link>
</div>
);
};
export default Footer;

View File

@ -1,21 +1,22 @@
import React from "react"
import Image from "next/image"
import Link from "next/link"
import React from "react";
import { Link } from "@remix-run/react";
type Props = {}
import Logo from "@/images/lume-logo-sm.png";
type Props = {};
export const Header = ({}: Props) => {
return (
<header className="w-full flex flex-row justify-between relative">
<div className="flex flex-col">
<Link href="/">
<Link to="/">
<Web3NewsLogo />
<div className="relative mt-1">
<Image
<image
className="-right-8 -top-3 absolute"
width={28}
height={24}
src="/lume-logo-sm.png"
src={Logo}
alt=""
/>
<span className="right-0 -top-[6px] absolute text-white text-opacity-50 text-sm font-normal font-secondary leading-7">
@ -26,21 +27,21 @@ export const Header = ({}: Props) => {
</div>
<div className="flex gap-3 font-normal flex-row text-gray-300 rounded">
<Link
href="/about"
to="/about"
className="hover:text-white p-2 px-4 hover:bg-gray-800 rounded"
>
About
</Link>
<Link
href="/donate"
to="/donate"
className="hover:text-white p-2 px-4 hover:bg-gray-800 rounded"
>
Contribute
</Link>
</div>
</header>
)
}
);
};
const Web3NewsLogo = ({ className }: { className?: string }) => {
return (
@ -61,7 +62,7 @@ const Web3NewsLogo = ({ className }: { className?: string }) => {
fill="#ACF9C0"
/>
</svg>
)
}
);
};
export default Header
export default Header;

View File

@ -1,76 +1,71 @@
"use client"
"use client";
import React, {
FormEvent,
type FormEvent,
useCallback,
useEffect,
useRef,
useState
} from "react"
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/outline" // Assuming usage of Heroicons for icons
import { flushSync } from "react-dom"
import Link from "next/link"
import { usePathname, useSearchParams, useRouter } from "next/navigation"
import { FILTER_TIMES, formatDate, getResults } from "@/utils"
useState,
} from "react";
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; // Assuming usage of Heroicons for icons
import { flushSync } from "react-dom";
import {
Link,
useLocation,
useNavigate,
useSearchParams,
} from "@remix-run/react";
import { FILTER_TIMES, formatDate, getResults } from "@/utils";
import {
Select,
SelectContent,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue
} from "./ui/select"
import { SitesCombobox } from "./SitesCombobox"
SelectValue,
} from "./ui/select";
import { SitesCombobox } from "./SitesCombobox";
type Props = {}
type Props = {};
const SearchBar = ({}: Props) => {
const searchParams = useSearchParams()
const router = useRouter()
const pathname = usePathname()
const [query, setQuery] = useState(searchParams?.get("q") ?? "")
const inputRef = useRef<HTMLInputElement>()
const [isLoading, setIsLoading] = useState(false)
const [activeInput, setActiveInput] = useState(true)
const [dirtyInput, setDirtyInput] = useState(false)
const [results, setResults] = useState<SearchResult[]>([])
const SearchBar = () => {
let navigate = useNavigate();
let { pathname } = useLocation();
let [searchParams] = useSearchParams();
const [query, setQuery] = useState(searchParams.get("q") ?? "");
const inputRef = useRef<HTMLInputElement>();
const [isLoading, setIsLoading] = useState(false);
const [activeInput, setActiveInput] = useState(true);
const [dirtyInput, setDirtyInput] = useState(false);
const [results, setResults] = useState<SearchResult[]>([]);
const handleSearch = useCallback(
async (event?: FormEvent<HTMLFormElement>) => {
event?.preventDefault()
setIsLoading(true)
const newSearchParams = new URLSearchParams(searchParams ?? undefined)
async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsLoading(true);
let newSearchParams = new URLSearchParams(searchParams.toString());
if (query) {
newSearchParams.set("q", query)
newSearchParams.set("q", query);
} else {
newSearchParams.delete("q")
newSearchParams.delete("q");
}
router.push(`${pathname}?${newSearchParams}`)
navigate(`${pathname}?${newSearchParams.toString()}`);
// Perform search and update results state
// Assume fetchResults is a function that fetches search results
// const searchResults = await fetchResults(query);
// Mock the search results
const searchResults = await getResults({ query })
const searchResults = await getResults({ query });
setResults(searchResults)
setIsLoading(false)
setActiveInput(false)
setResults(searchResults);
setIsLoading(false);
setActiveInput(false);
},
[
query,
setResults,
setIsLoading,
setActiveInput,
searchParams,
router,
pathname
]
)
[query, searchParams, navigate, pathname]
);
const isActive = results.length > 0 || dirtyInput
const isActive = results.length > 0 || dirtyInput;
return (
<div
@ -96,7 +91,7 @@ const SearchBar = ({}: Props) => {
<input
ref={(element) => {
if (element) {
inputRef.current = element
inputRef.current = element;
}
}}
className={`flex-grow inline bg-transparent text-white placeholder-gray-400 outline-none ring-none ${
@ -114,15 +109,15 @@ const SearchBar = ({}: Props) => {
style={
query
? {
width: `calc(${query.length}ch+2px)`
width: `calc(${query.length}ch+2px)`,
}
: undefined
}
onChange={(e) => {
if (!dirtyInput) {
setDirtyInput(true)
setDirtyInput(true);
}
setQuery(e.target.value)
setQuery(e.target.value);
}}
/>
{isActive ? (
@ -138,9 +133,9 @@ const SearchBar = ({}: Props) => {
className="block w-full flex-1 text-blue-300"
onClick={() => {
flushSync(() => {
setActiveInput(true)
})
inputRef.current?.focus()
setActiveInput(true);
});
inputRef.current?.focus();
}}
>
{'"'}
@ -156,13 +151,18 @@ const SearchBar = ({}: Props) => {
{/* Dropdown component should be here */}
<SitesCombobox />
{/* Dropdown component should be here */}
<Select defaultValue={'0'}>
<Select defaultValue={"0"}>
<SelectTrigger className="hover:bg-muted w-auto">
<SelectValue placeholder="Time ago"/>
<SelectValue placeholder="Time ago" />
</SelectTrigger>
<SelectContent>
{FILTER_TIMES.map((v) => (
<SelectItem value={String(v.value)} key={`FilteTimeSelectItem_${v.value}`}>{v.label}</SelectItem>
<SelectItem
value={String(v.value)}
key={`FilteTimeSelectItem_${v.value}`}
>
{v.label}
</SelectItem>
))}
</SelectContent>
</Select>
@ -175,7 +175,7 @@ const SearchBar = ({}: Props) => {
<hr className="my-4 border-1" />
{results.map((item) => (
<Link
href={`/article/${item.slug}`}
to={`/article/${item.slug}`}
key={item.id}
className="flex cursor-pointer flex-row items-center space-x-5 my-2 py-2 px-4 hover:bg-gray-800 rounded-md"
>
@ -185,7 +185,7 @@ const SearchBar = ({}: Props) => {
<h3 className="text-md font-semibold text-white">{item.title}</h3>
</Link>
))}
<Link href={`/search?q=${encodeURIComponent(query)}`}>
<Link to={`/search?q=${encodeURIComponent(query)}`}>
<button className="mt-4 flex justify-center items-center bg-secondary w-full py-7 text-white hover:bg-teal-800 transition-colors">
{results.length}+ search results for{" "}
<span className="text-blue-300 ml-1">{query}</span>
@ -195,13 +195,13 @@ const SearchBar = ({}: Props) => {
</>
)}
</div>
)
}
);
};
// Placeholder components for Shadcn
const LoadingComponent = () => {
// Replace with actual Shadcn Loading component
return <div>Loading...</div>
}
return <div>Loading...</div>;
};
export default SearchBar
export default SearchBar;

View File

@ -1,45 +1,50 @@
"use client"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import React, { FormEvent, useState } from "react"
import { Select, SelectContent, SelectTrigger, SelectItem, SelectValue } from "./ui/select"
import { SitesCombobox } from "./SitesCombobox"
import { FILTER_TIMES } from "@/utils"
import React, { FormEvent, useState } from "react";
import { useLocation, useNavigate } from "@remix-run/react";
import {
Select,
SelectContent,
SelectTrigger,
SelectItem,
SelectValue,
} from "./ui/select";
import { SitesCombobox } from "./SitesCombobox";
import { FILTER_TIMES } from "@/utils";
type Props = {
value: string
placeholder?: string
className?: string
value: string;
placeholder?: string;
className?: string;
filters?: {
sites: { value: string; label: string }[]
}
}
sites: { value: string; label: string }[];
};
};
const SimplifiedSearchBar = ({
value: initialValue,
placeholder,
filters,
className
className,
}: Props) => {
const searchParams = useSearchParams()
const pathname = usePathname()
const router = useRouter()
const [value, setValue] = useState<string>(initialValue)
let navigate = useNavigate();
let location = useLocation();
const [value, setValue] = useState<string>(initialValue);
const handleSearch = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
const newSearchParams = new URLSearchParams(searchParams ?? undefined)
event.preventDefault();
const newSearchParams = new URLSearchParams(location.search);
if (value) {
newSearchParams.set("q", value)
newSearchParams.set("q", value);
} else {
newSearchParams.delete("q")
newSearchParams.delete("q");
}
router.push(`${pathname}?${newSearchParams}`)
}
navigate(`${location.pathname}?${newSearchParams}`);
};
return (
<form
className={`flex items-center text-lg border-b border-primary pb-2`}
className={`flex items-center text-lg border-b border-primary pb-2 ${className}`}
onSubmit={handleSearch}
>
<div className="flex-1 flex flex-row max-w-full">
@ -83,7 +88,7 @@ const SimplifiedSearchBar = ({
</Select>
</div>
</form>
)
}
);
};
export default SimplifiedSearchBar
export default SimplifiedSearchBar;

View File

@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils"
import { cn } from "@/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
@ -31,26 +31,26 @@ const buttonVariants = cva(
size: "default",
},
}
)
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
);
}
)
Button.displayName = "Button"
);
Button.displayName = "Button";
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,4 +1,4 @@
import prisma, { Article } from "../lib/prisma.ts";
import prisma, { Article } from "@/lib/prisma";
export type ApiResponse<T = Record<string, any>> = {
data: T[];
current: number;

51
app/root.tsx Normal file
View File

@ -0,0 +1,51 @@
import { LinksFunction, MetaFunction } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import Header from "@/components/LayoutHeader"; // Adjust the import path as needed
import Footer from "@/components/LayoutFooter"; // Adjust the import path as needed
import globalStyles from "./styles/global.css";
import { cssBundleHref } from "@remix-run/css-bundle"; // Adjust the import path as needed
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: globalStyles },
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
// Add your Google font links here
// Example: { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400&display=swap" },
// Example: { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Jaldi:wght@400&display=swap" },
];
export const meta: MetaFunction = () => [
{
charset: "utf-8",
},
{ viewport: "width=device-width,initial-scale=1" },
];
export default function Root() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body className="font-main bg-gray-900 flex">
<main className="dark flex w-full min-h-screen flex-col md:px-40 items-center py-16 mx-auto">
<Header />
<Outlet />
<Footer />
</main>
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}

View File

@ -1,5 +1,7 @@
import { ArrowIcon } from "@/components/ArrowIcon"
import * as GraphicSection from "@/components/GraphicSection"
import { ArrowIcon } from "@/components/ArrowIcon";
import * as GraphicSection from "@/components/GraphicSection";
import Logo from "@/images/lume-logo-bg.png";
export default function Page() {
return (
@ -40,7 +42,7 @@ export default function Page() {
>
<GraphicSection.Background>
<img
src="/lume-logo-bg.png"
src={Logo}
className="background opacity-50 transition-transform duration-500 transform-gpu absolute -top-[100px] -left-10"
alt=""
aria-hidden
@ -60,5 +62,5 @@ export default function Page() {
</GraphicSection.Foreground>
</GraphicSection.Root>
</span>
)
);
}

View File

@ -1,4 +1,4 @@
import Link from "next/link";
import { Link } from "@remix-run/react";
export default function Page() {
return (
@ -34,7 +34,7 @@ export default function Page() {
<p>So help us in our goals to level-up Web3.</p>
<Link href="https://gitcoin.com">
<Link to="https://gitcoin.com">
<button
className={`my-6 p-8 text-gray-500 bg-gray-800 hover:bg-gray-800/70`}
>
@ -42,5 +42,5 @@ export default function Page() {
</button>
</Link>
</span>
)
);
}

View File

@ -1,29 +1,27 @@
"use client"
import { ArrowLeftIcon } from "@radix-ui/react-icons"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { ArrowLeftIcon } from "@radix-ui/react-icons";
import { Link, Outlet, useLocation } from "@remix-run/react";
const TEXT_DICT = {
"/about": {
headline: "Sharing community news on the open, user-owned web you deserve.",
tagline: "Learn about our community"
},
"/donate": {
headline: "We think people should have free access to information no matter how they choose to access it.",
tagline: "Help us break the pattern"
}
}
"/about": {
headline: "Sharing community news on the open, user-owned web you deserve.",
tagline: "Learn about our community",
},
"/donate": {
headline:
"We think people should have free access to information no matter how they choose to access it.",
tagline: "Help us break the pattern",
},
};
export default function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const text = TEXT_DICT[pathname! as '/about' | '/donate']
export default function Layout() {
const { pathname } = useLocation();
const text = TEXT_DICT[pathname! as "/about" | "/donate"];
return (
<section className="w-full">
<header className="text-white mt-10 pb-3 border-b-2 border-primary">
<h2>{text.headline}</h2>
</header>
<Link href="/">
<Link to="/">
<button className="my-4 -ml-3 px-3 py-2 text-gray-400 hover:bg-gray-800 hover:text-white rounded">
<ArrowLeftIcon className="w-4 h-4 inline mr-2 -mt-1" />
Back to Home
@ -39,16 +37,18 @@ export default function Layout({ children }: { children: React.ReactNode }) {
</ol>
</nav>
</aside>
<section className="w-full">{children}</section>
<section className="w-full">
<Outlet />
</section>
</div>
</section>
)
);
}
const AsideItem = ({ title, href }: { title: string; href: string }) => {
const pathname = usePathname()
const { pathname } = useLocation();
return (
<Link href={href}>
<Link to={href}>
<li>
<button
className={`w-[calc(100%-20px)] mb-3 p-8 text-gray-500 bg-gray-800 text-start ${
@ -61,5 +61,5 @@ const AsideItem = ({ title, href }: { title: string; href: string }) => {
</button>
</li>
</Link>
)
}
);
};

View File

@ -1,34 +1,37 @@
import Feed from "@/components/Feed";
import SearchBar from "@/components/SearchBar";
import { ApiResponse, fetchFeedData } from "@/lib/feed.ts";
import { ApiResponse, fetchFeedData } from "@/lib/feed";
import * as GraphicSection from "@/components/GraphicSection";
import { ArrowIcon } from "@/components/ArrowIcon";
import { GetServerSideProps } from "next";
import { Article } from "@/lib/prisma.ts";
import { Article } from "@/lib/prisma";
type Props = {
import Logo from "@/images/lume-logo-bg.png";
import { json, LoaderFunction, redirect } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
type LoaderData = {
data: ApiResponse<Article>;
};
export const getServerSideProps: GetServerSideProps<Props> = async ({
req,
params,
}) => {
if (!req.headers.referer && params?.q) {
return {
redirect: {
destination: `/search?q=${params?.q}`,
permanent: false,
},
};
export let loader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url);
const referer = request.headers.get("referer");
const queryParam = url.searchParams.get("q");
// Handle redirection based on referer and query parameters
if (!referer && queryParam) {
return redirect(`/search?q=${queryParam}`);
}
// Fetch your data here
const data = await fetchFeedData({});
return { props: { data } };
// Return the fetched data as JSON
return json({ data });
};
export default async function Home({ data }: Props) {
export default function Index() {
let { data } = useLoaderData<LoaderData>();
return (
<>
<SearchBar />
@ -47,7 +50,7 @@ export default async function Home({ data }: Props) {
>
<GraphicSection.Background>
<img
src="/lume-logo-bg.png"
src={Logo}
className="background transition-transform duration-500 transform-gpu absolute -top-[320px] -right-10"
alt=""
aria-hidden

View File

@ -1,11 +1,17 @@
import { fetchFeedData } from "@/lib/feed.ts";
import { type NextRequest, NextResponse } from "next/server";
import { json, LoaderFunctionArgs } from "@remix-run/node";
import { fetchFeedData } from "@/lib/feed";
export async function GET(req: NextRequest) {
const { filter, page = "0" } = req.nextUrl.searchParams as any as {
filter: "latest" | "day" | "week" | "month";
page: string;
};
type Filter = "latest" | "day" | "week" | "month";
export async function loader({ params }: LoaderFunctionArgs) {
let filter: Filter | null = null;
let page = "0";
if (params?.searchParams) {
({ filter, page = "0" } = params.searchParams as any as {
filter: Filter;
page: string;
});
}
try {
// Define the limit of articles per page
@ -26,8 +32,10 @@ export async function GET(req: NextRequest) {
// Fetch data using the fetchFeedData function
const dataResponse = await fetchFeedData(queryParams);
return NextResponse.json(dataResponse);
return json(dataResponse);
} catch (error) {
return NextResponse.json({ error: "Internal Server Error" });
throw new Response("Internal Server Error", {
status: 500,
});
}
}

View File

@ -1,21 +1,14 @@
"use client"
import { ArrowLeftIcon } from "@heroicons/react/24/outline"
import Link from "next/link"
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
// page https://www.businessinsider.com/web3-music-platforms-blockchain-even-sound-iyk-streaming-services-artists-2023-9#:~:text=Web3%20music%20platforms%20are%20combining,and%20'create%20dope%20consumer%20experiences'&text=Web3%20music%20platforms%20are%20changing,and%20ways%20to%20reach%20fans.
import React from "react"
import React from "react";
import { Link } from "@remix-run/react";
type Props = {
params: {
slug: string
}
}
const Page = ({ params }: Props) => {
const Page = () => {
// TODO: Explore based on the slug, we can also change the slug to be like the id or something the backend understands
// We can also pre-render the article from the backend
return (
<>
<Link href="/" className="w-full mt-1">
<Link to="/" className="w-full mt-1">
<button className="px-3 py-2 text-gray-400 hover:bg-gray-800 hover:text-white rounded">
<ArrowLeftIcon className="w-4 h-4 inline mr-2 -mt-1" />
Go Back Home
@ -28,7 +21,7 @@ const Page = ({ params }: Props) => {
></iframe>
</div>
</>
)
}
);
};
export default Page
export default Page;

View File

@ -1,18 +1,13 @@
import React, { FormEvent } from "react"
import Link from "next/link"
import { ArrowLeftIcon } from "@heroicons/react/24/outline"
import { formatDate, getResults } from "@/utils"
import SimplifiedSearchBar from "@/components/SimplifiedSearchBar"
import React from "react";
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { formatDate, getResults } from "@/utils";
import SimplifiedSearchBar from "@/components/SimplifiedSearchBar";
import { Link, useSearchParams } from "@remix-run/react";
type Props = {
searchParams: {
q?: string
}
}
const Page = async ({ searchParams }: Props) => {
const query = searchParams.q ?? ""
const results = await getResults({ query: searchParams.q })
const Page = async () => {
const [searchParams] = useSearchParams();
const query = searchParams.get("q") ?? "";
const results = await getResults({ query: query });
return (
<div className="w-full items-center text-lg">
@ -23,7 +18,7 @@ const Page = async ({ searchParams }: Props) => {
}
/>
<Link href="/">
<Link to="/">
<button className="my-4 -ml-3 px-3 py-2 text-gray-400 hover:bg-gray-800 hover:text-white rounded">
<ArrowLeftIcon className="w-4 h-4 inline mr-2 -mt-1" />
Go Back Home
@ -34,7 +29,7 @@ const Page = async ({ searchParams }: Props) => {
<>
{results.map((item) => (
<Link
href={`/article/${item.slug}`}
to={`/article/${item.slug}`}
key={item.id}
className="flex cursor-pointer flex-row items-center space-x-5 my-2 py-2 px-4 hover:bg-gray-800 rounded-md"
>
@ -44,7 +39,7 @@ const Page = async ({ searchParams }: Props) => {
<h3 className="text-md font-semibold text-white">{item.title}</h3>
</Link>
))}
<Link href={`/search?q=${encodeURIComponent(query)}`}>
<Link to={`/search?q=${encodeURIComponent(query)}`}>
<button className="rounded mt-4 flex justify-center items-center bg-gray-800 mx-auto w-44 py-7 text-white hover:bg-gray-800/50 transition-colors">
Load More
</button>
@ -52,7 +47,7 @@ const Page = async ({ searchParams }: Props) => {
</>
)}
</div>
)
}
);
};
export default Page
export default Page;

View File

@ -1,4 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
module.exports = nextConfig

8353
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,14 +2,20 @@
"name": "web3.news",
"version": "0.1.0",
"private": true,
"type": "module",
"prisma": {
"seed": "ts-node-esm prisma/seed.mts"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"build": "run-s \"build:*\"",
"build:css": "npm run generate:css -- --style=compressed",
"build:remix": "remix build",
"dev": "run-p \"dev:*\"",
"dev:css": "npm run generate:css -- --watch",
"dev:remix": "remix dev",
"generate:css": "sass styles/:app/styles/",
"start": "remix-serve build",
"typecheck": "tsc",
"bridge": "ts-node-esm bridge.mts"
},
"dependencies": {
@ -22,14 +28,18 @@
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@remix-run/css-bundle": "^2.4.0",
"@remix-run/node": "^2.4.0",
"@remix-run/react": "^2.4.0",
"@remix-run/serve": "^2.4.0",
"cheerio": "^1.0.0-rc.12",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"date-fns": "^2.30.0",
"next": "14.0.2",
"react": "^18",
"react-dom": "^18",
"isbot": "^3.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"slugify": "^1.6.6",
"swr": "^2.2.4",
"tailwind-merge": "^2.0.0",
@ -37,6 +47,7 @@
},
"devDependencies": {
"@faker-js/faker": "^8.3.1",
"@remix-run/dev": "^2.4.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
@ -44,11 +55,13 @@
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.0.2",
"npm-run-all": "^4.1.5",
"postcss": "^8",
"prisma": "^5.6.0",
"sass": "^1.69.5",
"tailwindcss": "^3.3.0",
"tailwindcss-animate": "^1.0.7",
"ts-node": "^10.9.1",
"typescript": "^5"
"typescript": "^5.2.2"
}
}

View File

@ -1,6 +1,6 @@
module.exports = {
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
};

8
remix.config.js Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('@remix-run/dev').AppConfig} */
export default {
ignoredRouteFiles: ["**/.*"],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// publicPath: "/build/",
// serverBuildPath: "build/index.js",
};

2
remix.env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/node" />

View File

@ -1,89 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import { S5Client } from "@lumeweb/s5-js";
import xml2js from "xml2js";
import prisma from "../../../../lib/prisma.ts";
import * as cheerio from "cheerio";
import slugify from "slugify";
import path from "path";
export async function POST(req: NextRequest) {
const client = new S5Client("https://s5.web3portal.com");
const data = await req.json();
const meta = (await client.getMetadata(data.cid)) as any;
const fileMeta = meta.metadata as any;
const paths = fileMeta.paths as {
[file: string]: {
cid: string;
};
};
if (!("sitemap.xml" in paths)) {
return NextResponse.error();
}
const sitemapData = await client.downloadData(paths["sitemap.xml"].cid);
const sitemap = await xml2js.parseStringPromise(sitemapData);
const urls = sitemap.urlset.url.map((urlEntry: any) => {
const url = urlEntry.loc[0];
let pathname = new URL(url).pathname;
// Normalize and remove leading and trailing slashes from the path
pathname = path.normalize(pathname).replace(/^\/|\/$/g, "");
// Function to determine if a URL path represents a directory
const isDirectory = (pathname: string) => {
// Check if the path directly maps to a file in the paths object
return !paths.hasOwnProperty(pathname);
};
// Check if the path is a directory and look for a directory index
if (isDirectory(pathname)) {
for (const file of fileMeta.tryFiles) {
const indexPath = path.join(pathname, file);
if (paths.hasOwnProperty(indexPath)) {
pathname = indexPath;
break;
}
}
}
// Fetch cid after confirming the final path
const cid = paths[pathname]?.cid;
return { url, cid, path: pathname }; // including cid in return object after final path is determined
});
for (const { url, cid } of urls) {
if (cid) {
const exists = await prisma.article.findUnique({
where: { cid },
});
if (!exists) {
// Fetch and parse the content using CID
const contentData = Buffer.from(
await client.downloadData(cid),
).toString();
const $ = cheerio.load(contentData);
const title = $("title").text(); // Extract the title from the content
const record = {
title,
url,
cid: cid,
createdAt: new Date(),
updatedAt: new Date(),
slug: slugify(new URL(url).pathname),
siteKey: slugify(data.site as string),
};
// Insert a new record into the database
await prisma.article.create({
data: record,
});
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,76 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 248, 45%, 7%;
--popover-foreground: 0, 0%, 100%;
--primary: 136, 87%, 83%;
--primary-foreground: black;
--secondary: 169 46% 37%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 0 0% 32%;
--muted-foreground: 0 0% 32%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--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%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 248, 45%, 7%;
--popover-foreground: 0, 0%, 100%;
--primary: 136, 87%, 83%;
--primary-foreground: black;
--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%;
--accent: 217.2 32.6% 17.5%;
--accent-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%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground font-main;
}
}

View File

@ -1,35 +0,0 @@
import type { Metadata } from "next"
import { Jaldi, Be_Vietnam_Pro } from "next/font/google"
import Header from "@/components/LayoutHeader"
import Footer from "@/components/LayoutFooter"
import "./globals.css"
const beVietnamPro = Be_Vietnam_Pro({ weight: ["400"], subsets: ["latin"], variable: "--font-be-vietnam-pro" })
const jaldi = Jaldi({
subsets: ["latin"],
weight: ["400"],
variable: "--font-jaldi"
})
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app"
}
export default function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={`font-main bg-gray-900 flex ${beVietnamPro.variable} ${jaldi.variable}`}>
<main className="dark flex w-full min-h-screen flex-col md:px-40 items-center py-16 mx-auto">
<Header />
{children}
<Footer />
</main>
</body>
</html>
)
}

View File

@ -1,14 +0,0 @@
import Link from "next/link"
import React from "react"
type Props = {}
const Footer = ({}: Props) => {
return (<div className="w-full mt-5 flex flex-row items-center justify-center text-gray-400">
<Link href="/about" className="hover:text-white hover:underline">About Web3.news</Link>
<div className="h-7 w-[1px] bg-current mx-4" />
<Link href="/donate" className="hover:text-white hover:underline">Contribute to the cause</Link>
</div>)
}
export default Footer

12
src/types.d.ts vendored
View File

@ -1,12 +0,0 @@
type SearchResult = {
id: number
timestamp: Date
title: string
description: string
slug: string
}
type SelectOptions = {
value: string
label: string
}

76
styles/global.scss Normal file
View File

@ -0,0 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 248, 45%, 7%;
--popover-foreground: 0, 0%, 100%;
--primary: 136, 87%, 83%;
--primary-foreground: black;
--secondary: 169 46% 37%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 0 0% 32%;
--muted-foreground: 0 0% 32%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--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%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 248, 45%, 7%;
--popover-foreground: 0, 0%, 100%;
--primary: 136, 87%, 83%;
--primary-foreground: black;
--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%;
--accent: 217.2 32.6% 17.5%;
--accent-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%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground font-main;
}
}

View File

@ -1,12 +1,8 @@
import type { Config } from 'tailwindcss'
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
content: ["./app/**/*.{js,jsx,ts,tsx}"],
theme: {
container: {
center: true,
@ -62,12 +58,12 @@ const config: Config = {
},
keyframes: {
"accordion-down": {
from: { height: '0' },
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: '0' },
to: { height: "0" },
},
},
animation: {
@ -77,6 +73,6 @@ const config: Config = {
},
},
plugins: [require("tailwindcss-animate")],
}
};
export default config
export default config;

View File

@ -1,41 +1,34 @@
{
"include": [
"remix.env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"compilerOptions": {
"target": "es2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
"DOM",
"DOM.Iterable",
"ES2022"
],
"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"
}
],
"esModuleInterop": true,
"jsx": "react-jsx",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"target": "ES2022",
"strict": true,
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
"./app/*"
],
"~/*": [
"./app/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
},
// Remix takes care of building everything in `remix build`.
"noEmit": true
}
}