├── .eslintrc.json ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .storybook ├── main.js └── preview.js ├── .vscode ├── launch.json └── settings.json ├── README.md ├── commitlint.config.js ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── next.svg ├── thirteen.svg ├── time-cat.jpg └── vercel.svg ├── src ├── components │ ├── cards │ │ └── cat │ │ │ ├── CatCard.mocks.ts │ │ │ ├── CatCard.module.css │ │ │ ├── CatCard.stories.tsx │ │ │ └── CatCard.tsx │ ├── layouts │ │ ├── pageTransition │ │ │ ├── PageTransition.mocks.ts │ │ │ ├── PageTransition.module.css │ │ │ ├── PageTransition.stories.tsx │ │ │ └── PageTransition.tsx │ │ ├── primary │ │ │ ├── PrimaryLayout.mocks.ts │ │ │ ├── PrimaryLayout.module.css │ │ │ ├── PrimaryLayout.stories.tsx │ │ │ └── PrimaryLayout.tsx │ │ └── sidebar │ │ │ ├── SidebarLayout.mocks.ts │ │ │ ├── SidebarLayout.module.css │ │ │ ├── SidebarLayout.stories.tsx │ │ │ └── SidebarLayout.tsx │ └── templates │ │ └── base │ │ ├── BaseTemplate.mocks.ts │ │ ├── BaseTemplate.module.css │ │ ├── BaseTemplate.stories.tsx │ │ └── BaseTemplate.tsx ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── about.tsx │ ├── contact.tsx │ ├── globals.css │ ├── index.tsx │ └── page.d.ts └── stories │ ├── Button.stories.tsx │ ├── Button.tsx │ ├── Header.stories.tsx │ ├── Header.tsx │ ├── Introduction.stories.mdx │ ├── Page.stories.tsx │ ├── Page.tsx │ ├── assets │ ├── code-brackets.svg │ ├── colors.svg │ ├── comments.svg │ ├── direction.svg │ ├── flow.svg │ ├── plugin.svg │ ├── repo.svg │ └── stackalt.svg │ ├── button.css │ ├── header.css │ └── page.css ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:storybook/recommended", // 新加入 4 | "next", 5 | "next/core-web-vitals", 6 | "eslint:recommended" 7 | ], 8 | // 新加入 9 | "overrides": [ 10 | { 11 | "files": ["*.stories.@(ts|tsx|js|jsx|mjs|cjs)"], 12 | "rules": { 13 | // example of overriding a rule 14 | "storybook/hierarchy-separator": "error" 15 | } 16 | } 17 | ], 18 | "globals": { 19 | "React": "readonly" 20 | }, 21 | "rules": { 22 | "no-unused-vars": [1, { "args": "after-used", "argsIgnorePattern": "^_" }], 23 | "react/no-unescaped-entities": "off" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn build 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .yarn 2 | .next 3 | dist 4 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../src/**/*.stories.mdx", 4 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | /** 暴露 public 目录给到 stotrybook,作为静态资源目录 */ 7 | "staticDirs": ['../public'], 8 | "addons": [ 9 | "@storybook/addon-links", 10 | "@storybook/addon-essentials", 11 | "@storybook/addon-interactions" 12 | ], 13 | "framework": "@storybook/react", 14 | "core": { 15 | "builder": "@storybook/builder-webpack5" 16 | } 17 | } -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import '../src/pages/globals.css'; 3 | import * as NextImage from 'next/image'; 4 | 5 | const BREAKPOINTS_INT = { 6 | xs: 375, 7 | sm: 600, 8 | md: 900, 9 | lg: 1200, 10 | xl: 1536, 11 | }; 12 | 13 | const customViewports = Object.fromEntries( 14 | Object.entries(BREAKPOINTS_INT).map(([key, val], idx) => { 15 | console.log(val); 16 | return [ 17 | key, 18 | { 19 | name: key, 20 | styles: { 21 | width: `${val}px`, 22 | height: `${(idx + 5) * 10}vh`, 23 | }, 24 | }, 25 | ]; 26 | }) 27 | ); 28 | 29 | // Allow Storybook to handle Next's component 30 | const OriginalNextImage = NextImage.default; 31 | 32 | Object.defineProperty(NextImage, 'default', { 33 | configurable: true, 34 | value: (props) => , 35 | }); 36 | 37 | 38 | 39 | export const parameters = { 40 | actions: { argTypesRegex: '^on[A-Z].*' }, 41 | controls: { 42 | matchers: { 43 | color: /(background|color)$/i, 44 | date: /Date$/, 45 | }, 46 | }, 47 | viewport: { viewports: customViewports }, 48 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug server-side", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "npm run dev" 9 | }, 10 | { 11 | "name": "Next.js: debug client-side", 12 | "type": "pwa-chrome", 13 | "request": "launch", 14 | "url": "http://localhost:3000" 15 | }, 16 | { 17 | "name": "Next.js: debug full stack", 18 | "type": "node-terminal", 19 | "request": "launch", 20 | "command": "npm run dev", 21 | "console": "integratedTerminal", 22 | "serverReadyAction": { 23 | "pattern": "started server on .+, url: (https?://.+)", 24 | "uriFormat": "%s", 25 | "action": "debugWithChrome" 26 | } 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.formatOnSave": true, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll": true, 8 | "source.organizeImports": true 9 | } 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js 项目最佳实践 2 | 3 | [原文地址](https://juejin.cn/post/7194410416879960125) 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // build: 影响构建系统或外部依赖项的更改(示例范围:gulp、broccoli、npm) 2 | // ci: 更改我们的 CI 配置文件和脚本(示例范围:Travis、Circle、BrowserStack、SauceLabs) 3 | // docs: 文档修改 4 | // feat: 一个新的功能 5 | // fix: 一个 bug 修复 6 | // perf: 提升性能的代码修改 7 | // refactor: 既不修复错误也不添加功能的代码更改 8 | // style: 不影响代码含义的更改(空格、格式、缺少分号等) 9 | // test: 添加缺失的测试或更正现有测试 10 | 11 | 12 | module.exports = { 13 | extends: ['@commitlint/config-conventional'], 14 | rules: { 15 | 'body-leading-blank': [1, 'always'], 16 | 'body-max-line-length': [2, 'always', 100], 17 | 'footer-leading-blank': [1, 'always'], 18 | 'footer-max-line-length': [2, 'always', 100], 19 | 'header-max-length': [2, 'always', 100], 20 | 'scope-case': [2, 'always', 'lower-case'], 21 | 'subject-case': [ 22 | 2, 23 | 'never', 24 | ['sentence-case', 'start-case', 'pascal-case', 'upper-case'], 25 | ], 26 | 'subject-empty': [2, 'never'], 27 | 'subject-full-stop': [2, 'never', '.'], 28 | 'type-case': [2, 'always', 'lower-case'], 29 | 'type-empty': [2, 'never'], 30 | 'type-enum': [ 31 | 2, 32 | 'always', 33 | [ 34 | 'build', 35 | 'chore', 36 | 'ci', 37 | 'docs', 38 | 'feat', 39 | 'fix', 40 | 'perf', 41 | 'refactor', 42 | 'revert', 43 | 'style', 44 | 'test', 45 | 'translation', 46 | 'security', 47 | 'changeset', 48 | ], 49 | ], 50 | }, 51 | }; -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | domains: ['i.pravatar.cc'], 6 | }, 7 | }; 8 | 9 | module.exports = nextConfig; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-fullstack-app-template-zn", 3 | "version": "0.1.0", 4 | "private": true, 5 | "author": "zidan", 6 | "scripts": { 7 | "dev": "cross-env NODE_OPTIONS='--inspect' next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "prettier": "prettier --write .", 12 | "prepare": "husky install", 13 | "storybook": "start-storybook -p 6006", 14 | "build-storybook": "build-storybook" 15 | }, 16 | "dependencies": { 17 | "@next/font": "13.1.6", 18 | "@types/node": "18.11.18", 19 | "@types/react": "18.0.27", 20 | "@types/react-dom": "18.0.10", 21 | "eslint": "8.33.0", 22 | "eslint-config-next": "13.1.6", 23 | "next": "13.1.6", 24 | "react": "18.2.0", 25 | "react-dom": "18.2.0", 26 | "typescript": "4.9.4", 27 | "framer-motion": "^10.12.17" 28 | }, 29 | "engines": { 30 | "node": ">=16.0.0", 31 | "yarn": ">=1.22.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.20.12", 35 | "@commitlint/cli": "^17.4.2", 36 | "@commitlint/config-conventional": "^17.4.2", 37 | "@storybook/addon-actions": "^6.5.16", 38 | "@storybook/addon-essentials": "^6.5.16", 39 | "@storybook/addon-interactions": "^6.5.16", 40 | "@storybook/addon-links": "^6.5.16", 41 | "@storybook/builder-webpack5": "^6.5.16", 42 | "@storybook/manager-webpack5": "^6.5.16", 43 | "@storybook/react": "^6.5.16", 44 | "@storybook/testing-library": "^0.0.13", 45 | "babel-loader": "^8.3.0", 46 | "cross-env": "^7.0.3", 47 | "eslint-plugin-storybook": "^0.6.10", 48 | "husky": "^8.0.3", 49 | "prettier": "^2.8.3", 50 | "util": "^0.12.5" 51 | }, 52 | "resolutions": { 53 | "webpack": "^5" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zidanDirk/nextjs-fullstack-app-template-zn/6188eedd14a34fd24ee2f6cc68190e49af05b3a6/public/favicon.ico -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/time-cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zidanDirk/nextjs-fullstack-app-template-zn/6188eedd14a34fd24ee2f6cc68190e49af05b3a6/public/time-cat.jpg -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/cards/cat/CatCard.mocks.ts: -------------------------------------------------------------------------------- 1 | import { ICatCard } from './CatCard'; 2 | 3 | const base: ICatCard = { 4 | tag: 'Felines', 5 | title: `What's new in Cats`, 6 | body: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Sequi perferendis molestiae non nemo doloribus. Doloremque, nihil! At ea atque quidem!', 7 | author: 'Alex', 8 | time: '2h ago', 9 | }; 10 | 11 | export const mockCatCardProps = { 12 | base, 13 | }; -------------------------------------------------------------------------------- /src/components/cards/cat/CatCard.module.css: -------------------------------------------------------------------------------- 1 | /* @import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300..700&display=swap'); */ 2 | 3 | .container { 4 | margin: 1rem; 5 | } 6 | 7 | .container * { 8 | box-sizing: border-box; 9 | padding: 0; 10 | margin: 0; 11 | } 12 | 13 | .card__image { 14 | max-width: 100%; 15 | display: block; 16 | object-fit: cover; 17 | } 18 | 19 | .card { 20 | font-family: 'Quicksand', sans-serif; 21 | display: flex; 22 | flex-direction: column; 23 | width: clamp(20rem, calc(20rem + 2vw), 22rem); 24 | overflow: hidden; 25 | box-shadow: 0 0.1rem 1rem rgba(0, 0, 0, 0.1); 26 | border-radius: 1em; 27 | background: #ece9e6; 28 | background: linear-gradient(to right, #ffffff, #ece9e6); 29 | } 30 | 31 | .card__body { 32 | padding: 1rem; 33 | display: flex; 34 | flex-direction: column; 35 | gap: 0.5rem; 36 | } 37 | 38 | .tag { 39 | align-self: flex-start; 40 | padding: 0.25em 0.75em; 41 | border-radius: 1em; 42 | font-size: 0.75rem; 43 | } 44 | 45 | .tag-blue { 46 | background: #56ccf2; 47 | background: linear-gradient(to bottom, #2f80ed, #56ccf2); 48 | color: #fafafa; 49 | } 50 | 51 | .card__body h4 { 52 | font-size: 1.5rem; 53 | text-transform: capitalize; 54 | } 55 | 56 | .card__footer { 57 | display: flex; 58 | padding: 1rem; 59 | margin-top: auto; 60 | } 61 | 62 | .user { 63 | display: flex; 64 | gap: 0.5rem; 65 | } 66 | 67 | .user__image { 68 | border-radius: 50%; 69 | } 70 | 71 | .user__info > small { 72 | color: #666; 73 | } -------------------------------------------------------------------------------- /src/components/cards/cat/CatCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 2 | import CatCard, { ICatCard } from './CatCard'; 3 | import { mockCatCardProps } from './CatCard.mocks'; 4 | 5 | export default { 6 | title: 'cards/CatCard', 7 | component: CatCard, 8 | argTypes: {}, 9 | } as ComponentMeta; 10 | 11 | const Template: ComponentStory = (args) => ( 12 | 13 | ); 14 | 15 | export const Base = Template.bind({}); 16 | 17 | Base.args = { 18 | ...mockCatCardProps.base, 19 | } as ICatCard; 20 | -------------------------------------------------------------------------------- /src/components/cards/cat/CatCard.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import styles from './CatCard.module.css'; 3 | 4 | export interface ICatCard { 5 | tag: string; 6 | title: string; 7 | body: string; 8 | author: string; 9 | time: string; 10 | } 11 | 12 | const CatCard: React.FC = ({ tag, title, body, author, time }) => { 13 | return ( 14 |
15 |
16 |
17 | card__image 24 |
25 |
26 | {tag} 27 |

{title}

28 |

{body}

29 |
30 |
31 |
32 | user__image 39 |
40 |
{author}
41 | {time} 42 |
43 |
44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | 51 | export default CatCard; -------------------------------------------------------------------------------- /src/components/layouts/pageTransition/PageTransition.mocks.ts: -------------------------------------------------------------------------------- 1 | import { IPageTransition } from './PageTransition'; 2 | 3 | const base: IPageTransition = { 4 | children: '{{component}}' 5 | }; 6 | 7 | export const mockIPageTransitionProps = { 8 | base, 9 | }; -------------------------------------------------------------------------------- /src/components/layouts/pageTransition/PageTransition.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zidanDirk/nextjs-fullstack-app-template-zn/6188eedd14a34fd24ee2f6cc68190e49af05b3a6/src/components/layouts/pageTransition/PageTransition.module.css -------------------------------------------------------------------------------- /src/components/layouts/pageTransition/PageTransition.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 2 | import PageTransition, { IPageTransition } from './PageTransition'; 3 | import { mockIPageTransitionProps } from './PageTransition.mocks'; 4 | 5 | export default { 6 | title: 'layouts/PrimaryLayout', 7 | component: PageTransition, 8 | argTypes: {}, 9 | } as ComponentMeta; 10 | 11 | const Template: ComponentStory = (args) => ( 12 | 13 | ); 14 | 15 | export const Base = Template.bind({}); 16 | 17 | Base.args = { 18 | ...mockIPageTransitionProps.base, 19 | } as IPageTransition; 20 | -------------------------------------------------------------------------------- /src/components/layouts/pageTransition/PageTransition.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import { useRouter } from 'next/router'; 3 | 4 | export interface IPageTransition extends React.ComponentPropsWithoutRef<'div'> {} 5 | 6 | const variants = { 7 | hidden: { opacity: 0, x: -200, y: 32 }, 8 | enter: { opacity: 1, x: 32, y: 32 }, 9 | exit: { opacity: 0, x: -200, y: 32 }, 10 | } 11 | 12 | const PageTransition: React.FC = ({ children }) => { 13 | const router = useRouter() 14 | const pageKey = router.asPath 15 | 16 | return ( 17 | 25 | {children} 26 | 27 | ); 28 | }; 29 | 30 | export default PageTransition; 31 | -------------------------------------------------------------------------------- /src/components/layouts/primary/PrimaryLayout.mocks.ts: -------------------------------------------------------------------------------- 1 | import { IPrimaryLayout } from './PrimaryLayout'; 2 | 3 | const base: IPrimaryLayout = { 4 | children: '{{component}}' 5 | }; 6 | 7 | export const mockPrimaryLayoutProps = { 8 | base, 9 | }; -------------------------------------------------------------------------------- /src/components/layouts/primary/PrimaryLayout.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | height: calc(100vh - 64px); 4 | background-color: white; 5 | } 6 | 7 | .main > section { 8 | padding: 32px; 9 | } -------------------------------------------------------------------------------- /src/components/layouts/primary/PrimaryLayout.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 2 | import PrimaryLayout, { IPrimaryLayout } from './PrimaryLayout'; 3 | import { mockPrimaryLayoutProps } from './PrimaryLayout.mocks'; 4 | 5 | export default { 6 | title: 'layouts/PrimaryLayout', 7 | component: PrimaryLayout, 8 | argTypes: {}, 9 | } as ComponentMeta; 10 | 11 | const Template: ComponentStory = (args) => ( 12 | 13 | ); 14 | 15 | export const Base = Template.bind({}); 16 | 17 | Base.args = { 18 | ...mockPrimaryLayoutProps.base, 19 | } as IPrimaryLayout; 20 | -------------------------------------------------------------------------------- /src/components/layouts/primary/PrimaryLayout.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import styles from './PrimaryLayout.module.css'; 3 | 4 | export interface IPrimaryLayout extends React.ComponentPropsWithoutRef<'div'> {} 5 | 6 | const PrimaryLayout: React.FC = ({ children }) => { 7 | return ( 8 | <> 9 | 10 | Primary Layout Example 11 | 12 |
{children}
13 | 14 | ); 15 | }; 16 | 17 | export default PrimaryLayout; 18 | -------------------------------------------------------------------------------- /src/components/layouts/sidebar/SidebarLayout.mocks.ts: -------------------------------------------------------------------------------- 1 | import { ISidebarLayout } from './SidebarLayout'; 2 | 3 | const base: ISidebarLayout = { 4 | }; 5 | 6 | export const mockSidebarLayoutProps = { 7 | base, 8 | }; -------------------------------------------------------------------------------- /src/components/layouts/sidebar/SidebarLayout.module.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | width: 250px; 6 | background-color: #fafafa; 7 | padding: 32px; 8 | border-right: 1px solid #eaeaea; 9 | z-index: 10; 10 | } 11 | 12 | .nav > a { 13 | margin: 8px 0; 14 | text-decoration: none; 15 | background: white; 16 | border-radius: 4px; 17 | font-size: 14px; 18 | padding: 12px 16px; 19 | text-transform: uppercase; 20 | font-weight: 600; 21 | letter-spacing: 0.025em; 22 | color: #333; 23 | border: 1px solid #eaeaea; 24 | transition: all 0.125s ease; 25 | } 26 | 27 | .nav > a:hover { 28 | background-color: #eaeaea; 29 | } 30 | 31 | .input { 32 | margin: 32px 0; 33 | text-decoration: none; 34 | background: white; 35 | border-radius: 4px; 36 | border: 1px solid #eaeaea; 37 | font-size: 14px; 38 | padding: 8px 16px; 39 | height: 28px; 40 | } -------------------------------------------------------------------------------- /src/components/layouts/sidebar/SidebarLayout.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 2 | import SidebarLayout, { ISidebarLayout } from './SidebarLayout'; 3 | import { mockSidebarLayoutProps } from './SidebarLayout.mocks'; 4 | 5 | export default { 6 | title: 'layouts/SidebarLayout', 7 | component: SidebarLayout, 8 | argTypes: {}, 9 | } as ComponentMeta; 10 | 11 | const Template: ComponentStory = (args) => ( 12 | 13 | ); 14 | 15 | export const Base = Template.bind({}); 16 | 17 | Base.args = { 18 | ...mockSidebarLayoutProps.base, 19 | } as ISidebarLayout; 20 | -------------------------------------------------------------------------------- /src/components/layouts/sidebar/SidebarLayout.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import styles from './SidebarLayout.module.css'; 3 | 4 | export interface ISidebarLayout {} 5 | 6 | const SidebarLayout: React.FC = () => { 7 | return ( 8 | 20 | ); 21 | }; 22 | 23 | export default SidebarLayout; -------------------------------------------------------------------------------- /src/components/templates/base/BaseTemplate.mocks.ts: -------------------------------------------------------------------------------- 1 | import { IBaseTemplate } from './BaseTemplate'; 2 | 3 | const base: IBaseTemplate = { 4 | sampleTextProp: 'Hello world!', 5 | }; 6 | 7 | export const mockBaseTemplateProps = { 8 | base, 9 | }; -------------------------------------------------------------------------------- /src/components/templates/base/BaseTemplate.module.css: -------------------------------------------------------------------------------- 1 | .component { 2 | } -------------------------------------------------------------------------------- /src/components/templates/base/BaseTemplate.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 2 | import BaseTemplate, { IBaseTemplate } from './BaseTemplate'; 3 | import { mockBaseTemplateProps } from './BaseTemplate.mocks' 4 | 5 | export default { 6 | title: 'templates/BaseTemplate', 7 | component: BaseTemplate, 8 | argTypes: {}, 9 | } as ComponentMeta; 10 | 11 | const Template: ComponentStory = (args) => ( 12 | 13 | ); 14 | 15 | export const Base = Template.bind({}); 16 | 17 | Base.args = { 18 | ...mockBaseTemplateProps.base, 19 | } as IBaseTemplate; 20 | -------------------------------------------------------------------------------- /src/components/templates/base/BaseTemplate.tsx: -------------------------------------------------------------------------------- 1 | import styles from './BaseTemplate.module.css'; 2 | 3 | export interface IBaseTemplate { 4 | sampleTextProp: string; 5 | } 6 | 7 | const BaseTemplate: React.FC = ({ 8 | sampleTextProp 9 | }) => { 10 | return
{ sampleTextProp }
; 11 | }; 12 | 13 | export default BaseTemplate; 14 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence } from 'framer-motion'; 2 | import type { AppProps } from 'next/app'; 3 | import './globals.css'; 4 | import { NextPageWithLayout } from './page'; 5 | 6 | interface AppPropsWithLayout extends AppProps { 7 | Component: NextPageWithLayout; 8 | } 9 | 10 | function MyApp({ Component, pageProps }: AppPropsWithLayout) { 11 | // Use the layout defined at the page level, if available 12 | 13 | const getLayout = Component.getLayout || ((page) => page); 14 | 15 | return getLayout( 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export default MyApp; 23 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Head, Html, Main, NextScript } from 'next/document'; 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | ); 21 | } 22 | } 23 | 24 | export default MyDocument; -------------------------------------------------------------------------------- /src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import PageTransition from '../components/layouts/pageTransition/PageTransition'; 2 | import PrimaryLayout from '../components/layouts/primary/PrimaryLayout'; 3 | import SidebarLayout from '../components/layouts/sidebar/SidebarLayout'; 4 | import { NextPageWithLayout } from './page'; 5 | 6 | const About: NextPageWithLayout = () => { 7 | return ( 8 |
9 |

Layout Example (About)

10 |

11 | This example adds a property getLayout to your page, 12 | allowing you to return a React component for the layout. This allows you 13 | to define the layout on a per-page basis. Since we're returning a 14 | function, we can have complex nested layouts if desired. 15 |

16 |

17 | When navigating between pages, we want to persist page state (input 18 | values, scroll position, etc.) for a Single-Page Application (SPA) 19 | experience. 20 |

21 |

22 | This layout pattern will allow for state persistence because the React 23 | component tree is persisted between page transitions. To preserve state, 24 | we need to prevent the React component tree from being discarded between 25 | page transitions. 26 |

27 |

Try It Out

28 |

29 | To visualize this, try tying in the search input in the{' '} 30 | Sidebar and then changing routes. You'll notice the 31 | input state is persisted. 32 |

33 |
34 | ); 35 | }; 36 | 37 | export default About; 38 | 39 | About.getLayout = (page) => { 40 | return ( 41 | 42 | 43 | 44 | {page} 45 | 46 | 47 | ); 48 | }; -------------------------------------------------------------------------------- /src/pages/contact.tsx: -------------------------------------------------------------------------------- 1 | import PageTransition from '../components/layouts/pageTransition/PageTransition'; 2 | import PrimaryLayout from '../components/layouts/primary/PrimaryLayout'; 3 | import SidebarLayout from '../components/layouts/sidebar/SidebarLayout'; 4 | import { NextPageWithLayout } from './page'; 5 | 6 | const Contact: NextPageWithLayout = () => { 7 | return ( 8 |
9 |

Layout Example (Contact)

10 |
Contact superZidan by wechat
11 |
wechat id: superZidan41
12 |
13 | ); 14 | }; 15 | 16 | export default Contact; 17 | 18 | Contact.getLayout = (page) => { 19 | return ( 20 | 21 | 22 | 23 | {page} 24 | 25 | 26 | ); 27 | }; -------------------------------------------------------------------------------- /src/pages/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .btn-primary { 7 | @apply border-0 p-2 px-6 bg-slate-100 rounded-md; 8 | } 9 | } -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import CatCard from '../components/cards/cat/CatCard'; 2 | import { mockCatCardProps } from '../components/cards/cat/CatCard.mocks'; 3 | import PageTransition from '../components/layouts/pageTransition/PageTransition'; 4 | import PrimaryLayout from '../components/layouts/primary/PrimaryLayout'; 5 | import SidebarLayout from '../components/layouts/sidebar/SidebarLayout'; 6 | import { NextPageWithLayout } from './page'; 7 | 8 | const Home: NextPageWithLayout = () => { 9 | return ( 10 |
11 |

12 | Welcome to Next.js! 13 |

14 | 15 |
16 | ); 17 | }; 18 | export default Home; 19 | 20 | 21 | Home.getLayout = (page) => { 22 | return ( 23 | 24 | 25 | 26 | {page} 27 | 28 | 29 | ); 30 | }; -------------------------------------------------------------------------------- /src/pages/page.d.ts: -------------------------------------------------------------------------------- 1 | import { NextPage } from 'next'; 2 | import { ComponentType, ReactElement, ReactNode } from 'react'; 3 | 4 | export type NextPageWithLayout

= NextPage

& { 5 | getLayout?: (_page: ReactElement) => ReactNode; 6 | layout?: ComponentType; 7 | }; 8 | -------------------------------------------------------------------------------- /src/stories/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | 4 | import { Button } from './Button'; 5 | 6 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | export default { 8 | title: 'Example/Button', 9 | component: Button, 10 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 11 | argTypes: { 12 | backgroundColor: { control: 'color' }, 13 | }, 14 | } as ComponentMeta; 15 | 16 | // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args 17 | const Template: ComponentStory = (args) => 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/stories/Header.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | 4 | import { Header } from './Header'; 5 | 6 | export default { 7 | title: 'Example/Header', 8 | component: Header, 9 | parameters: { 10 | // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: 'fullscreen', 12 | }, 13 | } as ComponentMeta; 14 | 15 | const Template: ComponentStory = (args) =>

; 16 | 17 | export const LoggedIn = Template.bind({}); 18 | LoggedIn.args = { 19 | user: { 20 | name: 'Jane Doe', 21 | }, 22 | }; 23 | 24 | export const LoggedOut = Template.bind({}); 25 | LoggedOut.args = {}; 26 | -------------------------------------------------------------------------------- /src/stories/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Button } from './Button'; 4 | import './header.css'; 5 | 6 | type User = { 7 | name: string; 8 | }; 9 | 10 | interface HeaderProps { 11 | user?: User; 12 | onLogin: () => void; 13 | onLogout: () => void; 14 | onCreateAccount: () => void; 15 | } 16 | 17 | export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( 18 |
19 |
20 |
21 | 22 | 23 | 27 | 31 | 35 | 36 | 37 |

Acme

38 |
39 |
40 | {user ? ( 41 | <> 42 | 43 | Welcome, {user.name}! 44 | 45 |
54 |
55 |
56 | ); 57 | -------------------------------------------------------------------------------- /src/stories/Introduction.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs'; 2 | import Code from './assets/code-brackets.svg'; 3 | import Colors from './assets/colors.svg'; 4 | import Comments from './assets/comments.svg'; 5 | import Direction from './assets/direction.svg'; 6 | import Flow from './assets/flow.svg'; 7 | import Plugin from './assets/plugin.svg'; 8 | import Repo from './assets/repo.svg'; 9 | import StackAlt from './assets/stackalt.svg'; 10 | 11 | 12 | 13 | 116 | 117 | # Welcome to Storybook 118 | 119 | Storybook helps you build UI components in isolation from your app's business logic, data, and context. 120 | That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA. 121 | 122 | Browse example stories now by navigating to them in the sidebar. 123 | View their code in the `stories` directory to learn how they work. 124 | We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages. 125 | 126 |
Configure
127 | 128 | 174 | 175 |
Learn
176 | 177 | 207 | 208 |
209 | TipEdit the Markdown in{' '} 210 | stories/Introduction.stories.mdx 211 |
212 | -------------------------------------------------------------------------------- /src/stories/Page.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import { within, userEvent } from '@storybook/testing-library'; 4 | import { Page } from './Page'; 5 | 6 | export default { 7 | title: 'Example/Page', 8 | component: Page, 9 | parameters: { 10 | // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout 11 | layout: 'fullscreen', 12 | }, 13 | } as ComponentMeta; 14 | 15 | const Template: ComponentStory = (args) => ; 16 | 17 | export const LoggedOut = Template.bind({}); 18 | 19 | export const LoggedIn = Template.bind({}); 20 | 21 | // More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing 22 | LoggedIn.play = async ({ canvasElement }) => { 23 | const canvas = within(canvasElement); 24 | const loginButton = await canvas.getByRole('button', { name: /Log in/i }); 25 | await userEvent.click(loginButton); 26 | }; 27 | -------------------------------------------------------------------------------- /src/stories/Page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Header } from './Header'; 4 | import './page.css'; 5 | 6 | type User = { 7 | name: string; 8 | }; 9 | 10 | export const Page: React.VFC = () => { 11 | const [user, setUser] = React.useState(); 12 | 13 | return ( 14 |
15 |
setUser({ name: 'Jane Doe' })} 18 | onLogout={() => setUser(undefined)} 19 | onCreateAccount={() => setUser({ name: 'Jane Doe' })} 20 | /> 21 | 22 |
23 |

Pages in Storybook

24 |

25 | We recommend building UIs with a{' '} 26 | 27 | component-driven 28 | {' '} 29 | process starting with atomic components and ending with pages. 30 |

31 |

32 | Render pages with mock data. This makes it easy to build and review page states without 33 | needing to navigate to them in your app. Here are some handy patterns for managing page 34 | data in Storybook: 35 |

36 |
    37 |
  • 38 | Use a higher-level connected component. Storybook helps you compose such data from the 39 | "args" of child component stories 40 |
  • 41 |
  • 42 | Assemble data in the page component from your services. You can mock these services out 43 | using Storybook. 44 |
  • 45 |
46 |

47 | Get a guided tutorial on component-driven development at{' '} 48 | 49 | Storybook tutorials 50 | 51 | . Read more in the{' '} 52 | 53 | docs 54 | 55 | . 56 |

57 |
58 | Tip Adjust the width of the canvas with the{' '} 59 | 60 | 61 | 66 | 67 | 68 | Viewports addon in the toolbar 69 |
70 |
71 |
72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/stories/assets/code-brackets.svg: -------------------------------------------------------------------------------- 1 | illustration/code-brackets -------------------------------------------------------------------------------- /src/stories/assets/colors.svg: -------------------------------------------------------------------------------- 1 | illustration/colors -------------------------------------------------------------------------------- /src/stories/assets/comments.svg: -------------------------------------------------------------------------------- 1 | illustration/comments -------------------------------------------------------------------------------- /src/stories/assets/direction.svg: -------------------------------------------------------------------------------- 1 | illustration/direction -------------------------------------------------------------------------------- /src/stories/assets/flow.svg: -------------------------------------------------------------------------------- 1 | illustration/flow -------------------------------------------------------------------------------- /src/stories/assets/plugin.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /src/stories/assets/repo.svg: -------------------------------------------------------------------------------- 1 | illustration/repo -------------------------------------------------------------------------------- /src/stories/assets/stackalt.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /src/stories/button.css: -------------------------------------------------------------------------------- 1 | .storybook-button { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 700; 4 | border: 0; 5 | border-radius: 3em; 6 | cursor: pointer; 7 | display: inline-block; 8 | line-height: 1; 9 | } 10 | .storybook-button--primary { 11 | color: white; 12 | background-color: #1ea7fd; 13 | } 14 | .storybook-button--secondary { 15 | color: #333; 16 | background-color: transparent; 17 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 18 | } 19 | .storybook-button--small { 20 | font-size: 12px; 21 | padding: 10px 16px; 22 | } 23 | .storybook-button--medium { 24 | font-size: 14px; 25 | padding: 11px 20px; 26 | } 27 | .storybook-button--large { 28 | font-size: 16px; 29 | padding: 12px 24px; 30 | } 31 | -------------------------------------------------------------------------------- /src/stories/header.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 4 | padding: 15px 20px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | 10 | svg { 11 | display: inline-block; 12 | vertical-align: top; 13 | } 14 | 15 | h1 { 16 | font-weight: 900; 17 | font-size: 20px; 18 | line-height: 1; 19 | margin: 6px 0 6px 10px; 20 | display: inline-block; 21 | vertical-align: top; 22 | } 23 | 24 | button + button { 25 | margin-left: 10px; 26 | } 27 | 28 | .welcome { 29 | color: #333; 30 | font-size: 14px; 31 | margin-right: 10px; 32 | } 33 | -------------------------------------------------------------------------------- /src/stories/page.css: -------------------------------------------------------------------------------- 1 | section { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-size: 14px; 4 | line-height: 24px; 5 | padding: 48px 20px; 6 | margin: 0 auto; 7 | max-width: 600px; 8 | color: #333; 9 | } 10 | 11 | section h2 { 12 | font-weight: 900; 13 | font-size: 32px; 14 | line-height: 1; 15 | margin: 0 0 4px; 16 | display: inline-block; 17 | vertical-align: top; 18 | } 19 | 20 | section p { 21 | margin: 1em 0; 22 | } 23 | 24 | section a { 25 | text-decoration: none; 26 | color: #1ea7fd; 27 | } 28 | 29 | section ul { 30 | padding-left: 30px; 31 | margin: 1em 0; 32 | } 33 | 34 | section li { 35 | margin-bottom: 8px; 36 | } 37 | 38 | section .tip { 39 | display: inline-block; 40 | border-radius: 1em; 41 | font-size: 11px; 42 | line-height: 12px; 43 | font-weight: 700; 44 | background: #e7fdd8; 45 | color: #66bf3c; 46 | padding: 4px 12px; 47 | margin-right: 10px; 48 | vertical-align: top; 49 | } 50 | 51 | section .tip-wrapper { 52 | font-size: 13px; 53 | line-height: 20px; 54 | margin-top: 40px; 55 | margin-bottom: 40px; 56 | } 57 | 58 | section .tip-wrapper svg { 59 | display: inline-block; 60 | height: 12px; 61 | width: 12px; 62 | margin-right: 4px; 63 | vertical-align: top; 64 | margin-top: 3px; 65 | } 66 | 67 | section .tip-wrapper svg path { 68 | fill: #1ea7fd; 69 | } 70 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": ["./src/*"] 25 | } 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/pages/index.js"], 28 | "exclude": ["node_modules"] 29 | } 30 | --------------------------------------------------------------------------------