diff --git a/.eslintrc.js b/.eslintrc.js index dc4c768..76e3e8f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,6 +20,17 @@ module.exports = { '@typescript-eslint/no-import-type-side-effects': 'error', '@typescript-eslint/no-inferrable-types': 'warn', 'func-style': ['error', 'expression'], + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['libs/*'], + message: 'Please import with path alias like `@libs/*`' + } + ] + } + ], 'object-shorthand': ['warn', 'always'] }, overrides: [ diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index c19aa5e..8c91323 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -9,17 +9,20 @@ module.exports = { '@next/next/no-html-link-for-pages': [ 'error', require('path').join(__dirname, 'src/app') - ], - 'react/function-component-definition': [ - 'error', - { - namedComponents: 'function-declaration' - } ] }, overrides: [ + // TODO: If there is another way to solve the '@next/babel' error, remove below object + { + files: ['*.js'], + parser: 'espree', + parserOptions: { + ecmaVersion: 2020 + } + }, { files: ['*.tsx'], + excludedFiles: ['src/components/ui/*.tsx'], rules: { 'react/function-component-definition': [ 'error', @@ -27,7 +30,8 @@ module.exports = { namedComponents: 'function-declaration' } ], - 'func-style': ['off'] + 'func-style': ['off'], + 'no-restricted-imports': ['error'] } }, { diff --git a/frontend/package.json b/frontend/package.json index e7db452..e8926b1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,11 @@ "dependencies": { "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.1", + "@hookform/resolvers": "^3.3.4", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-slot": "^1.0.2", "@storybook/manager-api": "^7.6.13", "@tailwindcss/forms": "^0.5.7", "class-variance-authority": "^0.7.0", @@ -21,10 +25,11 @@ "moment": "^2.30.1", "next": "14.1.0", "next-auth": "^4.24.6", + "next-themes": "^0.2.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.50.1", "sonner": "^1.4.0", - "sooner": "^1.1.4", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", "zod": "^3.22.4" diff --git a/frontend/public/next.svg b/frontend/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/frontend/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg deleted file mode 100644 index d2f8422..0000000 --- a/frontend/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index ac66726..bb0c4d0 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -44,7 +44,3 @@ html { html::-webkit-scrollbar { display: none; } - -html { - background-color: rgb(20, 21, 23); -} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 2acc88d..ffab773 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,13 +1,16 @@ import Footer from '@/components/Footer' import Header from '@/components/Header' +import { ThemeProvider } from '@/components/ThemeProvider' import Toaster from '@/components/ui/sooner' +import { cn } from '@/lib/utils' import type { Metadata } from 'next' import { Noto_Sans_KR } from 'next/font/google' import './globals.css' -const gothicA1 = Noto_Sans_KR({ +const noto = Noto_Sans_KR({ subsets: ['latin'], - weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'] + weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], + variable: '--font-noto' }) export const metadata: Metadata = { @@ -23,19 +26,29 @@ export default function RootLayout({ }>) { return ( - -
-
-
- {children} -
- + + +
+
+
+ {children} +
+
+ +
) diff --git a/frontend/src/app/login/_components/LoginForm.tsx b/frontend/src/app/login/_components/LoginForm.tsx index ec9a739..7f70631 100644 --- a/frontend/src/app/login/_components/LoginForm.tsx +++ b/frontend/src/app/login/_components/LoginForm.tsx @@ -1,146 +1,93 @@ 'use client' -import { ExclamationCircleIcon } from '@heroicons/react/20/solid' -import { UserIcon, KeyIcon } from '@heroicons/react/24/outline' -import clsx from 'clsx' +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { LoginFormSchema } from '@/lib/forms' +import { zodResolver } from '@hookform/resolvers/zod' import { signIn } from 'next-auth/react' import { useState } from 'react' -import { useFormState } from 'react-dom' +import { useForm } from 'react-hook-form' import { toast } from 'sonner' -import { z } from 'zod' - -const LoginFormSchema = z.object({ - username: z.string().min(1), - password: z.string().min(1) -}) - -interface LoginFormState { - errors?: { - username?: string[] - password?: string[] - } - message?: string | null -} +import type { z } from 'zod' export default function LoginForm() { - const [validationError, setValidationError] = useState(false) + const [isFetching, setIsFetching] = useState(false) - const loginAction = async (prevState: LoginFormState, formData: FormData) => { - const validatedFields = LoginFormSchema.safeParse({ - username: formData.get('username'), - password: formData.get('password') - }) - - if (!validatedFields.success) { - setValidationError(true) - return { - errors: validatedFields.error.flatten().fieldErrors, - message: '입력하지 않은 필드가 존재합니다' - } + const form = useForm>({ + resolver: zodResolver(LoginFormSchema), + defaultValues: { + username: '', + password: '' } + }) + const onSubmit = async (data: z.infer) => { try { + setIsFetching(true) const res = await signIn('credentials', { - ...validatedFields.data, + data, redirect: false }) if (!res?.error) { toast.success('로그인 되었습니다') - return { ...prevState, message: '로그인 되었습니다' } } else { console.log(res.error) toast.error('로그인 실패') - return { ...prevState, message: '로그인 실패' } } } catch (error) { toast.error('로그인 실패') - return { ...prevState, message: '로그인 실패' } + } finally { + setIsFetching(false) } } - const initState = { message: null, errors: {} } - const [state, dispatch] = useFormState(loginAction, initState) - return ( -
-
-
+ + ( + + Username + + + + + )} - > - -
-
-
- - {state.errors?.username && ( -
-
- )} -
-
-
+ ( + + Password + + + + + )} + /> +
-
- {validationError && ( -

- 입력되지 않은 필드가 존재합니다 -

- )} - -
+ 로그인 + + + ) } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index f051ff4..e856047 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -4,10 +4,10 @@ export default function Home() { return (
-

+

SKKU ROYALS

-

+

American Football Team

@@ -15,16 +15,17 @@ export default function Home() { hero image
-

+

SKKU ROYALS

-

+

American Football Team

diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx deleted file mode 100644 index 49288a3..0000000 --- a/frontend/src/components/Button.tsx +++ /dev/null @@ -1,81 +0,0 @@ -export enum ButtonSize { - xs = 'xs', - sm = 'sm', - md = 'md', - lg = 'lg', - xl = 'xl' -} - -interface ButtonProps { - content: string - size: ButtonSize - type: 'submit' | 'button' | 'reset' - accent?: boolean - // eslint-disable-next-line @typescript-eslint/no-explicit-any - icon?: any -} - -export default function Button({ - content, - size, - type, - icon: Icon -}: ButtonProps) { - const render = (size: ButtonSize) => { - switch (size) { - case ButtonSize.xs: - return ( - - ) - case ButtonSize.sm: - return ( - - ) - case ButtonSize.lg: - return ( - - ) - case ButtonSize.xl: - return ( - - ) - case ButtonSize.md: - default: - return ( - - ) - } - } - - return <>{render(size)} -} diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx index 5a7afa4..828bd9f 100644 --- a/frontend/src/components/Footer.tsx +++ b/frontend/src/components/Footer.tsx @@ -66,7 +66,7 @@ export default function Footer() { {item.name}