refactor: migrate to remix
This commit is contained in:
parent
f6e627e045
commit
2d339f2ebe
|
@ -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(() => {
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 };
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
@ -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;
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
};
|
|
@ -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
|
|
@ -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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,4 +0,0 @@
|
||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {}
|
|
||||||
|
|
||||||
module.exports = nextConfig
|
|
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
module.exports = {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
/** @type {import('@remix-run/dev').AppConfig} */
|
||||||
|
export default {
|
||||||
|
ignoredRouteFiles: ["**/.*"],
|
||||||
|
// appDirectory: "app",
|
||||||
|
// assetsBuildDirectory: "public/build",
|
||||||
|
// publicPath: "/build/",
|
||||||
|
// serverBuildPath: "build/index.js",
|
||||||
|
};
|
|
@ -0,0 +1,2 @@
|
||||||
|
/// <reference types="@remix-run/dev" />
|
||||||
|
/// <reference types="@remix-run/node" />
|
|
@ -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 |
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -1,12 +0,0 @@
|
||||||
type SearchResult = {
|
|
||||||
id: number
|
|
||||||
timestamp: Date
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
slug: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelectOptions = {
|
|
||||||
value: string
|
|
||||||
label: string
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue