feat: 亮色暗色模式切换。
This commit is contained in:
parent
d3301da0ad
commit
305eb667f4
1
.env
1
.env
@ -1,2 +1,3 @@
|
||||
NEXT_PUBLIC_FIRST_NAME=Ivan
|
||||
NEXT_PUBLIC_LAST_NAME=Li
|
||||
BACKEND_URI=http://127.0.0.1:7132/graphql
|
||||
|
@ -1,6 +1,98 @@
|
||||
import { ApolloClient, InMemoryCache } from "@apollo/client";
|
||||
import { useMemo } from "react";
|
||||
import { ApolloClient, from, HttpLink, InMemoryCache } from "@apollo/client";
|
||||
import { concatPagination } from "@apollo/client/utilities";
|
||||
import { equals, mergeDeepWith } from "ramda";
|
||||
import { onError } from "@apollo/client/link/error";
|
||||
|
||||
export const client = new ApolloClient({
|
||||
uri: "/api/graphql",
|
||||
cache: new InMemoryCache(),
|
||||
});
|
||||
|
||||
export const APOLLO_STATE_PROP_NAME = "__APOLLO_STATE__";
|
||||
|
||||
let apolloClient: ApolloClient<InMemoryCache>;
|
||||
|
||||
function createApolloClient() {
|
||||
const httpLink = new HttpLink({
|
||||
uri:
|
||||
typeof window === "undefined"
|
||||
? process.env.BACKEND_URI
|
||||
: "/api/graphql", // Server URL (must be absolute)
|
||||
credentials: "same-origin", // Additional fetch() options like `credentials` or `headers`
|
||||
});
|
||||
|
||||
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
||||
if (graphQLErrors)
|
||||
graphQLErrors.forEach(({ message, locations, path }) =>
|
||||
console.log(
|
||||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
||||
)
|
||||
);
|
||||
|
||||
if (networkError) console.log(`[Network error]: ${networkError}`);
|
||||
});
|
||||
return new ApolloClient({
|
||||
ssrMode: typeof window === "undefined",
|
||||
link: from([errorLink, httpLink]),
|
||||
cache: new InMemoryCache({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
allPosts: concatPagination(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function initializeApollo(initialState = null) {
|
||||
const _apolloClient = apolloClient ?? createApolloClient();
|
||||
|
||||
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
|
||||
// gets hydrated here
|
||||
if (initialState) {
|
||||
// Get existing cache, loaded during client side data fetching
|
||||
const existingCache = _apolloClient.extract();
|
||||
|
||||
// Merge the existing cache into data passed from getStaticProps/getServerSideProps
|
||||
const data = mergeDeepWith(
|
||||
(a, b) => {
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
return [
|
||||
...a,
|
||||
...b.filter((bi) => a.every((ai) => !equals(ai, bi))),
|
||||
];
|
||||
} else {
|
||||
return b;
|
||||
}
|
||||
},
|
||||
initialState,
|
||||
existingCache
|
||||
);
|
||||
|
||||
// Restore the cache with the merged data
|
||||
_apolloClient.cache.restore(data);
|
||||
}
|
||||
// For SSG and SSR always create a new Apollo Client
|
||||
if (typeof window === "undefined") return _apolloClient;
|
||||
// Create the Apollo Client once in the client
|
||||
if (!apolloClient) apolloClient = _apolloClient;
|
||||
|
||||
return _apolloClient;
|
||||
}
|
||||
|
||||
export function addApolloState(client, pageProps) {
|
||||
if (pageProps?.props) {
|
||||
pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract();
|
||||
}
|
||||
|
||||
return pageProps;
|
||||
}
|
||||
|
||||
export function useApollo(pageProps) {
|
||||
const state = pageProps[APOLLO_STATE_PROP_NAME];
|
||||
const store = useMemo(() => initializeApollo(state), [state]);
|
||||
return store;
|
||||
}
|
||||
|
@ -10,3 +10,14 @@ export const ARTICLE_FOR_HOME = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ARTICLE = gql`
|
||||
query Article($id: String!) {
|
||||
article(id: $id) {
|
||||
id
|
||||
title
|
||||
content
|
||||
publishedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
56
commons/theme.tsx
Normal file
56
commons/theme.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import React, { createContext, FC, useContext, useEffect, useState } from "react";
|
||||
|
||||
const getInitialTheme = (): Mode => {
|
||||
if (typeof window === "undefined") {
|
||||
return 'auto';
|
||||
}
|
||||
const text = window?.localStorage?.getItem('theme');
|
||||
switch (text) {
|
||||
case 'dark': return 'dark';
|
||||
case 'light': return 'light';
|
||||
default: return 'auto';
|
||||
}
|
||||
};
|
||||
|
||||
type RawMode = "light" | "dark";
|
||||
type Mode = RawMode | "auto";
|
||||
|
||||
interface Content {
|
||||
mode: Mode;
|
||||
setMode: (mode: Mode) => void;
|
||||
}
|
||||
|
||||
export const ThemeContext = createContext<Content>({
|
||||
mode: 'error' as Mode,
|
||||
setMode: (_) => {console.log},
|
||||
});
|
||||
|
||||
export const ThemeProvider: FC = ({ children }) => {
|
||||
const [mode, setMode] = useState<Mode>(() => getInitialTheme());
|
||||
|
||||
useEffect(() => {
|
||||
if (window) {
|
||||
localStorage.setItem("theme", mode);
|
||||
let _mode = mode;
|
||||
if (_mode === "auto") {
|
||||
_mode =
|
||||
window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
if (_mode === "dark") {
|
||||
window.document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
window.document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ mode, setMode }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useTheme () {
|
||||
return useContext(ThemeContext);
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
.wrapper {
|
||||
@apply bg-green-400 text-white;
|
||||
@apply bg-green-400 text-white min-h-screen;
|
||||
:global(.dark) & {
|
||||
@apply bg-gray-800 text-gray-400;
|
||||
}
|
||||
}
|
||||
.sidebar {
|
||||
@apply overflow-hidden flex flex-col fixed top-0;
|
||||
@apply text-center shadow-2xl;
|
||||
height: 100vh;
|
||||
@apply text-center shadow-2xl h-screen;
|
||||
padding-top: 10vh;
|
||||
}
|
||||
|
||||
|
10
components/switch-theme.tsx
Normal file
10
components/switch-theme.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { useTheme } from "../commons/theme";
|
||||
|
||||
export const SwitchTheme = () => {
|
||||
const { setMode, mode } = useTheme();
|
||||
|
||||
if (mode === "light") {
|
||||
return <button onClick={() => setMode("dark")}>暗色</button>;
|
||||
}
|
||||
return <button onClick={() => setMode("light")}>亮色</button>;
|
||||
};
|
42
package-lock.json
generated
42
package-lock.json
generated
@ -13,6 +13,7 @@
|
||||
"next": "10.2.0",
|
||||
"postcss-import": "^14.0.1",
|
||||
"postcss-nested": "^5.0.5",
|
||||
"ramda": "^0.27.1",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2"
|
||||
},
|
||||
@ -27,6 +28,7 @@
|
||||
"@types/graphql": "^14.5.0",
|
||||
"@types/postcss-import": "^12.0.0",
|
||||
"@types/postcss-nested": "^4.2.3",
|
||||
"@types/ramda": "^0.27.40",
|
||||
"@types/react": "^17.0.4",
|
||||
"@types/tailwindcss": "^2.0.2",
|
||||
"autoprefixer": "^10.2.5",
|
||||
@ -2470,6 +2472,15 @@
|
||||
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ramda": {
|
||||
"version": "0.27.40",
|
||||
"resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.27.40.tgz",
|
||||
"integrity": "sha512-V99ZfTH2tqVYdLDAlgh2uT+N074HPgqnAsMjALKSBqogYd0HbuuGMqNukJ6fk9Ml/Htaus76fsc4Yh3p7q1VdQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ts-toolbelt": "^6.15.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "17.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.4.tgz",
|
||||
@ -7681,6 +7692,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ramda": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz",
|
||||
"integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw=="
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
@ -8932,6 +8948,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ts-toolbelt": {
|
||||
"version": "6.15.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz",
|
||||
"integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
@ -11517,6 +11539,15 @@
|
||||
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/ramda": {
|
||||
"version": "0.27.40",
|
||||
"resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.27.40.tgz",
|
||||
"integrity": "sha512-V99ZfTH2tqVYdLDAlgh2uT+N074HPgqnAsMjALKSBqogYd0HbuuGMqNukJ6fk9Ml/Htaus76fsc4Yh3p7q1VdQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ts-toolbelt": "^6.15.1"
|
||||
}
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "17.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.4.tgz",
|
||||
@ -15684,6 +15715,11 @@
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"dev": true
|
||||
},
|
||||
"ramda": {
|
||||
"version": "0.27.1",
|
||||
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz",
|
||||
"integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw=="
|
||||
},
|
||||
"randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
@ -16688,6 +16724,12 @@
|
||||
"resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",
|
||||
"integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw=="
|
||||
},
|
||||
"ts-toolbelt": {
|
||||
"version": "6.15.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz",
|
||||
"integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==",
|
||||
"dev": true
|
||||
},
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
|
@ -16,6 +16,7 @@
|
||||
"next": "10.2.0",
|
||||
"postcss-import": "^14.0.1",
|
||||
"postcss-nested": "^5.0.5",
|
||||
"ramda": "^0.27.1",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2"
|
||||
},
|
||||
@ -30,6 +31,7 @@
|
||||
"@types/graphql": "^14.5.0",
|
||||
"@types/postcss-import": "^12.0.0",
|
||||
"@types/postcss-nested": "^4.2.3",
|
||||
"@types/ramda": "^0.27.40",
|
||||
"@types/react": "^17.0.4",
|
||||
"@types/tailwindcss": "^2.0.2",
|
||||
"autoprefixer": "^10.2.5",
|
||||
|
@ -4,19 +4,22 @@ import React from "react";
|
||||
import { GlobalSidebar } from "../components/layouts/global-sidebar";
|
||||
import styles from "./_app.module.css";
|
||||
import { ApolloProvider } from "@apollo/client";
|
||||
import { client } from "../commons/graphql/client";
|
||||
import { useApollo } from "../commons/graphql/client";
|
||||
import { ThemeProvider, useTheme } from '../commons/theme';
|
||||
import { SwitchTheme } from '../components/switch-theme';
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
const apolloClient = useApollo(pageProps);
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<ThemeProvider>
|
||||
<div className={styles.page}>
|
||||
<GlobalSidebar className={styles.sidebar} />
|
||||
<div className={styles.primary}>
|
||||
<header className={styles.pageHeader}>
|
||||
<h1>{"Ivan Li 的个人博客"}</h1>
|
||||
<div className={styles.actions}>
|
||||
<button onClick={() => switchTheme("light")}>亮色</button>
|
||||
<button onClick={() => switchTheme("dark")}>暗色</button>
|
||||
<SwitchTheme />
|
||||
</div>
|
||||
</header>
|
||||
<div className={styles.wrapper}>
|
||||
@ -24,20 +27,9 @@ function MyApp({ Component, pageProps }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</ApolloProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function switchTheme(mode: "light" | "dark" | "auto") {
|
||||
if (mode === "auto") {
|
||||
mode = "light";
|
||||
}
|
||||
|
||||
if (mode === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
|
38
pages/articles/[id].tsx
Normal file
38
pages/articles/[id].tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { FC } from "react";
|
||||
import styles from "./article.module.css";
|
||||
import { GetServerSideProps, GetStaticProps } from "next";
|
||||
import { addApolloState, initializeApollo } from "../../commons/graphql/client";
|
||||
import { ARTICLE } from "../../commons/graphql/queries";
|
||||
import { Article } from "../../commons/graphql/generated";
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
interface Props {
|
||||
article: Article;
|
||||
}
|
||||
const ArticleDetails: FC<Props> = ({ article }) => {
|
||||
// const router = useRouter()
|
||||
// const { data } = useQuery<{ article: Article }>(ARTICLE, {
|
||||
// variables: router.query,
|
||||
// });
|
||||
return (
|
||||
<main className={styles.articleDetails}>
|
||||
<h1>{article.title}</h1>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
|
||||
const apolloClient = initializeApollo();
|
||||
|
||||
const { data } = await apolloClient.query<{ article: Article }>({
|
||||
query: ARTICLE,
|
||||
variables: params,
|
||||
});
|
||||
|
||||
return addApolloState(apolloClient, {
|
||||
props: { article: data.article },
|
||||
});
|
||||
};
|
||||
|
||||
export default ArticleDetails;
|
0
pages/articles/article.module.css
Normal file
0
pages/articles/article.module.css
Normal file
@ -1,23 +1,46 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { Article } from '../commons/graphql/generated';
|
||||
import { ARTICLE_FOR_HOME } from '../commons/graphql/queries';
|
||||
import styles from './index.module.css';
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { GetServerSideProps } from 'next';
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import { addApolloState, initializeApollo } from '../commons/graphql/client';
|
||||
import { Article } from "../commons/graphql/generated";
|
||||
import { ARTICLE_FOR_HOME } from "../commons/graphql/queries";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
export default function Index() {
|
||||
const { data, loading } = useQuery<{articles: Article[]}>(ARTICLE_FOR_HOME);
|
||||
const { data, loading } = useQuery<{ articles: Article[] }>(ARTICLE_FOR_HOME);
|
||||
|
||||
return <main className={styles.index}>
|
||||
return (
|
||||
<main className={styles.index}>
|
||||
<ol>
|
||||
{
|
||||
data?.articles?.map(article => <Item article={article} key={article.id} />)
|
||||
}
|
||||
{data?.articles?.map((article) => (
|
||||
<Item article={article} key={article.id} />
|
||||
))}
|
||||
</ol>
|
||||
</main>;
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function Item({article}: {article: Article}) {
|
||||
return <li className={styles.item}>
|
||||
function Item({ article }: { article: Article }) {
|
||||
return (
|
||||
<Link href={`/articles/${article.id}`}>
|
||||
<li className={styles.item}>
|
||||
<h2 className={styles.title}>{article.title}</h2>
|
||||
<p className={styles.description}>{article.content}</p>
|
||||
</li>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
|
||||
const apolloClient = initializeApollo();
|
||||
|
||||
await apolloClient.query({
|
||||
query: ARTICLE_FOR_HOME,
|
||||
});
|
||||
|
||||
return addApolloState(apolloClient, {
|
||||
props: {},
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue
Block a user