feat: 亮色暗色模式切换。

This commit is contained in:
Ivan Li 2021-05-02 20:04:54 +08:00
parent d3301da0ad
commit 305eb667f4
12 changed files with 314 additions and 48 deletions

1
.env
View File

@ -1,2 +1,3 @@
NEXT_PUBLIC_FIRST_NAME=Ivan NEXT_PUBLIC_FIRST_NAME=Ivan
NEXT_PUBLIC_LAST_NAME=Li NEXT_PUBLIC_LAST_NAME=Li
BACKEND_URI=http://127.0.0.1:7132/graphql

View File

@ -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({ export const client = new ApolloClient({
uri: "/api/graphql", uri: "/api/graphql",
cache: new InMemoryCache(), 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;
}

View File

@ -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
View 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);
}

View File

@ -1,13 +1,12 @@
.wrapper { .wrapper {
@apply bg-green-400 text-white; @apply bg-green-400 text-white min-h-screen;
:global(.dark) & { :global(.dark) & {
@apply bg-gray-800 text-gray-400; @apply bg-gray-800 text-gray-400;
} }
} }
.sidebar { .sidebar {
@apply overflow-hidden flex flex-col fixed top-0; @apply overflow-hidden flex flex-col fixed top-0;
@apply text-center shadow-2xl; @apply text-center shadow-2xl h-screen;
height: 100vh;
padding-top: 10vh; padding-top: 10vh;
} }

View 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
View File

@ -13,6 +13,7 @@
"next": "10.2.0", "next": "10.2.0",
"postcss-import": "^14.0.1", "postcss-import": "^14.0.1",
"postcss-nested": "^5.0.5", "postcss-nested": "^5.0.5",
"ramda": "^0.27.1",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2" "react-dom": "17.0.2"
}, },
@ -27,6 +28,7 @@
"@types/graphql": "^14.5.0", "@types/graphql": "^14.5.0",
"@types/postcss-import": "^12.0.0", "@types/postcss-import": "^12.0.0",
"@types/postcss-nested": "^4.2.3", "@types/postcss-nested": "^4.2.3",
"@types/ramda": "^0.27.40",
"@types/react": "^17.0.4", "@types/react": "^17.0.4",
"@types/tailwindcss": "^2.0.2", "@types/tailwindcss": "^2.0.2",
"autoprefixer": "^10.2.5", "autoprefixer": "^10.2.5",
@ -2470,6 +2472,15 @@
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
"dev": true "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": { "node_modules/@types/react": {
"version": "17.0.4", "version": "17.0.4",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.4.tgz",
@ -7681,6 +7692,11 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "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": { "node_modules/tslib": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
@ -11517,6 +11539,15 @@
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
"dev": true "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": { "@types/react": {
"version": "17.0.4", "version": "17.0.4",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.4.tgz",
@ -15684,6 +15715,11 @@
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"dev": true "dev": true
}, },
"ramda": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz",
"integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw=="
},
"randombytes": { "randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "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", "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",
"integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==" "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": { "tslib": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",

View File

@ -16,6 +16,7 @@
"next": "10.2.0", "next": "10.2.0",
"postcss-import": "^14.0.1", "postcss-import": "^14.0.1",
"postcss-nested": "^5.0.5", "postcss-nested": "^5.0.5",
"ramda": "^0.27.1",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2" "react-dom": "17.0.2"
}, },
@ -30,6 +31,7 @@
"@types/graphql": "^14.5.0", "@types/graphql": "^14.5.0",
"@types/postcss-import": "^12.0.0", "@types/postcss-import": "^12.0.0",
"@types/postcss-nested": "^4.2.3", "@types/postcss-nested": "^4.2.3",
"@types/ramda": "^0.27.40",
"@types/react": "^17.0.4", "@types/react": "^17.0.4",
"@types/tailwindcss": "^2.0.2", "@types/tailwindcss": "^2.0.2",
"autoprefixer": "^10.2.5", "autoprefixer": "^10.2.5",

View File

@ -4,19 +4,22 @@ import React from "react";
import { GlobalSidebar } from "../components/layouts/global-sidebar"; import { GlobalSidebar } from "../components/layouts/global-sidebar";
import styles from "./_app.module.css"; import styles from "./_app.module.css";
import { ApolloProvider } from "@apollo/client"; 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 }) { function MyApp({ Component, pageProps }) {
const apolloClient = useApollo(pageProps);
return ( return (
<ApolloProvider client={client}> <ApolloProvider client={apolloClient}>
<ThemeProvider>
<div className={styles.page}> <div className={styles.page}>
<GlobalSidebar className={styles.sidebar} /> <GlobalSidebar className={styles.sidebar} />
<div className={styles.primary}> <div className={styles.primary}>
<header className={styles.pageHeader}> <header className={styles.pageHeader}>
<h1>{"Ivan Li 的个人博客"}</h1> <h1>{"Ivan Li 的个人博客"}</h1>
<div className={styles.actions}> <div className={styles.actions}>
<button onClick={() => switchTheme("light")}></button> <SwitchTheme />
<button onClick={() => switchTheme("dark")}></button>
</div> </div>
</header> </header>
<div className={styles.wrapper}> <div className={styles.wrapper}>
@ -24,20 +27,9 @@ function MyApp({ Component, pageProps }) {
</div> </div>
</div> </div>
</div> </div>
</ThemeProvider>
</ApolloProvider> </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; export default MyApp;

38
pages/articles/[id].tsx Normal file
View 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;

View File

View File

@ -1,23 +1,46 @@
import { useQuery } from '@apollo/client'; import { useQuery } from "@apollo/client";
import { Article } from '../commons/graphql/generated'; import { GetServerSideProps } from 'next';
import { ARTICLE_FOR_HOME } from '../commons/graphql/queries'; import Link from "next/link";
import styles from './index.module.css'; 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() { 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> <ol>
{ {data?.articles?.map((article) => (
data?.articles?.map(article => <Item article={article} key={article.id} />) <Item article={article} key={article.id} />
} ))}
</ol> </ol>
</main>; </main>
);
} }
function Item({article}: {article: Article}) { function Item({ article }: { article: Article }) {
return <li className={styles.item}> return (
<Link href={`/articles/${article.id}`}>
<li className={styles.item}>
<h2 className={styles.title}>{article.title}</h2> <h2 className={styles.title}>{article.title}</h2>
<p className={styles.description}>{article.content}</p> <p className={styles.description}>{article.content}</p>
</li> </li>
</Link>
);
} }
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
const apolloClient = initializeApollo();
await apolloClient.query({
query: ARTICLE_FOR_HOME,
});
return addApolloState(apolloClient, {
props: {},
});
};