chore: auto import

This commit is contained in:
Ou 2024-10-30 15:06:21 +08:00
parent cd67d52ca7
commit 0f187da2f8
33 changed files with 273 additions and 256 deletions

94
auto-imports.app.d.ts vendored Normal file
View File

@ -0,0 +1,94 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const $: typeof import('clsx')['default']
const Author: typeof import('./shared/consts')['Author']
const Homepage: typeof import('./shared/consts')['Homepage']
const Interval: typeof import('./shared/consts')['Interval']
const TTL: typeof import('./shared/consts')['TTL']
const Timer: typeof import('./src/utils/index')['Timer']
const Version: typeof import('./shared/consts')['Version']
const atom: typeof import('jotai')['atom']
const atomFamily: typeof import('jotai/utils')['atomFamily']
const atomWithDefault: typeof import('jotai/utils')['atomWithDefault']
const atomWithHash: typeof import('jotai/utils')['atomWithHash']
const atomWithObservable: typeof import('jotai/utils')['atomWithObservable']
const atomWithReducer: typeof import('jotai/utils')['atomWithReducer']
const atomWithReset: typeof import('jotai/utils')['atomWithReset']
const atomWithStorage: typeof import('jotai/utils')['atomWithStorage']
const columns: typeof import('./shared/metadata')['columns']
const createJSONStorage: typeof import('jotai/utils')['createJSONStorage']
const createRef: typeof import('react')['createRef']
const currentColumnIDAtom: typeof import('./src/atoms/index')['currentColumnIDAtom']
const currentSourcesAtom: typeof import('./src/atoms/index')['currentSourcesAtom']
const delay: typeof import('./shared/utils')['delay']
const downloadMetadata: typeof import('./src/hooks/useSync')['downloadMetadata']
const fixedColumnIds: typeof import('./shared/metadata')['fixedColumnIds']
const focusSourcesAtom: typeof import('./src/atoms/index')['focusSourcesAtom']
const forwardRef: typeof import('react')['forwardRef']
const freezeAtom: typeof import('jotai/utils')['freezeAtom']
const freezeAtomCreator: typeof import('jotai/utils')['freezeAtomCreator']
const goToTopAtom: typeof import('./src/atoms/index')['goToTopAtom']
const hiddenColumns: typeof import('./shared/metadata')['hiddenColumns']
const lazy: typeof import('react')['lazy']
const loadable: typeof import('jotai/utils')['loadable']
const memo: typeof import('react')['memo']
const metadata: typeof import('./shared/metadata')['metadata']
const originSources: typeof import('./shared/sources')['originSources']
const preprocessMetadata: typeof import('./src/atoms/primitiveMetadataAtom')['preprocessMetadata']
const primitiveMetadataAtom: typeof import('./src/atoms/primitiveMetadataAtom')['primitiveMetadataAtom']
const projectDir: typeof import('./shared/dir')['projectDir']
const randomItem: typeof import('./shared/utils')['randomItem']
const randomUUID: typeof import('./shared/utils')['randomUUID']
const refetchSourcesAtom: typeof import('./src/atoms/index')['refetchSourcesAtom']
const relativeTime: typeof import('./shared/utils')['relativeTime']
const safeParseString: typeof import('./src/utils/index')['safeParseString']
const selectAtom: typeof import('jotai/utils')['selectAtom']
const sources: typeof import('./shared/sources')['sources']
const splitAtom: typeof import('jotai/utils')['splitAtom']
const startTransition: typeof import('react')['startTransition']
const toastAtom: typeof import('./src/hooks/useToast')['toastAtom']
const typeSafeObjectEntries: typeof import('./shared/type.util')['typeSafeObjectEntries']
const typeSafeObjectFromEntries: typeof import('./shared/type.util')['typeSafeObjectFromEntries']
const typeSafeObjectValues: typeof import('./shared/type.util')['typeSafeObjectValues']
const uploadMetadata: typeof import('./src/hooks/useSync')['uploadMetadata']
const useAtom: typeof import('jotai')['useAtom']
const useAtomCallback: typeof import('jotai/utils')['useAtomCallback']
const useAtomValue: typeof import('jotai')['useAtomValue']
const useCallback: typeof import('react')['useCallback']
const useContext: typeof import('react')['useContext']
const useDark: typeof import('./src/hooks/useDark')['useDark']
const useDebugValue: typeof import('react')['useDebugValue']
const useDeferredValue: typeof import('react')['useDeferredValue']
const useEffect: typeof import('react')['useEffect']
const useFocus: typeof import('./src/hooks/useFocus')['useFocus']
const useFocusWith: typeof import('./src/hooks/useFocus')['useFocusWith']
const useHydrateAtoms: typeof import('jotai/utils')['useHydrateAtoms']
const useId: typeof import('react')['useId']
const useImperativeHandle: typeof import('react')['useImperativeHandle']
const useInsertionEffect: typeof import('react')['useInsertionEffect']
const useLayoutEffect: typeof import('react')['useLayoutEffect']
const useLogin: typeof import('./src/hooks/useLogin')['useLogin']
const useMemo: typeof import('react')['useMemo']
const useOnReload: typeof import('./src/hooks/useOnReload')['useOnReload']
const usePWA: typeof import('./src/hooks/usePWA')['usePWA']
const useReducer: typeof import('react')['useReducer']
const useReducerAtom: typeof import('jotai/utils')['useReducerAtom']
const useRef: typeof import('react')['useRef']
const useRelativeTime: typeof import('./src/hooks/useRelativeTime')['useRelativeTime']
const useResetAtom: typeof import('jotai/utils')['useResetAtom']
const useSearchBar: typeof import('./src/hooks/useSearch')['useSearchBar']
const useSetAtom: typeof import('jotai')['useSetAtom']
const useState: typeof import('react')['useState']
const useSync: typeof import('./src/hooks/useSync')['useSync']
const useSyncExternalStore: typeof import('react')['useSyncExternalStore']
const useToast: typeof import('./src/hooks/useToast')['useToast']
const useTransition: typeof import('react')['useTransition']
const verifyPrimitiveMetadata: typeof import('./shared/verify')['verifyPrimitiveMetadata']
const waitForAll: typeof import('jotai/utils')['waitForAll']
}

55
nitro.config.ts Normal file
View File

@ -0,0 +1,55 @@
import process from "node:process"
import { join } from "node:path"
import viteNitro from "vite-plugin-with-nitro"
import { RollopGlob } from "./tools/rollup-glob"
import { projectDir } from "./shared/dir"
const nitroOption: Parameters<typeof viteNitro>[0] = {
experimental: {
database: true,
},
rollupConfig: {
plugins: [RollopGlob()],
},
sourceMap: false,
database: {
default: {
connector: "sqlite",
},
},
imports: {
dirs: ["server/utils", "shared"],
},
preset: "node-server",
alias: {
"@shared": join(projectDir, "shared"),
"#": join(projectDir, "server"),
},
}
if (process.env.VERCEL) {
nitroOption.preset = "vercel-edge"
// You can use other online database, do it yourself. For more info: https://db0.unjs.io/connectors
nitroOption.database = undefined
} else if (process.env.CF_PAGES) {
nitroOption.preset = "cloudflare-pages"
nitroOption.database = {
default: {
connector: "cloudflare-d1",
options: {
bindingName: "NEWSNOW_DEV",
},
},
}
} else if (process.env.BUN) {
nitroOption.preset = "bun"
nitroOption.database = {
default: {
connector: "bun-sqlite",
},
}
}
export default function () {
return viteNitro(nitroOption)
}

View File

@ -3,7 +3,6 @@
"type": "module",
"version": "0.0.8",
"private": true,
"commit-id": "",
"packageManager": "pnpm@9.12.1",
"author": {
"url": "https://github.com/ourongxing/",
@ -94,7 +93,6 @@
"vite": "^5.4.8",
"vite-plugin-pwa": "^0.20.5",
"vite-plugin-with-nitro": "0.0.3",
"vite-tsconfig-paths": "^5.0.1",
"vitest": "^2.1.2",
"workbox-window": "^7.1.0",
"wrangler": "^3.80.3"

41
pnpm-lock.yaml generated
View File

@ -216,9 +216,6 @@ importers:
vite-plugin-with-nitro:
specifier: 0.0.3
version: 0.0.3(better-sqlite3@11.5.0)(idb-keyval@6.2.1)(typescript@5.6.3)(vite@5.4.10(@types/node@22.7.9)(terser@5.36.0))
vite-tsconfig-paths:
specifier: ^5.0.1
version: 5.0.1(typescript@5.6.3)(vite@5.4.10(@types/node@22.7.9)(terser@5.36.0))
vitest:
specifier: ^2.1.2
version: 2.1.3(@types/node@22.7.9)(terser@5.36.0)
@ -3914,9 +3911,6 @@ packages:
resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==}
engines: {node: '>=18'}
globrex@0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
goober@2.1.16:
resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==}
peerDependencies:
@ -5666,16 +5660,6 @@ packages:
ts-pattern@5.5.0:
resolution: {integrity: sha512-jqbIpTsa/KKTJYWgPNsFNbLVpwCgzXfFJ1ukNn4I8hMwyQzHMJnk/BqWzggB0xpkILuKzaO/aMYhS0SkaJyKXg==}
tsconfck@3.1.4:
resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==}
engines: {node: ^18 || >=20}
hasBin: true
peerDependencies:
typescript: ^5.0.0
peerDependenciesMeta:
typescript:
optional: true
tslib@2.8.0:
resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==}
@ -5979,14 +5963,6 @@ packages:
peerDependencies:
vite: ^5
vite-tsconfig-paths@5.0.1:
resolution: {integrity: sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==}
peerDependencies:
vite: '*'
peerDependenciesMeta:
vite:
optional: true
vite@5.4.10:
resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==}
engines: {node: ^18.0.0 || >=20.0.0}
@ -10305,8 +10281,6 @@ snapshots:
slash: 5.1.0
unicorn-magic: 0.1.0
globrex@0.1.2: {}
goober@2.1.16(csstype@3.1.3):
dependencies:
csstype: 3.1.3
@ -12184,10 +12158,6 @@ snapshots:
ts-pattern@5.5.0: {}
tsconfck@3.1.4(typescript@5.6.3):
optionalDependencies:
typescript: 5.6.3
tslib@2.8.0: {}
tsx@4.19.1:
@ -12551,17 +12521,6 @@ snapshots:
- webpack-sources
- xml2js
vite-tsconfig-paths@5.0.1(typescript@5.6.3)(vite@5.4.10(@types/node@22.7.9)(terser@5.36.0)):
dependencies:
debug: 4.3.7(supports-color@9.4.0)
globrex: 0.1.2
tsconfck: 3.1.4(typescript@5.6.3)
optionalDependencies:
vite: 5.4.10(@types/node@22.7.9)(terser@5.36.0)
transitivePeerDependencies:
- supports-color
- typescript
vite@5.4.10(@types/node@22.7.9)(terser@5.36.0):
dependencies:
esbuild: 0.21.5

50
pwa.config.ts Normal file
View File

@ -0,0 +1,50 @@
import process from "node:process"
import type { VitePWAOptions } from "vite-plugin-pwa"
import { VitePWA } from "vite-plugin-pwa"
const pwaOption: Partial<VitePWAOptions> = {
includeAssets: ["icon.svg", "apple-touch-icon.png"],
manifest: {
name: "NewsNow",
short_name: "NewsNow",
description: "Elegant reading of real-time and hottest news",
theme_color: "#F14D42",
icons: [
{
src: "pwa-192x192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "pwa-512x512.png",
sizes: "512x512",
type: "image/png",
},
{
src: "pwa-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
},
{
src: "pwa-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
workbox: {
navigateFallbackDenylist: [/^\/api/],
},
devOptions: {
enabled: process.env.SW_DEV === "true",
type: "module",
navigateFallback: "index.html",
},
}
export default function pwa() {
return VitePWA(pwaOption)
}

View File

@ -1,5 +1,4 @@
import process from "node:process"
import { verifyPrimitiveMetadata } from "@shared/verify"
import { UserTable } from "#/database/user"
export default defineEventHandler(async (event) => {

View File

@ -1,6 +1,4 @@
import { TTL } from "@shared/consts"
import type { SourceID, SourceResponse } from "@shared/types"
import { sources } from "@shared/sources"
import { getters } from "#/getters"
import { getCacheTable } from "#/database/cache"

View File

@ -1,4 +1,3 @@
import { typeSafeObjectEntries } from "@shared/type.util"
import type { SourceID } from "@shared/types"
import * as x from "glob:./sources/{*.ts,**/index.ts}"
import type { SourceGetter } from "./types"

View File

@ -1,11 +1,6 @@
import { atom } from "jotai"
import type { FixedColumnID, SourceID } from "@shared/types"
import { sources } from "@shared/sources"
import { primitiveMetadataAtom } from "./primitiveMetadataAtom"
import type { Update } from "./types"
export { primitiveMetadataAtom, preprocessMetadata } from "./primitiveMetadataAtom"
export const focusSourcesAtom = atom((get) => {
return get(primitiveMetadataAtom).data.focus
}, (get, set, update: Update<SourceID[]>) => {

View File

@ -1,10 +1,5 @@
import { fixedColumnIds, metadata } from "@shared/metadata"
import { typeSafeObjectEntries, typeSafeObjectFromEntries } from "@shared/type.util"
import type { PrimitiveAtom } from "jotai"
import { atom } from "jotai"
import type { FixedColumnID, PrimitiveMetadata, SourceID } from "@shared/types"
import { verifyPrimitiveMetadata } from "@shared/verify"
import { sources } from "@shared/sources"
import type { Update } from "./types"
function createPrimitiveMetadataAtom(

View File

@ -1,18 +1,12 @@
import type { NewsItem, SourceID, SourceResponse } from "@shared/types"
import { useQuery } from "@tanstack/react-query"
import clsx from "clsx"
import { AnimatePresence, motion, useInView } from "framer-motion"
import { useAtom } from "jotai"
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"
import { sources } from "@shared/sources"
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
import { ofetch } from "ofetch"
import { useWindowSize } from "react-use"
import { OverlayScrollbar } from "../common/overlay-scrollbar"
import { refetchSourcesAtom } from "~/atoms"
import { useRelativeTime } from "~/hooks/useRelativeTime"
import { safeParseString } from "~/utils"
import { useFocusWith } from "~/hooks/useFocus"
export interface ItemsProps extends React.HTMLAttributes<HTMLDivElement> {
id: SourceID
@ -38,7 +32,7 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
return (
<div
ref={ref}
className={clsx(
className={$(
"flex flex-col h-500px rounded-2xl p-4 cursor-default",
"backdrop-blur-5 transition-opacity-300",
isDragged && "op-50",
@ -114,10 +108,10 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
return (
<>
<div className={clsx("flex justify-between mx-2 mt-0 mb-2 items-center")}>
<div className={$("flex justify-between mx-2 mt-0 mb-2 items-center")}>
<div className="flex gap-2 items-center">
<a
className={clsx("w-8 h-8 rounded-full bg-cover hover:animate-spin")}
className={$("w-8 h-8 rounded-full bg-cover hover:animate-spin")}
target="_blank"
href={sources[id].home}
title={sources[id].desc}
@ -133,34 +127,34 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
>
{sources[id].name}
</span>
{sources[id]?.title && <span className={clsx("text-sm", `color-${sources[id].color} bg-base op-80 bg-op-50! px-1 rounded`)}>{sources[id].title}</span>}
{sources[id]?.title && <span className={$("text-sm", `color-${sources[id].color} bg-base op-80 bg-op-50! px-1 rounded`)}>{sources[id].title}</span>}
</span>
<span className="text-xs op-70"><UpdatedTime isError={isError} updatedTime={data?.updatedTime} /></span>
</span>
</div>
<div className={clsx("flex gap-2 text-lg", `color-${sources[id].color}`)}>
<div className={$("flex gap-2 text-lg", `color-${sources[id].color}`)}>
<button
type="button"
className={clsx("btn i-ph:arrow-counter-clockwise-duotone", isFetching && "animate-spin i-ph:circle-dashed-duotone")}
className={$("btn i-ph:arrow-counter-clockwise-duotone", isFetching && "animate-spin i-ph:circle-dashed-duotone")}
onClick={manualRefetch}
/>
<button
type="button"
className={clsx("btn", isFocused ? "i-ph:star-fill" : "i-ph:star-duotone")}
className={$("btn", isFocused ? "i-ph:star-fill" : "i-ph:star-duotone")}
onClick={toggleFocus}
/>
{handleListeners && (
<button
{...handleListeners}
type="button"
className={clsx("btn", "i-ph:dots-six-vertical-duotone", "cursor-grab")}
className={$("btn", "i-ph:dots-six-vertical-duotone", "cursor-grab")}
/>
)}
</div>
</div>
<OverlayScrollbar
className={clsx([
className={$([
"h-full p-2 overflow-y-auto rounded-2xl bg-base bg-op-70!",
isFreshFetching && `animate-pulse`,
`sprinkle-${sources[id].color}`,
@ -170,7 +164,7 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
}}
defer
>
<div className={clsx("transition-opacity-500", isFreshFetching && "op-20")}>
<div className={$("transition-opacity-500", isFreshFetching && "op-20")}>
{!!data?.items?.length && (sources[id].type === "hottest" ? <NewsListHot items={data.items} /> : <NewsListTimeLine items={data.items} />)}
</div>
</OverlayScrollbar>
@ -202,7 +196,7 @@ function DiffNumber({ diff }: { diff: number }) {
initial={{ opacity: 0, y: -15 }}
animate={{ opacity: 0.5, y: -7 }}
exit={{ opacity: 0, y: -15 }}
className={clsx("absolute left-0 text-xs", diff < 0 ? "text-green" : "text-red")}
className={$("absolute left-0 text-xs", diff < 0 ? "text-green" : "text-red")}
>
{diff > 0 ? `+${diff}` : diff}
</motion.span>
@ -243,12 +237,12 @@ function NewsListHot({ items }: { items: NewsItem[] }) {
target="_blank"
key={item.id}
title={item.extra?.hover}
className={clsx(
className={$(
"flex gap-2 items-center mb-2 items-stretch relative",
"hover:bg-neutral-400/10 rounded-md pr-1 visited:(text-neutral-400)",
)}
>
<span className={clsx("bg-neutral-400/10 min-w-6 flex justify-center items-center rounded-md text-sm")}>
<span className={$("bg-neutral-400/10 min-w-6 flex justify-center items-center rounded-md text-sm")}>
{i + 1}
</span>
{!!item.extra?.diff && <DiffNumber diff={item.extra.diff} />}
@ -282,7 +276,7 @@ function NewsListTimeLine({ items }: { items: NewsItem[] }) {
</span>
</span>
<a
className={clsx("ml-2 px-1 hover:bg-neutral-400/10 rounded-md visited:(text-neutral-400/80)")}
className={$("ml-2 px-1 hover:bg-neutral-400/10 rounded-md visited:(text-neutral-400/80)")}
href={width < 768 ? item.mobileUrl || item.url : item.url}
title={item.extra?.hover}
target="_blank"

View File

@ -14,12 +14,9 @@ import {
} from "@dnd-kit/core"
import type { AnimateLayoutChanges } from "@dnd-kit/sortable"
import { SortableContext, arrayMove, defaultAnimateLayoutChanges, rectSortingStrategy, useSortable } from "@dnd-kit/sortable"
import { useAtom } from "jotai"
import type { SourceID } from "@shared/types"
import { CSS } from "@dnd-kit/utilities"
import { motion } from "framer-motion"
import { sources } from "@shared/sources"
import clsx from "clsx"
import type { ItemsProps } from "./card"
import { CardWrapper } from "./card"
import { currentSourcesAtom } from "~/atoms"
@ -135,15 +132,15 @@ function DndWrapper({ items, setItems, children }: PropsWithChildren<DndProps>)
function CardOverlay({ id }: { id: SourceID }) {
return (
<div className={clsx(
<div className={$(
"flex flex-col rounded-2xl p-4 backdrop-blur-5",
`bg-${sources[id].color}-500 dark:bg-${sources[id].color} bg-op-40!`,
)}
>
<div className={clsx("flex justify-between mx-2 items-center")}>
<div className={$("flex justify-between mx-2 items-center")}>
<div className="flex gap-2 items-center">
<div
className={clsx("w-8 h-8 rounded-full bg-cover")}
className={$("w-8 h-8 rounded-full bg-cover")}
style={{
backgroundImage: `url(/icons/${id.split("-")[0]}.png)`,
}}
@ -153,15 +150,15 @@ function CardOverlay({ id }: { id: SourceID }) {
<span className="text-xl font-bold">
{sources[id].name}
</span>
{sources[id]?.title && <span className={clsx("text-sm", `color-${sources[id].color} bg-base op-80 bg-op-50! px-1 rounded`)}>{sources[id].title}</span>}
{sources[id]?.title && <span className={$("text-sm", `color-${sources[id].color} bg-base op-80 bg-op-50! px-1 rounded`)}>{sources[id].title}</span>}
</span>
<span className="text-xs op-70"></span>
</span>
</div>
<div className={clsx("flex gap-2 text-lg", `color-${sources[id].color}`)}>
<div className={$("flex gap-2 text-lg", `color-${sources[id].color}`)}>
<button
type="button"
className={clsx("i-ph:dots-six-vertical-duotone", "cursor-grabbing")}
className={$("i-ph:dots-six-vertical-duotone", "cursor-grabbing")}
/>
</div>
</div>

View File

@ -1,8 +1,5 @@
import type { FixedColumnID } from "@shared/types"
import { useAtom } from "jotai"
import { useEffect } from "react"
import { useTitle } from "react-use"
import { metadata } from "@shared/metadata"
import { NavBar } from "../navbar"
import { Dnd } from "./dnd"
import { currentColumnIDAtom } from "~/atoms"

View File

@ -1,9 +1,7 @@
import type { UseOverlayScrollbarsParams } from "overlayscrollbars-react"
import { useOverlayScrollbars } from "overlayscrollbars-react"
import type { HTMLProps, PropsWithChildren } from "react"
import { useCallback, useMemo, useRef } from "react"
import { defu } from "defu"
import { useSetAtom } from "jotai"
import { useMount } from "react-use"
import { goToTopAtom } from "~/atoms"

View File

@ -2,15 +2,9 @@ import { Command } from "cmdk"
import { useMount } from "react-use"
import type { SourceID } from "@shared/types"
import { useMemo, useRef, useState } from "react"
import { sources } from "@shared/sources"
import clsx from "clsx"
import { typeSafeObjectEntries } from "@shared/type.util"
import pinyin from "@shared/pinyin.json"
import { columns } from "@shared/metadata"
import { OverlayScrollbar } from "../overlay-scrollbar"
import { useSearchBar } from "~/hooks/useSearch"
import { CardWrapper } from "~/components/column/card"
import { useFocusWith } from "~/hooks/useFocus"
import "./cmdk.css"
@ -128,7 +122,7 @@ function SourceItem({ item }: {
>
<span className="flex gap-2 items-center">
<span
className={clsx("w-4 h-4 rounded-md bg-cover")}
className={$("w-4 h-4 rounded-md bg-cover")}
style={{
backgroundImage: `url(/icons/${item.id.split("-")[0]}.png)`,
}}
@ -136,7 +130,7 @@ function SourceItem({ item }: {
<span>{item.name}</span>
<span className="text-xs text-neutral-400/80 self-end mb-3px">{item.title}</span>
</span>
<span className={clsx(isFocused ? "i-ph-star-fill" : "i-ph-star-duotone", "bg-primary op-40")}></span>
<span className={$(isFocused ? "i-ph-star-fill" : "i-ph-star-duotone", "bg-primary op-40")}></span>
</Command.Item>
)
}

View File

@ -1,10 +1,7 @@
import clsx from "clsx"
import { AnimatePresence, motion } from "framer-motion"
import { useAtomValue, useSetAtom } from "jotai"
import { useCallback, useMemo, useRef } from "react"
import { useHoverDirty, useMount, useUpdateEffect, useWindowSize } from "react-use"
import type { ToastItem } from "~/atoms/types"
import { toastAtom } from "~/hooks/useToast"
import { Timer } from "~/utils"
const WIDTH = 320
@ -91,11 +88,11 @@ function Item({ info }: { info: ToastItem }) {
opacity: 1,
},
}}
className={clsx(
className={$(
"bg-base rounded-lg shadow-xl relative",
)}
>
<div className={clsx(
<div className={$(
`bg-${color}-500 dark:bg-${color} bg-op-40! p2 backdrop-blur-5 rounded-lg w-full`,
"flex items-center gap-2",
)}

View File

@ -1,5 +1,3 @@
import { Author, Homepage } from "@shared/consts"
export function Footer() {
return (
<>

View File

@ -1,14 +1,9 @@
import { Link } from "@tanstack/react-router"
import { useCallback } from "react"
import { useAtomValue, useSetAtom } from "jotai"
import { useIsFetching } from "@tanstack/react-query"
import clsx from "clsx"
import type { SourceID } from "@shared/types"
import { Homepage, Version } from "@shared/consts"
import { NavBar } from "../navbar"
import { Menu } from "./menu"
import { currentSourcesAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms"
import { useSearchBar } from "~/hooks/useSearch"
export function Search() {
const { toggle } = useSearchBar()
@ -22,7 +17,7 @@ function GoTop() {
<button
type="button"
title="Go To Top"
className={clsx("i-ph:arrow-fat-up-duotone", ok ? "op-50 btn" : "op-0")}
className={$("i-ph:arrow-fat-up-duotone", ok ? "op-50 btn" : "op-0")}
onClick={goToTop}
/>
)
@ -49,7 +44,7 @@ function Refresh() {
<button
type="button"
title="Refresh"
className={clsx("i-ph:arrow-counter-clockwise-duotone btn", isFetching && "animate-spin i-ph:circle-dashed-duotone")}
className={$("i-ph:arrow-counter-clockwise-duotone btn", isFetching && "animate-spin i-ph:circle-dashed-duotone")}
onClick={refreshAll}
/>
)

View File

@ -1,16 +1,11 @@
import { Homepage } from "@shared/consts"
import clsx from "clsx"
import { motion } from "framer-motion"
import { useEffect, useRef, useState } from "react"
import { useHoverDirty } from "react-use"
import { useDark } from "~/hooks/useDark"
import { useLogin } from "~/hooks/useLogin"
function ThemeToggle() {
const { isDark, toggleDark } = useDark()
return (
<li onClick={toggleDark}>
<span className={clsx("inline-block", isDark ? "i-ph-moon-stars-duotone" : "i-ph-sun-dim-duotone")} />
<span className={$("inline-block", isDark ? "i-ph-moon-stars-duotone" : "i-ph-sun-dim-duotone")} />
<span>
{isDark ? "黑暗模式" : "白天模式"}
</span>
@ -50,7 +45,7 @@ export function Menu() {
<div className="absolute right-0 z-99 bg-transparent pt-8 top-0">
<motion.div
id="dropdown-menu"
className={clsx([
className={$([
"w-200px",
"bg-primary backdrop-blur-5 bg-op-70! rounded-lg shadow-xl",
])}

View File

@ -1,15 +1,12 @@
import { fixedColumnIds, metadata } from "@shared/metadata"
import { Link } from "@tanstack/react-router"
import clsx from "clsx"
import { useAtomValue } from "jotai"
import { currentColumnIDAtom } from "~/atoms"
import { useSearchBar } from "~/hooks/useSearch"
export function NavBar() {
const currentId = useAtomValue(currentColumnIDAtom)
const { toggle } = useSearchBar()
return (
<span className={clsx([
<span className={$([
"flex p-3 rounded-2xl bg-primary/1 text-sm",
"shadow shadow-primary/20 hover:shadow-primary/50 transition-shadow-500",
])}
@ -17,7 +14,7 @@ export function NavBar() {
<button
type="button"
onClick={() => toggle(true)}
className={clsx(
className={$(
"px-2 hover:(bg-primary/10 rounded-md) op-70 dark:op-90",
)}
>
@ -28,7 +25,7 @@ export function NavBar() {
key={columnId}
to="/c/$column"
params={{ column: columnId }}
className={clsx(
className={$(
"px-2 hover:(bg-primary/10 rounded-md)",
currentId === columnId ? "color-primary font-bold" : "op-70 dark:op-90",
)}

View File

@ -1,7 +1,5 @@
import { useMemo } from "react"
import { useMedia, useUpdateEffect } from "react-use"
import { atomWithStorage } from "jotai/utils"
import { useAtom } from "jotai"
export declare type ColorScheme = "dark" | "light" | "auto"

View File

@ -1,5 +1,3 @@
import { useCallback, useMemo } from "react"
import { useAtom } from "jotai"
import type { SourceID } from "@shared/types"
import { focusSourcesAtom } from "~/atoms"

View File

@ -1,7 +1,3 @@
import { useAtomValue } from "jotai"
import { atomWithStorage } from "jotai/utils"
import { useCallback } from "react"
const userAtom = atomWithStorage<{
name?: string
avatar?: string

View File

@ -1,6 +1,3 @@
import { relativeTime } from "@shared/utils"
import { atom, useAtomValue } from "jotai"
import { useEffect, useState } from "react"
import { useMount } from "react-use"
/**

View File

@ -1,6 +1,3 @@
import { atom, useAtom } from "jotai"
import { useCallback } from "react"
const searchBarAtom = atom(false)
export function useSearchBar() {

View File

@ -1,13 +1,11 @@
import type { PrimitiveMetadata } from "@shared/types"
import { useAtom } from "jotai"
import { ofetch } from "ofetch"
import { useDebounce, useMount } from "react-use"
import { useLogin } from "./useLogin"
import { useToast } from "./useToast"
import { preprocessMetadata, primitiveMetadataAtom } from "~/atoms"
import { safeParseString } from "~/utils"
export async function uploadMetadata(metadata: PrimitiveMetadata) {
async function uploadMetadata(metadata: PrimitiveMetadata) {
const jwt = safeParseString(localStorage.getItem("jwt"))
if (!jwt) return
await ofetch("/api/me/sync", {
@ -22,7 +20,7 @@ export async function uploadMetadata(metadata: PrimitiveMetadata) {
})
}
export async function downloadMetadata(): Promise<PrimitiveMetadata | undefined> {
async function downloadMetadata(): Promise<PrimitiveMetadata | undefined> {
const jwt = safeParseString(localStorage.getItem("jwt"))
if (!jwt) return
const { data, updatedTime } = await ofetch("/api/me/sync", {

View File

@ -1,5 +1,3 @@
import { atom, useSetAtom } from "jotai"
import { useCallback } from "react"
import type { ToastItem } from "~/atoms/types"
export const toastAtom = atom<ToastItem[]>([])

View File

@ -4,14 +4,10 @@ import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import type { QueryClient } from "@tanstack/react-query"
import clsx from "clsx"
import { Header } from "~/components/header"
import { useOnReload } from "~/hooks/useOnReload"
import { GlobalOverlayScrollbar } from "~/components/common/overlay-scrollbar"
import { useSync } from "~/hooks/useSync"
import { Footer } from "~/components/footer"
import { Toast } from "~/components/common/toast"
import { usePWA } from "~/hooks/usePWA"
import { SearchBar } from "~/components/common/search-bar"
export const Route = createRootRouteWithContext<{
@ -42,14 +38,14 @@ function RootComponent() {
usePWA()
return (
<>
<GlobalOverlayScrollbar className={clsx([
<GlobalOverlayScrollbar className={$([
"h-full overflow-x-auto px-4",
"md:(px-10)",
"lg:(px-24)",
])}
>
<header
className={clsx([
className={$([
"grid items-center py-4 px-5",
"lg:(py-6)",
"sticky top-0 z-10 backdrop-blur-md",
@ -60,7 +56,7 @@ function RootComponent() {
>
<Header />
</header>
<main className={clsx([
<main className={$([
"mt-2",
"min-h-[calc(100vh-180px)]",
"md:(min-h-[calc(100vh-175px)])",

View File

@ -1,5 +1,4 @@
import { createFileRoute, redirect } from "@tanstack/react-router"
import { fixedColumnIds } from "@shared/metadata"
import { Column } from "~/components/column"
export const Route = createFileRoute("/c/$column")({

View File

@ -1,5 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"
import { useAtomValue } from "jotai"
import { focusSourcesAtom } from "~/atoms"
import { Column } from "~/components/column"

View File

@ -9,5 +9,5 @@
"@shared/*": ["shared/*"]
}
},
"include": ["src", "shared"]
"include": ["src", "shared", "./auto-imports.app.d.ts"]
}

View File

@ -2,118 +2,49 @@ import process from "node:process"
import { join } from "node:path"
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react-swc"
import nitro from "vite-plugin-with-nitro"
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
import tsconfigPath from "vite-tsconfig-paths"
import unocss from "unocss/vite"
import dotenv from "dotenv"
import type { VitePWAOptions } from "vite-plugin-pwa"
import { VitePWA } from "vite-plugin-pwa"
import autoImport from "unplugin-auto-import/vite"
import nitro from "./nitro.config"
import { projectDir } from "./shared/dir"
import { RollopGlob } from "./tools/rollup-glob"
import pwa from "./pwa.config"
dotenv.config({
path: join(projectDir, ".env.server"),
})
const pwaOption: Partial<VitePWAOptions> = {
includeAssets: ["icon.svg", "apple-touch-icon.png"],
manifest: {
name: "NewsNow",
short_name: "NewsNow",
description: "Elegant reading of real-time and hottest news",
theme_color: "#F14D42",
icons: [
{
src: "pwa-192x192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "pwa-512x512.png",
sizes: "512x512",
type: "image/png",
},
{
src: "pwa-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
},
{
src: "pwa-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
workbox: {
navigateFallbackDenylist: [/^\/api/],
},
devOptions: {
enabled: process.env.SW_DEV === "true",
type: "module",
navigateFallback: "index.html",
},
}
const nitroOption: Parameters<typeof nitro>[0] = {
experimental: {
database: true,
},
rollupConfig: {
plugins: [RollopGlob()],
},
sourceMap: false,
database: {
default: {
connector: "sqlite",
},
},
preset: "node-server",
alias: {
"@shared": join(projectDir, "shared"),
"#": join(projectDir, "server"),
},
}
if (process.env.VERCEL) {
nitroOption.preset = "vercel-edge"
// You can use other online database, do it yourself. For more info: https://db0.unjs.io/connectors
nitroOption.database = undefined
} else if (process.env.CF_PAGES) {
nitroOption.preset = "cloudflare-pages"
nitroOption.database = {
default: {
connector: "cloudflare-d1",
options: {
bindingName: "NEWSNOW_DB",
},
},
}
} else if (process.env.BUN) {
nitroOption.preset = "bun"
nitroOption.database = {
default: {
connector: "bun-sqlite",
},
}
}
export default defineConfig({
define: {
__LOGIN_URL__: process.env.G_CLIENT_ID ? `"https://github.com/login/oauth/authorize?client_id=${process.env.G_CLIENT_ID}"` : `"/api/login"`,
},
build: {
sourcemap: true,
minify: false,
},
resolve: {
alias: {
"~": join(projectDir, "src"),
"@shared": join(projectDir, "shared"),
},
},
plugins: [
tsconfigPath(),
TanStackRouterVite({
autoCodeSplitting: true,
// error with unplugin-auto-import and vite-plugin-pwa
// autoCodeSplitting: true,
}),
autoImport({
dirs: ["src/hooks", "shared", "src/utils", "src/atoms"],
imports: ["react", "jotai", "jotai/utils", {
clsx: [
["default", "$"],
],
}],
dts: "auto-imports.app.d.ts",
}),
unocss(),
react(),
VitePWA(pwaOption),
nitro(nitroOption),
pwa(),
nitro(),
],
})

View File

@ -1,7 +1,8 @@
import { join } from "node:path"
import { defineConfig } from "vitest/config"
import autoImport from "unplugin-auto-import/vite"
import tsconfigPath from "vite-tsconfig-paths"
import { resolveModuleExportNames } from "mlly"
import { projectDir } from "./shared/dir"
const h3Exports = await resolveModuleExportNames("h3", {
url: import.meta.url,
@ -13,19 +14,24 @@ export default defineConfig({
environment: "node",
include: ["server/**/*.test.ts", "shared/**/*.test.ts", "test/**/*.test.ts"],
},
resolve: {
alias: {
"@shared": join(projectDir, "shared"),
"#": join(projectDir, "server"),
},
},
plugins: [
tsconfigPath(),
// https://github.com/unjs/nitro/blob/v2/src/core/config/resolvers/imports.ts
autoImport({
imports: ["vitest", {
imports: [{
from: "h3",
imports: h3Exports.filter(n => !/^[A-Z]/.test(n) && n !== "use"),
}, {
from: "ofetch",
imports: ["$fetch", "ofetch"],
}],
dirs: ["server/utils"],
dts: "dist/.nitro/types/nitro-imports.d.ts",
dirs: ["server/utils", "shared"],
dts: false,
}),
],
})