mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-31 10:58:04 +08:00
feat: ui
This commit is contained in:
parent
9eeee30e8c
commit
179358c919
@ -36,9 +36,13 @@ export function Dnd() {
|
|||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
variants={{
|
variants={{
|
||||||
|
hidden: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
visible: {
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
transition: {
|
transition: {
|
||||||
delayChildren: 0.2,
|
delayChildren: 0.1,
|
||||||
staggerChildren: 0.1,
|
staggerChildren: 0.1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import type { ColumnID } from "@shared/types"
|
import type { ColumnID } from "@shared/types"
|
||||||
import { useAtom } from "jotai"
|
import { useAtom } from "jotai"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
|
import { useTitle } from "react-use"
|
||||||
|
import { metadata } from "@shared/metadata"
|
||||||
|
import { NavBar } from "../navbar"
|
||||||
import { Dnd } from "./dnd"
|
import { Dnd } from "./dnd"
|
||||||
import { NavBar } from "./navbar"
|
|
||||||
import { currentColumnIDAtom } from "~/atoms"
|
import { currentColumnIDAtom } from "~/atoms"
|
||||||
|
|
||||||
export function Column({ id }: { id: ColumnID }) {
|
export function Column({ id }: { id: ColumnID }) {
|
||||||
@ -10,10 +12,18 @@ export function Column({ id }: { id: ColumnID }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentColumnID(id)
|
setCurrentColumnID(id)
|
||||||
}, [id, setCurrentColumnID])
|
}, [id, setCurrentColumnID])
|
||||||
|
|
||||||
|
useTitle(`NewsNow | ${metadata[id].name}`)
|
||||||
|
if (id === currentColumnID) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavBar id={id} />
|
<div className="flex justify-center md:hidden">
|
||||||
{ currentColumnID === id && <Dnd />}
|
<NavBar />
|
||||||
|
</div>
|
||||||
|
<div className="mt-10">
|
||||||
|
<Dnd />
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
import { columnIds, metadata } from "@shared/metadata"
|
|
||||||
import type { ColumnID } from "@shared/types"
|
|
||||||
import { Link } from "@tanstack/react-router"
|
|
||||||
import clsx from "clsx"
|
|
||||||
import { useTitle } from "react-use"
|
|
||||||
|
|
||||||
export function NavBar({ id }: { id: ColumnID }) {
|
|
||||||
useTitle(`NewsNow | ${metadata[id].name}`)
|
|
||||||
return (
|
|
||||||
<div className="w-full flex justify-center">
|
|
||||||
<span className={clsx([
|
|
||||||
"flex mb-4 p-3 rounded-2xl bg-primary/1",
|
|
||||||
"shadow shadow-primary/20 hover:shadow-primary/50 transition-shadow-500",
|
|
||||||
"md:(z-100 mb-6)",
|
|
||||||
])}
|
|
||||||
>
|
|
||||||
{columnIds.map(columnId => (
|
|
||||||
<Link
|
|
||||||
key={columnId}
|
|
||||||
to="/c/$column"
|
|
||||||
params={{ column: columnId }}
|
|
||||||
className={clsx(
|
|
||||||
"text-sm px-2",
|
|
||||||
id === columnId ? "color-primary font-bold" : "op-70 dark:op-90 hover:(bg-primary/15 rounded-md)",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{metadata[columnId].name}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
35
src/components/common/toast.tsx
Normal file
35
src/components/common/toast.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import clsx from "clsx"
|
||||||
|
import { Toaster } from "sonner"
|
||||||
|
|
||||||
|
export function Toast() {
|
||||||
|
return (
|
||||||
|
<Toaster
|
||||||
|
toastOptions={{
|
||||||
|
duration: 10000000,
|
||||||
|
unstyled: true,
|
||||||
|
classNames: {
|
||||||
|
toast: clsx(
|
||||||
|
"flex gap-1 p-1 rounded-xl backdrop-blur-5 items-center bg-op-40! w-full",
|
||||||
|
"bg-blue",
|
||||||
|
"data-[type=error]:(bg-red)",
|
||||||
|
"data-[type=success]:(bg-green)",
|
||||||
|
"data-[type=info]:(bg-blue)",
|
||||||
|
"data-[type=warning]:(bg-yellow)",
|
||||||
|
),
|
||||||
|
icon: "text-white ml-1 dark:text-dark-600 text-op-80!",
|
||||||
|
content: "bg-base bg-op-70! p-2 rounded-md color-base w-full backdrop-blur-md",
|
||||||
|
title: "font-normal text-base",
|
||||||
|
description: "color-base text-op-80! text-sm",
|
||||||
|
actionButton: "bg-base bg-op-70! rounded-md py-2 w-4em backdrop-blur-md hover:(bg-base bg-op-60!)",
|
||||||
|
closeButton: "bg-base bg-op-50! border-0 hover:(bg-base bg-op-70!)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
closeButton
|
||||||
|
expand
|
||||||
|
style={{
|
||||||
|
top: 10,
|
||||||
|
}}
|
||||||
|
position="top-center"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -5,76 +5,23 @@ import { useIsFetching } from "@tanstack/react-query"
|
|||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
import type { SourceID } from "@shared/types"
|
import type { SourceID } from "@shared/types"
|
||||||
import { Homepage, Version } from "@shared/consts"
|
import { Homepage, Version } from "@shared/consts"
|
||||||
import { useLocalStorage } from "react-use"
|
import { NavBar } from "../navbar"
|
||||||
import { useDark } from "~/hooks/useDark"
|
import { Menu } from "./menu"
|
||||||
import { currentSourcesAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms"
|
import { currentSourcesAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms"
|
||||||
|
|
||||||
function ThemeToggle() {
|
|
||||||
const { toggleDark } = useDark()
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title="Toggle Dark Mode"
|
|
||||||
className="i-ph-sun-dim-duotone dark:i-ph-moon-stars-duotone btn"
|
|
||||||
onClick={toggleDark}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function LoginIn() {
|
|
||||||
// useLocalStorage 默认会自动序列化
|
|
||||||
const [info] = useLocalStorage<{ name: string, avatar: string }>("user_info")
|
|
||||||
const [jwt, _setJwt] = useLocalStorage<string>("user_jwt", undefined, {
|
|
||||||
raw: true,
|
|
||||||
})
|
|
||||||
if (jwt) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn"
|
|
||||||
title={info?.name ?? ""}
|
|
||||||
onClick={() => {
|
|
||||||
// setJwt("")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="h-6 w-6 rounded-full bg-cover border p-1 border-primary-600 dark:border-primary"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
backgroundImage: `url(${info?.avatar})`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
title="Login in with GitHub"
|
|
||||||
className="i-ph:sign-in-duotone btn"
|
|
||||||
href={`https://github.com/login/oauth/authorize?client_id=${__G_CLIENT_ID__}`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function GoTop() {
|
function GoTop() {
|
||||||
const { ok, fn: goToTop } = useAtomValue(goToTopAtom)
|
const { ok, fn: goToTop } = useAtomValue(goToTopAtom)
|
||||||
return (
|
return (
|
||||||
ok && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title="Go To Top"
|
title="Go To Top"
|
||||||
className="i-ph:arrow-fat-up-duotone btn"
|
className={clsx("i-ph:arrow-fat-up-duotone", ok ? "op-50 btn" : "op-0")}
|
||||||
onClick={goToTop}
|
onClick={goToTop}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
export function GithubIcon() {
|
|
||||||
return <a className="i-ph-github-logo-duotone inline-block btn" href={Homepage} title="Project Homepage" />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function RefreshButton() {
|
function Refresh() {
|
||||||
const currentSources = useAtomValue(currentSourcesAtom)
|
const currentSources = useAtomValue(currentSourcesAtom)
|
||||||
const setRefetchSource = useSetAtom(refetchSourcesAtom)
|
const setRefetchSource = useSetAtom(refetchSourcesAtom)
|
||||||
const refreshAll = useCallback(() => {
|
const refreshAll = useCallback(() => {
|
||||||
@ -115,16 +62,17 @@ export function Header() {
|
|||||||
</p>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<a target="_blank" className="btn text-sm ml-1 font-mono">
|
<a target="_blank" href={`${Homepage}/release/tag/${Version}`} className="btn text-sm ml-1 font-mono">
|
||||||
{`v${Version}`}
|
{`v${Version}`}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
<span className="hidden md:inline-block">
|
||||||
|
<NavBar />
|
||||||
|
</span>
|
||||||
<span className="flex gap-2 items-center text-xl text-primary-600 dark:text-primary">
|
<span className="flex gap-2 items-center text-xl text-primary-600 dark:text-primary">
|
||||||
<GoTop />
|
<GoTop />
|
||||||
<RefreshButton />
|
<Refresh />
|
||||||
<ThemeToggle />
|
<Menu />
|
||||||
<GithubIcon />
|
|
||||||
{ __ENABLE_LOGIN__ && <LoginIn />}
|
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
85
src/components/header/menu.tsx
Normal file
85
src/components/header/menu.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { Homepage } from "@shared/consts"
|
||||||
|
import clsx from "clsx"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
import { useRef, useState } from "react"
|
||||||
|
import { useClickAway } 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>
|
||||||
|
{isDark ? "黑暗模式" : "白天模式"}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Menu() {
|
||||||
|
const { loggedIn, login, logout, enabledLogin, userInfo } = useLogin()
|
||||||
|
const [shown, show] = useState(false)
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
useClickAway(ref, () => {
|
||||||
|
show(false)
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<span ref={ref} className="relative">
|
||||||
|
<span className="flex items-center scale-90" onClick={() => show(!shown)}>
|
||||||
|
{
|
||||||
|
enabledLogin && loggedIn && userInfo.avatar
|
||||||
|
? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="h-6 w-6 rounded-full bg-cover"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
backgroundImage: `url(${userInfo.avatar})`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
: <button type="button" className="btn i-si:more-muted-horiz-circle-duotone" />
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
{shown && (
|
||||||
|
<motion.div
|
||||||
|
id="dropdown-menu"
|
||||||
|
className={clsx([
|
||||||
|
"absolute top-2rem right-0 z-99 w-200px",
|
||||||
|
"bg-primary p-1 backdrop-blur-5 bg-op-40! rounded-xl",
|
||||||
|
])}
|
||||||
|
initial={{
|
||||||
|
scale: 0.9,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
scale: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ol className="bg-base bg-op-70! backdrop-blur-md p-2 rounded-md color-base text-base">
|
||||||
|
{enabledLogin && !loggedIn && (
|
||||||
|
<li onClick={login}>
|
||||||
|
<span className="i-ph:sign-in-duotone inline-block" />
|
||||||
|
<span>Github 账号登录</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{enabledLogin && loggedIn && (
|
||||||
|
<li onClick={logout}>
|
||||||
|
<span className="i-ph:sign-out-duotone inline-block" />
|
||||||
|
<span>退出登录</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<ThemeToggle />
|
||||||
|
<li onClick={() => window.open(Homepage)}>
|
||||||
|
<span className="i-ph:github-logo-duotone inline-block" />
|
||||||
|
<span>Star on Github </span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
30
src/components/navbar.tsx
Normal file
30
src/components/navbar.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { columnIds, metadata } from "@shared/metadata"
|
||||||
|
import { Link } from "@tanstack/react-router"
|
||||||
|
import clsx from "clsx"
|
||||||
|
import { useAtomValue } from "jotai"
|
||||||
|
import { currentColumnIDAtom } from "~/atoms"
|
||||||
|
|
||||||
|
export function NavBar() {
|
||||||
|
const currentId = useAtomValue(currentColumnIDAtom)
|
||||||
|
return (
|
||||||
|
<span className={clsx([
|
||||||
|
"flex p-3 rounded-2xl bg-primary/1",
|
||||||
|
"shadow shadow-primary/20 hover:shadow-primary/50 transition-shadow-500",
|
||||||
|
])}
|
||||||
|
>
|
||||||
|
{columnIds.map(columnId => (
|
||||||
|
<Link
|
||||||
|
key={columnId}
|
||||||
|
to="/c/$column"
|
||||||
|
params={{ column: columnId }}
|
||||||
|
className={clsx(
|
||||||
|
"text-sm px-2 hover:(bg-primary/10 rounded-md)",
|
||||||
|
currentId === columnId ? "color-primary font-bold" : "op-70 dark:op-90",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{metadata[columnId].name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
@ -4,9 +4,7 @@ import { useLocalStorage, useMedia } from "react-use"
|
|||||||
export declare type ColorScheme = "dark" | "light" | "auto"
|
export declare type ColorScheme = "dark" | "light" | "auto"
|
||||||
|
|
||||||
export function useDark(key = "color-scheme", defaultColorScheme: ColorScheme = "auto") {
|
export function useDark(key = "color-scheme", defaultColorScheme: ColorScheme = "auto") {
|
||||||
const [colorScheme, setColorScheme] = useLocalStorage(key, defaultColorScheme, {
|
const [colorScheme, setColorScheme] = useLocalStorage(key, defaultColorScheme)
|
||||||
raw: true,
|
|
||||||
})
|
|
||||||
const prefersDarkMode = useMedia("(prefers-color-scheme: dark)")
|
const prefersDarkMode = useMedia("(prefers-color-scheme: dark)")
|
||||||
const isDark = useMemo(() => colorScheme === "auto" ? prefersDarkMode : colorScheme === "dark", [colorScheme, prefersDarkMode])
|
const isDark = useMemo(() => colorScheme === "auto" ? prefersDarkMode : colorScheme === "dark", [colorScheme, prefersDarkMode])
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useEffect } from "react"
|
import { useBeforeUnload, useMount } from "react-use"
|
||||||
import { useBeforeUnload } from "react-use"
|
|
||||||
|
|
||||||
export function useOnReload(fn?: () => Promise<void> | void, fallback?: () => Promise<void> | void) {
|
export function useOnReload(fn?: () => Promise<void> | void, fallback?: () => Promise<void> | void) {
|
||||||
useBeforeUnload(() => {
|
useBeforeUnload(() => {
|
||||||
@ -7,7 +6,7 @@ export function useOnReload(fn?: () => Promise<void> | void, fallback?: () => Pr
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useMount(() => {
|
||||||
const _ = localStorage.getItem("quitTime")
|
const _ = localStorage.getItem("quitTime")
|
||||||
const quitTime = _ ? Number(_) : 0
|
const quitTime = _ ? Number(_) : 0
|
||||||
if (!Number.isNaN(quitTime) && Date.now() - quitTime < 1000) {
|
if (!Number.isNaN(quitTime) && Date.now() - quitTime < 1000) {
|
||||||
@ -15,6 +14,5 @@ export function useOnReload(fn?: () => Promise<void> | void, fallback?: () => Pr
|
|||||||
} else {
|
} else {
|
||||||
fallback?.()
|
fallback?.()
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
})
|
||||||
}, [])
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { useAtomValue } from "jotai"
|
import { useAtomValue } from "jotai"
|
||||||
import { useMemo } from "react"
|
|
||||||
import { focusSourcesAtom } from "~/atoms"
|
import { focusSourcesAtom } from "~/atoms"
|
||||||
import { Column } from "~/components/column"
|
import { Column } from "~/components/column"
|
||||||
import { } from "cookie-es"
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
component: IndexComponent,
|
component: IndexComponent,
|
||||||
@ -11,9 +9,5 @@ export const Route = createFileRoute("/")({
|
|||||||
|
|
||||||
function IndexComponent() {
|
function IndexComponent() {
|
||||||
const focusSources = useAtomValue(focusSourcesAtom)
|
const focusSources = useAtomValue(focusSourcesAtom)
|
||||||
const id = useMemo(() => {
|
return <Column id={focusSources.length ? "focus" : "realtime"} />
|
||||||
return focusSources.length ? "focus" : "realtime"
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
return <Column id={id} />
|
|
||||||
}
|
}
|
||||||
|
@ -47,3 +47,7 @@ button:disabled {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#dropdown-menu li {
|
||||||
|
--at-apply: hover:bg-neutral-400/10 rounded-md flex items-center p-1 gap-1;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user