Compare commits

..

1 Commits

Author SHA1 Message Date
Ivan Li
6f959b1d30 feat: 适配小屏自适应 2021-07-15 21:09:15 +08:00
19 changed files with 4541 additions and 4172 deletions

2
.vscode/launch.json vendored
View File

@ -8,7 +8,7 @@
"type": "pwa-chrome", "type": "pwa-chrome",
"request": "launch", "request": "launch",
"name": "Launch Chrome against localhost", "name": "Launch Chrome against localhost",
"url": "http://user.localhost", "url": "http://blog.localhost",
"webRoot": "${workspaceFolder}", "webRoot": "${workspaceFolder}",
"userDataDir": "/Users/ivan/Projects/.chrome" "userDataDir": "/Users/ivan/Projects/.chrome"
} }

View File

@ -1,8 +1,8 @@
module.exports = { module.exports = {
client: { client: {
service: { service: {
name: "auth-center", name: 'blog-be',
url: "http://localhost:7152/graphql", url: 'http://api.blog.localhost/graphql'
}, }
}, }
}; };

View File

@ -1,5 +1,5 @@
overwrite: true overwrite: true
schema: "http://localhost:7152/graphql" schema: "http://localhost:7132/graphql"
generates: generates:
commons/graphql/generated.tsx: commons/graphql/generated.tsx:
plugins: plugins:

View File

@ -14,18 +14,22 @@ export type Scalars = {
DateTime: any; DateTime: any;
}; };
export type Account = { export type Article = {
__typename?: 'Account'; __typename?: 'Article';
id: Scalars['ID']; id: Scalars['ID'];
name: Scalars['String']; title: Scalars['String'];
nick: Scalars['String']; content: Scalars['String'];
password: Scalars['String']; publishedAt?: Maybe<Scalars['DateTime']>;
registeredAt: Scalars['DateTime']; tags: Array<Scalars['String']>;
html: Scalars['String'];
description?: Maybe<Scalars['String']>;
}; };
export type CreateAccountInput = { export type CreateArticleInput = {
name: Scalars['String']; title: Scalars['String'];
password: Scalars['String']; content: Scalars['String'];
publishedAt?: Maybe<Scalars['DateTime']>;
tags: Array<Scalars['String']>;
}; };
@ -34,62 +38,44 @@ export type Hello = {
message: Scalars['String']; message: Scalars['String'];
}; };
export type LoggedInfo = {
__typename?: 'LoggedInfo';
account: Account;
/** jwt, access application */
accessToken: Scalars['String'];
/** jwt, refresh token in auth center */
refreshToken: Scalars['String'];
};
export type LoginInput = {
name: Scalars['String'];
password: Scalars['String'];
};
export type Mutation = { export type Mutation = {
__typename?: 'Mutation'; __typename?: 'Mutation';
createAccount: Account; createArticle: Article;
updateAccount: Account; updateArticle: Article;
removeAccount: Account; removeArticle: Scalars['Int'];
login: LoggedInfo;
}; };
export type MutationCreateAccountArgs = { export type MutationCreateArticleArgs = {
createAccountInput: CreateAccountInput; createArticleInput: CreateArticleInput;
}; };
export type MutationUpdateAccountArgs = { export type MutationUpdateArticleArgs = {
updateAccountInput: UpdateAccountInput; updateArticleInput: UpdateArticleInput;
}; };
export type MutationRemoveAccountArgs = { export type MutationRemoveArticleArgs = {
id: Scalars['String']; id: Scalars['String'];
}; };
export type MutationLoginArgs = {
loginInput: LoginInput;
};
export type Query = { export type Query = {
__typename?: 'Query'; __typename?: 'Query';
hello: Hello; hello: Hello;
accounts: Array<Account>; articles: Array<Article>;
account: Account; article: Article;
}; };
export type QueryAccountArgs = { export type QueryArticleArgs = {
id: Scalars['String']; id: Scalars['String'];
}; };
export type UpdateAccountInput = { export type UpdateArticleInput = {
name?: Maybe<Scalars['String']>; title?: Maybe<Scalars['String']>;
password?: Maybe<Scalars['String']>; content?: Maybe<Scalars['String']>;
publishedAt?: Maybe<Scalars['DateTime']>;
tags?: Maybe<Array<Scalars['String']>>;
id: Scalars['String']; id: Scalars['String'];
}; };

View File

@ -10,7 +10,7 @@
"types": [ "types": [
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Account", "name": "Article",
"description": null, "description": null,
"fields": [ "fields": [
{ {
@ -30,7 +30,7 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "name", "name": "title",
"description": null, "description": null,
"args": [], "args": [],
"type": { "type": {
@ -46,7 +46,7 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "nick", "name": "content",
"description": null, "description": null,
"args": [], "args": [],
"type": { "type": {
@ -62,33 +62,65 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "password", "name": "publishedAt",
"description": null, "description": null,
"args": [], "args": [],
"type": { "type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "registeredAt",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "DateTime", "name": "DateTime",
"ofType": null "ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "tags",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
} }
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "html",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": null,
"args": [],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
@ -121,12 +153,12 @@
}, },
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "CreateAccountInput", "name": "CreateArticleInput",
"description": null, "description": null,
"fields": null, "fields": null,
"inputFields": [ "inputFields": [
{ {
"name": "name", "name": "title",
"description": null, "description": null,
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
@ -142,7 +174,7 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "password", "name": "content",
"description": null, "description": null,
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
@ -156,6 +188,42 @@
"defaultValue": null, "defaultValue": null,
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
},
{
"name": "publishedAt",
"description": null,
"type": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "tags",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
} }
], ],
"interfaces": null, "interfaces": null,
@ -199,126 +267,24 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "LoggedInfo",
"description": null,
"fields": [
{
"name": "account",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Account",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "accessToken",
"description": "jwt, access application",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "refreshToken",
"description": "jwt, refresh token in auth center",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "LoginInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "name",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "password",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Mutation", "name": "Mutation",
"description": null, "description": null,
"fields": [ "fields": [
{ {
"name": "createAccount", "name": "createArticle",
"description": null, "description": null,
"args": [ "args": [
{ {
"name": "createAccountInput", "name": "createArticleInput",
"description": null, "description": null,
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "CreateAccountInput", "name": "CreateArticleInput",
"ofType": null "ofType": null
} }
}, },
@ -332,7 +298,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Account", "name": "Article",
"ofType": null "ofType": null
} }
}, },
@ -340,18 +306,18 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "updateAccount", "name": "updateArticle",
"description": null, "description": null,
"args": [ "args": [
{ {
"name": "updateAccountInput", "name": "updateArticleInput",
"description": null, "description": null,
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "UpdateAccountInput", "name": "UpdateArticleInput",
"ofType": null "ofType": null
} }
}, },
@ -365,7 +331,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Account", "name": "Article",
"ofType": null "ofType": null
} }
}, },
@ -373,7 +339,7 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "removeAccount", "name": "removeArticle",
"description": null, "description": null,
"args": [ "args": [
{ {
@ -397,41 +363,8 @@
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "OBJECT", "kind": "SCALAR",
"name": "Account", "name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "login",
"description": null,
"args": [
{
"name": "loginInput",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "LoginInput",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "LoggedInfo",
"ofType": null "ofType": null
} }
}, },
@ -444,6 +377,16 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "SCALAR",
"name": "Int",
"description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Query", "name": "Query",
@ -466,7 +409,7 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "accounts", "name": "articles",
"description": null, "description": null,
"args": [], "args": [],
"type": { "type": {
@ -480,7 +423,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Account", "name": "Article",
"ofType": null "ofType": null
} }
} }
@ -490,7 +433,7 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "account", "name": "article",
"description": null, "description": null,
"args": [ "args": [
{ {
@ -515,7 +458,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Account", "name": "Article",
"ofType": null "ofType": null
} }
}, },
@ -530,12 +473,12 @@
}, },
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "UpdateAccountInput", "name": "UpdateArticleInput",
"description": null, "description": null,
"fields": null, "fields": null,
"inputFields": [ "inputFields": [
{ {
"name": "name", "name": "title",
"description": null, "description": null,
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
@ -547,7 +490,7 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "password", "name": "content",
"description": null, "description": null,
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
@ -558,6 +501,38 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "publishedAt",
"description": null,
"type": {
"kind": "SCALAR",
"name": "DateTime",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "tags",
"description": null,
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "id", "name": "id",
"description": null, "description": null,

View File

@ -1,8 +1,14 @@
.wrapper { .wrapper {
@apply bg-green-400 text-white min-h-screen; @apply bg-green-400 text-white min-h-screen;
@apply absolute top-0 left-0 right-0 bottom-0 sm:static;
@apply -translate-x-full transform transition-transform sm:transition-none sm:translate-x-0;
:global(.dark) & { :global(.dark) & {
@apply bg-gray-800 text-gray-400; @apply bg-gray-800 text-gray-400;
} }
&.focusOpen {
@apply translate-x-0;
}
} }
.sidebar { .sidebar {
@apply overflow-hidden flex flex-col fixed top-0; @apply overflow-hidden flex flex-col fixed top-0;
@ -11,8 +17,9 @@
} }
.avatar { .avatar {
@apply w-16 h-16; @apply w-36 h-36 rounded-full;
@apply md:w-36 md:h-36 rounded-full; @apply sm:w-16 sm:h-16;
@apply md:w-36 md:h-36;
@apply mx-auto md:my-8 flex-none; @apply mx-auto md:my-8 flex-none;
:global(.dark) & { :global(.dark) & {
@ -21,14 +28,14 @@
} }
.name { .name {
@apply md:text-3xl md:tracking-wide font-mono; @apply sm:text-base md:text-3xl text-3xl md:tracking-wide font-mono;
@apply md:my-8 my-4 flex-none select-all; @apply md:my-8 my-4 flex-none select-all;
} }
.nav { .nav {
@apply my-auto text-left self-center; @apply my-auto text-left self-center;
} }
.navItem { .navItem {
@apply leading-8 md:text-lg cursor-pointer my-2; @apply leading-8 md:text-lg sm:text-base text-lg cursor-pointer my-2;
@apply tracking-widest; @apply tracking-widest;
small { small {
@apply text-xs md:inline hidden; @apply text-xs md:inline hidden;

View File

@ -1,19 +1,28 @@
import React, { FC } from "react"; import React, { FC, MouseEventHandler } from "react";
import styles from "./global-layout.module.css"; import styles from "./global-sidebar.module.css";
import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import classnames from 'classnames'; import classnames from "classnames";
const fullName = `${process.env.NEXT_PUBLIC_FIRST_NAME} ${process.env.NEXT_PUBLIC_LAST_NAME}`; const fullName = `${process.env.NEXT_PUBLIC_FIRST_NAME} ${process.env.NEXT_PUBLIC_LAST_NAME}`;
interface Props { interface Props {
className: string; className: string;
focusOpen?: boolean;
onClick?: MouseEventHandler;
} }
export const GlobalSidebar: FC<Props> = ({className}) => { export const GlobalSidebar: FC<Props> = ({
className,
focusOpen = false,
onClick,
}) => {
return ( return (
<div className={classnames(className, styles.wrapper)}> <div
className={classnames(className, styles.wrapper, {
[styles.focusOpen]: focusOpen,
})}
onClick={onClick}
>
<aside className={classnames(className, styles.sidebar)}> <aside className={classnames(className, styles.sidebar)}>
<img className={styles.avatar} src="/images/avatar.png" /> <img className={styles.avatar} src="/images/avatar.png" />
<h2 className={styles.name}>{fullName}</h2> <h2 className={styles.name}>{fullName}</h2>

7461
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
{ {
"name": "launch-fs", "name": "blog-fs",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 7153", "dev": "next dev -p 7133",
"build": "next build", "build": "next build",
"start": "next start -p 7153", "start": "next start -p 7133",
"predev": "npm run generate", "predev": "npm run generate",
"generate": "graphql-codegen --config codegen.yml" "generate": "graphql-codegen --config codegen.yml"
}, },
@ -14,14 +14,11 @@
"@fortawesome/fontawesome-svg-core": "^1.2.35", "@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14", "@fortawesome/react-fontawesome": "^0.1.14",
"@material-ui/core": "^4.12.0",
"@material-ui/icons": "^4.11.2",
"apollo-link-scalars": "^2.1.3", "apollo-link-scalars": "^2.1.3",
"babel-plugin-superjson-next": "^0.3.0", "babel-plugin-superjson-next": "^0.3.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"clsx": "^1.1.1",
"date-fns": "^2.22.1", "date-fns": "^2.22.1",
"formik": "^2.2.9",
"formik-material-ui": "^3.0.1",
"graphql": "^15.5.0", "graphql": "^15.5.0",
"graphql-scalars": "^1.10.0", "graphql-scalars": "^1.10.0",
"highlight.js": "^11.0.1", "highlight.js": "^11.0.1",
@ -31,8 +28,7 @@
"ramda": "^0.27.1", "ramda": "^0.27.1",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"superjson": "^1.7.4", "superjson": "^1.7.4"
"yup": "^0.32.9"
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/cli": "1.21.4", "@graphql-codegen/cli": "1.21.4",

View File

@ -1,3 +1,35 @@
.page { .page {
@apply flex bg-gray-300 min-h-screen h-full; @apply flex;
}
.sidebar {
@apply w-full md:w-64 sm:w-32 flex-none;
}
.primary {
@apply flex-auto w-screen min-w-0;
@apply bg-gray-100 text-gray-700;
:global(.dark) & {
@apply bg-gray-700 text-gray-300;
}
.wrapper {
@apply mx-auto max-w-screen-lg;
}
}
.pageHeader {
@apply p-3 bg-gray-50 flex justify-between;
:global(.dark) & {
@apply bg-gray-800;
}
}
.headerBtn {
@apply sm:hidden text-green-400;
@apply w-4 h-4 p-0 focus:outline-none;
&.opened {
@apply text-white;
}
& > * {
@apply absolute;
}
} }

View File

@ -1,19 +1,54 @@
import "../styles/globals.css"; import "../styles/globals.css";
import "tailwindcss/tailwind.css"; import "tailwindcss/tailwind.css";
import React from "react"; import React, { useState } from "react";
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 { useApollo } from "../commons/graphql/client"; import { useApollo } from "../commons/graphql/client";
import { ThemeProvider } from "../commons/theme"; import { ThemeProvider, useTheme } from "../commons/theme";
import { SwitchTheme } from "../components/switch-theme";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faCross,
faHamburger,
faTimes,
} from "@fortawesome/free-solid-svg-icons";
import clsx from "clsx";
function MyApp({ Component, pageProps }) { function MyApp({ Component, pageProps }) {
const apolloClient = useApollo(pageProps); const apolloClient = useApollo(pageProps);
const [focusOpenSidebar, setFocusOpenSidebar] = useState(false);
return ( return (
<ApolloProvider client={apolloClient}> <ApolloProvider client={apolloClient}>
<ThemeProvider> <ThemeProvider>
<div className={styles.page}> <div className={styles.page}>
<GlobalSidebar
className={styles.sidebar}
focusOpen={focusOpenSidebar}
onClick={() => setFocusOpenSidebar(false)}
/>
<div className={styles.primary}>
<header className={styles.pageHeader}>
<button
className={clsx(styles.headerBtn, {
[styles.opened]: focusOpenSidebar,
})}
onClick={() => setFocusOpenSidebar(!focusOpenSidebar)}
>
<FontAwesomeIcon
icon={focusOpenSidebar ? faTimes : faHamburger}
/>
</button>
<h1>{"Ivan Li 的个人博客"}</h1>
<div className={styles.actions}>
<SwitchTheme />
</div>
</header>
<div className={styles.wrapper}>
<Component {...pageProps} /> <Component {...pageProps} />
</div> </div>
</div>
</div>
</ThemeProvider> </ThemeProvider>
</ApolloProvider> </ApolloProvider>
); );

View File

@ -1,68 +0,0 @@
import React from "react";
import Document, { Html, Head, Main, NextScript } from "next/document";
import { ServerStyleSheets } from "@material-ui/core/styles";
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
// Resolution order
//
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render
//
// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render
//
// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render
// Render app and page and get the context of the page with collected side effects.
const sheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: [
...React.Children.toArray(initialProps.styles),
sheets.getStyleElement(),
],
};
};

54
pages/articles/[id].tsx Normal file
View File

@ -0,0 +1,54 @@
import { FC } from "react";
import styles from "./article.module.css";
import { GetServerSideProps } from "next";
import { addApolloState, initializeApollo } from "../../commons/graphql/client";
import { ARTICLE } from "../../commons/graphql/queries";
import { Article } from "../../commons/graphql/generated";
import { formatRelative } from "date-fns";
import { zhCN } from "date-fns/locale";
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}>
<article className={styles.article}>
<header>
<h1>{article.title}</h1>
<div className={styles.headerInfo}>
<address> Ivan Li </address>
<time>
{formatRelative(article.publishedAt, new Date(), {
locale: zhCN,
})}
</time>
</div>
</header>
<div dangerouslySetInnerHTML={{ __html: article.html }}></div>
<footer>
<div className={styles.articleEnd}>The End</div>
</footer>
</article>
</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

@ -0,0 +1,382 @@
.articleDetail {
}
.article {
@apply bg-gray-50 md:mx-2 overflow-hidden rounded-xl my-6;
:global(.dark) & {
@apply bg-gray-800;
}
& > header {
@apply my-8 mx-4;
h1 {
@apply text-2xl md:text-4xl font-medium mb-4;
:global(.dark) & {
@apply text-gray-200;
}
}
}
& > div {
@apply my-4 leading-8 mx-4 lg:mx-6 xl:mx-8 text-justify;
h1,
h2,
h3,
h4 {
@apply text-red-400;
:global(.dark) & {
@apply text-white;
}
}
h1 {
@apply hidden;
}
h2 {
@apply text-2xl mt-8 mb-4 font-semibold border-b border-red-500 leading-10;
:global(.dark) & {
@apply border-green-700;
}
}
h3 {
@apply text-xl mt-6 mb-2 font-light;
}
h4 {
@apply mt-4 font-medium;
}
p {
@apply my-4;
}
ul {
@apply pl-4;
li {
@apply list-disc;
}
}
ol {
@apply pl-4;
li {
@apply list-decimal;
}
}
a {
@apply text-red-400 underline;
:global(.dark) & {
@apply text-green-500;
}
&:hover {
@apply filter saturate-200;
}
}
code,
pre {
@apply font-mono rounded;
}
blockquote {
@apply bg-gray-400 bg-opacity-10 px-4 overflow-hidden border-l-2 border-green-400;
}
code:not(:global(.hljs)) {
@apply p-1 text-red-500 bg-red-100;
:global(.dark) & {
@apply bg-green-800 bg-opacity-20 text-green-400;
}
}
code:global(.hljs) {
@apply text-sm;
}
:global {
:not(:global(.dark)) & {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
}
.hljs {
color: #68615e;
background: #f1efee;
}
.hljs ::selection {
color: #a8a19f;
}
/* purposely do not highlight these things */
.hljs-formula,
.hljs-params,
.hljs-property {
}
/* base03 - #9c9491 - Comments, Invisibles, Line Highlighting */
.hljs-comment {
color: #9c9491;
}
/* base04 - #766e6b - Dark Foreground (Used for status bars) */
.hljs-tag {
color: #766e6b;
}
/* base05 - #68615e - Default Foreground, Caret, Delimiters, Operators */
.hljs-subst,
.hljs-punctuation,
.hljs-operator {
color: #68615e;
}
.hljs-operator {
opacity: 0.7;
}
/* base08 - Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted */
.hljs-bullet,
.hljs-variable,
.hljs-template-variable,
.hljs-selector-tag,
.hljs-name,
.hljs-deletion {
color: #f22c40;
}
/* base09 - Integers, Boolean, Constants, XML Attributes, Markup Link Url */
.hljs-symbol,
.hljs-number,
.hljs-link,
.hljs-attr,
.hljs-variable.constant_,
.hljs-literal {
color: #df5320;
}
/* base0A - Classes, Markup Bold, Search Text Background */
.hljs-title,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #c38418;
}
.hljs-strong {
font-weight: bold;
color: #c38418;
}
/* base0B - Strings, Inherited Class, Markup Code, Diff Inserted */
.hljs-code,
.hljs-addition,
.hljs-title.class_.inherited__,
.hljs-string {
color: #7b9726;
}
/* base0C - Support, Regular Expressions, Escape Characters, Markup Quotes */
.hljs-built_in,
.hljs-doctag,
.hljs-quote,
.hljs-keyword.hljs-atrule,
.hljs-regexp {
color: #3d97b8;
}
/* base0D - Functions, Methods, Attribute IDs, Headings */
.hljs-function .hljs-title,
.hljs-attribute,
.ruby .hljs-property,
.hljs-title.function_,
.hljs-section {
color: #407ee7;
}
/* base0E - Keywords, Storage, Selector, Markup Italic, Diff Changed */
.hljs-type,
.hljs-template-tag,
.diff .hljs-meta,
.hljs-keyword {
color: #6666ea;
}
.hljs-emphasis {
color: #6666ea;
font-style: italic;
}
/* base0F - Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?> */
.hljs-meta,
.hljs-meta .hljs-keyword,
.hljs-meta .hljs-string {
color: #c33ff3;
}
.hljs-meta .hljs-keyword,
.hljs-meta-keyword {
font-weight: bold;
}
}
:global(.dark) & {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
}
.hljs {
color: #929181;
background: #22221b;
}
.hljs ::selection {
color: #5f5e4e;
}
/* purposely do not highlight these things */
.hljs-formula,
.hljs-params,
.hljs-property {
}
/* base03 - #6c6b5a - Comments, Invisibles, Line Highlighting */
.hljs-comment {
color: #6c6b5a;
}
/* base04 - #878573 - Dark Foreground (Used for status bars) */
.hljs-tag {
color: #878573;
}
/* base05 - #929181 - Default Foreground, Caret, Delimiters, Operators */
.hljs-subst,
.hljs-punctuation,
.hljs-operator {
color: #929181;
}
.hljs-operator {
opacity: 0.7;
}
/* base08 - Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted */
.hljs-bullet,
.hljs-variable,
.hljs-template-variable,
.hljs-selector-tag,
.hljs-name,
.hljs-deletion {
color: #ba6236;
}
/* base09 - Integers, Boolean, Constants, XML Attributes, Markup Link Url */
.hljs-symbol,
.hljs-number,
.hljs-link,
.hljs-attr,
.hljs-variable.constant_,
.hljs-literal {
color: #ae7313;
}
/* base0A - Classes, Markup Bold, Search Text Background */
.hljs-title,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #a5980d;
}
.hljs-strong {
font-weight: bold;
color: #a5980d;
}
/* base0B - Strings, Inherited Class, Markup Code, Diff Inserted */
.hljs-code,
.hljs-addition,
.hljs-title.class_.inherited__,
.hljs-string {
color: #7d9726;
}
/* base0C - Support, Regular Expressions, Escape Characters, Markup Quotes */
.hljs-built_in,
.hljs-doctag, /* guessing */
.hljs-quote,
.hljs-keyword.hljs-atrule,
.hljs-regexp {
color: #5b9d48;
}
/* base0D - Functions, Methods, Attribute IDs, Headings */
.hljs-function .hljs-title,
.hljs-attribute,
.ruby .hljs-property,
.hljs-title.function_,
.hljs-section {
color: #36a166;
}
/* base0E - Keywords, Storage, Selector, Markup Italic, Diff Changed */
.hljs-type,
.hljs-template-tag,
.diff .hljs-meta,
.hljs-keyword {
color: #5f9182;
}
.hljs-emphasis {
color: #5f9182;
font-style: italic;
}
/* base0F - Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?> */
.hljs-meta,
.hljs-meta .hljs-keyword,
.hljs-meta .hljs-string {
color: #9d6c7c;
}
.hljs-meta .hljs-keyword,
.hljs-meta-keyword {
font-weight: bold;
}
}
}
}
}
.headerInfo {
@apply text-sm text-gray-500 pl-2;
:global(.dark) & {
@apply text-gray-400;
}
address, time {
@apply inline not-italic;
}
}
.articleEnd {
@apply text-center text-gray-400 text-sm my-6;
&:before,
&:after {
content: "※";
@apply mx-4 text-xs align-text-top;
}
}

View File

@ -1,10 +0,0 @@
.loginWrapper {
@apply flex sm:items-center justify-center flex-col w-screen px-4 sm:px-0 items-stretch bg-green-50;
}
.login {
}
.form {
@apply w-full md:w-96 py-4 px-8 shadow-md rounded-lg;
@apply bg-white;
}

View File

@ -1,123 +0,0 @@
import React, { useEffect, useState } from "react";
import styles from "./login.module.css";
import { Field, Form, Formik, FormikHelpers } from "formik";
import * as yup from "yup";
import { TextField } from "formik-material-ui";
import { Button, InputAdornment, makeStyles } from "@material-ui/core";
import classNames from "classnames";
import { AccountCircle, Lock } from "@material-ui/icons";
import { LoggedInfo, LoginInput } from "../../commons/graphql/generated";
import { useMutation } from "@apollo/client";
import { LOGIN } from "./mutations";
const useStyles = makeStyles({
field: {
margin: "16px 0",
},
loginBtn: {
margin: "8px 0",
},
});
export default function Index() {
const [messagePort, setMessagePort] = useState(() => undefined);
useEffect(() => {
const { port1, port2 } = new MessageChannel();
setMessagePort(port1);
port1.onmessage = (ev: MessageEvent) => {
console.log(ev);
};
window.opener?.postMessage("init-channel", "*", [port2]);
window.parent?.postMessage("init-channel", "*", [port2]);
}, []);
const [login, { loading: logining }] =
useMutation<{ login: LoggedInfo }>(LOGIN);
const submit = (values, helpers: FormikHelpers<LoggedInfo>) => {
login({
variables: {
input: values,
},
})
.then(({ data }) => {
messagePort.postMessage({
type: "logged",
payload: data.login,
});
})
.finally(() => {
helpers.setSubmitting(false);
});
};
const classes = useStyles();
return (
<div className={styles.loginWrapper}>
<main className={styles.login}>
<Formik
initialValues={
{
name: "",
password: "",
} as LoginInput
}
validationSchema={yup.object().shape({
name: yup.string().max(100, "怎么会有这么长的用户名!!"),
password: yup
.string()
.max(100, "不会吧不会吧,怎么会有这么长的密码?"),
})}
onSubmit={submit}
>
{({ isSubmitting }) => {
return (
<Form className={styles.form}>
<Field
className={classNames(classes.field)}
name="name"
component={TextField}
placeholder="账户 / Username"
size="medium"
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<AccountCircle />
</InputAdornment>
),
}}
/>
<Field
className={classNames(classes.field)}
name="password"
component={TextField}
placeholder="密码 / Password"
size="medium"
type="password"
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Lock />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
fullWidth
type="submit"
className={classes.loginBtn}
disabled={isSubmitting}
>
/ Login
</Button>
</Form>
);
}}
</Formik>
</main>
</div>
);
}

View File

@ -1,14 +0,0 @@
import { gql } from "@apollo/client";
export const LOGIN = gql`
mutation Login($input: LoginInput!) {
login(loginInput: $input) {
account {
id
name
nick
}
accessToken
refreshToken
}
}
`;

View File

@ -2,13 +2,45 @@ import { useQuery } from "@apollo/client";
import { GetServerSideProps } from 'next'; import { GetServerSideProps } from 'next';
import Link from "next/link"; import Link from "next/link";
import React from "react"; 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);
return ( return (
<main> <main className={styles.index}>
<h1>Launch</h1> <ol>
<hr /> {data?.articles?.map((article) => (
<p>Powered By Ivan.</p> <Item article={article} key={article.id} />
))}
</ol>
</main> </main>
); );
}
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.description}</p>
</li>
</Link>
);
}
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
const apolloClient = initializeApollo();
await apolloClient.query({
query: ARTICLE_FOR_HOME,
});
return addApolloState(apolloClient, {
props: {},
});
}; };

View File

@ -1,5 +1,4 @@
module.exports = { module.exports = {
mode: "jit",
purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"], purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"],
darkMode: "class", // or 'media' or 'class' darkMode: "class", // or 'media' or 'class'
theme: { theme: {