diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9e61337 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM node:18-alpine +WORKDIR /app +COPY yarn.lock ./ +RUN yarn +COPY . . +RUN yarn build +ENV NODE_ENV=production +CMD ["yarn", "start"] +EXPOSE 3000 diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 218e576..3013d52 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,80 +1,7 @@ -'use client' +import Template from '@/components/Templates' -import Atom from '@/components/atoms' -import { useAuthForm } from '@/hooks/useAuthForm' -import { useFormField } from '@/hooks/useFormField' -import styles from '@/styles/(auth)/Auth.module.css' - -const Login = () => { - const emailField = useFormField('') - const passwordField = useFormField('') - - const formValues = { - email: emailField.value, - password: passwordField.value, - } - - const { - isFormValid, - isSubmitting, - submissionError, - handleSubmit, - emailError, - passwordError, - } = useAuthForm('login', formValues) - - const handleFormSubmit = async (e: React.FormEvent) => { - e.preventDefault() - await handleSubmit(e) - } - - return ( -
-
-

Вход

-

Войдите в свой аккаунт

-
-
- - - {submissionError && ( -
{submissionError}
- )} - - Вход - - -
- ) +const LoginPage = () => { + return } -export default Login +export default LoginPage diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index 820226d..1e8996c 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -1,107 +1,7 @@ -'use client' +import Template from '@/components/Templates' -import Atom from '@/components/atoms/index' -import { useAuthForm } from '@/hooks/useAuthForm' -import { useFormField } from '@/hooks/useFormField' -import styles from '@/styles/(auth)/Auth.module.css' - -const Register = () => { - const fullNameField = useFormField('') - const emailField = useFormField('') - const passwordField = useFormField('') - const confirmPasswordField = useFormField('') - - const formValues = { - fullName: fullNameField.value, - email: emailField.value, - password: passwordField.value, - confirmPassword: confirmPasswordField.value, - } - - const { - isFormValid, - isSubmitting, - submissionError, - handleSubmit, - emailError, - passwordError, - fullNameError, - confirmPasswordError, - } = useAuthForm('register', formValues) - - const handleFormSubmit = (e: React.FormEvent) => { - handleSubmit(e) - } - - return ( -
-
-

Регистрация

-

Создайте аккаунт

-
-
- - - - - {submissionError && ( -
{submissionError}
- )} - - Регистрация - - -
- ) +const RegisterPage = () => { + return } -export default Register +export default RegisterPage diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 56ecfec..1640070 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,5 +1,5 @@ -const Dashboard = () => { +const DashboardPage = () => { return

Главная страница

} -export default Dashboard +export default DashboardPage diff --git a/app/modules/page.tsx b/app/modules/page.tsx index b34fe9e..f98c875 100644 --- a/app/modules/page.tsx +++ b/app/modules/page.tsx @@ -1,48 +1,79 @@ +'use client' + import Atom from '@/components/atoms' import { Bell } from 'lucide-react' +import { useState } from 'react' + +const ModulesPage = () => { + const [inputValue, setInputValue] = useState('') + const [touched, setTouched] = useState(false) + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value) + } + + const handleInputBlur = () => { + setTouched(true) + } -const Modules = () => { return (
-

Размеры кнопок

- - Small Primary - - - Medium Primary - - - Large Primary - - -

Варианты кнопок

- - Primary - - - Secondary - - - Danger - - -

Кнопки с иконками

- - - - -

Заблокированная кнопка

- - ТЕКСТ БЛЯТЬ - +
+

Размеры кнопок

+ + Small Primary + + + Medium Primary + + + Large Primary + + +

Варианты кнопок

+ + Primary + + + Secondary + + + Danger + + +

Кнопки с иконками

+ + + + +

Заблокированная кнопка

+ + Disabled Button + +
+
+

Текстовые поля

+ + handleInputChange(e as React.ChangeEvent) + } + onBlur={handleInputBlur} + error={touched && inputValue === '' ? 'This field is required' : null} + touched={touched} + /> +
) } -export default Modules +export default ModulesPage diff --git a/app/page.tsx b/app/page.tsx index 265161c..063b9eb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,5 @@ const Home = () => { - return
Page component
+ return

Page component

} export default Home diff --git a/app/profile/page.tsx b/app/profile/page.tsx index b5b9631..67cbbfb 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -1,7 +1,14 @@ +'use server' + import Profile from '@/components/Templates/Profile/Profile' +import { getProfile } from '@/services/profileService' +import { cookies } from 'next/headers' + +const ProfilePage = async () => { + const token = cookies().get('token')?.value || '' + const name = await getProfile(token) -const App = () => { - return + return } -export default App +export default ProfilePage diff --git a/middleware.ts b/middleware.ts index 6a3e910..7455ff9 100644 --- a/middleware.ts +++ b/middleware.ts @@ -39,5 +39,5 @@ export async function middleware(request: NextRequest) { } export const config = { - matcher: ['/modules/:path*'], + matcher: ['/modules/:path*', '/profile/:path*'], } diff --git a/next.config.mjs b/next.config.mjs index 1d61478..0ba6325 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,6 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + distDir: 'build', +} export default nextConfig diff --git a/package.json b/package.json index 22df30d..14e64e0 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "dev": "next dev", + "dev": "next dev --turbo", "build": "next build", "start": "next start", "lint": "next lint", @@ -30,11 +30,11 @@ }, "dependencies": { "@reduxjs/toolkit": "^2.2.7", + "axios": "^1.7.5", "jose": "^5.7.0", "js-cookie": "^3.0.5", "lucide-react": "^0.427.0", "next": "14.2.5", - "playwright": "^1.46.1", "react": "^18", "react-dom": "^18", "react-loading-skeleton": "^3.4.0", @@ -66,7 +66,6 @@ "husky": "^9.1.4", "jest": "^29.7.0", "lint-staged": "^15.2.9", - "postcss": "^8", "prettier": "3.3.3", "storybook": "^8.2.9", "stylelint": "^16.8.1", diff --git a/shell.nix b/shell.nix index 344a5bd..5c5b022 100644 --- a/shell.nix +++ b/shell.nix @@ -14,8 +14,8 @@ pkgs.mkShell { # Автоматическая установка зависимостей при запуске nix-shell if [ ! -d node_modules ]; then echo "Installing npm dependencies..." - npm install - fi + yarn + fi # Автоматический запуск проекта в режиме разработки echo "Starting development server..." diff --git a/src/components/Templates/Login/Login.tsx b/src/components/Templates/Login/Login.tsx new file mode 100644 index 0000000..da89370 --- /dev/null +++ b/src/components/Templates/Login/Login.tsx @@ -0,0 +1,96 @@ +'use client' + +import Atom from '@/components/atoms' +import { useAuthForm } from '@/hooks/useAuthForm' +import { useFormField } from '@/hooks/useFormField' +import styles from '@/styles/(auth)/Auth.module.css' + +const Login = () => { + const emailField = useFormField('') + const passwordField = useFormField('') + + const formValues = { + email: emailField.value, + password: passwordField.value, + } + + const { + isFormValid, + isSubmitting, + submissionError, + handleSubmit, + emailError, + passwordError, + } = useAuthForm('login', formValues) + + const handleFormSubmit = async (e: React.FormEvent) => { + e.preventDefault() + await handleSubmit(e) + } + + return ( +
+
+

Вход

+

Войдите в свой аккаунт

+
+
+ + emailField.handleChange(e as React.ChangeEvent) + } + onBlur={(e) => + emailField.handleBlur(e as React.FocusEvent) + } + autoComplete="email" + /> + + | React.ChangeEvent, + ) => void + } + onBlur={ + passwordField.handleBlur as ( + e: + | React.FocusEvent + | React.FocusEvent, + ) => void + } + autoComplete="current-password" + /> + {submissionError && ( +
{submissionError}
+ )} + + Вход + + +
+ ) +} + +export default Login diff --git a/src/components/Templates/Profile/Profile.tsx b/src/components/Templates/Profile/Profile.tsx index 9e0e4ff..a3e132b 100644 --- a/src/components/Templates/Profile/Profile.tsx +++ b/src/components/Templates/Profile/Profile.tsx @@ -12,7 +12,7 @@ const Profile: React.FC = ({ name }) => {
- Preview ITB Profile + Просмотреть профиль
diff --git a/src/components/Templates/Register/Register.tsx b/src/components/Templates/Register/Register.tsx new file mode 100644 index 0000000..392d357 --- /dev/null +++ b/src/components/Templates/Register/Register.tsx @@ -0,0 +1,148 @@ +'use client' + +import Atom from '@/components/atoms/index' +import { useAuthForm } from '@/hooks/useAuthForm' +import { useFormField } from '@/hooks/useFormField' +import styles from '@/styles/(auth)/Auth.module.css' + +const Register = () => { + const fullNameField = useFormField('') + const emailField = useFormField('') + const passwordField = useFormField('') + const confirmPasswordField = useFormField('') + + const formValues = { + fullName: fullNameField.value, + email: emailField.value, + password: passwordField.value, + confirmPassword: confirmPasswordField.value, + } + + const { + isFormValid, + isSubmitting, + submissionError, + handleSubmit, + emailError, + passwordError, + fullNameError, + confirmPasswordError, + } = useAuthForm('register', formValues) + + const handleFormSubmit = (e: React.FormEvent) => { + handleSubmit(e) + } + + return ( +
+
+

Регистрация

+

Создайте аккаунт

+
+
+ + | React.ChangeEvent, + ) => void + } + onBlur={ + fullNameField.handleBlur as ( + e: + | React.FocusEvent + | React.FocusEvent, + ) => void + } + autoComplete="name" + /> + + + emailField.handleChange(e as React.ChangeEvent) + } + onBlur={(e) => + emailField.handleBlur(e as React.FocusEvent) + } + autoComplete="email" + /> + + | React.ChangeEvent, + ) => void + } + onBlur={ + passwordField.handleBlur as ( + e: + | React.FocusEvent + | React.FocusEvent, + ) => void + } + autoComplete="new-password" + /> + + | React.ChangeEvent, + ) => void + } + onBlur={ + confirmPasswordField.handleBlur as ( + e: + | React.FocusEvent + | React.FocusEvent, + ) => void + } + autoComplete="new-password" + /> + {submissionError && ( + {submissionError} + )} + + Регистрация + + +
+ ) +} + +export default Register diff --git a/src/components/Templates/index.ts b/src/components/Templates/index.ts index db5fee8..47ccfb7 100644 --- a/src/components/Templates/index.ts +++ b/src/components/Templates/index.ts @@ -1,7 +1,11 @@ +import Login from './Login/Login' import Profile from './Profile/Profile' +import Register from './Register/Register' export const Template = { Profile, + Login, + Register, } export default Template diff --git a/src/components/atoms/Input/InputField.css b/src/components/atoms/Input/InputField.css new file mode 100644 index 0000000..10e2637 --- /dev/null +++ b/src/components/atoms/Input/InputField.css @@ -0,0 +1,25 @@ +.field label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; +} + +.field input { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + font-size: 1rem; + outline: none; + transition: border-color 0.3s; +} + +.field input:focus { + border-color: #2563eb; +} + +.field__error { + color: #f87171; + font-size: 0.875rem; + margin-top: 0.5rem; +} diff --git a/src/components/atoms/Input/InputField.tsx b/src/components/atoms/Input/InputField.tsx index fe18a9d..ec6e8f4 100644 --- a/src/components/atoms/Input/InputField.tsx +++ b/src/components/atoms/Input/InputField.tsx @@ -1,38 +1,79 @@ -import styles from '@/styles/(auth)/Auth.module.css' -import { - DEFAULT_AUTO_COMPLETE, - DEFAULT_REQUIRED, - DEFAULT_TYPE, -} from './constants' +import './InputField.css' import { InputFieldProps } from './interfaces' const InputField: React.FC = ({ id, label, - type = DEFAULT_TYPE, placeholder, value, - error, - touched, onChange, onBlur, - required = DEFAULT_REQUIRED, - autoComplete = DEFAULT_AUTO_COMPLETE, -}) => ( -
- - - {touched && error && {error}} -
-) + error, + touched, + type = 'text', + options = [], + autoComplete, +}) => { + const handleChange = ( + e: + | React.ChangeEvent + | React.ChangeEvent, + ) => { + if (type === 'select' && e.target instanceof HTMLSelectElement) { + onChange(e as React.ChangeEvent) + } else if (type !== 'select' && e.target instanceof HTMLInputElement) { + onChange(e as React.ChangeEvent) + } + } + + const handleBlur = ( + e: React.FocusEvent | React.FocusEvent, + ) => { + if (type === 'select' && e.target instanceof HTMLSelectElement) { + onBlur(e as React.FocusEvent) + } else if (type !== 'select' && e.target instanceof HTMLInputElement) { + onBlur(e as React.FocusEvent) + } + } + + if (type === 'select') { + return ( +
+ + + {error && touched && {error}} +
+ ) + } + + return ( +
+ + + {error && touched && {error}} +
+ ) +} export default InputField diff --git a/src/components/atoms/Input/interfaces.ts b/src/components/atoms/Input/interfaces.ts index cc7b32c..6076386 100644 --- a/src/components/atoms/Input/interfaces.ts +++ b/src/components/atoms/Input/interfaces.ts @@ -1,13 +1,19 @@ +import { ChangeEvent, FocusEvent } from 'react' + export interface InputFieldProps { id: string label: string - type?: string placeholder?: string value: string - error: string | null + onChange: ( + e: ChangeEvent | ChangeEvent, + ) => void + onBlur: ( + e: FocusEvent | FocusEvent, + ) => void + error?: string | null touched: boolean - onChange: (e: React.ChangeEvent) => void - onBlur: () => void - required?: boolean + type?: 'text' | 'password' | 'email' | 'select' | 'number' + options?: string[] autoComplete?: string } diff --git a/src/components/molecules/Profile/EditSection.tsx b/src/components/molecules/Profile/EditSection.tsx new file mode 100644 index 0000000..827633a --- /dev/null +++ b/src/components/molecules/Profile/EditSection.tsx @@ -0,0 +1,104 @@ +import Atom from '@/components/atoms' +import { handleFormKeyDown } from '@/utils/handleFormKeyDown' +import React from 'react' +import styles from './ProfileSegmentation.module.css' +import { EditSectionProps } from './interfaces' + +const languageLevels = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2'] + +const EditSection: React.FC = ({ + label, + items, + fieldNames, + fieldLabels, + stateSetter, + isEditMode, + onSave, + toggleEdit, +}) => { + const handleInputChange = ( + e: React.ChangeEvent, + index: number, + field: string, + ) => { + const value = e.target.value + stateSetter((prev) => { + const newState = [...prev] + newState[index] = { ...newState[index], [field]: value } + return newState + }) + } + + const renderInputFields = () => ( +
handleFormKeyDown(e, onSave)} + onSubmit={(e) => e.preventDefault()} + className={styles.edit__container} + > + {items.map((item, index) => ( +
+ {fieldNames.map((field, fieldIndex) => ( + handleInputChange(e, index, field)} + onBlur={() => {}} + error={null} + touched={false} + type={field === 'level' ? 'select' : 'text'} + options={field === 'level' ? languageLevels : []} + /> + ))} +
+ ))} + + Сохранить + +
+ ) + + const renderDisplayFields = () => { + const allFieldsFilled = items.every((item) => + fieldNames.every((field) => item[field] !== '' && item[field] !== 0), + ) + + return ( +
+ {allFieldsFilled ? ( + <> + {items.map((item, index) => ( +

+ {fieldNames + .map((field) => item[field]) + .filter((value) => value !== '' && value !== 0) + .join(' - ')} +

+ ))} + + Редактировать + + + ) : ( + <> +

Добавьте ваши {label.toLowerCase()}.

+ + Добавить + + + )} +
+ ) + } + + return ( +
+

{label}

+ {isEditMode ? renderInputFields() : renderDisplayFields()} +
+ ) +} + +export default EditSection diff --git a/src/components/molecules/Profile/ProfileHeader.tsx b/src/components/molecules/Profile/ProfileHeader.tsx index 8bad995..63f32df 100644 --- a/src/components/molecules/Profile/ProfileHeader.tsx +++ b/src/components/molecules/Profile/ProfileHeader.tsx @@ -12,7 +12,7 @@ export const ProfileHeader: React.FC = ({ name }) => {
{firstLetter}

{name}

- NEW + Новичок @daniil_smith
) diff --git a/src/components/molecules/Profile/ProfileInfoSection.tsx b/src/components/molecules/Profile/ProfileInfoSection.tsx index ae1624e..3ff98b7 100644 --- a/src/components/molecules/Profile/ProfileInfoSection.tsx +++ b/src/components/molecules/Profile/ProfileInfoSection.tsx @@ -4,12 +4,12 @@ export const ProfileInfoSection: React.FC = () => { return (
- From - Romania + Страна + Румыния
- Member since - Aug 2024 + Дата регистрации + Авг 2024
) diff --git a/src/components/molecules/Profile/ProfileSegmentation.tsx b/src/components/molecules/Profile/ProfileSegmentation.tsx index d14705e..9d0447b 100644 --- a/src/components/molecules/Profile/ProfileSegmentation.tsx +++ b/src/components/molecules/Profile/ProfileSegmentation.tsx @@ -1,11 +1,14 @@ 'use client' import Atom from '@/components/atoms' -import { useState } from 'react' +import React, { useState } from 'react' +import EditSection from './EditSection' import { Certification, Education, Language, Skill } from './interfaces' import styles from './ProfileSegmentation.module.css' export const ProfileSegmentation: React.FC = () => { + const currentYear = new Date().getFullYear() + const [isEditing, setIsEditing] = useState({ description: false, languages: false, @@ -16,172 +19,119 @@ export const ProfileSegmentation: React.FC = () => { const [description, setDescription] = useState('') const [languages, setLanguages] = useState([ - { name: 'English', level: 'Basic' }, + { name: '', level: '' }, + ]) + const [skills, setSkills] = useState([{ name: '', experience: '' }]) + const [education, setEducation] = useState([ + { country: '', university: '', title: '', major: '', year: currentYear }, + ]) + const [certification, setCertification] = useState([ + { certificate: '', certifiedFrom: '', year: currentYear }, ]) - const [skills, setSkills] = useState([]) - const [education, setEducation] = useState([]) - const [certification, setCertification] = useState([]) const toggleEdit = (field: keyof typeof isEditing) => { setIsEditing((prev) => ({ ...prev, [field]: !prev[field] })) } - const updateDescription = (e: React.ChangeEvent) => { - setDescription(e.target.value) - } + const saveDescription = () => toggleEdit('description') + const saveLanguages = () => toggleEdit('languages') + const saveSkills = () => toggleEdit('skills') + const saveEducation = () => toggleEdit('education') + const saveCertification = () => toggleEdit('certification') return ( <>
-

Description

+

Описание

{isEditing.description ? ( -
-