diff --git a/public/images/artists/KUUMAA.jpeg b/public/images/artists/KUUMA.jpeg similarity index 100% rename from public/images/artists/KUUMAA.jpeg rename to public/images/artists/KUUMA.jpeg diff --git a/public/images/artists/Mirella.jpeg b/public/images/artists/Mirella.jpg similarity index 100% rename from public/images/artists/Mirella.jpeg rename to public/images/artists/Mirella.jpg diff --git a/public/images/artists/Robin_Packalen.jpeg b/public/images/artists/RobinPackalen.jpeg similarity index 100% rename from public/images/artists/Robin_Packalen.jpeg rename to public/images/artists/RobinPackalen.jpeg diff --git a/public/index.html b/public/index.html index dc0fb7a..5d89dc9 100644 --- a/public/index.html +++ b/public/index.html @@ -17,6 +17,10 @@ + + + + diff --git a/src/app/routes.js b/src/app/routes.js index b4f0688..3a12a0d 100644 --- a/src/app/routes.js +++ b/src/app/routes.js @@ -1,22 +1,24 @@ -import { FeedPage } from '../pages/feed/index.js'; -// import { SignInPage, RegisterPage } from 'pages/sign-in'; +import { Header } from "../widgets/header/index.js"; +import { FeedPage } from "../pages/feed/index.js"; +import { SignUpPage } from "../pages/sign-up/index.js"; +import { SignInPage } from "../pages/sign-in/index.js"; -export const LAYOUT = []; +export const LAYOUT = [Header]; /** * Define pages and their views */ export const PAGES = [ - { - path: '/', - view: FeedPage, - }, - // { - // path: '/signin', - // view: SignInPage, - // }, - // { - // path: '/register', - // view: RegisterPage, - // }, + { + path: "/", + view: FeedPage, + }, + { + path: "/signin", + view: SignInPage, + }, + { + path: "/signup", + view: SignUpPage, + }, ]; diff --git a/src/entities/user/api/api.js b/src/entities/user/api/api.js new file mode 100644 index 0000000..6df1b8d --- /dev/null +++ b/src/entities/user/api/api.js @@ -0,0 +1,32 @@ +import { API_ENDPOINTS } from "../../../shared/lib/index.js"; +import { POST } from "../../../shared/api/index.js"; + +/** + * Sends sign in request to the server using user data + * + * @param {Object} user - user data (username and password) + * @returns {Promise} response from the server + */ +export const signInRequest = async (user) => { + return await POST(API_ENDPOINTS.SIGN_IN, { body: user }); +}; + +/** + * Sends sign up request to server using user data + * + * @param {Object} user - user data to register (email, username, password) + * @returns {Promise} response from the server + */ +export const signUpRequest = async (user) => { + return await POST(API_ENDPOINTS.SIGN_UP, { body: user }); +}; + +/** + * Sends sign out request to server using user data + * + * @returns {Promise} response from the server + */ +export const signOutRequest = async () => { + return await POST(API_ENDPOINTS.SIGN_OUT); +}; + diff --git a/src/entities/user/index.js b/src/entities/user/index.js new file mode 100644 index 0000000..ba588d5 --- /dev/null +++ b/src/entities/user/index.js @@ -0,0 +1 @@ +export { signInRequest, signUpRequest, signOutRequest } from './api/api.js'; diff --git a/src/entities/user/model/store.js b/src/entities/user/model/store.js new file mode 100644 index 0000000..c664ca9 --- /dev/null +++ b/src/entities/user/model/store.js @@ -0,0 +1,123 @@ +import { STATUS_CODES } from "../../../shared/lib/index.js"; +import { eventBus } from "../../../shared/lib/index.js"; +import { signInRequest, signUpRequest, signOutRequest } from "../api/api.js"; + +class UserStore { + constructor() { + this.storage = { + user: this.getUser() || {}, + error: null, + }; + } + + getUser() { + const user = localStorage.getItem('user'); + return user ? JSON.parse(user) : null; + } + + saveUser(user) { + localStorage.setItem('user', JSON.stringify(user)); + } + + removeUser() { + localStorage.removeItem('user'); + } + + signIn = async (user) => { + try { + const response = await signInRequest(user); + + switch (response.status) { + case STATUS_CODES.OK: + const userData = { + username: response.data.user.username, + email: response.data.user.email, + token: response.data.token, + isAuthorized: true, + }; + this.storage.user = userData; + this.saveUser(userData); + this.storage.error = null; + eventBus.emit('signInSuccess', this.storage.user); + break; + + case STATUS_CODES.UNAUTHORIZED: + this.storage.user = { + isAuthorized: false, + }; + this.storage.error = response.error; + eventBus.emit('signInError', this.storage.error); + break; + + default: + console.error('undefined status code:', response.status); + } + } catch (error) { + this.storage.error = error; + eventBus.emit('signInError', error); + console.error('unable to sign in: ', error); + } + }; + + + signUp = async (user) => { + try { + const response = await signUpRequest(user); + + switch (response.status) { + case STATUS_CODES.OK: + const userData = { + username: response.data.user.username, + email: response.data.user.email, + token: response.data.token, + isAuthorized: true, + }; + this.storage.user = userData; + this.saveUser(userData); + this.storage.error = null; + eventBus.emit('signUpSuccess', this.storage.user); + break; + + case STATUS_CODES.UNAUTHORIZED: + this.storage.user = { + isAuthorized: false, + }; + this.storage.error = response.error; + eventBus.emit('signUpError', this.storage.error); + break; + + default: + console.error('undefined status code:', response.status); + } + } catch (error) { + this.storage.error = error; + eventBus.emit('signUpError', error); + console.error('unable to sign up: ', error); + } + }; + + + signOut = async () => { + try { + const response = await signOutRequest(); + + switch (response.status) { + case STATUS_CODES.OK: + this.storage.user = { isAuthorized: false }; + this.removeUser(); + this.storage.error = null; + eventBus.emit('signOutSuccess'); + break; + + default: + console.error('undefined status code:', response.status); + } + } catch (error) { + this.storage.error = error; + eventBus.emit('signOutError', error); + console.error('unable to connect to server: ', error); + } + }; +}; + +export const userStore = new UserStore(); \ No newline at end of file diff --git a/src/index.css b/src/index.css index 04c2bcd..a6a3c6b 100644 --- a/src/index.css +++ b/src/index.css @@ -3,12 +3,9 @@ @import './widgets/player/ui/player.css'; @import './widgets/trackList/ui/trackList.css'; @import './widgets/artistList/ui/artistList.css'; - -body { - font-family: 'Sansation', sans-serif; - margin: 0; - padding: 0; -} +@import './widgets/header/ui/Header.css'; +@import './pages/sign-up/ui/SignUp.css'; +@import './pages/sign-in/ui/SignIn.css'; h1, h2, @@ -50,6 +47,9 @@ body { rgba(168, 72, 10, 1) 0%, rgba(44, 25, 17, 1) 100%); background-attachment: fixed; + font-family: 'Sansation', sans-serif; + margin: 0; + padding: 0; } .message-success { diff --git a/src/pages/error/index.js b/src/pages/error/index.js index ed6ad04..7ff5c8d 100644 --- a/src/pages/error/index.js +++ b/src/pages/error/index.js @@ -1 +1 @@ -export { ErrorPage } from './ui/ErrorPage.js'; +export { ErrorPage } from './ui/Error.js'; diff --git a/src/pages/error/ui/Error.css b/src/pages/error/ui/Error.css new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/error/ui/Error.hbs b/src/pages/error/ui/Error.hbs new file mode 100644 index 0000000..81df8e4 --- /dev/null +++ b/src/pages/error/ui/Error.hbs @@ -0,0 +1,4 @@ +
+
Ошибка
+
{{message}}
+
diff --git a/src/pages/error/ui/Error.js b/src/pages/error/ui/Error.js new file mode 100644 index 0000000..81cbeb3 --- /dev/null +++ b/src/pages/error/ui/Error.js @@ -0,0 +1,12 @@ +import { eventBus } from "../../../shared/lib/eventbus.js"; + +export class ErrorPage { + constructor() { + this.root = document.querySelector("#root"); + } + + render(message = "Что-то пошло не так") { + const template = Handlebars.templates["error.hbs"]; + this.root.innerHTML = template({ message }); + } + } \ No newline at end of file diff --git a/src/pages/error/ui/ErrorPage.js b/src/pages/error/ui/ErrorPage.js deleted file mode 100644 index 12ce83b..0000000 --- a/src/pages/error/ui/ErrorPage.js +++ /dev/null @@ -1,5 +0,0 @@ -import { Page } from '../../../shared/ui/Page.js'; - -export class ErrorPage extends Page { - // ... -} diff --git a/src/pages/sign-in/api/register.js b/src/pages/sign-in/api/register.js deleted file mode 100644 index 7d2e4c7..0000000 --- a/src/pages/sign-in/api/register.js +++ /dev/null @@ -1,6 +0,0 @@ -import { POST } from "shared/api"; - -export const register = async ({ request }) => { - // ... -}; - diff --git a/src/pages/sign-in/api/sign-in.js b/src/pages/sign-in/api/sign-in.js deleted file mode 100644 index c34ae93..0000000 --- a/src/pages/sign-in/api/sign-in.js +++ /dev/null @@ -1,6 +0,0 @@ -import { POST } from "shared/api"; - -export const signIn = async ({ request }) => { - // ... -}; - diff --git a/src/pages/sign-in/index.js b/src/pages/sign-in/index.js index dc69461..de42ae4 100644 --- a/src/pages/sign-in/index.js +++ b/src/pages/sign-in/index.js @@ -1,4 +1,2 @@ -export { RegisterPage } from "./ui/RegisterPage"; -export { register } from "./api/register"; -export { SignInPage } from "./ui/SignInPage"; -export { signIn } from "./api/sign-in"; +export { SignInPage } from "./ui/SignIn.js"; + diff --git a/src/pages/sign-in/ui/RegisterPage.js b/src/pages/sign-in/ui/RegisterPage.js deleted file mode 100644 index ee5c4ed..0000000 --- a/src/pages/sign-in/ui/RegisterPage.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Page } from "shared/ui"; - -import { register } from "../api/register"; - -export class RegisterPage extends Page { - // ... -} - diff --git a/src/pages/sign-in/ui/SignIn.css b/src/pages/sign-in/ui/SignIn.css new file mode 100644 index 0000000..2651f17 --- /dev/null +++ b/src/pages/sign-in/ui/SignIn.css @@ -0,0 +1,115 @@ +.login { + height: 100%; +} + +.login__container { + max-width: 1280px; + margin: 0 auto; +} + +.login__intro { + min-height: 142px; + margin: 0 auto; + margin-top: 31px; + background-color: rgba(241.39, 241.39, 241.39, 0.1); + border-radius: 9px; + width: 80%; +} + +.login__title { + padding-top: 41px; + color: #f1f1f1; + font-size: 25px; + font-weight: 400; + text-align: center; +} + +.login__subtitle { + margin-top: 5px; + color: #f1f1f1; + font-size: 16px; + font-weight: 400; + text-align: center; +} + +.login__info { + margin-bottom: 35px; + color: #f1f1f1; + font-size: 10px; + font-weight: 400; + text-align: center; +} + +.login__form { + margin-top: 71px; + display: flex; + justify-content: center; +} + +.login__input { + display: block; + width: 430px; + height: 30px; + padding: 10px; + margin-top: 30px; + background: #f1f1f1; + border-radius: 9px; + border: none; + color: #1f1f1f; + font-size: 22px; + font-weight: 400; +} + +.login__input:first-child { + margin-top: 0px; +} + +.login__submit_btn { + margin-top: 28px; + width: 450px; + height: 50px; + padding: 10px; + background: #ff2d55; + border-radius: 9px; + border: none; + color: #f1f1f1; + font-size: 24px; + font-weight: 400; + text-align: center; +} + +.login__remember_me { + display: flex; + justify-content: center; + align-items: center; +} + +.login__checkbox { + width: 1.3em; + height: 1.3em; + margin-right: 5px; + accent-color: #ff2d55; +} + +.login__additional { + margin-top: 14px; + padding: 0 30px 0 30px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.login__remember { + color: #f1f1f1; + font-size: 16px; + font-weight: 400; +} + +.login__forget_password { + display: block; + color: #f1f1f1; + font-size: 16px; + font-weight: 400; + text-decoration: none; +} + diff --git a/src/pages/sign-in/ui/SignIn.hbs b/src/pages/sign-in/ui/SignIn.hbs new file mode 100644 index 0000000..e3bfc71 --- /dev/null +++ b/src/pages/sign-in/ui/SignIn.hbs @@ -0,0 +1,43 @@ + diff --git a/src/pages/sign-in/ui/SignIn.js b/src/pages/sign-in/ui/SignIn.js new file mode 100644 index 0000000..39a40f7 --- /dev/null +++ b/src/pages/sign-in/ui/SignIn.js @@ -0,0 +1,78 @@ +import { eventBus } from "../../../shared/lib/eventbus.js"; +import { userStore } from "../../../entities/user/model/store.js"; + +export class SignInPage { + parent; + + constructor() { + this.parent = document.querySelector("#root"); + + this.handleSubmit = this.handleSubmit.bind(this); + this.handleSignInSuccess = this.handleSignInSuccess.bind(this); + this.handleSignInError = this.handleSignInError.bind(this); + } + + render() { + const template = Handlebars.templates["SignIn.hbs"]; + this.parent.innerHTML = template(); + this.bindEvents(); + this.onEvents(); + } + + bindEvents() { + const form = document.querySelector("#login-form"); + if (form) { + form.addEventListener("submit", this.handleSubmit); + } + } + + onEvents() { + eventBus.on("signInSuccess", this.handleSignInSuccess); + eventBus.on("signInError", this.handleSignInError); + } + + offEvents() { + eventBus.off("signInSuccess", this.handleSignInSuccess); + eventBus.off("signInError", this.handleSignInError); + } + + async handleSubmit(event) { + event.preventDefault(); + + const username = document.querySelector("#username").value; + const password = document.querySelector("#password").value; + + const user = { username, password }; + + if (!username || !password) { + this.showMessage("All fields are required.", "error"); + return; + } + + await userStore.signIn(user); + } + + handleSignInSuccess() { + eventBus.emit("navigate", "/"); + } + + handleSignInError(error) { + this.showMessage(`Error: ${error.message || error}`, "error"); + } + + showMessage(message, type) { + const messageBox = document.querySelector("#message-box"); + if (messageBox) { + messageBox.textContent = message; + messageBox.className = `message-box ${type}`; + } + } + + destructor() { + this.offEvents(); + const form = document.querySelector("#login-form"); + if (form) { + form.removeEventListener("submit", this.handleSubmit); + } + } +} diff --git a/src/pages/sign-in/ui/SignInPage.js b/src/pages/sign-in/ui/SignInPage.js deleted file mode 100644 index b3035f6..0000000 --- a/src/pages/sign-in/ui/SignInPage.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Page } from "shared/ui"; - -import { signIn } from "../api/sign-in"; - -export class SignInPage extends Page { - // ... -} - diff --git a/src/pages/sign-up/index.js b/src/pages/sign-up/index.js new file mode 100644 index 0000000..08a3908 --- /dev/null +++ b/src/pages/sign-up/index.js @@ -0,0 +1,2 @@ +export { SignUpPage } from "./ui/SignUp.js"; + diff --git a/src/pages/sign-up/ui/SignUp.css b/src/pages/sign-up/ui/SignUp.css new file mode 100644 index 0000000..d81349f --- /dev/null +++ b/src/pages/sign-up/ui/SignUp.css @@ -0,0 +1,98 @@ +.register { + height: 100%; +} + +.register__container { + max-width: 1280px; + margin: 0 auto; +} + +.register__intro { + min-height: 142px; + margin: 0 auto; + margin-top: 31px; + background-color: rgba(241.39, 241.39, 241.39, 0.1); + border-radius: 9px; + width: 80%; +} + +.register__title { + padding-top: 41px; + color: #f1f1f1; + font-size: 25px; + font-weight: 400; + text-align: center; +} + +.register__subtitle { + margin-top: 5px; + color: #f1f1f1; + font-size: 16px; + font-weight: 400; + text-align: center; +} + +.register__info { + margin-bottom: 35px; + color: #f1f1f1; + font-size: 10px; + font-weight: 400; + text-align: center; +} + +.register__form { + margin-top: 71px; + display: flex; + justify-content: center; +} + +.register__input { + display: block; + width: 430px; + height: 30px; + padding: 10px; + margin-top: 30px; + background: #f1f1f1; + border-radius: 9px; + border: none; + color: #1f1f1f; + font-size: 22px; + font-weight: 400; +} + +.register__input:first-child { + margin-top: 0px; +} + +.register__submit_btn { + margin-top: 28px; + width: 450px; + height: 50px; + padding: 10px; + background: #ff2d55; + border-radius: 9px; + border: none; + color: #f1f1f1; + font-size: 24px; + font-weight: 400; + text-align: center; +} + +.register__additional { + margin-left: 77px; + margin-top: 13px; + color: #f1f1f1; + font-size: 16px; + font-weight: 400; +} + +.register__enter_account { + margin-left: 23px; + text-decoration: none; + font-weight: 700; +} + +.register__enter_account:visited { + color: #f1f1f1; +} + diff --git a/src/pages/sign-up/ui/SignUp.hbs b/src/pages/sign-up/ui/SignUp.hbs new file mode 100644 index 0000000..54a27fd --- /dev/null +++ b/src/pages/sign-up/ui/SignUp.hbs @@ -0,0 +1,23 @@ +
+
+
+

NovaMusic

+

Музакальный стриминговый

+
Слушайте музыку, создавайте плейлисты с друзьями, узнавайте первыми о новинках
+
+
+ +
+
+ + + + +
+ Уже есть аккаунт? +
+
+
+
+
+ diff --git a/src/pages/sign-up/ui/SignUp.js b/src/pages/sign-up/ui/SignUp.js new file mode 100644 index 0000000..56ddbd3 --- /dev/null +++ b/src/pages/sign-up/ui/SignUp.js @@ -0,0 +1,79 @@ +import { eventBus } from "../../../shared/lib/eventbus.js"; +import { userStore } from "../../../entities/user/model/store.js"; + +export class SignUpPage { + parent; + + constructor() { + this.parent = document.querySelector("#root"); + + this.handleSubmit = this.handleSubmit.bind(this); + this.handleSignUpSuccess = this.handleSignUpSuccess.bind(this); + this.handleSignUpError = this.handleSignUpError.bind(this); + } + + render() { + const template = Handlebars.templates["SignUp.hbs"]; + this.parent.innerHTML = template(); + this.bindEvents(); + this.onEvents(); + } + + bindEvents() { + const form = document.querySelector("#signup-form"); + if (form) { + form.addEventListener("submit", this.handleSubmit); + } + } + + onEvents() { + eventBus.on("signUpSuccess", this.handleSignUpSuccess); + eventBus.on("signUpError", this.handleSignUpError); + } + + offEvents() { + eventBus.off("signUpSuccess", this.handleSignUpSuccess); + eventBus.off("signUpError", this.handleSignUpError); + } + + async handleSubmit(event) { + event.preventDefault(); + + const email = document.querySelector("#email").value; + const username = document.querySelector("#username").value; + const password = document.querySelector("#password").value; + + const user = { email, username, password }; + + if (!email || !username || !password) { + this.showMessage("All fields are required.", "error"); + return; + } + + await userStore.signUp(user); + } + + handleSignUpSuccess() { + eventBus.emit("navigate", "/"); + } + + handleSignUpError(error) { + this.showMessage(`Error: ${error.message || error}`, "error"); + } + + showMessage(message, type) { + const messageBox = document.querySelector("#message-box"); + if (messageBox) { + messageBox.textContent = message; + messageBox.className = `message-box ${type}`; + } + } + + destructor() { + this.offEvents(); + const form = document.querySelector("#signup-form"); + if (form) { + form.removeEventListener("submit", this.handleSubmit); + } + } +} diff --git a/src/shared/api/client.js b/src/shared/api/client.js index e7b3425..db6d314 100644 --- a/src/shared/api/client.js +++ b/src/shared/api/client.js @@ -1,8 +1,8 @@ const HTTP_METHODS = { - GET: 'GET', - POST: 'POST', - PUT: 'PUT', - DELETE: 'DELETE', + GET: "GET", + POST: "POST", + PUT: "PUT", + DELETE: "DELETE", }; /** @@ -13,37 +13,41 @@ const HTTP_METHODS = { * @returns {Object} - The response containing data or error. */ const request = async (method, url, options = {}) => { - const { body = null, headers = {} } = options; + const { body = null, headers = {} } = options; - const requestOptions = { - method, - credentials: 'include', - headers: { - 'Content-Type': 'application/json; charset=utf-8', - ...headers, - }, - body: body ? JSON.stringify(body) : null, - }; + const requestOptions = { + method, + credentials: "include", + headers: { + "Content-Type": "application/json; charset=utf-8", + ...headers, + }, + body: body ? JSON.stringify(body) : null, + }; - try { - const response = await fetch(url, requestOptions); + try { + const response = await fetch(url, requestOptions); - let data = null; - try { - data = await response.json(); - } catch (e) { - data = null; - } + let data = null; + try { + data = await response.json(); + } catch (e) { + data = null; + } - if (!response.ok) { - return { data: null, error: data || { message: 'Unknown error' } }; - } + if (!response.ok) { + return { + status: response.status, + data: null, + error: data || { message: "unknown error" }, + }; + } - return { data, error: null }; - } catch (err) { - console.error('request failed:', err); - return { data: null, error: { message: 'Server error' } }; - } + return { status: response.status, data, error: null }; + } catch (err) { + console.error("request failed:", err); + return { status: 500, data: null, error: { message: "server error" } }; + } }; /** @@ -53,7 +57,7 @@ const request = async (method, url, options = {}) => { * @returns {Object} - The response containing data or error. */ export const GET = async (url, options = {}) => - request(HTTP_METHODS.GET, url, options); + request(HTTP_METHODS.GET, url, options); /** * Perform POST request. @@ -62,7 +66,7 @@ export const GET = async (url, options = {}) => * @returns {Object} - The response containing data or error. */ export const POST = async (url, options = {}) => - request(HTTP_METHODS.POST, url, options); + request(HTTP_METHODS.POST, url, options); /** * Perform PUT request. @@ -71,7 +75,7 @@ export const POST = async (url, options = {}) => * @returns {Object} - The response containing data or error. */ export const PUT = async (url, options = {}) => - request(HTTP_METHODS.PUT, url, options); + request(HTTP_METHODS.PUT, url, options); /** * Perform DELETE request. @@ -80,4 +84,4 @@ export const PUT = async (url, options = {}) => * @returns {Object} - The response containing data or error. */ export const DELETE = async (url, options = {}) => - request(HTTP_METHODS.DELETE, url, options); + request(HTTP_METHODS.DELETE, url, options); diff --git a/src/shared/config/backend.js b/src/shared/config/api.js similarity index 100% rename from src/shared/config/backend.js rename to src/shared/config/api.js diff --git a/src/shared/config/index.js b/src/shared/config/index.js index 83ae3bb..33b3682 100644 --- a/src/shared/config/index.js +++ b/src/shared/config/index.js @@ -1 +1 @@ -export { API_URL } from './backend.js'; +export { API_URL } from './api.js'; diff --git a/src/shared/header/index.js b/src/shared/header/index.js deleted file mode 100644 index 26b1e57..0000000 --- a/src/shared/header/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Header } from "./ui/Header"; diff --git a/src/shared/header/ui/Header.js b/src/shared/header/ui/Header.js deleted file mode 100644 index 7e67470..0000000 --- a/src/shared/header/ui/Header.js +++ /dev/null @@ -1,5 +0,0 @@ -import { Page } from "shared/ui"; - -export class Header extends Page { - // ... -} diff --git a/src/shared/lib/constants.js b/src/shared/lib/constants.js new file mode 100644 index 0000000..56b5eac --- /dev/null +++ b/src/shared/lib/constants.js @@ -0,0 +1,16 @@ +import { API_URL } from "../config/index.js"; + +export const HTTP_STATUS = { + OK: 200, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + INTERNAL_SERVER_ERROR: 500, +}; + +export const API_ENDPOINTS = { + SIGN_IN: `${API_URL}/api/v1/auth/login`, + SIGN_UP: `${API_URL}/api/v1/auth/register`, + SIGN_OUT: `${API_URL}/api/v1/auth/logout`, +}; diff --git a/src/shared/lib/eventbus.js b/src/shared/lib/eventbus.js index 2b7b5f1..cca3b6e 100644 --- a/src/shared/lib/eventbus.js +++ b/src/shared/lib/eventbus.js @@ -9,20 +9,27 @@ class EventBus { if (!this.events[event]) { this.events[event] = new Set(); } + this.events[event].add(callback); } off(event, callback) { - if (!this.events.has(event)) return; + if (!this.events[event]) { + return; + } + this.events[event].delete(callback); } emit(event, ...args) { - if (!this.events.has(event)) return; + if (!this.events[event]) { + return; + } + this.events[event].forEach((callback) => { callback(...args); }); } } -export default new EventBus(); +export const eventBus = new EventBus(); diff --git a/src/shared/lib/index.js b/src/shared/lib/index.js new file mode 100644 index 0000000..6bf99a8 --- /dev/null +++ b/src/shared/lib/index.js @@ -0,0 +1,2 @@ +export { HTTP_STATUS as STATUS_CODES, API_ENDPOINTS } from './constants.js'; +export { eventBus } from './eventbus.js'; \ No newline at end of file diff --git a/src/shared/lib/router.js b/src/shared/lib/router.js index 99c1411..6ece871 100644 --- a/src/shared/lib/router.js +++ b/src/shared/lib/router.js @@ -1,118 +1,131 @@ -// import { ErrorView } from '../../pages/error/index.js'; +import { eventBus } from "../../../shared/lib/eventbus.js"; export class Router { - constructor() { - this.layout = []; - this.routes = []; - this.currentView = null; - - this.onPopState = this.onPopState.bind(this); - } - - /** - * Registers a path with its corresponding view - * - * @param {string} path - path pattern to register. - * @param {Function} view - view constructor associated with path - */ - registerPath(path, view) { - const regexPath = path.replace(/\{(\w+)\}/g, '([^/]+)'); - this.routes.push({ path: new RegExp(`^${regexPath}$`), view: view }); - } - - /** - * Registers layout view that will be rendered when layout is updated - * - * @param {Function} view - layout view constructor to register - */ - registerLayout(view) { - this.layout.push(new view(this)); - } - - /** - * Renders all registered layout views - */ - renderLayout() { - this.layout.forEach((item) => item.render()); - } - - /** - * Sets up event listeners for navigation - */ - listen() { - window.addEventListener('popstate', this.onPopState); - this.renderLayout(); - this.goTo(window.location.pathname); - } - - /** - * Stop listening navigation events - */ - stop() { - window.removeEventListener('popstate', this.onPopState); - } - - /** - * Popstate event handler - */ - async onPopState() { - await this.goToImpl(); - } - - /** - * Navigates to specified path, updating browser history - * - * @param {string} path - path to navigate to - * @returns {Promise} promise that resolves when navigation is complete - */ - async goTo(path) { - window.history.pushState({}, '', path); - await this.goToImpl(); - } - - /** - * Implements navigation logic - * - * @returns {Promise} promise that resolves when the rendering is complete - */ - async goToImpl() { - const currentPath = window.location.pathname; - const targetRoute = this.findRoute(currentPath); - - this.currentView?.destructor(); - - if (targetRoute) { - this.currentView = new targetRoute.view(this); - - await this.currentView?.render(); - - if (targetRoute.updateLayout) { - this.renderLayout(); - } - } else { - // this.currentView = new ErrorView(); - // await this.currentView.render(); - } - } - - /** - * Finds route matching specified path - * - * @param {string} path - path to match against registered routes. - * @returns {Object|null} object containing route parameters and view or nil - */ - findRoute(path) { - for (const route of this.routes) { - const match = route.path.exec(path); - if (match) { - const params = match.slice(1); - return { - params: params, - view: route.view, - updateLayout: route.updateLayout, - }; - } - } - return null; - } + constructor() { + this.layout = []; + this.routes = []; + this.currentView = null; + + this.onPopState = this.onPopState.bind(this); + this.onNavigate = this.onNavigate.bind(this); + } + + /** + * Registers a path with its corresponding view + * + * @param {string} path - path pattern to register. + * @param {Function} view - view constructor associated with path + */ + registerPath(path, view) { + const regexPath = path.replace(/\{(\w+)\}/g, '([^/]+)'); + this.routes.push({ path: new RegExp(`^${regexPath}$`), view: view }); + } + + /** + * Registers layout view that will be rendered when layout is updated + * + * @param {Function} view - layout view constructor to register + */ + registerLayout(view) { + this.layout.push(new view(this)); + } + + /** + * Renders all registered layout views + */ + renderLayout() { + this.layout.forEach((item) => item.render()); + } + + /** + * Sets up event listeners for navigation + */ + listen() { + window.addEventListener('popstate', this.onPopState); + eventBus.on('navigate', this.onNavigate); // Listen for 'navigate' events + + this.renderLayout(); + this.goTo(window.location.pathname); + } + + /** + * Stop listening to navigation events + */ + stop() { + window.removeEventListener('popstate', this.onPopState); + eventBus.off('navigate', this.onNavigate); // Stop listening for 'navigate' events + } + + /** + * Popstate event handler for browser back/forward navigation + */ + async onPopState() { + await this.goToImpl(); + } + + /** + * Event handler for 'navigate' events emitted from eventBus + * + * @param {string} path - The path to navigate to + */ + async onNavigate(path) { + await this.goTo(path); + } + + /** + * Navigates to the specified path and updates browser history + * + * @param {string} path - path to navigate to + * @returns {Promise} promise that resolves when navigation is complete + */ + async goTo(path) { + window.history.pushState({}, '', path); + await this.goToImpl(); + } + + /** + * Implements the navigation logic for rendering views + * + * @returns {Promise} promise that resolves when the rendering is complete + */ + async goToImpl() { + const currentPath = window.location.pathname; + const targetRoute = this.findRoute(currentPath); + + this.currentView?.destructor?.(); + console.log("destructor", this.currentView); + + if (targetRoute) { + this.currentView = new targetRoute.view(this); + await this.currentView?.render(); + + if (targetRoute.updateLayout) { + this.renderLayout(); + } + } else { + // this.currentView = new ErrorView(); + // await this.currentView.render(); + } + } + + /** + * Finds a route matching the specified path + * + * @param {string} path - path to match against registered routes. + * @returns {Object|null} object containing route parameters and view or null if not found + */ + findRoute(path) { + for (const route of this.routes) { + const match = route.path.exec(path); + if (match) { + const params = match.slice(1); + return { + params: params, + view: route.view, + updateLayout: route.updateLayout, + }; + } + } + return null; + } } diff --git a/src/widgets/header/index.js b/src/widgets/header/index.js new file mode 100644 index 0000000..6162387 --- /dev/null +++ b/src/widgets/header/index.js @@ -0,0 +1 @@ +export { Header } from "./ui/Header.js"; diff --git a/src/widgets/header/ui/Header.css b/src/widgets/header/ui/Header.css new file mode 100644 index 0000000..8d2eef6 --- /dev/null +++ b/src/widgets/header/ui/Header.css @@ -0,0 +1,55 @@ +.header__container { + max-width: 1280px; + margin: 0 auto; +} + +.header { + width: 100%; + position: sticky; + top: 0; + background-color: black; +} + +.header__row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 224px 14px 192px; +} + +.header__btn_logo { + color: #f1f1f1; + font-size: 25px; + font-weight: 400; + background-color: black; + border: none; +} + +.header__btn_logo:hover { + opacity: 0.5; +} + +.header__btn_signup { + color: #f1f1f1; + font-size: 25px; + font-weight: 400; + background-color: black; + border: none; + margin-left: 45px; +} + +.header__btn_signup:hover { + opacity: 0.5; +} + +.header__btn_login { + color: #f1f1f1; + font-size: 25px; + font-weight: 400; + background-color: black; + border: none; +} + +.header__btn_login:hover { + opacity: 0.5; +} diff --git a/src/widgets/header/ui/Header.hbs b/src/widgets/header/ui/Header.hbs new file mode 100644 index 0000000..e91527b --- /dev/null +++ b/src/widgets/header/ui/Header.hbs @@ -0,0 +1,27 @@ +
+
+
+ +
+
+ {{#if user}} + + {{else}} + + + {{/if}} +
+
+
diff --git a/src/widgets/header/ui/Header.js b/src/widgets/header/ui/Header.js new file mode 100644 index 0000000..be9f70c --- /dev/null +++ b/src/widgets/header/ui/Header.js @@ -0,0 +1,64 @@ +import { eventBus } from "../../../shared/lib/index.js"; +import { userStore } from "../../../entities/user/model/store.js"; + +export class Header { + parent; + + constructor() { + this.parent = document.querySelector("#header"); + } + + render() { + const template = Handlebars.templates["Header.hbs"]; + const user = userStore.getUser(); + this.parent.innerHTML = template({ user }); + + this.bindEvents(); + this.onEvents(); + } + + bindEvents() { + this.parent.addEventListener("click", (event) => { + if (event.target.id === "header_logout_button") { + this.handleSignOut(); + } else if (event.target.id === "header_login_button") { + this.handleSignIn(); + } else if (event.target.id === "header_signup_button") { + this.handleSignup(); + } + }); + } + + onEvents() { + eventBus.on("signInSuccess", this.render); + eventBus.on("signUpSuccess", this.render); + eventBus.on("signOutSuccess", this.render); + } + + offEvents() { + eventBus.off("signUpSuccess", this.render); + eventBus.off("signUpError", this.render); + eventBus.off("signOutSuccess", this.render); + } + + async handleSignOut() { + try { + await userStore.signOut(); + eventBus.emit("navigate", "/"); + } catch (error) { + console.error("unable to sign out", error); + } + } + + handleSignIn() { + eventBus.emit("navigate", "/signin"); + } + + handleSignup() { + eventBus.emit("navigate", "/signup"); + } + + destructor() { + this.offEvents(); + } +}