diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 0000000..e493205 --- /dev/null +++ b/.stylelintignore @@ -0,0 +1 @@ +app/globals.css diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 149d815..04fbae2 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,6 +1,6 @@ 'use client' -import Layout from '@/components/layout/index' +import Layout from '@/components/layout' import { useAuthForm } from '@/hooks/useAuthForm' import { useFormField } from '@/hooks/useFormField' import styles from '~/styles/Auth.module.css' @@ -23,21 +23,13 @@ const Login = () => { passwordError, } = useAuthForm('login', formValues) - const handleFormSubmit = (e: React.FormEvent) => { - handleSubmit(e) - } - return (

Вход

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

-
+ { diff --git a/app/head.tsx b/app/head.tsx index 9caa886..9c2d879 100644 --- a/app/head.tsx +++ b/app/head.tsx @@ -10,7 +10,7 @@ const seo = { 'Freelance, IT, Technology, Software Development, Web Development, UX/UI Design, IT Services, Remote Work, IT Jobs', robots: 'index, follow', canonical: 'https://itbrz.ru', - ogImage: '/og-image.png', + ogImage: '/opengraph-image.png', themeColor: '#007bff', language: 'ru', } @@ -46,7 +46,7 @@ export const RootHead = () => ( - + {/* */} diff --git a/app/profile/page.tsx b/app/profile/page.tsx new file mode 100644 index 0000000..27ea6f9 --- /dev/null +++ b/app/profile/page.tsx @@ -0,0 +1,54 @@ +import Layout from '@/components/layout' + +const ProfilePage = () => { + const mockProfileData = { + avatarUrl: 'https://via.placeholder.com/150', + name: 'Иван Иванов', + nickname: 'ivan_dev', + status: 'Доступен', + bio: 'Я опытный разработчик с более чем 5 годами опыта в разработке веб-приложений. Моя специализация - фронтенд разработка с использованием React и TypeScript.', + skills: [ + { name: 'JavaScript', level: 90 }, + { name: 'TypeScript', level: 85 }, + { name: 'React', level: 80 }, + { name: 'Next.js', level: 75 }, + { name: 'Tailwind CSS', level: 70 }, + ], + projects: [ + { + title: 'Проект 1', + description: 'Описание проекта 1', + link: '#', + image: 'https://via.placeholder.com/400', + }, + { + title: 'Проект 2', + description: 'Описание проекта 2', + link: '#', + image: 'https://via.placeholder.com/400', + }, + ], + reviews: [ + { + client: 'ООО "Компания"', + comment: 'Отличная работа! Быстро и качественно.', + rating: 5, + clientImage: 'https://via.placeholder.com/50', + }, + { + client: 'ИП "Клиент"', + comment: 'Хороший специалист, рекомендую!', + rating: 4, + clientImage: 'https://via.placeholder.com/50', + }, + ], + } + + return ( +
+ +
+ ) +} + +export default ProfilePage diff --git a/app/sitemap.xml b/app/sitemap.xml deleted file mode 100644 index 478b2a4..0000000 --- a/app/sitemap.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - https://it-birjha.ru/dashboard - daily - 1.0 - - - https://it-birjha.ru/register - monthly - 0.8 - - - https://it-birjha.ru/login - monthly - 0.8 - - \ No newline at end of file diff --git a/package.json b/package.json index b56660e..fb1deae 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "itb-front-next", + "na`m`e": "itb-front-next", "version": "0.1.0", "private": true, "type": "module", @@ -27,6 +27,7 @@ "dependencies": { "@reduxjs/toolkit": "^2.2.7", "js-cookie": "^3.0.5", + "lucide-react": "^0.427.0", "next": "14.2.5", "prettier": "^3.3.1", "react": "^18", @@ -51,7 +52,7 @@ "prettier": "3.3.3", "stylelint": "^16.8.1", "stylelint-config-standard": "^36.0.0", - "tailwindcss": "^3.4.1", + "tailwindcss": "^3.4.10", "ts-jest": "^29.2.4", "ts-node": "^10.9.2", "typescript": "^5" diff --git a/public/og-image.png b/public/og-image.png deleted file mode 100644 index 92399d7..0000000 Binary files a/public/og-image.png and /dev/null differ diff --git a/public/opengraph-image.png b/public/opengraph-image.png new file mode 100644 index 0000000..369a605 Binary files /dev/null and b/public/opengraph-image.png differ diff --git a/public/sitemap.xml b/public/sitemap.xml index be8ab5f..478b2a4 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1 +1,22 @@ -https://it-birjha.ru/dashboarddaily1.0https://it-birjha.ru/registermonthly0.8https://it-birjha.ru/loginmonthly0.8 \ No newline at end of file + + + + https://it-birjha.ru/dashboard + daily + 1.0 + + + https://it-birjha.ru/register + monthly + 0.8 + + + https://it-birjha.ru/login + monthly + 0.8 + + \ No newline at end of file diff --git a/src/components/layout/Header/Header.module.css b/src/components/layout/Header/Header.module.css index 684b557..5901adf 100644 --- a/src/components/layout/Header/Header.module.css +++ b/src/components/layout/Header/Header.module.css @@ -1,25 +1,8 @@ .header { - display: -webkit-box; display: flex; - -webkit-box-align: center; - -ms-flex-align: center; + justify-content: space-between; align-items: center; - height: 5em; - padding: 15px; -} - -.header__logo { - width: 100px; - height: 100%; - background-color: tomato; - margin-right: auto; -} - -.header__buttons { - display: -webkit-box; - display: flex; - -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; - gap: 30px; + padding: 10px 20px; + background-color: #fff; + border-bottom: 1px solid #ddd; } diff --git a/src/components/layout/Header/Header.tsx b/src/components/layout/Header/Header.tsx index ababffa..a1e8704 100644 --- a/src/components/layout/Header/Header.tsx +++ b/src/components/layout/Header/Header.tsx @@ -1,33 +1,47 @@ 'use client' +import { initializeAuth } from '@/redux/slices/authSlice' import { RootState } from '@/redux/store' +import Cookies from 'js-cookie' import Link from 'next/link' -import { useSelector } from 'react-redux' +import { useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux' import styles from './Header.module.css' export const Header = () => { + const dispatch = useDispatch() const isLoggedIn = useSelector((state: RootState) => state.auth.isLoggedIn) + // Убедитесь, что состояние инициализируется один раз + useEffect(() => { + const token = Cookies.get('token') + const loggedIn = localStorage.getItem('isLoggedIn') === 'true' + dispatch(initializeAuth()) + + // Если есть токен или состояние в localStorage указывает на авторизацию + if (token || loggedIn) { + dispatch(initializeAuth()) + } + }, [dispatch]) + + // Прямое использование состояния isLoggedIn return (
-
- +
    +
  • + Сделать заказ +
  • + {!isLoggedIn && ( + <> +
  • + Вход +
  • +
  • + Регистрация +
  • + + )} +
) } diff --git a/src/components/layout/Profile/Profile.d.ts b/src/components/layout/Profile/Profile.d.ts new file mode 100644 index 0000000..f3a22f5 --- /dev/null +++ b/src/components/layout/Profile/Profile.d.ts @@ -0,0 +1,29 @@ +export interface Skill { + name: string + level: number +} + +export interface Project { + title: string + description: string + link: string + image: string +} + +export interface Review { + client: string + comment: string + rating: number + clientImage: string +} + +export interface ProfileProps { + avatarUrl: string + name: string + nickname: string + status: string + bio: string + skills: Skill[] + projects: Project[] + reviews: Review[] +} diff --git a/src/components/layout/Profile/Profile.module.css b/src/components/layout/Profile/Profile.module.css new file mode 100644 index 0000000..6aed4d0 --- /dev/null +++ b/src/components/layout/Profile/Profile.module.css @@ -0,0 +1,79 @@ +.container { + background-color: #f9fafb; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0 0 0 10%); +} + +.header { + display: flex; + align-items: center; + margin-bottom: 1.5rem; +} + +.avatar { + width: 80px; + height: 80px; + border-radius: 50%; + margin-right: 1rem; +} + +.info { + flex-grow: 1; +} + +.name { + font-size: 1.75rem; + font-weight: bold; +} + +.nickname, +.status { + font-size: 1rem; + color: #6b7280; +} + +.section { + margin-top: 2rem; +} + +.skill__item { + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; +} + +.skillbar__container { + width: 60%; + background-color: #e5e7eb; + border-radius: 0.25rem; + height: 0.75rem; +} + +.skillbar { + background-color: #3b82f6; + height: 100%; + border-radius: 0.25rem; +} + +.project__card { + background-color: #fff; + border-radius: 8px; + padding: 1rem; + box-shadow: 0 1px 3px rgba(0 0 0 10%); + max-width: 250px; + margin-bottom: 1rem; +} + +.project__image { + width: 100%; + border-radius: 6px; + margin-bottom: 0.75rem; +} + +.review__item { + background-color: #fff; + border-radius: 8px; + padding: 1rem; + box-shadow: 0 1px 3px rgba(0 0 0 10%); +} diff --git a/src/components/layout/Profile/Profile.tsx b/src/components/layout/Profile/Profile.tsx new file mode 100644 index 0000000..9d3d08c --- /dev/null +++ b/src/components/layout/Profile/Profile.tsx @@ -0,0 +1,101 @@ +'use client' + +import { useEffect } from 'react' +import { ProfileProps } from './Profile.d' +import styles from './Profile.module.css' + +export const Profile: React.FC = ({ + avatarUrl, + name, + nickname, + status, + bio, + skills, + projects, + reviews, +}) => { + useEffect(() => { + const skillbars = document.querySelectorAll(`.${styles.skillbar}`) + skillbars.forEach((bar) => { + const width = bar.getAttribute('data-width') + if (width) { + setTimeout(() => { + ;(bar as HTMLElement).style.width = width + }, 100) + } + }) + }, []) + + return ( +
+
+ {`${name}'s +
+

{name}

+

@{nickname}

+

{status}

+
+
+ +
+

Обо мне

+

{bio}

+
+ +
+

Навыки

+
    + {skills.map((skill) => ( +
  • + {skill.name} +
    +
    +
    +
  • + ))} +
+
+ +
+

Проекты

+
+ {projects.map((project) => ( +
+ {project.title} +

{project.title}

+

{project.description}

+ + Подробнее + +
+ ))} +
+
+ +
+

Отзывы

+
+ {reviews.map((review, index) => ( +
+ {review.client} +

{review.client}

+

{review.comment}

+

Рейтинг: {review.rating} / 5

+
+ ))} +
+
+
+ ) +} diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts index 49a10ce..bcfe7cb 100644 --- a/src/components/layout/index.ts +++ b/src/components/layout/index.ts @@ -1,9 +1,11 @@ import { Header } from './Header/Header' import { InputField } from './InputField/InputField' +import { Profile } from './Profile/Profile' export const Layout = { Header, InputField, + Profile, } export default Layout diff --git a/src/hooks/useAuthForm.ts b/src/hooks/useAuthForm.ts index 4fef63f..2fd7074 100644 --- a/src/hooks/useAuthForm.ts +++ b/src/hooks/useAuthForm.ts @@ -1,9 +1,11 @@ 'use client' import { useValidation } from '@/hooks/useValidation' +import { login } from '@/redux/slices/authSlice' import { authService, AuthType } from '@/services/authService' import { useRouter } from 'next/navigation' import { useEffect, useState } from 'react' +import { useDispatch } from 'react-redux' export const useAuthForm = ( authType: AuthType, @@ -14,8 +16,11 @@ export const useAuthForm = ( const [submissionError, setSubmissionError] = useState('') const [isSubmitted, setIsSubmitted] = useState(false) + const dispatch = useDispatch() const router = useRouter() + const isRegistration = authType === 'register' + const { validateForm, emailError, @@ -23,15 +28,16 @@ export const useAuthForm = ( fullNameError, confirmPasswordError, } = useValidation( - formValues.email || '', - formValues.password || '', - formValues.fullName || '', - formValues.confirmPassword || '', + formValues.email, + formValues.password, + formValues.fullName, + formValues.confirmPassword, + isRegistration, ) useEffect(() => { setIsFormValid(validateForm()) - }, [validateForm]) + }, [validateForm]) // Используем validateForm как зависимость const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -42,7 +48,8 @@ export const useAuthForm = ( setSubmissionError('') try { - await authService(authType, formValues) + const data = await authService(authType, formValues) + dispatch(login()) setIsSubmitted(true) router.push('/dashboard') } catch (error) { diff --git a/src/hooks/useValidation.ts b/src/hooks/useValidation.ts index a2a1ec9..7567be0 100644 --- a/src/hooks/useValidation.ts +++ b/src/hooks/useValidation.ts @@ -1,17 +1,17 @@ -import { useState } from 'react' - import { validateConfirmPassword, validateEmail, validateFullName, validatePassword, } from '@/utils/validation' +import { useCallback, useState } from 'react' export const useValidation = ( email: string, password: string, fullName?: string, confirmPassword?: string, + isRegistration: boolean = false, ) => { const [emailError, setEmailError] = useState(null) const [passwordError, setPasswordError] = useState(null) @@ -20,42 +20,61 @@ export const useValidation = ( string | null >(null) - const validateEmailField = () => { - setEmailError(validateEmail(email)) - } + const validateEmailField = useCallback((): boolean => { + const error = validateEmail(email) + setEmailError(error) + return !error + }, [email]) - const validatePasswordField = () => { - setPasswordError(validatePassword(password)) - } + const validatePasswordField = useCallback((): boolean => { + const error = validatePassword(password) + setPasswordError(error) + return !error + }, [password]) - const validateFullNameField = () => { + const validateFullNameField = useCallback((): boolean => { if (fullName) { - setFullNameError(validateFullName(fullName)) + const error = validateFullName(fullName) + setFullNameError(error) + return !error } else { setFullNameError(null) + return true } - } + }, [fullName]) - const validateConfirmPasswordField = () => { + const validateConfirmPasswordField = useCallback((): boolean => { if (confirmPassword !== undefined && password) { - setConfirmPasswordError( - validateConfirmPassword(password, confirmPassword), - ) + const error = validateConfirmPassword(password, confirmPassword) + setConfirmPasswordError(error) + return !error } else { setConfirmPasswordError(null) + return true } - } + }, [confirmPassword, password]) - const validateForm = () => { - validateEmailField() - validatePasswordField() - validateFullNameField() - validateConfirmPasswordField() + const validateForm = useCallback((): boolean => { + const isEmailValid = validateEmailField() + const isPasswordValid = validatePasswordField() + const isFullNameValid = isRegistration ? validateFullNameField() : true + const isConfirmPasswordValid = isRegistration + ? validateConfirmPasswordField() + : true return ( - !emailError && !passwordError && !fullNameError && !confirmPasswordError + isEmailValid && + isPasswordValid && + isFullNameValid && + isConfirmPasswordValid ) - } + }, [ + validateEmailField, + validatePasswordField, + validateFullNameField, + validateConfirmPasswordField, + isRegistration, + ]) return { validateForm, diff --git a/src/redux/slices/authSlice.ts b/src/redux/slices/authSlice.ts index e0be124..ef9936b 100644 --- a/src/redux/slices/authSlice.ts +++ b/src/redux/slices/authSlice.ts @@ -1,13 +1,12 @@ import { createSlice } from '@reduxjs/toolkit' +import Cookies from 'js-cookie' interface AuthState { isLoggedIn: boolean } const initialState: AuthState = { - isLoggedIn: - typeof window !== 'undefined' && - localStorage.getItem('isLoggedIn') === 'true', + isLoggedIn: false, } const authSlice = createSlice({ @@ -16,13 +15,24 @@ const authSlice = createSlice({ reducers: { login: (state) => { state.isLoggedIn = true - if (typeof window !== 'undefined') { - localStorage.setItem('isLoggedIn', 'true') - } + localStorage.setItem('isLoggedIn', 'true') + Cookies.set('token', 'your-token', { + expires: 7, + secure: true, + sameSite: 'strict', + }) + }, + logout: (state) => { + state.isLoggedIn = false + localStorage.removeItem('isLoggedIn') + Cookies.remove('token') + }, + initializeAuth: (state) => { + const loggedIn = localStorage.getItem('isLoggedIn') === 'true' + state.isLoggedIn = loggedIn }, }, }) -export const { login } = authSlice.actions - +export const { login, logout, initializeAuth } = authSlice.actions export default authSlice.reducer diff --git a/src/redux/store.ts b/src/redux/store.ts index 2668274..02eb6af 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -1,15 +1,14 @@ import { configureStore } from '@reduxjs/toolkit' -import authReducer from './slices/authSlice' - -const preloadedState = {} +import authReducer, { initializeAuth } from './slices/authSlice' const store = configureStore({ reducer: { auth: authReducer, }, - preloadedState, }) +store.dispatch(initializeAuth()) + export type RootState = ReturnType export type AppDispatch = typeof store.dispatch diff --git a/src/services/authService.ts b/src/services/authService.ts index bd2499f..3eae973 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -22,6 +22,7 @@ export const authService = async ( } const data = await response.json() + // Сохранение токена в cookie Cookies.set('token', data.token, { expires: 7, secure: true, diff --git a/tailwind.config.ts b/tailwind.config.ts index 1af3b8f..a44e157 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -2,9 +2,8 @@ import type { Config } from 'tailwindcss' const config: Config = { content: [ - './src/pages/**/*.{js,ts,jsx,tsx,mdx}', './src/components/**/*.{js,ts,jsx,tsx,mdx}', - './src/app/**/*.{js,ts,jsx,tsx,mdx}', + './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { @@ -17,4 +16,5 @@ const config: Config = { }, plugins: [], } + export default config