├── .npmrc
├── .prettierignore
├── src
├── components
│ ├── layouts
│ │ ├── pageTransition
│ │ │ ├── PageTransition.module.css
│ │ │ ├── PageTransition.mocks.ts
│ │ │ ├── PageTransition.stories.tsx
│ │ │ └── PageTransition.tsx
│ │ ├── sidebar
│ │ │ ├── SidebarLayout.mocks.ts
│ │ │ ├── SidebarLayout.tsx
│ │ │ ├── SidebarLayout.stories.tsx
│ │ │ └── SidebarLayout.module.css
│ │ └── primary
│ │ │ ├── PrimaryLayout.module.css
│ │ │ ├── PrimaryLayout.mocks.ts
│ │ │ ├── PrimaryLayout.tsx
│ │ │ └── PrimaryLayout.stories.tsx
│ ├── templates
│ │ └── base
│ │ │ ├── BaseTemplate.module.css
│ │ │ ├── BaseTemplate.mocks.ts
│ │ │ ├── BaseTemplate.tsx
│ │ │ └── BaseTemplate.stories.tsx
│ └── cards
│ │ └── cat
│ │ ├── CatCard.mocks.ts
│ │ ├── CatCard.stories.tsx
│ │ ├── CatCard.module.css
│ │ └── CatCard.tsx
├── pages
│ ├── globals.css
│ ├── page.d.ts
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── contact.tsx
│ ├── index.tsx
│ └── about.tsx
└── stories
│ ├── header.css
│ ├── Header.stories.tsx
│ ├── button.css
│ ├── Page.stories.tsx
│ ├── Button.tsx
│ ├── assets
│ ├── direction.svg
│ ├── flow.svg
│ ├── code-brackets.svg
│ ├── comments.svg
│ ├── repo.svg
│ ├── plugin.svg
│ ├── stackalt.svg
│ └── colors.svg
│ ├── Button.stories.tsx
│ ├── page.css
│ ├── Header.tsx
│ ├── Page.tsx
│ └── Introduction.stories.mdx
├── README.md
├── .husky
├── pre-commit
├── pre-push
└── commit-msg
├── public
├── favicon.ico
├── time-cat.jpg
├── vercel.svg
├── thirteen.svg
└── next.svg
├── .prettierrc
├── next.config.js
├── .vscode
├── settings.json
└── launch.json
├── .storybook
├── main.js
└── preview.js
├── .gitignore
├── .eslintrc.json
├── tsconfig.json
├── commitlint.config.js
└── package.json
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .yarn
2 | .next
3 | dist
4 | node_modules
--------------------------------------------------------------------------------
/src/components/layouts/pageTransition/PageTransition.module.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/templates/base/BaseTemplate.module.css:
--------------------------------------------------------------------------------
1 | .component {
2 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Next.js 项目最佳实践
2 |
3 | [原文地址](https://juejin.cn/post/7194410416879960125)
4 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zidanDirk/nextjs-fullstack-app-template-zn/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/time-cat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zidanDirk/nextjs-fullstack-app-template-zn/HEAD/public/time-cat.jpg
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx --no -- commitlint --edit ""
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/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/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/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/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 | };
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
9 |
10 |
11 | Home
12 |
13 |
14 | About
15 |
16 |
17 | Contact
18 |
19 |
20 | );
21 | };
22 |
23 | export default SidebarLayout;
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/stories/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './button.css';
3 |
4 | interface ButtonProps {
5 | /**
6 | * Is this the principal call to action on the page?
7 | */
8 | primary?: boolean;
9 | /**
10 | * What background color to use
11 | */
12 | backgroundColor?: string;
13 | /**
14 | * How large should the button be?
15 | */
16 | size?: 'small' | 'medium' | 'large';
17 | /**
18 | * Button contents
19 | */
20 | label: string;
21 | /**
22 | * Optional click handler
23 | */
24 | onClick?: () => void;
25 | }
26 |
27 | /**
28 | * Primary UI component for user interaction
29 | */
30 | export const Button = ({
31 | primary = false,
32 | size = 'medium',
33 | backgroundColor,
34 | label,
35 | ...props
36 | }: ButtonProps) => {
37 | const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
38 | return (
39 |
45 | {label}
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/src/stories/assets/direction.svg:
--------------------------------------------------------------------------------
1 | illustration/direction
--------------------------------------------------------------------------------
/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) => ;
18 |
19 | export const Primary = Template.bind({});
20 | // More on args: https://storybook.js.org/docs/react/writing-stories/args
21 | Primary.args = {
22 | primary: true,
23 | label: 'Button',
24 | };
25 |
26 | export const Secondary = Template.bind({});
27 | Secondary.args = {
28 | label: 'Button',
29 | };
30 |
31 | export const Large = Template.bind({});
32 | Large.args = {
33 | size: 'large',
34 | label: 'Button',
35 | };
36 |
37 | export const Small = Template.bind({});
38 | Small.args = {
39 | size: 'small',
40 | label: 'Button',
41 | };
42 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/stories/assets/flow.svg:
--------------------------------------------------------------------------------
1 | illustration/flow
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/stories/assets/code-brackets.svg:
--------------------------------------------------------------------------------
1 | illustration/code-brackets
--------------------------------------------------------------------------------
/src/stories/assets/comments.svg:
--------------------------------------------------------------------------------
1 | illustration/comments
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------
/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.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 |
24 |
25 |
26 |
{tag}
27 |
{title}
28 |
{body}
29 |
30 |
31 |
32 |
39 |
40 |
{author}
41 | {time}
42 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 |
51 | export default CatCard;
--------------------------------------------------------------------------------
/src/stories/assets/repo.svg:
--------------------------------------------------------------------------------
1 | illustration/repo
--------------------------------------------------------------------------------
/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 |
56 | );
57 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/stories/assets/plugin.svg:
--------------------------------------------------------------------------------
1 | illustration/plugin
--------------------------------------------------------------------------------
/src/stories/assets/stackalt.svg:
--------------------------------------------------------------------------------
1 | illustration/stackalt
--------------------------------------------------------------------------------
/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/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 | Tip Edit the Markdown in{' '}
210 | stories/Introduction.stories.mdx
211 |
212 |
--------------------------------------------------------------------------------
/src/stories/assets/colors.svg:
--------------------------------------------------------------------------------
1 | illustration/colors
--------------------------------------------------------------------------------