mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 03:09:14 +08:00
feat: ui
This commit is contained in:
parent
9eeee30e8c
commit
179358c919
@ -36,9 +36,13 @@ export function Dnd() {
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={{
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
delayChildren: 0.2,
|
||||
delayChildren: 0.1,
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
|
@ -1,8 +1,10 @@
|
||||
import type { ColumnID } 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 { NavBar } from "./navbar"
|
||||
import { currentColumnIDAtom } from "~/atoms"
|
||||
|
||||
export function Column({ id }: { id: ColumnID }) {
|
||||
@ -10,10 +12,18 @@ export function Column({ id }: { id: ColumnID }) {
|
||||
useEffect(() => {
|
||||
setCurrentColumnID(id)
|
||||
}, [id, setCurrentColumnID])
|
||||
return (
|
||||
<>
|
||||
<NavBar id={id} />
|
||||
{ currentColumnID === id && <Dnd />}
|
||||
</>
|
||||
)
|
||||
|
||||
useTitle(`NewsNow | ${metadata[id].name}`)
|
||||
if (id === currentColumnID) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-center md:hidden">
|
||||
<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 type { SourceID } from "@shared/types"
|
||||
import { Homepage, Version } from "@shared/consts"
|
||||
import { useLocalStorage } from "react-use"
|
||||
import { useDark } from "~/hooks/useDark"
|
||||
import { NavBar } from "../navbar"
|
||||
import { Menu } from "./menu"
|
||||
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() {
|
||||
const { ok, fn: goToTop } = useAtomValue(goToTopAtom)
|
||||
return (
|
||||
ok && (
|
||||
<button
|
||||
type="button"
|
||||
title="Go To Top"
|
||||
className="i-ph:arrow-fat-up-duotone btn"
|
||||
onClick={goToTop}
|
||||
/>
|
||||
)
|
||||
<button
|
||||
type="button"
|
||||
title="Go To Top"
|
||||
className={clsx("i-ph:arrow-fat-up-duotone", ok ? "op-50 btn" : "op-0")}
|
||||
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 setRefetchSource = useSetAtom(refetchSourcesAtom)
|
||||
const refreshAll = useCallback(() => {
|
||||
@ -115,16 +62,17 @@ export function Header() {
|
||||
</p>
|
||||
</span>
|
||||
</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}`}
|
||||
</a>
|
||||
</span>
|
||||
<span className="hidden md:inline-block">
|
||||
<NavBar />
|
||||
</span>
|
||||
<span className="flex gap-2 items-center text-xl text-primary-600 dark:text-primary">
|
||||
<GoTop />
|
||||
<RefreshButton />
|
||||
<ThemeToggle />
|
||||
<GithubIcon />
|
||||
{ __ENABLE_LOGIN__ && <LoginIn />}
|
||||
<Refresh />
|
||||
<Menu />
|
||||
</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 function useDark(key = "color-scheme", defaultColorScheme: ColorScheme = "auto") {
|
||||
const [colorScheme, setColorScheme] = useLocalStorage(key, defaultColorScheme, {
|
||||
raw: true,
|
||||
})
|
||||
const [colorScheme, setColorScheme] = useLocalStorage(key, defaultColorScheme)
|
||||
const prefersDarkMode = useMedia("(prefers-color-scheme: dark)")
|
||||
const isDark = useMemo(() => colorScheme === "auto" ? prefersDarkMode : colorScheme === "dark", [colorScheme, prefersDarkMode])
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useEffect } from "react"
|
||||
import { useBeforeUnload } from "react-use"
|
||||
import { useBeforeUnload, useMount } from "react-use"
|
||||
|
||||
export function useOnReload(fn?: () => Promise<void> | void, fallback?: () => Promise<void> | void) {
|
||||
useBeforeUnload(() => {
|
||||
@ -7,7 +6,7 @@ export function useOnReload(fn?: () => Promise<void> | void, fallback?: () => Pr
|
||||
return false
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
useMount(() => {
|
||||
const _ = localStorage.getItem("quitTime")
|
||||
const quitTime = _ ? Number(_) : 0
|
||||
if (!Number.isNaN(quitTime) && Date.now() - quitTime < 1000) {
|
||||
@ -15,6 +14,5 @@ export function useOnReload(fn?: () => Promise<void> | void, fallback?: () => Pr
|
||||
} else {
|
||||
fallback?.()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
})
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { useAtomValue } from "jotai"
|
||||
import { useMemo } from "react"
|
||||
import { focusSourcesAtom } from "~/atoms"
|
||||
import { Column } from "~/components/column"
|
||||
import { } from "cookie-es"
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: IndexComponent,
|
||||
@ -11,9 +9,5 @@ export const Route = createFileRoute("/")({
|
||||
|
||||
function IndexComponent() {
|
||||
const focusSources = useAtomValue(focusSourcesAtom)
|
||||
const id = useMemo(() => {
|
||||
return focusSources.length ? "focus" : "realtime"
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
return <Column id={id} />
|
||||
return <Column id={focusSources.length ? "focus" : "realtime"} />
|
||||
}
|
||||
|
@ -46,4 +46,8 @@ button:disabled {
|
||||
*, a, button {
|
||||
cursor: default;
|
||||
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