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 { formatDate } from "@/utils";
import * as ScrollArea from "@radix-ui/react-scroll-area"; import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Article } from "../lib/prisma.ts"; import { Article } from "@/lib/prisma";
import useSWR from "swr"; 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()); const fetcher = (url: string) => fetch(url).then((r) => r.json());
@ -42,7 +42,7 @@ const Feed = ({
currentPage === 0 currentPage === 0
? { data: initialData, current: 0, next: 5 } ? { data: initialData, current: 0, next: 5 }
: undefined, // Use initialData only for the first page : undefined, // Use initialData only for the first page
}, }
); );
useEffect(() => { 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 React from "react";
import Image from "next/image" import { Link } from "@remix-run/react";
import Link from "next/link"
type Props = {} import Logo from "@/images/lume-logo-sm.png";
type Props = {};
export const Header = ({}: Props) => { export const Header = ({}: Props) => {
return ( return (
<header className="w-full flex flex-row justify-between relative"> <header className="w-full flex flex-row justify-between relative">
<div className="flex flex-col"> <div className="flex flex-col">
<Link href="/"> <Link to="/">
<Web3NewsLogo /> <Web3NewsLogo />
<div className="relative mt-1"> <div className="relative mt-1">
<Image <image
className="-right-8 -top-3 absolute" className="-right-8 -top-3 absolute"
width={28} width={28}
height={24} height={24}
src="/lume-logo-sm.png" src={Logo}
alt="" alt=""
/> />
<span className="right-0 -top-[6px] absolute text-white text-opacity-50 text-sm font-normal font-secondary leading-7"> <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>
<div className="flex gap-3 font-normal flex-row text-gray-300 rounded"> <div className="flex gap-3 font-normal flex-row text-gray-300 rounded">
<Link <Link
href="/about" to="/about"
className="hover:text-white p-2 px-4 hover:bg-gray-800 rounded" className="hover:text-white p-2 px-4 hover:bg-gray-800 rounded"
> >
About About
</Link> </Link>
<Link <Link
href="/donate" to="/donate"
className="hover:text-white p-2 px-4 hover:bg-gray-800 rounded" className="hover:text-white p-2 px-4 hover:bg-gray-800 rounded"
> >
Contribute Contribute
</Link> </Link>
</div> </div>
</header> </header>
) );
} };
const Web3NewsLogo = ({ className }: { className?: string }) => { const Web3NewsLogo = ({ className }: { className?: string }) => {
return ( return (
@ -61,7 +62,7 @@ const Web3NewsLogo = ({ className }: { className?: string }) => {
fill="#ACF9C0" fill="#ACF9C0"
/> />
</svg> </svg>
) );
} };
export default Header export default Header;

View File

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

View File

@ -1,8 +1,8 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils" import { cn } from "@/utils";
const buttonVariants = cva( 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", "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", size: "default",
}, },
} }
) );
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean asChild?: boolean;
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
ref={ref} ref={ref}
{...props} {...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>> = { export type ApiResponse<T = Record<string, any>> = {
data: T[]; data: T[];
current: number; 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 { ArrowIcon } from "@/components/ArrowIcon";
import * as GraphicSection from "@/components/GraphicSection" import * as GraphicSection from "@/components/GraphicSection";
import Logo from "@/images/lume-logo-bg.png";
export default function Page() { export default function Page() {
return ( return (
@ -40,7 +42,7 @@ export default function Page() {
> >
<GraphicSection.Background> <GraphicSection.Background>
<img <img
src="/lume-logo-bg.png" src={Logo}
className="background opacity-50 transition-transform duration-500 transform-gpu absolute -top-[100px] -left-10" className="background opacity-50 transition-transform duration-500 transform-gpu absolute -top-[100px] -left-10"
alt="" alt=""
aria-hidden aria-hidden
@ -60,5 +62,5 @@ export default function Page() {
</GraphicSection.Foreground> </GraphicSection.Foreground>
</GraphicSection.Root> </GraphicSection.Root>
</span> </span>
) );
} }

View File

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

View File

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

View File

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

View File

@ -1,11 +1,17 @@
import { fetchFeedData } from "@/lib/feed.ts"; import { json, LoaderFunctionArgs } from "@remix-run/node";
import { type NextRequest, NextResponse } from "next/server"; import { fetchFeedData } from "@/lib/feed";
export async function GET(req: NextRequest) { type Filter = "latest" | "day" | "week" | "month";
const { filter, page = "0" } = req.nextUrl.searchParams as any as {
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; page: string;
}; });
}
try { try {
// Define the limit of articles per page // Define the limit of articles per page
@ -26,8 +32,10 @@ export async function GET(req: NextRequest) {
// Fetch data using the fetchFeedData function // Fetch data using the fetchFeedData function
const dataResponse = await fetchFeedData(queryParams); const dataResponse = await fetchFeedData(queryParams);
return NextResponse.json(dataResponse); return json(dataResponse);
} catch (error) { } 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 { ArrowLeftIcon } from "@heroicons/react/24/outline"
import Link from "next/link"
// 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. // 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 = { const Page = () => {
params: {
slug: string
}
}
const Page = ({ params }: Props) => {
// TODO: Explore based on the slug, we can also change the slug to be like the id or something the backend understands // 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 // We can also pre-render the article from the backend
return ( 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"> <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" /> <ArrowLeftIcon className="w-4 h-4 inline mr-2 -mt-1" />
Go Back Home Go Back Home
@ -28,7 +21,7 @@ const Page = ({ params }: Props) => {
></iframe> ></iframe>
</div> </div>
</> </>
) );
} };
export default Page export default Page;

View File

@ -1,18 +1,13 @@
import React, { FormEvent } from "react" import React from "react";
import Link from "next/link" import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { ArrowLeftIcon } from "@heroicons/react/24/outline" import { formatDate, getResults } from "@/utils";
import { formatDate, getResults } from "@/utils" import SimplifiedSearchBar from "@/components/SimplifiedSearchBar";
import SimplifiedSearchBar from "@/components/SimplifiedSearchBar" import { Link, useSearchParams } from "@remix-run/react";
type Props = { const Page = async () => {
searchParams: { const [searchParams] = useSearchParams();
q?: string const query = searchParams.get("q") ?? "";
} const results = await getResults({ query: query });
}
const Page = async ({ searchParams }: Props) => {
const query = searchParams.q ?? ""
const results = await getResults({ query: searchParams.q })
return ( return (
<div className="w-full items-center text-lg"> <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"> <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" /> <ArrowLeftIcon className="w-4 h-4 inline mr-2 -mt-1" />
Go Back Home Go Back Home
@ -34,7 +29,7 @@ const Page = async ({ searchParams }: Props) => {
<> <>
{results.map((item) => ( {results.map((item) => (
<Link <Link
href={`/article/${item.slug}`} to={`/article/${item.slug}`}
key={item.id} 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" 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> <h3 className="text-md font-semibold text-white">{item.title}</h3>
</Link> </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"> <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 Load More
</button> </button>
@ -52,7 +47,7 @@ const Page = async ({ searchParams }: Props) => {
</> </>
)} )}
</div> </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", "name": "web3.news",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module",
"prisma": { "prisma": {
"seed": "ts-node-esm prisma/seed.mts" "seed": "ts-node-esm prisma/seed.mts"
}, },
"scripts": { "scripts": {
"dev": "next dev", "build": "run-s \"build:*\"",
"build": "next build", "build:css": "npm run generate:css -- --style=compressed",
"start": "next start", "build:remix": "remix build",
"lint": "next lint", "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" "bridge": "ts-node-esm bridge.mts"
}, },
"dependencies": { "dependencies": {
@ -22,14 +28,18 @@
"@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",
"@radix-ui/react-slot": "^1.0.2", "@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", "cheerio": "^1.0.0-rc.12",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"cmdk": "^0.2.0", "cmdk": "^0.2.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"next": "14.0.2", "isbot": "^3.7.1",
"react": "^18", "react": "^18.2.0",
"react-dom": "^18", "react-dom": "^18.2.0",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"swr": "^2.2.4", "swr": "^2.2.4",
"tailwind-merge": "^2.0.0", "tailwind-merge": "^2.0.0",
@ -37,6 +47,7 @@
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^8.3.1", "@faker-js/faker": "^8.3.1",
"@remix-run/dev": "^2.4.0",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
@ -44,11 +55,13 @@
"autoprefixer": "^10.0.1", "autoprefixer": "^10.0.1",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "14.0.2", "eslint-config-next": "14.0.2",
"npm-run-all": "^4.1.5",
"postcss": "^8", "postcss": "^8",
"prisma": "^5.6.0", "prisma": "^5.6.0",
"sass": "^1.69.5",
"tailwindcss": "^3.3.0", "tailwindcss": "^3.3.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5" "typescript": "^5.2.2"
} }
} }

View File

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

View File

@ -1,41 +1,34 @@
{ {
"include": [
"remix.env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"compilerOptions": { "compilerOptions": {
"target": "es2017",
"lib": [ "lib": [
"dom", "DOM",
"dom.iterable", "DOM.Iterable",
"esnext" "ES2022"
], ],
"allowJs": true,
"allowImportingTsExtensions": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "esModuleInterop": true,
"incremental": true, "jsx": "react-jsx",
"plugins": [ "moduleResolution": "Bundler",
{ "resolveJsonModule": true,
"name": "next" "target": "ES2022",
} "strict": true,
], "allowJs": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": { "paths": {
"@/*": [ "@/*": [
"./src/*" "./app/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
], ],
"exclude": [ "~/*": [
"node_modules" "./app/*"
] ]
},
// Remix takes care of building everything in `remix build`.
"noEmit": true
}
} }