From 305eb667f470f28fbf7853040e1e444a7d621354 Mon Sep 17 00:00:00 2001 From: Ivan Li Date: Sun, 2 May 2021 20:04:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=AE=E8=89=B2=E6=9A=97=E8=89=B2?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=88=87=E6=8D=A2=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 + commons/graphql/client.ts | 94 ++++++++++++++++++++- commons/graphql/queries.ts | 11 +++ commons/theme.tsx | 56 ++++++++++++ components/layouts/global-layout.module.css | 5 +- components/switch-theme.tsx | 10 +++ package-lock.json | 42 +++++++++ package.json | 2 + pages/_app.tsx | 44 ++++------ pages/articles/[id].tsx | 38 +++++++++ pages/articles/article.module.css | 0 pages/index.tsx | 59 +++++++++---- 12 files changed, 314 insertions(+), 48 deletions(-) create mode 100644 commons/theme.tsx create mode 100644 components/switch-theme.tsx create mode 100644 pages/articles/[id].tsx create mode 100644 pages/articles/article.module.css diff --git a/.env b/.env index a7c8b37..65fa936 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ NEXT_PUBLIC_FIRST_NAME=Ivan NEXT_PUBLIC_LAST_NAME=Li +BACKEND_URI=http://127.0.0.1:7132/graphql diff --git a/commons/graphql/client.ts b/commons/graphql/client.ts index 7ef5ab4..ec8ca8b 100644 --- a/commons/graphql/client.ts +++ b/commons/graphql/client.ts @@ -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; + +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; +} diff --git a/commons/graphql/queries.ts b/commons/graphql/queries.ts index e7d16bd..e20bdff 100644 --- a/commons/graphql/queries.ts +++ b/commons/graphql/queries.ts @@ -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 + } + } +`; diff --git a/commons/theme.tsx b/commons/theme.tsx new file mode 100644 index 0000000..0f90b75 --- /dev/null +++ b/commons/theme.tsx @@ -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({ + mode: 'error' as Mode, + setMode: (_) => {console.log}, +}); + +export const ThemeProvider: FC = ({ children }) => { + const [mode, setMode] = useState(() => 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 ( + + {children} + + ); +}; + +export function useTheme () { + return useContext(ThemeContext); +} diff --git a/components/layouts/global-layout.module.css b/components/layouts/global-layout.module.css index 7d01aeb..5b0204b 100644 --- a/components/layouts/global-layout.module.css +++ b/components/layouts/global-layout.module.css @@ -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; } diff --git a/components/switch-theme.tsx b/components/switch-theme.tsx new file mode 100644 index 0000000..7a43ebf --- /dev/null +++ b/components/switch-theme.tsx @@ -0,0 +1,10 @@ +import { useTheme } from "../commons/theme"; + +export const SwitchTheme = () => { + const { setMode, mode } = useTheme(); + + if (mode === "light") { + return ; + } + return ; +}; diff --git a/package-lock.json b/package-lock.json index 1df6491..fbac027 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c9ce114..fdbb3b5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/_app.tsx b/pages/_app.tsx index ba8613f..8ea5271 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -4,40 +4,32 @@ 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 ( - -
- -
-
-

{"Ivan Li 的个人博客"}

-
- - + + +
+ +
+
+

{"Ivan Li 的个人博客"}

+
+ +
+
+
+
-
-
-
-
+
); } -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; diff --git a/pages/articles/[id].tsx b/pages/articles/[id].tsx new file mode 100644 index 0000000..3e53994 --- /dev/null +++ b/pages/articles/[id].tsx @@ -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 = ({ article }) => { + // const router = useRouter() + // const { data } = useQuery<{ article: Article }>(ARTICLE, { + // variables: router.query, + // }); + return ( +
+

{article.title}

+
+ ); +}; + +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; diff --git a/pages/articles/article.module.css b/pages/articles/article.module.css new file mode 100644 index 0000000..e69de29 diff --git a/pages/index.tsx b/pages/index.tsx index e149619..ab4ac4e 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -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
-
    - { - data?.articles?.map(article => ) - } -
-
; + return ( +
+
    + {data?.articles?.map((article) => ( + + ))} +
+
+ ); } -function Item({article}: {article: Article}) { - return
  • -

    {article.title}

    -

    {article.content}

    -
  • -} \ No newline at end of file +function Item({ article }: { article: Article }) { + return ( + +
  • +

    {article.title}

    +

    {article.content}

    +
  • + + ); +} + + +export const getServerSideProps: GetServerSideProps = async ({ params }) => { + const apolloClient = initializeApollo(); + + await apolloClient.query({ + query: ARTICLE_FOR_HOME, + }); + + return addApolloState(apolloClient, { + props: {}, + }); +}; \ No newline at end of file