diff --git a/front/doc/component.md b/front/doc/component.md new file mode 100644 index 00000000..f2a19801 --- /dev/null +++ b/front/doc/component.md @@ -0,0 +1,116 @@ +# `Component` + +-------------------------------------------------------------------------------- + +The Component class is a custom HTML element that extends the HTMLElement class. +It provides a foundation for creating reusable and customizable web components. + +This class is an abstract class, and must not be instantiated. + +-------------------------------------------------------------------------------- + +# Methods to be redefined + +## render + +Returns the HTML content to be rendered inside the component. + +```javascript +render() { + const message = 'Hello World!' + return ` +
+

${message}

+
+ `; +} +``` + +## style + +Returns the specific CSS content to be rendered inside the component. + +```javascript +style() { + return ` + + `; +} +``` + +## postRender + +Executed after the component has been rendered. + +```javascript +postRender() { + this.title = this.querySelector('h1'); + super.addComponentEventListener('click', this.handleClick); +} +``` + +## update + +Executed when the component is updated. + +```javascript +update() { + this.title.textContent = 'updated!'; +} +``` + +-------------------------------------------------------------------------------- + +# Inherited Methods + +## addComponentEventListener + +Adds an event listener to the component. + +Component event listener ensures that the "this" instance in the +callback is always defined as the instance of the component. Additionally, this +system prevents event listener leaks even when the callbacks are anonymous +functions. + +```javascript +this.username = this.querySelector('#username'); +super.addComponentEventListener(this.username, 'input', this.#usernameHandler); +``` + +### Parameters + +> | name | data type | description | type | +> |------------|-------------|-------------------------------------------------------------------|------------| +> | `element` | HTMLElement | Selected HTMLElement | Required | +> | `event` | String | A case-sensitive string representing the event type to listen for | Required | +> | `callback` | Function | Function call when an event is trigger | Required | + +## removeComponentEventListener + +Removes an event listener from the component. + +```javascript +super.removeComponentEventListener(this.username, 'input'); +``` + +### Parameters + +> | name | data type | description | type | +> |------------|-------------|-----------------------------------------------------------------------------------|------------| +> | `element` | HTMLElement | Selected HTMLElement | Required | +> | `event` | String | A string which specifies the type of event for which to remove an event listener | Required | + + +## removeAllComponentEventListeners + +Removes all event listeners from the component. + +Automatically called when a component is removed from the DOM. + +```javascript +super.removeAllComponentEventListeners(); +``` diff --git a/front/doc/components.md b/front/doc/components.md deleted file mode 100644 index c280e1bc..00000000 --- a/front/doc/components.md +++ /dev/null @@ -1,56 +0,0 @@ -# Components documentation - --------------------------------------------------------------------------------- - -## `/auth/signup` - -### Signup component - -
- GET /auth/signup - -#### Responses - -> | http code | content-type | response | -> |-----------|--------------|-------------------------| -> | `200` | `text/html` | `signup form component` | - -
- - --------------------------------------------------------------------------------- - -## `/auth/signin` - -### Signin component - -
- GET /auth/signin - -#### Responses - -> | http code | content-type | response | -> |-----------|--------------|-------------------------| -> | `200` | `text/html` | `signin form component` | - -
- - --------------------------------------------------------------------------------- -## `/auth/reset-password-email` - -### Signup component - -
- GET /auth/reset-password-email - -#### Responses - -> | http code | content-type | response | -> |-----------|--------------|---------------------------------------| -> | `200` | `text/html` | `reset password email form component` | - -
- - --------------------------------------------------------------------------------- diff --git a/front/doc/cookies.md b/front/doc/cookies.md new file mode 100644 index 00000000..86c2c712 --- /dev/null +++ b/front/doc/cookies.md @@ -0,0 +1,62 @@ +# `Cookies` + +-------------------------------------------------------------------------------- + +The Cookies class provides a simple and convenient way to manage cookies in a +web application. It offers methods for retrieving, adding, and removing cookies +with additional options for security. + +It is defined as static, so there's no need to create an instance. + +-------------------------------------------------------------------------------- + +## get + +Returns the value of the cookie with the specified name. + +```javascript +Cookies.get('username'); +``` + +### Parameters + +> | name | data type | description | type | +> |--------|-----------|------------------------|----------| +> | `name` | String | The name of the cookie | Required | + +### Return + +> | data type | value | description | +> |-----------|-----------------------------------|-----------------------------------| +> | null | null | The cookie does not exist | +> | String | The value of the specified cookie | The value of the specified cookie | + +## add + +Adds a new cookie with the given name and value. + +```javascript +Cookies.add('username', 'John Doe'); +``` + +### Parameters + +> | name | data type | description | type | +> |---------|-----------|-------------------------|----------| +> | `name` | String | The name of the cookie | Required | +> | `value` | String | The value of the cookie | Required | + + +## remove + +Removes the cookie with the specified name. + +```javascript +Cookies.remove('username'); +``` + +### Parameters + +> | name | data type | description | type | +> |--------|-----------|------------------------|----------| +> | `name` | String | The name of the cookie | Required | diff --git a/front/doc/front.md b/front/doc/front.md index 4bfbc2bf..f0ed5d93 100644 --- a/front/doc/front.md +++ b/front/doc/front.md @@ -5,29 +5,19 @@ The front-end microservice operates without a database and is solely used to construct our single-page application using a component-based architecture. The constraints of the 42 subjects require us not to use any front-end frameworks, -which is why we've developed our own system to easily load components without -requiring extensive JavaScript. +which is why we've developed our own system to easily load components using +custom elements. -## `loadComponent` - -### Load a component from JS Vanilla without reload the page - -```async function loadComponent(uri, parentId, setState = true)``` - -### Parameters - -> | name | data type | description | type | -> |------------|-----------|---------------------------------------------------------------|-------------------------| -> | `uri` | String | Component URI | Required | -> | `parentId` | String | Identifier of the parent where the component will be inserted | Required | -> | `setState` | Boolean | Add component to browser history | Optional (default=true) | - -#### Return +-------------------------------------------------------------------------------- -> | data type | value | description | -> |-----------|-------|--------------------------------------| -> | Boolean | false | error, component could not be loaded | -> | Boolean | true | component successfully loaded | +> ### [Router](router.md) +> +> ##### A client side router for single page application --------------------------------------------------------------------------------- +> ### [Component](component.md) +> +> ##### A custom HTML element for building web components +> ### [Cookies](cookies.md) +> +> ##### A simple and convenient way to manage cookies in a web application diff --git a/front/doc/router.md b/front/doc/router.md new file mode 100644 index 00000000..89e0aef6 --- /dev/null +++ b/front/doc/router.md @@ -0,0 +1,105 @@ +# `Router` + +-------------------------------------------------------------------------------- + +The Router class is a class designed to facilitate client-side routing in web +applications. +It allows to define routes and associated custom elements, +enabling seamless navigation within a single-page application (SPA). + +The router automatically manages page history in the browser. + +-------------------------------------------------------------------------------- + +## Instantiation + +To use the Router class, instantiate it with the following parameters: + +```javascript +const app = document.querySelector('#app'); // Replace '#app' with the selector of your application container +const router = new Router(app, [ + // Define your routes here using the Route class +]); +``` + +### Parameters + +> | name | data type | description | type | +> |----------|-------------|------------------------|----------| +> | `app` | HTMLElement | Application container | Required | +> | `routes` | Array | Array of route objects | Optional | + +## addRoute + +Add a new route to the router. +Each route consists of a path and a custom element associated with that path. + +```javascript +router.addRoute('/example/', 'example-component'); +``` + +#### Route Parameters + +Routes can include parameters denoted by :param in the path. +These parameters are captured and passed to the associated custom element. + +```javascript +router.addRoute('/users/:id/', 'user-profile-component'); +``` + +#### Default Route + +If no routes match the current path, a default route (with an empty path) is +used. +This is useful for defining a home page or fallback route. + +```javascript +router.addRoute('', 'home-component'); +``` + +### Parameters + +> | name | data type | description | type | +> |-----------------|-----------|-----------------------------------|----------| +> | `path` | String | Route path | Required | +> | `customElement` | String | Name of custom element to display | Required | + +## navigate + +Navigate between routes by specifying the new path. + +```javascript +router.navigate('/example/'); +``` + +### Parameters + +> | name | data type | description | type | +> |------------------|-------------|-------------------------------------|------------| +> | `newPath` | String | New path to navigate | Required | + +### Return + +> | data type | value | description | +> |-------------|-------------|-------------------------| +> | null | null | Error, path not found | +> | HTMLElement | HTMLElement | Custom element instance | + +-------------------------------------------------------------------------------- + +## Example + +```javascript +const app = document.querySelector('#app'); + +const router = new Router(app, [ + new Route('/singleplayer/', 'singleplayer-component'), + new Route('/multiplayer/', 'multiplayer-component'), + new Route('/tournaments/', 'tournaments-component'), + new Route('/signin/', 'signin-component'), + new Route('/signup/', 'signup-component'), + new Route('', 'home-component'), +]); + +window.router = router; +``` diff --git a/front/src/auth_components/__init__.py b/front/src/auth_components/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/front/src/auth_components/admin.py b/front/src/auth_components/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/front/src/auth_components/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/front/src/auth_components/apps.py b/front/src/auth_components/apps.py deleted file mode 100644 index 3edfce87..00000000 --- a/front/src/auth_components/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class AuthComponentsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'auth_components' diff --git a/front/src/auth_components/migrations/__init__.py b/front/src/auth_components/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/front/src/auth_components/models.py b/front/src/auth_components/models.py deleted file mode 100644 index 71a83623..00000000 --- a/front/src/auth_components/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/front/src/auth_components/templates/reset_password_email/reset_password_email.css b/front/src/auth_components/templates/reset_password_email/reset_password_email.css deleted file mode 100644 index c16292f8..00000000 --- a/front/src/auth_components/templates/reset_password_email/reset_password_email.css +++ /dev/null @@ -1,51 +0,0 @@ -.active { - font-family: 'JetBrains Mono', monospace; -} - -#login { - height: 100vh; -} - -.login-card { - width: 550px; -} - -#github-btn { - background-color: #000000; - color: #ffffff; -} - -#github-btn:hover { - background-color: #252525; - color: #ffffff; -} - -#intra-btn { - background-color: #ffffff; - color: #000000;" -} - - -#intra-btn:hover { - background-color: #f6f6f6; - color: #000000;" -} - -.form-group mb-4 { - display: flex; - align-items: center; -} - -#password-feedback { - white-space: pre-line; - word-wrap: break-word; -} - -.eye-box:hover { - background: #efefef; - color: #2d2d2d; -} - -.invalid-feedback p { - margin: 0; -} diff --git a/front/src/auth_components/templates/reset_password_email/reset_password_email.html b/front/src/auth_components/templates/reset_password_email/reset_password_email.html deleted file mode 100644 index 4d075cad..00000000 --- a/front/src/auth_components/templates/reset_password_email/reset_password_email.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends 'component.html' %} - -{% block html %} -
-
-
-

Reset password

-
-
- -
-
- -
-
-
- -
- -
-
-
-{% endblock %} diff --git a/front/src/auth_components/templates/reset_password_email/reset_password_email.js b/front/src/auth_components/templates/reset_password_email/reset_password_email.js deleted file mode 100644 index e69de29b..00000000 diff --git a/front/src/auth_components/templates/signin/signin.css b/front/src/auth_components/templates/signin/signin.css deleted file mode 100644 index 3927e584..00000000 --- a/front/src/auth_components/templates/signin/signin.css +++ /dev/null @@ -1,56 +0,0 @@ -.active { - font-family: 'JetBrains Mono', monospace; -} - -#login { - height: 100vh; -} - -.login-card { - width: 550px; -} - -#github-btn { - background-color: #000000; - color: #ffffff; -} - -#github-btn:hover { - background-color: #252525; - color: #ffffff; -} - -#intra-btn { - background-color: #ffffff; - color: #000000;" -} - - -#intra-btn:hover { - background-color: #f6f6f6; - color: #000000;" -} - -.form-group mb-4 { - display: flex; - align-items: center; -} - -#password-feedback { - white-space: pre-line; - word-wrap: break-word; -} - -.eye-box:hover { - background: #efefef; - color: #2d2d2d; -} - -.invalid-feedback p { - margin: 0; -} - -#forgot-password, #dont-have-account { - font-size: 13px; - color: rgb(13, 110, 253); -} diff --git a/front/src/auth_components/templates/signin/signin.js b/front/src/auth_components/templates/signin/signin.js deleted file mode 100644 index 3c73dabc..00000000 --- a/front/src/auth_components/templates/signin/signin.js +++ /dev/null @@ -1,41 +0,0 @@ -const email = document.querySelector('#email'); -email.addEventListener('input', function () { - if (!isValidEmail(email.value)) { - email.classList.remove('is-valid') - email.classList.add('is-invalid'); - } else { - email.classList.remove('is-invalid') - email.classList.add('is-valid'); - } -}); - -function isValidEmail(email) { - const emailRegex = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/; - return emailRegex.test(email); -} - -const forgotPassword = document.querySelector('#forgot-password'); -forgotPassword.addEventListener('click', function () { - loadComponent('auth/reset-password-email/', 'content'); -}) - -const dontHaveAccount = document.querySelector('#dont-have-account'); -dontHaveAccount.addEventListener('click', function () { - loadComponent('auth/signup/', 'content'); -}) - -const signinBtn = document.querySelector('#signin-btn'); -signinBtn.addEventListener('click', function (event) { - event.preventDefault(); - signin(); -}) -const signinForm = document.querySelector('#signin-form'); -signinForm.addEventListener('submit', function (event) { - event.preventDefault(); - signin(); -}) - -function signin() { - logNav(); - homeNav(); -} \ No newline at end of file diff --git a/front/src/auth_components/templates/signup/signup.css b/front/src/auth_components/templates/signup/signup.css deleted file mode 100644 index 3dcd2f59..00000000 --- a/front/src/auth_components/templates/signup/signup.css +++ /dev/null @@ -1,56 +0,0 @@ -.active { - font-family: 'JetBrains Mono', monospace; -} - -#login { - height: 100vh; -} - -.login-card { - width: 550px; -} - -#github-btn { - background-color: #000000; - color: #ffffff; -} - -#github-btn:hover { - background-color: #252525; - color: #ffffff; -} - -#intra-btn { - background-color: #ffffff; - color: #000000;" -} - - -#intra-btn:hover { - background-color: #f6f6f6; - color: #000000;" -} - -.form-group mb-4 { - display: flex; - align-items: center; -} - -#password-feedback { - white-space: pre-line; - word-wrap: break-word; -} - -.eye-box:hover { - background: #efefef; - color: #2d2d2d; -} - -.invalid-feedback p { - margin: 0; -} - -#have-account { - font-size: 13px; - color: rgb(13, 110, 253); -} diff --git a/front/src/auth_components/templates/signup/signup.js b/front/src/auth_components/templates/signup/signup.js deleted file mode 100644 index 67847d1e..00000000 --- a/front/src/auth_components/templates/signup/signup.js +++ /dev/null @@ -1,121 +0,0 @@ -const username = document.querySelector('#username'); -username.addEventListener('input', function () { - if (username.value.length === 0) { - username.classList.remove('is-valid') - username.classList.add('is-invalid'); - } else { - username.classList.remove('is-invalid') - username.classList.add('is-valid'); - } -}); - -const email = document.querySelector('#email'); -email.addEventListener('input', function () { - if (!isValidEmail(email.value)) { - email.classList.remove('is-valid') - email.classList.add('is-invalid'); - } else { - email.classList.remove('is-invalid') - email.classList.add('is-valid'); - } -}); - -const password = document.querySelector('#password'); -const confirmPassword = document.querySelector('#confirm-password'); -password.addEventListener('input', function () { - const validity = isValidPassword(password.value); - if (validity.length !== 0) { - password.classList.remove('is-valid') - password.classList.add('is-invalid'); - const feedback = document.querySelector('#password-feedback'); - feedback.innerHTML = ''; - let htmlMessage = document.createElement('p'); - htmlMessage.textContent = validity[0]; - feedback.appendChild(htmlMessage); - } else { - password.classList.remove('is-invalid') - password.classList.add('is-valid'); - } -}); - -confirmPassword.addEventListener('input', function () { - if (confirmPassword.value !== password.value) { - confirmPassword.classList.remove('is-valid') - confirmPassword.classList.add('is-invalid'); - } else { - confirmPassword.classList.remove('is-invalid') - confirmPassword.classList.add('is-valid'); - } -}); - -function isValidEmail(email) { - const emailRegex = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/; - return emailRegex.test(email); -} - -function isValidPassword(password) { - const uppercaseRegex = /[A-Z]/; - const numberRegex = /[0-9]/; - const specialCharacterRegex = /[!@#$%^&*()_+]/; - - const missingRequirements = []; - if (password.length < 8) { - missingRequirements.push('Password must be at least 8 characters long.'); - } - if (password.length > 20) { - missingRequirements.push('Password must contain a maximum of 20 characters.'); - } - if (!uppercaseRegex.test(password)) { - missingRequirements.push('Password must contain at least one uppercase letter.'); - } - if (!numberRegex.test(password)) { - missingRequirements.push('Password must contain at least one number.'); - } - if (!specialCharacterRegex.test(password)) { - missingRequirements.push('Password must contain at least one special character.'); - } - return missingRequirements; -} - -const passwordEyeIcon = document.querySelector('#password-eye'); -let passwordEyeIconStatus = false; - -passwordEyeIcon.addEventListener('click', function () { - if (!passwordEyeIconStatus) { - passwordEyeIcon.children[0].classList.remove('fa-eye'); - passwordEyeIcon.children[0].classList.add('fa-eye-slash'); - password.type = 'text'; - passwordEyeIconStatus = true; - } - else { - passwordEyeIcon.children[0].classList.remove('fa-eye-slash'); - passwordEyeIcon.children[0].classList.add('fa-eye'); - password.type = 'password'; - passwordEyeIconStatus = false; - } -}) - -const confirmPasswordEyeIcon = document.querySelector('#confirm-password-eye'); -let confirmPasswordEyeIconStatus = false; - -confirmPasswordEyeIcon.addEventListener('click', function () { - if (!confirmPasswordEyeIconStatus) { - confirmPasswordEyeIcon.children[0].classList.remove('fa-eye'); - confirmPasswordEyeIcon.children[0].classList.add('fa-eye-slash'); - confirmPassword.type = 'text'; - confirmPasswordEyeIconStatus = true; - } - else { - confirmPasswordEyeIcon.children[0].classList.remove('fa-eye-slash'); - confirmPasswordEyeIcon.children[0].classList.add('fa-eye'); - confirmPassword.type = 'password'; - confirmPasswordEyeIconStatus = false; - } -}) - -const haveAccount = document.querySelector('#have-account'); - -haveAccount.addEventListener('click', function () { - loadComponent('auth/signin/', 'content'); -}) - diff --git a/front/src/auth_components/tests.py b/front/src/auth_components/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/front/src/auth_components/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/front/src/auth_components/urls.py b/front/src/auth_components/urls.py deleted file mode 100644 index aa231920..00000000 --- a/front/src/auth_components/urls.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -URL configuration for front project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/4.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import path -from .views import signup, signin, reset_password_email - -app_name = "auth_components" - -urlpatterns = [ - path('signup/', signup), - path('signin/', signin), - path('reset-password-email/', reset_password_email), -] diff --git a/front/src/auth_components/views.py b/front/src/auth_components/views.py deleted file mode 100644 index 4a57635a..00000000 --- a/front/src/auth_components/views.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.shortcuts import render -from front.component import generate_component - -def signup(request): - return generate_component(request, "signup") - - -def signin(request): - return generate_component(request, "signin") - -def reset_password_email(request): - return generate_component(request, "reset_password_email") diff --git a/front/src/front/component.py b/front/src/front/component.py deleted file mode 100644 index 6a09cf86..00000000 --- a/front/src/front/component.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.shortcuts import render -def generate_component(request, component, default_css=True, default_js=True): - context = {} - if default_css: - context['css'] = f"{component}/{component}.css" - if default_js: - context["js"] = f"{component}/{component}.js" - response = render(request, f"{component}/{component}.html", context=context) - return response diff --git a/front/src/front/settings.py b/front/src/front/settings.py index 8553aef8..6b13984d 100644 --- a/front/src/front/settings.py +++ b/front/src/front/settings.py @@ -37,7 +37,6 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'auth_components', ] MIDDLEWARE = [ diff --git a/front/src/front/static/favicon.png b/front/src/front/static/favicon.png new file mode 100644 index 00000000..0bc8f58c Binary files /dev/null and b/front/src/front/static/favicon.png differ diff --git a/front/src/front/static/js/Cookies.js b/front/src/front/static/js/Cookies.js new file mode 100644 index 00000000..a9d56c46 --- /dev/null +++ b/front/src/front/static/js/Cookies.js @@ -0,0 +1,23 @@ +export class Cookies { + + static get(name) { + let cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + let c = cookies[i].trim(); + if (c.indexOf(name) === 0) { + return c.substring(name.length + 1, c.length); + } + } + return null; + } + + static add(name, value) { + document.cookie = name + '=' + value + ';path=/;SameSite=Strict;Secure'; + } + + static remove(name) { + document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;SameSite=Strict;Secure'; + } +} + +export default { Cookies }; \ No newline at end of file diff --git a/front/src/front/static/js/Router.js b/front/src/front/static/js/Router.js new file mode 100644 index 00000000..c1e3fee1 --- /dev/null +++ b/front/src/front/static/js/Router.js @@ -0,0 +1,84 @@ +export class Router { + constructor(app, routes= [] ) { + this.routes = [] + Object.assign(this.routes, routes); + this.app = app; + window.addEventListener('popstate', (event) => { + this.#loadRoute(document.location.pathname); + }); + this.#loadRoute(document.location.pathname); + } + + addRoute(path, customElement) { + this.routes.push(new Route(path, customElement)); + } + + navigate(newPath) { + const result = this.#loadRoute(newPath); + if (result !== null && window.location.pathname !== newPath) { + window.history.pushState({}, '', newPath); + } + return result; + } + + #findMatchingRoute(path) { + let defaultRoute = null; + for (const route of this.routes) { + const parametersValues = path.match(route.pathRegex); + if (parametersValues) { + parametersValues.shift(); + return {route, parametersValues}; + } + if (defaultRoute === null && route.path.length === 0) { + defaultRoute = route; + } + } + return {route: defaultRoute, parametersValues: []}; + } + + #loadRoute(path) { + const {route , parametersValues} = this.#findMatchingRoute(path); + if (route === null) { + console.error(`Route not found`); + return null; + } + const customElement = document.createElement(route.customElement); + Router.#setParametersInElement(customElement, route.pathParameters, parametersValues); + this.app.innerHTML = ''; + this.app.appendChild(customElement); + return customElement; + } + + static #setParametersInElement(element, parameters, values) { + for (let i = 0; i < parameters.length; i++) { + element.setAttribute(parameters[i], values[i]); + } + return element; + } +} + +export class Route { + constructor(path, customElement) { + this.path = path; + this.customElement = customElement; + this.#setPathParameters(); + this.#setPathRegex(); + } + + #setPathParameters() { + const matchParameters = this.path.match(/:[a-zA-Z]+/g); + if (matchParameters === null) { + this.pathParameters = []; + } else { + this.pathParameters = matchParameters.map(param => param.slice(1)); + } + } + + #setPathRegex() { + const parsedPath = this.path.replace(/:[a-zA-Z]+/g, '([a-zA-Z0-9-]+)'); + this.pathRegex = new RegExp(`^${parsedPath}$`); + } + +} + +export default { Router, Route }; \ No newline at end of file diff --git a/front/src/front/static/js/Theme.js b/front/src/front/static/js/Theme.js new file mode 100644 index 00000000..f7498105 --- /dev/null +++ b/front/src/front/static/js/Theme.js @@ -0,0 +1,25 @@ +import {Cookies} from "./Cookies.js"; + +export class Theme { + + static defaultTheme = 'light'; + + static set(theme) { + Cookies.add('theme', theme); + document.querySelector('body').setAttribute('data-bs-theme', theme); + } + + static get() { + const theme = Cookies.get('theme'); + if (theme === null) { + return Theme.defaultTheme; + } + return theme; + } + + static init() { + Theme.set(Theme.get()); + } +} + +export default { Theme }; \ No newline at end of file diff --git a/front/src/front/static/js/components/Component.js b/front/src/front/static/js/components/Component.js new file mode 100644 index 00000000..bdd0fdd0 --- /dev/null +++ b/front/src/front/static/js/components/Component.js @@ -0,0 +1,72 @@ +export class Component extends HTMLElement { + constructor() { + super(); + this.rendered = false; + this.componentEventListeners = []; + } + + connectedCallback() { + if (!this.rendered) { + this.innerHTML = this.render() + this.style(); + this.rendered = true; + this.postRender(); + } + } + + disconnectedCallback() { + this.removeAllComponentEventListeners(); + } + + attributeChangedCallback(name, oldValue, newValue) { + this.update(); + } + + addComponentEventListener(element, event, callback) { + if (!this.componentEventListeners[event]) { + this.componentEventListeners[event] = []; + } + const eventCallback = callback.bind(this); + this.componentEventListeners[event].push({element, eventCallback}); + element.addEventListener(event, eventCallback); + } + + removeComponentEventListener(element, event) { + const eventListeners = this.componentEventListeners[event]; + + if (eventListeners) { + for (const eventListener of eventListeners) { + if (eventListener.element === element) { + element.removeEventListener(event, eventListener.eventCallback); + eventListeners.splice(eventListeners.indexOf(eventListener), 1); + } + } + } + } + removeAllComponentEventListeners() { + for (const event in this.componentEventListeners) { + const eventListeners = this.componentEventListeners[event]; + for (const eventListener of eventListeners) { + eventListener.element.removeEventListener(event, eventListener.eventCallback); + } + } + this.componentEventListeners = []; + } + + render() { + return ''; + } + + update() { + this.innerHTML = this.render() + this.style(); + } + + style() { + return ''; + } + + postRender() { + } + +} + +export default { Component }; diff --git a/front/src/front/static/js/components/Home.js b/front/src/front/static/js/components/Home.js new file mode 100644 index 00000000..9570e058 --- /dev/null +++ b/front/src/front/static/js/components/Home.js @@ -0,0 +1,21 @@ +import {Component} from "./Component.js"; + +export class Home extends Component { + constructor() { + super(); + } + render() { + return (` + + `); + } + style() { + return (` + + `); + } +} + +export default { Home }; \ No newline at end of file diff --git a/front/src/front/static/js/components/Multiplayer.js b/front/src/front/static/js/components/Multiplayer.js new file mode 100644 index 00000000..08e4e885 --- /dev/null +++ b/front/src/front/static/js/components/Multiplayer.js @@ -0,0 +1,22 @@ +import {Component} from "./Component.js"; + +export class Multiplayer extends Component { + constructor() { + super(); + } + render() { + return (` + +

Multiplayer

+ `); + } + style() { + return (` + + `); + } +} + +export default { Multiplayer }; \ No newline at end of file diff --git a/front/src/front/static/js/components/Navbar.js b/front/src/front/static/js/components/Navbar.js new file mode 100644 index 00000000..7324d024 --- /dev/null +++ b/front/src/front/static/js/components/Navbar.js @@ -0,0 +1,162 @@ +import {Component} from "./Component.js"; +import { Cookies } from "../Cookies.js"; + +export class Navbar extends Component { + constructor() { + super(); + } + + logout() { + Cookies.remove('jwt'); + window.router.navigate('/'); + } + postRender() { + super.addComponentEventListener(this.querySelector('#home'), 'click', this.navigate); + super.addComponentEventListener(this.querySelector('#singleplayer'), 'click', this.navigate); + super.addComponentEventListener(this.querySelector('#multiplayer'), 'click', this.navigate); + super.addComponentEventListener(this.querySelector('#tournaments'), 'click', this.navigate); + const disablePaddingTop = this.getAttribute('disable-padding-top'); + if (disablePaddingTop !== 'true') { + const navbarHeight = this.querySelector('.navbar').offsetHeight; + document.body.style.paddingTop = navbarHeight + 'px'; + } else { + document.body.style.paddingTop = '0px'; + } + const logout = this.querySelector('#logout'); + if (logout) { + super.addComponentEventListener(logout, 'click', this.logout); + } + } + + navigate(event) { + window.router.navigate(`/${event.target.id}/`); + } + + generateNavLink(linkId) { + const activeLink = this.getAttribute('nav-active'); + const navLink = document.createElement('a') + navLink.setAttribute('id', linkId); + navLink.classList.add('nav-link'); + if (activeLink === linkId) { + navLink.classList.add('active'); + } + navLink.text = linkId.charAt(0).toUpperCase() + linkId.slice(1); + return (navLink.outerHTML); + } + + logNavPart() { + const jwt = Cookies.get('jwt'); + const auth = jwt !== undefined && jwt !== null && jwt !== ''; + if (auth) { + return (this.logPart()); + } + return (this.logOutPart()); + } + + render() { + return (` + + `); + } + + logPart() { + return (` +
+ + + + +
+ `); + } + + logOutPart() { + return (` +
+ + +
+ `); + } + + style() { + return (` + + `); + } +} + +export default { Navbar }; \ No newline at end of file diff --git a/front/src/front/static/js/components/ResetPassword.js b/front/src/front/static/js/components/ResetPassword.js new file mode 100644 index 00000000..f6e69a87 --- /dev/null +++ b/front/src/front/static/js/components/ResetPassword.js @@ -0,0 +1,95 @@ +import {Component} from "./Component.js"; + +export class ResetPassword extends Component { + constructor() { + super(); + } + render() { + return (` + +
+
+
+

Reset password

+
+
+ +
+
+ +
+
+
+ +
+
+
+
+ `); + } + style() { + return (` + + `); + } +} + +export default { ResetPassword }; \ No newline at end of file diff --git a/front/src/auth_components/templates/signin/signin.html b/front/src/front/static/js/components/Signin.js similarity index 74% rename from front/src/auth_components/templates/signin/signin.html rename to front/src/front/static/js/components/Signin.js index 713c03af..c18837ae 100644 --- a/front/src/auth_components/templates/signin/signin.html +++ b/front/src/front/static/js/components/Signin.js @@ -1,6 +1,38 @@ -{% extends 'component.html' %} +import {Component} from "./Component.js"; +import {Cookies} from "../Cookies.js"; +import {InputValidator} from "../utils/InputValidator.js"; +import {BootstrapUtils} from "../utils/BootstrapUtils.js"; -{% block html %} +export class Signin extends Component { + constructor() { + super(); + } + + signin() { + Cookies.add('jwt', 'jwt'); + window.router.navigate('/'); + } + + postRender() { + super.addComponentEventListener(this.querySelector('#forgot-password'), 'click', () => { + window.router.navigate('/reset-password/'); + }); + super.addComponentEventListener(this.querySelector('#dont-have-account'), 'click', () => { + window.router.navigate('/signup/'); + }); + super.addComponentEventListener(this.querySelector('#signin-btn'), 'click', (event) => { + event.preventDefault(); + this.signin(); + }); + super.addComponentEventListener(this.querySelector('#signin-form'), 'submit', (event) => { + event.preventDefault(); + this.signin(); + }); + } + + render() { + return (` +
@@ -10,7 +42,7 @@

Sign in

-
+
Please enter a valid email.
@@ -77,7 +109,7 @@

Sign in

inkscape:window-maximized="0" inkscape:current-layer="Calque_1"/> - Sign in
-{% endblock %} + `); + } + style() { + return (` + + `); + } +} + +export default { Signin }; \ No newline at end of file diff --git a/front/src/auth_components/templates/signup/signup.html b/front/src/front/static/js/components/Signup.js similarity index 58% rename from front/src/auth_components/templates/signup/signup.html rename to front/src/front/static/js/components/Signup.js index d22ba48a..021f44d2 100644 --- a/front/src/auth_components/templates/signup/signup.html +++ b/front/src/front/static/js/components/Signup.js @@ -1,7 +1,105 @@ -{% extends 'component.html' %} +import {Component} from "./Component.js"; +import {InputValidator} from "../utils/InputValidator.js"; +import {BootstrapUtils} from "../utils/BootstrapUtils.js"; -{% block html %} -
+ window.router.navigate('/signin/') + ); + } + + render() { + return (` + +
-{% endblock %} + `); + } + style() { + return (` + + `); + } +} + +export default { Signup }; \ No newline at end of file diff --git a/front/src/front/static/js/components/Singleplayer.js b/front/src/front/static/js/components/Singleplayer.js new file mode 100644 index 00000000..d06737c7 --- /dev/null +++ b/front/src/front/static/js/components/Singleplayer.js @@ -0,0 +1,22 @@ +import {Component} from "./Component.js"; + +export class Singleplayer extends Component { + constructor() { + super(); + } + render() { + return (` + +

Singleplayer

+ `); + } + style() { + return (` + + `); + } +} + +export default { Singleplayer }; \ No newline at end of file diff --git a/front/src/front/static/js/components/ThemeButton.js b/front/src/front/static/js/components/ThemeButton.js new file mode 100644 index 00000000..f6f51e57 --- /dev/null +++ b/front/src/front/static/js/components/ThemeButton.js @@ -0,0 +1,46 @@ +import {Component} from "./Component.js"; +import {Theme} from "../Theme.js"; + +export class ThemeButton extends Component { + constructor() { + super(); + } + + switchTheme() { + if (Theme.get() === 'light') { + Theme.set('dark'); + } else { + Theme.set('light'); + } + this.switchBtn.classList.toggle('btn-outline-light'); + this.switchBtn.classList.toggle('btn-outline-dark'); + } + + postRender() { + this.switchBtn = this.querySelector('#switch-btn'); + super.addComponentEventListener(this.switchBtn, 'click', this.switchTheme); + } + + render() { + const theme = Theme.get(); + const btnClass = theme === 'light' ? 'btn-outline-dark' : 'btn-outline-light'; + return (` + + `); + } + style() { + return (` + + `); + } +} + +export default { ThemeButton }; \ No newline at end of file diff --git a/front/src/front/static/js/components/Tournaments.js b/front/src/front/static/js/components/Tournaments.js new file mode 100644 index 00000000..c5927d3b --- /dev/null +++ b/front/src/front/static/js/components/Tournaments.js @@ -0,0 +1,22 @@ +import {Component} from "./Component.js"; + +export class Tournaments extends Component { + constructor() { + super(); + } + render() { + return (` + +

Tournaments

+ `); + } + style() { + return (` + + `); + } +} + +export default { Tournaments }; \ No newline at end of file diff --git a/front/src/front/static/js/main.js b/front/src/front/static/js/main.js new file mode 100644 index 00000000..602ba0f3 --- /dev/null +++ b/front/src/front/static/js/main.js @@ -0,0 +1,37 @@ +import { Router, Route } from './Router.js'; +import { Home } from "./components/Home.js"; +import { Navbar } from "./components/Navbar.js"; +import {Singleplayer} from "./components/Singleplayer.js"; +import {Tournaments} from "./components/Tournaments.js"; +import {Multiplayer} from "./components/Multiplayer.js"; +import {Theme} from "./Theme.js"; +import {ThemeButton} from "./components/ThemeButton.js"; +import {Signin} from "./components/Signin.js"; +import {Signup} from "./components/Signup.js"; +import {ResetPassword} from "./components/ResetPassword.js"; + +Theme.init(); + +customElements.define('home-component', Home); +customElements.define('navbar-component', Navbar); +customElements.define('singleplayer-component', Singleplayer); +customElements.define('multiplayer-component', Multiplayer); +customElements.define('tournaments-component', Tournaments); +customElements.define('theme-button-component', ThemeButton); +customElements.define('signin-component', Signin); +customElements.define('signup-component', Signup); +customElements.define('reset-password-component', ResetPassword); + +const app = document.querySelector('#app'); + +const router = new Router(app, [ + new Route('/singleplayer/', 'singleplayer-component'), + new Route('/multiplayer/', 'multiplayer-component'), + new Route('/tournaments/', 'tournaments-component'), + new Route('/signin/', 'signin-component'), + new Route('/signup/', 'signup-component'), + new Route('', 'home-component'), + new Route('/reset-password/', 'reset-password-component'), +]); + +window.router = router; diff --git a/front/src/front/static/js/script.js b/front/src/front/static/js/script.js deleted file mode 100644 index 6defb1c6..00000000 --- a/front/src/front/static/js/script.js +++ /dev/null @@ -1,85 +0,0 @@ -let isDarkMode = false; - -const HOST = window.location.protocol + '//' - + window.location.host + '/'; - -const navbarBrandElement = document.querySelector('.navbar-brand'); -navbarBrandElement.addEventListener('click', homeNav); -history.pushState({'previousComponent': '/'}, null, HOST); - -const logPart = document.querySelector('#log-part'); -const logoutPart = document.querySelector('#logout-part') -const navUsername = document.querySelector('#nav-username'); -const navProfileImg = document.querySelector('#nav-profile-img'); -const body = document.querySelector('body'); -const switchButton = document.querySelector('#switch-btn'); - -function switchMode() { - if (!isDarkMode) { - body.setAttribute('data-bs-theme', 'dark'); - switchButton.classList.remove('btn-outline-dark'); - switchButton.setAttribute('class', 'btn btn-outline-light') - isDarkMode = true; - } else { - body.setAttribute('data-bs-theme', 'light'); - switchButton.classList.remove('btn-outline-light'); - switchButton.setAttribute('class', 'btn btn-outline-dark'); - isDarkMode = false; - } -} - -async function loadComponent(uri, parentId, setState = true) { - try { - if (setState) { - history.pushState({'previousComponent': uri}, null, HOST); - } - const response = await fetch(HOST + uri); - if (!response.ok) { - console.error('Request failed'); - return false; - } - const html = await response.text(); - const contentDiv = document.querySelector('#' + parentId); - contentDiv.innerHTML = html; - evalScript(parentId); - } catch (error) { - console.error('Error:', error); - return false; - } - return true; -} - -function evalScript(containerId) { - const scripts = document.querySelectorAll(`#${containerId} > script`); - for (let i = 0; i < scripts.length; i++) { - eval(scripts[i].innerText); - } -} - -function homeNav(event) { - const contentDiv = document.querySelector('#content'); - contentDiv.innerHTML = ''; - return false; -} - -function logNav() { - const username = 'tdameros'; - const profileImg = 'static/img/tdameros.jpg'; - navUsername.textContent = username; - navProfileImg.src = profileImg; - logoutPart.classList.add('d-none'); - logPart.classList.remove('d-none'); - const logoutButton = document.querySelector('#logout'); - logoutButton.addEventListener('click', logoutNav); -} - -function logoutNav() { - logPart.classList.add('d-none'); - logoutPart.classList.remove('d-none'); -} - -window.addEventListener('popstate', function (event) { - if (event.state != null) { - loadComponent(event.state['previousComponent'], 'content', false); - } -}); diff --git a/front/src/front/static/js/utils/BootstrapUtils.js b/front/src/front/static/js/utils/BootstrapUtils.js new file mode 100644 index 00000000..c2231aaa --- /dev/null +++ b/front/src/front/static/js/utils/BootstrapUtils.js @@ -0,0 +1,15 @@ +export class BootstrapUtils { + + static setInvalidInput(input) { + input.classList.remove('is-valid'); + input.classList.add('is-invalid'); + } + + static setValidInput(input) { + input.classList.remove('is-invalid'); + input.classList.add('is-valid'); + } + +} + +export default { BootstrapUtils }; \ No newline at end of file diff --git a/front/src/front/static/js/utils/InputValidator.js b/front/src/front/static/js/utils/InputValidator.js new file mode 100644 index 00000000..678361de --- /dev/null +++ b/front/src/front/static/js/utils/InputValidator.js @@ -0,0 +1,81 @@ +export class InputValidator { + + static usernameMaxLength = 20; + static usernameMinLength = 2; + static passwordMaxLength = 20; + static passwordMinLength = 8; + static emailMaxLength = 60; + static emailLocalMinLength = 5; + + constructor() { + + } + + static isAlphaNumeric(string) { + const alphaNumericRegex = /^[a-zA-Z0-9_]+$/; + return alphaNumericRegex.test(string); + } + + static #createValidityObject(missingRequirements) { + if (missingRequirements.length !== 0) + return {validity: false, missingRequirements}; + return {validity: true, missingRequirements}; + } + + static isValidEmail(email) { + const emailRegex = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/; + const missingRequirements = []; + if (email.length > InputValidator.emailMaxLength) { + missingRequirements.push(`Email must contain a maximum of ${InputValidator.emailMaxLength} characters.`); + } + if (!emailRegex.test(email)) { + missingRequirements.push('Invalid email format.'); + } + const emailLocal = email.split('@')[0]; + if (emailLocal.length < InputValidator.emailLocalMinLength) { + missingRequirements.push(`Email local part must contain at least ${InputValidator.emailLocalMinLength} characters.`); + } + return InputValidator.#createValidityObject(missingRequirements); + } + + static isValidUsername(username) { + const missingRequirements = []; + if (username.length < InputValidator.usernameMinLength) { + missingRequirements.push(`Username must be at least ${InputValidator.usernameMinLength} characters long.`); + } + if (username.length > InputValidator.usernameMaxLength) { + missingRequirements.push(`Username must contain a maximum of ${InputValidator.usernameMaxLength} characters.`); + } + if (!InputValidator.isAlphaNumeric(username)) { + missingRequirements.push('Username must contain only alphanumeric characters.'); + } + return InputValidator.#createValidityObject(missingRequirements); + } + + static isValidSecurePassword(password) { + const uppercaseRegex = /[A-Z]/; + const numberRegex = /[0-9]/; + const specialCharacterRegex = /[!@#$%^&*()_+]/; + const missingRequirements = []; + + if (password.length < InputValidator.passwordMinLength) { + missingRequirements.push(`Password must be at least ${InputValidator.passwordMinLength} characters long.`); + } + if (password.length > InputValidator.passwordMaxLength) { + missingRequirements.push(`Password must contain a maximum of ${InputValidator.passwordMaxLength} characters.`); + } + if (!uppercaseRegex.test(password)) { + missingRequirements.push('Password must contain at least one uppercase letter.'); + } + if (!numberRegex.test(password)) { + missingRequirements.push('Password must contain at least one number.'); + } + if (!specialCharacterRegex.test(password)) { + missingRequirements.push('Password must contain at least one special character.'); + } + return InputValidator.#createValidityObject(missingRequirements); + } + +} + +export default {InputValidator}; \ No newline at end of file diff --git a/front/src/front/templates/component.html b/front/src/front/templates/component.html deleted file mode 100644 index 844eedc5..00000000 --- a/front/src/front/templates/component.html +++ /dev/null @@ -1,17 +0,0 @@ -{% block html %} -{% endblock %} -{% if css %} - -{% endif %} - -{% if js %}} - -{% endif %} \ No newline at end of file diff --git a/front/src/front/templates/index.html b/front/src/front/templates/index.html index 9763576f..17235fb9 100644 --- a/front/src/front/templates/index.html +++ b/front/src/front/templates/index.html @@ -11,16 +11,15 @@ crossorigin="anonymous"> - + - - + -{% include 'navbar.html' %} -
+
\ No newline at end of file diff --git a/front/src/front/templates/navbar.html b/front/src/front/templates/navbar.html deleted file mode 100644 index 75c0bd39..00000000 --- a/front/src/front/templates/navbar.html +++ /dev/null @@ -1,72 +0,0 @@ -{% load static %} - \ No newline at end of file diff --git a/front/src/front/urls.py b/front/src/front/urls.py index d2d3467a..a6f0e454 100644 --- a/front/src/front/urls.py +++ b/front/src/front/urls.py @@ -15,11 +15,10 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include +from django.urls import re_path, include from .views import home urlpatterns = [ - path('admin/', admin.site.urls), - path('', home), - path('auth/', include("auth_components.urls")), + # path('admin/', admin.site.urls), + re_path(r'^', home), ]