diff --git a/generator/plopfile.mjs b/generator/plopfile.mjs index 7216035..6ea78c0 100644 --- a/generator/plopfile.mjs +++ b/generator/plopfile.mjs @@ -21,7 +21,6 @@ const PAGE_PATH = 'pagePath'; const COMPONENT = 'component'; const COMPONENT_NAME = 'componentName'; -const COMPONENT_TYPE = 'componentType'; const API_LIST = 'apiList'; const API = 'api'; @@ -106,23 +105,23 @@ const pageActions = data => { const componentActions = data => [ { type: 'add', - path: `${rootPath}/components/{{${COMPONENT_TYPE}}}/${pascalCasify(COMPONENT_NAME)}/${pascalCasify(COMPONENT_NAME)}.tsx`, + path: `${rootPath}/components/${pascalCasify(COMPONENT_NAME)}/${pascalCasify(COMPONENT_NAME)}.tsx`, templateFile: 'templates/component/component.hbs', }, { type: 'add', - path: `${rootPath}/components/{{${COMPONENT_TYPE}}}/${pascalCasify(COMPONENT_NAME)}/index.ts`, + path: `${rootPath}/components/${pascalCasify(COMPONENT_NAME)}/index.ts`, templateFile: 'templates/component/index.hbs', }, { type: 'add', - path: `${rootPath}/components/{{${COMPONENT_TYPE}}}/${pascalCasify(COMPONENT_NAME)}/styled.ts`, + path: `${rootPath}/components/${pascalCasify(COMPONENT_NAME)}/styled.ts`, templateFile: 'templates/component/styled.hbs', ...(data[RENDERING_TYPE] === 'SSR(Server-Side-Rendering)' && { skip: () => 'skipped' }), }, { type: 'add', - path: `${rootPath}/components/{{${COMPONENT_TYPE}}}/${pascalCasify(COMPONENT_NAME)}/${pascalCasify(COMPONENT_NAME)}.stories.ts`, + path: `${rootPath}/components/${pascalCasify(COMPONENT_NAME)}/${pascalCasify(COMPONENT_NAME)}.stories.ts`, templateFile: 'templates/component/stories.hbs', ...(!data[TEST_EXIST] && { skip: () => 'skipped', @@ -130,7 +129,7 @@ const componentActions = data => [ }, { type: 'add', - path: `${rootPath}/components/{{${COMPONENT_TYPE}}}/${pascalCasify(COMPONENT_NAME)}/${pascalCasify(COMPONENT_NAME)}.test.tsx`, + path: `${rootPath}/components/${pascalCasify(COMPONENT_NAME)}/${pascalCasify(COMPONENT_NAME)}.test.tsx`, templateFile: 'templates/component/test.hbs', ...(!data[TEST_EXIST] && { skip: () => 'skipped', @@ -173,13 +172,6 @@ export default function generator(plop) { message: 'Asset type', choices: [PAGE, COMPONENT, API], }, - { - type: 'list', - name: COMPONENT_TYPE, - when: answer => answer[ASSET_TYPE] === COMPONENT, - message: 'Component type', - choices: ['atoms', 'molecules', 'organisms', 'templates'], - }, { type: 'input', name: COMPONENT_NAME, diff --git a/generator/templates/component/stories.hbs b/generator/templates/component/stories.hbs index 59ac2b4..9fd6248 100644 --- a/generator/templates/component/stories.hbs +++ b/generator/templates/component/stories.hbs @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import {{pascalCase componentName}} from '.'; const meta = { - title: '{{pascalCase componentType}}/{{pascalCase componentName}}', + title: '{{pascalCase componentName}}', {{#if (is "CSR(Client-Side-Rendering)" renderingType)}} component: {{pascalCase componentName}}, {{/if}} diff --git a/package.json b/package.json index 6c54fd7..d37287b 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "test": "jest", + "test:unit": "jest --testPathPattern='src/app/api/.*\\.test\\.tsx?$|src/components/.*\\.test\\.tsx?$'", + "test:integration": "jest --testPathPattern='src/app/(?!api).*\\.test\\.tsx?$'", "g": "plop --plopfile ./generator/plopfile.mjs", "e2e": "start-server-and-test dev http://localhost:3000 \"cypress open --e2e\"", "e2e:headless": "start-server-and-test dev http://localhost:3000 \"cypress run --e2e\"" diff --git a/src/app/cars/page.tsx b/src/app/cars/page.tsx index bc5f910..d22e79d 100644 --- a/src/app/cars/page.tsx +++ b/src/app/cars/page.tsx @@ -1,41 +1,7 @@ import 'server-only'; -import Link from 'next/link'; - -import { API_HOST } from '~/constants/apiRelated'; - -export const dynamic = 'force-dynamic'; - -export interface Car { - createdAt: string; - driverName: string; - driverAvatar: string; - carName: string; - carManufacturer: string; - isAllocation: boolean; - carId: string; -} - -async function getCars(): Promise { - // You need to set base url in .env as origin of your url - const res = await fetch(API_HOST + '/api/cars'); - await new Promise(resolve => setTimeout(resolve, 4000)); - return res.json(); -} +import Cars from '~/components/Cars'; export default async function CarsPage(...props: any) { - console.log(props); - const cars = await getCars(); - - return ( -
- -
- ); + return ; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b762d11..05e0402 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -18,7 +18,9 @@ export const metadata: Metadata = { }, description: 'NextJS + AppROuter', other: { - 'naver-site-verification': process.env.NEXT_PUBLIC_NAVER_SITE_VERIFICATION, + ...(process.env.NEXT_PUBLIC_NAVER_SITE_VERIFICATION && { + 'naver-site-verification': process.env.NEXT_PUBLIC_NAVER_SITE_VERIFICATION, + }), }, }; diff --git a/src/app/page.stories.tsx b/src/app/page.stories.tsx deleted file mode 100644 index 387fe77..0000000 --- a/src/app/page.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import HomePage from './page'; - -const meta: Meta = { - title: 'page', - parameters: { - layout: 'centered', - }, - tags: ['autodocs'], - argTypes: {}, - component: HomePage, -}; - -export default meta; -type Story = StoryObj; - -export const Primary: Story = {}; diff --git a/src/app/page.test.tsx b/src/app/page.test.tsx deleted file mode 100644 index dae75e0..0000000 --- a/src/app/page.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { composeStories } from '@storybook/react'; -import { cleanup, screen } from '@testing-library/react'; - -import { afterAll, describe } from '@jest/globals'; - -import { customRender } from '~/libs/customRender'; - -import * as stories from './page.stories'; - -const { Primary: PrimaryTest } = composeStories(stories); - -afterAll(() => { - cleanup(); -}); - -describe('RootPage Test', () => { - it('Do RootPage test', async () => { - customRender(); - const link = screen.getByRole('link'); - expect(link).toHaveAttribute('href', '/cars'); - }); -}); diff --git a/src/app/page.tsx b/src/app/page.tsx index 333846b..7517afa 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,17 +1,47 @@ 'use client'; +import { useEffect, useState } from 'react'; + import Link from 'next/link'; -import Button from '~/components/atoms/Button'; +import Button from '~/components/Button'; import { RootPageStyled } from './styled'; export default function Home() { + const [isLoggedIn, setIsLoggedIn] = useState(false); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const sessionLogin = sessionStorage.getItem('isLoggedIn'); + + if (sessionLogin !== 'true') return; + + setIsLoggedIn(true); + }, []); + return ( - - - + {isLoggedIn ? ( + <> + + + + + + ) : ( + + + + )} ); } diff --git a/src/app/sign/in/page-ui.test.tsx b/src/app/sign/in/page-ui.test.tsx new file mode 100644 index 0000000..ab7d0ca --- /dev/null +++ b/src/app/sign/in/page-ui.test.tsx @@ -0,0 +1,45 @@ +import { fireEvent, render, screen } from '@testing-library/react'; + +import SignInPageUI from './page-ui'; + +describe('SignInPageUI', () => { + const mockOnSubmit = jest.fn(); + const mockSetUsername = jest.fn(); + const mockSetPassword = jest.fn(); + + beforeAll(() => { + render( + , + ); + }); + + it('Should render the SignInPageUI component', () => { + expect(document.querySelector('form')).toBeInTheDocument(); + expect(document.querySelector('input[type="username"]')).toBeInTheDocument(); + expect(document.querySelector('input[type="password"]')).toBeInTheDocument(); + expect(document.querySelector('button')).toBeInTheDocument(); + }); + + it('Should call setUsername when the username input changes', () => { + const usernameInput = screen.getByPlaceholderText('Username'); + const passwordInput = screen.getByPlaceholderText('Password'); + + fireEvent.change(usernameInput, { target: { value: 'testUser' } }); + fireEvent.change(passwordInput, { target: { value: 'password123' } }); + + expect(mockSetUsername).toHaveBeenCalledWith('testUser'); + expect(mockSetPassword).toHaveBeenCalledWith('password123'); + }); + + it('Should call onSubmit when the form is submitted', async () => { + fireEvent.submit(document.querySelector('form') as HTMLFormElement); + + expect(mockOnSubmit).toHaveBeenCalled(); + }); +}); diff --git a/src/app/sign/in/page-ui.tsx b/src/app/sign/in/page-ui.tsx new file mode 100644 index 0000000..41f0b7b --- /dev/null +++ b/src/app/sign/in/page-ui.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { FormEvent } from 'react'; + +import { SignInpageStyled } from './styled'; + +interface SignInPageUIProps { + onSubmit: (e: FormEvent) => void; + username: string; + password: string; + setUsername: (value: string) => void; + setPassword: (value: string) => void; +} + +const SignInPageUI = ({ + onSubmit, + username, + password, + setUsername, + setPassword, +}: SignInPageUIProps) => { + return ( + +
+ setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> + +
+
+ ); +}; + +export default SignInPageUI; diff --git a/src/app/sign/in/page.test.tsx b/src/app/sign/in/page.test.tsx new file mode 100644 index 0000000..5fc1d6b --- /dev/null +++ b/src/app/sign/in/page.test.tsx @@ -0,0 +1,38 @@ +import { fireEvent, render, screen } from '@testing-library/react'; + +import { useRouter } from 'next/navigation'; + +import SignInpage from './page'; + +jest.mock('next/navigation', () => ({ + useRouter: jest.fn(), +})); + +describe('SignInPage', () => { + beforeAll(() => { + render(); + }); + + it('Should store user data in sessionStorate and redirect to home on valid sign-in', () => { + const mockPush = jest.fn(); + (useRouter as jest.Mock).mockReturnValue({ push: mockPush }); + + fireEvent.change(screen.getByPlaceholderText('Username'), { target: { value: 'test' } }); + fireEvent.change(screen.getByPlaceholderText('Password'), { target: { value: 'test' } }); + fireEvent.click(screen.getByRole('button')); + + expect(sessionStorage.getItem('isLoggedIn')).toBe('true'); + + expect(mockPush).toHaveBeenCalledWith('/'); + }); + + it('Should show alert on invalid sign-in', () => { + const mockAlert = jest.spyOn(window, 'alert').mockImplementation(); + + fireEvent.change(screen.getByPlaceholderText('Username'), { target: { value: '' } }); + fireEvent.change(screen.getByPlaceholderText('Password'), { target: { value: '' } }); + fireEvent.click(screen.getByRole('button')); + + expect(mockAlert).toHaveBeenCalledWith('Please fill in all fields'); + }); +}); diff --git a/src/app/sign/in/page.tsx b/src/app/sign/in/page.tsx new file mode 100644 index 0000000..23267b0 --- /dev/null +++ b/src/app/sign/in/page.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { FormEvent, useState } from 'react'; + +import { useRouter } from 'next/navigation'; + +import SignInPageUI from './page-ui'; + +const SignInpage = () => { + const router = useRouter(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + + if (!username || !password) { + return alert('Please fill in all fields'); + } + + sessionStorage.setItem('isLoggedIn', 'true'); + router.push('/'); + }; + + return ( + + ); +}; + +export default SignInpage; diff --git a/src/app/sign/in/styled.ts b/src/app/sign/in/styled.ts new file mode 100644 index 0000000..8504170 --- /dev/null +++ b/src/app/sign/in/styled.ts @@ -0,0 +1,28 @@ +import styled from 'styled-components'; + +export const SignInpageStyled = styled.div` + width: 100dvw; + height: 100dvh; + form { + position: relative; + top: 50%; + transform: translateY(-50%); + + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + + input { + border: 1px solid rgba(10, 10, 10, 0.6); + border-radius: 4px; + } + + button { + background-color: #f8f8f8; + border: 1px solid rgba(10, 10, 10, 0.6); + border-radius: 4px; + cursor: pointer; + } + } +`; diff --git a/src/app/styled.ts b/src/app/styled.ts index 72c9ecb..66f8b8d 100644 --- a/src/app/styled.ts +++ b/src/app/styled.ts @@ -10,39 +10,3 @@ export const RootPageStyled = styled.main` cursor: pointer; } `; - -export const RootTemplateStyled = styled.div` - > header { - width: 100vw; - max-width: 100%; - height: ${props => props.theme.sizes.headerHeight}; - background-color: ${props => props.theme.colors.secondary}; - color: white; - } - - > nav { - width: 100vw; - max-width: 100%; - height: ${props => props.theme.sizes.navHeight}; - background-color: ${props => props.theme.colors.secondary}; - color: white; - } - - > aside { - position: fixed; - right: 0; - top: 0; - width: ${props => props.theme.sizes.asideWidth}; - height: 100%; - color: white; - background-color: ${props => props.theme.colors.secondary}; - } - - > footer { - width: 100vw; - max-width: 100%; - height: ${props => props.theme.sizes.footerHeight}; - background-color: ${props => props.theme.colors.secondary}; - color: white; - } -`; diff --git a/src/app/template.tsx b/src/app/template.tsx deleted file mode 100644 index 784c332..0000000 --- a/src/app/template.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client'; - -import { ReactNode } from 'react'; - -import { RootTemplateStyled } from './styled'; - -export default function RootTemplate({ children }: { children: ReactNode }) { - return ( - -
HEADER
- - -
{children}
-
FOOTER
-
- ); -} diff --git a/src/components/atoms/Button/Button.stories.ts b/src/components/Button/Button.stories.ts similarity index 100% rename from src/components/atoms/Button/Button.stories.ts rename to src/components/Button/Button.stories.ts diff --git a/src/components/atoms/Button/Button.test.tsx b/src/components/Button/Button.test.tsx similarity index 100% rename from src/components/atoms/Button/Button.test.tsx rename to src/components/Button/Button.test.tsx diff --git a/src/components/atoms/Button/Button.tsx b/src/components/Button/Button.tsx similarity index 100% rename from src/components/atoms/Button/Button.tsx rename to src/components/Button/Button.tsx diff --git a/src/components/atoms/Button/index.ts b/src/components/Button/index.ts similarity index 100% rename from src/components/atoms/Button/index.ts rename to src/components/Button/index.ts diff --git a/src/components/atoms/Button/styled.ts b/src/components/Button/styled.ts similarity index 100% rename from src/components/atoms/Button/styled.ts rename to src/components/Button/styled.ts diff --git a/src/app/cars/page.stories.tsx b/src/components/Cars/Cars.stories.ts similarity index 60% rename from src/app/cars/page.stories.tsx rename to src/components/Cars/Cars.stories.ts index 809f540..2bb2c2c 100644 --- a/src/app/cars/page.stories.tsx +++ b/src/components/Cars/Cars.stories.ts @@ -1,24 +1,25 @@ import type { Meta, StoryObj } from '@storybook/react'; -import CarsPage from './page'; +import Cars from '.'; + +const meta = { + title: 'Cars', + + loaders: [async ({ args }) => ({ Component: await Cars({ ...args }) })], + render: (_, { loaded: { Component } }) => { + return Component; + }, -const meta: Meta = { - title: 'cars/detail/page', parameters: { layout: 'centered', }, tags: ['autodocs'], argTypes: {}, - - loaders: [async ({ args }) => ({ Component: await CarsPage({ ...args }) })], - render: (_, { loaded: { Component } }) => { - return Component; - }, -}; +} satisfies Meta; export default meta; type Story = StoryObj; export const Primary: Story = { - args: { a: 1 }, + args: { latency: 1000 }, }; diff --git a/src/app/cars/page.test.tsx b/src/components/Cars/Cars.test.tsx similarity index 81% rename from src/app/cars/page.test.tsx rename to src/components/Cars/Cars.test.tsx index 105d2f4..6805979 100644 --- a/src/app/cars/page.test.tsx +++ b/src/components/Cars/Cars.test.tsx @@ -5,7 +5,7 @@ import { afterAll, describe } from '@jest/globals'; import { customRender } from '~/libs/customRender'; -import * as stories from './page.stories'; +import * as stories from './Cars.stories'; const { Primary: PrimaryTest } = composeStories(stories); @@ -13,8 +13,8 @@ afterAll(() => { cleanup(); }); -describe('CarsPage Test', () => { - it('Do CarsPage test', async () => { +describe('Cars Test', () => { + it('Do Cars test', async () => { await PrimaryTest.load(); customRender(); diff --git a/src/components/Cars/Cars.tsx b/src/components/Cars/Cars.tsx new file mode 100644 index 0000000..a46fbce --- /dev/null +++ b/src/components/Cars/Cars.tsx @@ -0,0 +1,44 @@ +import 'server-only'; + +import Link from 'next/link'; + +import { API_HOST } from '~/constants/apiRelated'; + +export interface Car { + createdAt: string; + driverName: string; + driverAvatar: string; + carName: string; + carManufacturer: string; + isAllocation: boolean; + carId: string; +} + +async function getCars(latency: number): Promise { + // You need to set base url in .env as origin of your url + const res = await fetch(API_HOST + '/api/cars'); + await new Promise(resolve => setTimeout(resolve, latency)); + return res.json(); +} + +interface CarsProps { + latency: number; +} + +const Cars = async ({ latency }: CarsProps) => { + const cars = await getCars(latency); + + return ( +
+
    + {cars.map(v => ( +
  • + {v.carName} +
  • + ))} +
+
+ ); +}; + +export default Cars; diff --git a/src/components/Cars/index.ts b/src/components/Cars/index.ts new file mode 100644 index 0000000..e9ab623 --- /dev/null +++ b/src/components/Cars/index.ts @@ -0,0 +1,2 @@ +export * from './Cars'; +export { default } from './Cars'; diff --git a/src/components/atoms/README.md b/src/components/atoms/README.md deleted file mode 100644 index baa0d64..0000000 --- a/src/components/atoms/README.md +++ /dev/null @@ -1 +0,0 @@ -# Atom folder diff --git a/src/components/molecules/README.md b/src/components/molecules/README.md deleted file mode 100644 index 2b070a1..0000000 --- a/src/components/molecules/README.md +++ /dev/null @@ -1 +0,0 @@ -# Molecule folder diff --git a/src/components/organisms/README.md b/src/components/organisms/README.md deleted file mode 100644 index e2a8dd9..0000000 --- a/src/components/organisms/README.md +++ /dev/null @@ -1 +0,0 @@ -# organism folder diff --git a/src/components/templates/README.md b/src/components/templates/README.md deleted file mode 100644 index 0dc2584..0000000 --- a/src/components/templates/README.md +++ /dev/null @@ -1 +0,0 @@ -# Template folder