First commit

This commit is contained in:
Andros Fenollosa 2022-04-19 21:14:01 +02:00
parent 8a9aebdf89
commit 12ba5461fd
37 changed files with 3082 additions and 0 deletions

18
Caddyfile Normal file
View File

@ -0,0 +1,18 @@
http://hello.localhost {
root * /usr/src/app/
encode gzip zstd
@notStatic {
not path /static/* /media/*
}
reverse_proxy @notStatic django:8000
file_server /static/*
file_server /media/*
}
http://webmail.localhost {
reverse_proxy mailhog:8025
}

16
Dockerfile Normal file
View File

@ -0,0 +1,16 @@
# Image
FROM python:3.10
# Display the Python output through the terminal
ENV PYTHONUNBUFFERED: 1
# Set work directory
WORKDIR /usr/src/app
# Add Python dependencies
## Update pip
RUN pip install --upgrade pip
## Copy requirements
COPY requirements.txt ./requirements.txt
## Install requirements
RUN pip3 install -r requirements.txt

0
app/__init__.py Normal file
View File

View File

116
app/app_template/actions.py Normal file
View File

@ -0,0 +1,116 @@
from .forms import LoginForm, SignupForm
from asgiref.sync import async_to_sync
from django.template.loader import render_to_string
from django.urls import reverse
from channels.auth import login, logout
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
from datetime import datetime
def send_page(self, page):
"""Render HTML and send page to client"""
# Prepare context data for page
context = {}
match page:
case "login":
context = {"form": LoginForm()}
case "signup":
context = {"form": SignupForm()}
# Add user to context if logged in
if "user" in self.scope:
context.update({ "user": self.scope["user"]})
context.update({"active_nav": page})
# Render HTML nav and send to client
self.send_html({
"selector": "#nav",
"html": render_to_string("components/_nav.html", context),
})
# Render HTML page and send to client
self.send_html({
"selector": "#main",
"html": render_to_string(f"pages/{page}.html", context),
"url": reverse(page),
})
# Hidrate page
match page:
case "home":
update_TODO(self)
def action_signup(self, data):
"""Sign up user"""
form = SignupForm(data)
user_exist = User.objects.filter(email=data["email"]).exists()
if form.is_valid() and data["password"] == data["password_confirm"] and not user_exist:
# Create user
user = User.objects.create_user(data["username"], data["email"], data["password"])
user.is_active = True
user.save()
# Login user
send_page(self, "login")
else:
# Send form errors
self.send_html({
"selector": "#main",
"html": render_to_string("pages/signup.html", {"form": form, "user_exist": user_exist, "passwords_do_not_match": data["password"] != data["password_confirm"]}),
"append": False,
"url": reverse("signup")
})
def action_login(self, data):
"""Log in user"""
form = LoginForm(data)
user = authenticate(username=data["email"], password=data["password"])
if form.is_valid() and user:
async_to_sync(login)(self.scope, user)
self.scope["session"].save()
send_page(self, "profile")
else:
self.send_html({
"selector": "#main",
"html": render_to_string("pages/login.html", {"form": form, "user_does_not_exist": user is None}),
"append": False,
"url": reverse("login")
})
def action_logout(self):
"""Log out user"""
async_to_sync(logout)(self.scope)
self.scope["session"].save()
send_page(self, "login")
def add_lap(self):
"""Add lap to Home page"""
# Send current time to client
self.send_html({
"selector": "#laps",
"html": render_to_string("components/_lap.html", {"time": datetime.now()}),
"append": True,
})
def add_task(self, data):
"""Add task from TODO section"""
# Add task to list
self.scope["session"]["tasks"].append(data["task"])
self.scope["session"].save()
# Update task list
update_TODO(self)
def update_TODO(self):
"""Update TODO list"""
self.send_html({
"selector": "#todo",
"html": render_to_string("components/_tasks.html", {"tasks": self.scope["session"]["tasks"]}),
"append": False,
})

6
app/app_template/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class SimpleAppConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "app.app_template"

View File

@ -0,0 +1,35 @@
from django.contrib.auth.models import User
from django.contrib.auth.backends import BaseBackend
class EmailBackend(BaseBackend):
"""
Email authentication backend
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
Authenticate a user based on email address as the user name.
"""
if "@" in username:
kwargs = {"email": username}
else:
kwargs = {"username": username}
try:
user = User.objects.get(**kwargs)
if user.check_password(password):
return user
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
# Future re-implementation with token for auto login
class TokenBackend(BaseBackend):
def authenticate(self, request, token=None):
pass

View File

@ -0,0 +1,55 @@
# app/app_template/consumers.py
from channels.generic.websocket import JsonWebsocketConsumer
import app.app_template.actions as actions
class ExampleConsumer(JsonWebsocketConsumer):
def connect(self):
"""Event when client connects"""
# Accept the connection
self.accept()
# Make session task list
if "tasks" not in self.scope["session"]:
self.scope["session"]["tasks"] = []
self.scope["session"].save()
def disconnect(self, close_code):
"""Event when client disconnects"""
pass
def receive_json(self, data_received):
"""
Event when data is received
All information will arrive in 2 variables:
"action", with the action to be taken
"data" with the information
"""
# Get the data
data = data_received["data"]
# Depending on the action we will do one task or another.
match data_received["action"]:
case "Change page":
actions.send_page(self, data["page"])
case "Signup":
actions.action_signup(self, data)
case "Login":
actions.action_login(self, data)
case "Logout":
actions.action_logout(self)
case "Add lap":
actions.add_lap(self)
case "Add task":
actions.add_task(self, data)
def send_html(self, event):
"""Event: Send html to client"""
data = {
"selector": event["selector"],
"html": event["html"],
"append": "append" in event and event["append"],
"url": event["url"] if "url" in event else "",
}
self.send_json(data)

39
app/app_template/forms.py Normal file
View File

@ -0,0 +1,39 @@
from django import forms
class LoginForm(forms.Form):
email = forms.CharField(
label="Email",
max_length=255,
widget=forms.EmailInput(attrs={"id": "login-email", "class": "input"}),
)
password = forms.CharField(
label="Password",
max_length=255,
widget=forms.PasswordInput(attrs={"id": "login-password", "class": "input"}),
)
class SignupForm(forms.Form):
username = forms.CharField(
label="Username",
max_length=255,
widget=forms.TextInput(attrs={"id": "signup-username", "class": "input"}),
)
email = forms.EmailField(
label="Email",
max_length=255,
widget=forms.EmailInput(attrs={"id": "signup-email", "class": "input"}),
)
password = forms.CharField(
label="Password",
max_length=255,
widget=forms.PasswordInput(attrs={"id": "signup-password", "class": "input"}),
)
password_confirm = forms.CharField(
label="Confirm Password",
max_length=255,
widget=forms.PasswordInput(
attrs={"id": "signup-password-confirm", "class": "input"}
),
)

View File

View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -0,0 +1,23 @@
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>Example website</title>
<link rel="stylesheet" href="{% static 'css/main.css' %}">
<script defer src="{% static 'js/index.js' %}"></script>
</head>
<body
data-host="{{ request.get_host }}"
data-scheme="{{ request.scheme }}"
>
<div class="container">
<header>
<nav id="nav" class="nav">{% include 'components/_nav.html' %}</nav>
</header>
<main id="main">{% include page %}</main>
<footer class="footer">My footer</footer>
</div>
</body>
</html>

View File

@ -0,0 +1,54 @@
<ul class="nav__ul" data-controller="navbar">
{# Links always visible #}
<li>
<a
href="#"
class="nav__link nav__link--page{% if active_nav == "home" %} active{% endif %}"
data-target="home"
>
Home
</a>
</li>
{% if user.is_authenticated %}
{# Links only if identified #}
<li>
<a
href="#"
class="nav__link nav__link--page{% if active_nav == "profile" %} active{% endif %}"
data-target="profile"
>
Profile
</a>
</li>
<li>
<a
href="#"
class="nav__link"
id="logout"
>
Logout
</a>
</li>
{% else %}
{# Links only if not identified #}
<li>
<a
href="#"
class="nav__link nav__link--page{% if active_nav == "login" %} active{% endif %}"
data-target="login"
>
Login
</a>
</li>
<li>
<a
href="#"
class="nav__link nav__link--page{% if active_nav == "signup" %} active{% endif %}"
data-target="signup"
data-action="click->message#displayUpdateForm"
>
Signup
</a>
</li>
{% endif %}
</ul>

View File

@ -0,0 +1,3 @@
{% for task in tasks %}
<li>{{ task }}</li>
{% endfor %}

View File

@ -0,0 +1 @@
<h1>404</h1>

View File

@ -0,0 +1,17 @@
<section>
<h1>Welcome to an example of browsing with WebSockets over the Wire</h1>
<p>You will be able to experience a simple structure with a registration, a login and a private page.</p>
</section>
<section>
<h2>Laps</h2>
<p>
<button id="add-lap">Add lap</button>
</p>
<ul id="laps"></ul>
</section>
<section>
<h2>TODO</h2>
<input type="text" id="task">
<button id="add-task">Add task</button>
<ul id="todo"></ul>
</section>

View File

@ -0,0 +1,8 @@
<h1>Login</h1>
<form id="login-form">
{% if user_does_not_exist %}
<h2>The user does not exist or the password is wrong.</h2>
{% endif %}
{{ form.as_p }}
<input type="submit" class="button" value="Login">
</form>

View File

@ -0,0 +1,5 @@
<h1>Profile</h1>
<h2>Username</h2>
<p>{{ user.username }}</p>
<h2>Email</h2>
<p>{{ user.email }}</p>

View File

@ -0,0 +1,11 @@
<h1>Signup</h1>
<form id="signup-form">
{% if user_exist %}
<p>Email already exist</p>
{% endif %}
{% if passwords_do_not_match %}
<p>Passwords do not match.</p>
{% endif %}
{{ form.as_p }}
<input type="submit" class="button" value="Signup">
</form>

41
app/app_template/views.py Normal file
View File

@ -0,0 +1,41 @@
from django.shortcuts import render, redirect
from .forms import LoginForm, SignupForm
from django.contrib.auth.decorators import login_required
def home(request):
return render(
request,
"base.html",
{
"page": "pages/home.html",
"active_nav": "home",
},
)
def login(request):
return render(
request,
"base.html",
{"page": "pages/login.html", "active_nav": "login", "form": LoginForm()},
)
def signup(request):
return render(
request,
"base.html",
{"page": "pages/signup.html", "active_nav": "signup", "form": SignupForm()},
)
@login_required
def profile(request):
return render(
request, "base.html", {"page": "pages/profile.html", "active_nav": "profile"}
)
def page_not_found(request, exception):
return render(request, "base.html", {"page": "pages/404.html"})

12
django-launcher.sh Normal file
View File

@ -0,0 +1,12 @@
#!/bin/sh
# Collect static files
python3 manage.py collectstatic --noinput
# Apply database migrations
python3 manage.py migrate
# Start server with debug mode
python3 manage.py runserver 0.0.0.0:8000
# Start server with production mode
#daphne -b 0.0.0.0 -p 8000 project_template.asgi:application

74
docker-compose.yaml Normal file
View File

@ -0,0 +1,74 @@
version: '3.8'
services:
django:
build:
context: ./
dockerfile: ./Dockerfile
entrypoint: bash ./django-launcher.sh
volumes:
- .:/usr/src/app/
environment:
DEBUG: "True"
ALLOWED_HOSTS: hello.localhost
SECRET_KEY: mysecret
DB_ENGINE: django.db.backends.postgresql
DB_NAME: hello_db
DB_USER: postgres
DB_PASSWORD: postgres
DB_HOST: postgresql
DB_PORT: 5432
DOMAIN: hello.localhost
DOMAIN_URL: http://hello.localhost
REDIS_HOST: redis
REDIS_PORT: 6379
STATIC_URL: /static/
STATIC_ROOT: static
MEDIA_URL: /media/
DEFAULT_FROM_EMAIL: no-reply@hello.localhost
EMAIL_HOST: mailhog
EMAIL_USE_TLS: "False"
EMAIL_USE_SSL: "False"
EMAIL_PORT: 1025
EMAIL_USER:
EMAIL_PASSWORD:
expose:
- 8000
depends_on:
- postgresql
- redis
postgresql:
image: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: hello_db
volumes:
- ./postgres_data:/var/lib/postgresql/data/
expose:
- 5432
redis:
image: redis:alpine
expose:
- 6379
caddy:
image: caddy:alpine
ports:
- 80:80
- 443:443
volumes:
- .:/usr/src/app/
- ./Caddyfile:/etc/caddy/Caddyfile
- ./caddy_data:/data
depends_on:
- django
mailhog:
image: mailhog/mailhog:latest
expose:
- 1025
- 8025

22
manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_template.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

View File

29
project_template/asgi.py Normal file
View File

@ -0,0 +1,29 @@
# project_template/asgi.py
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_template.settings")
from django.conf import settings
django.setup()
from django.core.asgi import get_asgi_application
from channels.security.websocket import OriginValidator
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import re_path
from app.app_template.consumers import ExampleConsumer
application = ProtocolTypeRouter(
{
# Django's ASGI application to handle traditional HTTP requests
"http": get_asgi_application(),
# WebSocket handler
"websocket": OriginValidator(AuthMiddlewareStack(
URLRouter(
[
re_path(r"^ws/example/$", ExampleConsumer.as_asgi()),
]
)
), settings.ALLOWED_HOSTS)
}
)

View File

@ -0,0 +1,160 @@
"""
Django settings for project_template project.
Generated by 'django-admin startproject' using Django 4.0.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get("DEBUG", "True") == "True"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS").split(",")
# Application definition
INSTALLED_APPS = [
"channels",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"app.app_template",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "project_template.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "project_template.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": os.environ.get("DB_ENGINE"),
"NAME": os.environ.get("DB_NAME"),
"USER": os.environ.get("DB_USER"),
"PASSWORD": os.environ.get("DB_PASSWORD"),
"HOST": os.environ.get("DB_HOST"),
"PORT": os.environ.get("DB_PORT"),
}
}
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_ROOT = os.environ.get("STATIC_ROOT")
STATIC_URL = os.environ.get("STATIC_URL")
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = os.environ.get("MEDIA_URL")
DOMAIN = os.environ.get("DOMAIN")
DOMAIN_URL = os.environ.get("DOMAIN_URL")
CSRF_TRUSTED_ORIGINS = [DOMAIN_URL]
"""EMAIL CONFIG"""
DEFAULT_FROM_EMAIL = os.environ.get("EMAIL_ADDRESS")
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS") == "True"
EMAIL_USE_SSL = os.environ.get("EMAIL_USE_SSL") == "True"
EMAIL_HOST = os.environ.get("EMAIL_HOST")
EMAIL_PORT = os.environ.get("EMAIL_PORT")
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [(os.environ.get("REDIS_HOST"), os.environ.get("REDIS_PORT"))],
},
},
}
ASGI_APPLICATION = "project_template.asgi.application"
AUTHENTICATION_BACKENDS = (
'app.app_template.backends.EmailBackend',
)

28
project_template/urls.py Normal file
View File

@ -0,0 +1,28 @@
"""project_template URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.0/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 app.app_template import views
urlpatterns = [
path("", views.home, name="home"),
path("login/", views.login, name="login"),
path("signup/", views.signup, name="signup"),
path("profile/", views.profile, name="profile"),
path("admin/", admin.site.urls),
]
handler404 = "app.app_template.views.page_not_found"

16
project_template/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for project_template project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_template.settings")
application = get_wsgi_application()

15
requirements.txt Normal file
View File

@ -0,0 +1,15 @@
# Django
django===4.0
# Django Server
daphne===3.0.2
asgiref===3.4.1
# Manipulate images
Pillow===8.2.0
# Kit utilities
django-extensions===3.1.3
# PostgreSQL driver
psycopg2===2.9.1
# Django Channels
channels==3.0.4
# Redis Layer
channels_redis===3.2.0

69
static/css/main.css Executable file
View File

@ -0,0 +1,69 @@
/* Global styles */
:root {
--color__background: #f6f4f3;
--color__gray: #ccc;
--color__black: #000;
--color__active: #00a0ff;
}
* {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
box-sizing: border-box;
}
body {
margin: 0;
background-color: var(--color__background);
}
/* General classes for small components */
.container {
margin: 0 auto;
padding: 1rem 0;
max-width: 40rem;
}
.nav__ul {
display: flex;
list-style: none;
padding: 0;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.nav__link.active {
color: var(--color__active);
text-decoration: none;
}
.button {
display: inline-block;
padding: 0.5rem 1rem;
background-color: var(--color__gray);
border: 0;
cursor: pointer;
text-decoration: none;
}
.button:hover {
filter: brightness(90%);
}
.input {
display: block;
width: 100%;
outline: none;
padding: .5rem;
resize: none;
border: 1px solid var(--color__gray);
box-sizing: border-box;
}
.footer {
margin-top: 1rem;
text-align: center;
}

View File

@ -0,0 +1,28 @@
import { Controller } from "../vendors/stimulus.js"
import { sendData } from "../webSocketsCli.js"
export default class extends Controller {
static targets = [ "author", "text" ]
/**
* Send new message
* @param {Event} event
* @return {void}
*/
add(event) {
event.preventDefault();
// Prepare the information we will send
const newData = {
"action": "add message",
"data": {
"author": this.authorTarget.value,
"text": this.textTarget.value
}
};
// Send the data to the server
sendData(newData, window.myWebSocket);
// Clear message form
this.textTarget.value = "";
}
}

View File

@ -0,0 +1,116 @@
import { Controller } from "../vendors/stimulus.js"
import { sendData } from "../webSocketsCli.js"
export default class extends Controller {
static targets = [ "item", "paginator" ]
connect() {
this.enableInfiniteScroll();
}
/*
FUNCTIONS
*/
/**
* Switches to the next page when the last message is displayed.
*/
enableInfiniteScroll() {
const lastMessage = this.itemTargets.at(-1);
// Turn the page when the last message is displayed.
const observerLastMessage = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (!this.isLastPage()) this.goToNextPage();
}
});
});
observerLastMessage.observe(lastMessage);
}
/**
* Get current page stored in #paginator as dataset
* @returns {number}
*/
getCurrentPage() {
return parseInt(this.paginatorTarget.dataset.page);
}
/**
* Check if we are on the last page
* @returns {boolean}
*/
isLastPage() {
return parseInt(this.paginatorTarget.dataset.totalPages) === this.getCurrentPage();
}
/**
* Switch to the next page
* @param {Event} event
* @return {void}
*/
goToNextPage(event) {
// Prepare the information we will send
const newData = {
"action": "list messages",
"data": {
"page": this.getCurrentPage() + 1,
}
};
// Send the data to the server
sendData(newData, myWebSocket);
}
/**
* Displays the update form
* @param {Event} event
* @return {void}
*/
displayUpdateForm(event) {
const message = {
"action": "open edit page",
"data": {
"id": event.target.dataset.id
}
};
sendData(message, window.myWebSocket);
}
/**
* Update message
* @param {Event} event
* @return {void}
*/
updateMessage(event) {
event.preventDefault();
const message = {
"action": "update message",
"data": {
"id": event.target.dataset.id,
"author": event.target.querySelector("#message-form__author--update").value,
"text": event.target.querySelector("#message-form__text--update").value
}
};
sendData(message, myWebSocket);
}
/**
* Delete message
* @param {Event} event
* @return {void}
*/
deleteMessage(event) {
const message = {
"action": "delete message",
"data": {
"id": event.target.dataset.id
}
};
sendData(message, window.myWebSocket);
}
}

View File

@ -0,0 +1,28 @@
import { Controller } from "../vendors/stimulus.js"
import { sendData } from "../webSocketsCli.js"
export default class extends Controller {
static targets = [ "page" ]
/**
* Send new message
* @param {Event} event
* @return {void}
*/
add(event) {
event.preventDefault();
// Prepare the information we will send
const newData = {
"action": "add message",
"data": {
"author": this.authorTarget.value,
"text": this.textTarget.value
}
};
// Send the data to the server
sendData(newData, window.myWebSocket);
// Clear message form
this.textTarget.value = "";
}
}

View File

@ -0,0 +1,25 @@
import { Controller } from "../vendors/stimulus.js"
import { sendData } from "../webSocketsCli.js"
export default class extends Controller {
static targets = [ "author", "text" ]
/**
* Update message
* @param {Event} event
* @return {void}
*/
update(event) {
event.preventDefault();
const message = {
"action": "update message",
"data": {
"id": event.target.dataset.id,
"author": this.authorTarget.value,
"text": this.textTarget.value
}
};
sendData(message, myWebSocket);
}
}

15
static/js/main.js Executable file
View File

@ -0,0 +1,15 @@
import {connect, startEvents} from './webSocketsCli.js';
import { Application } from "./vendors/stimulus.js"
import navbarController from "./controllers/navbar.js"
/*
INITIALIZATION
*/
// WebSocket connection
connect();
startEvents();
// Stimulus
window.Stimulus = Application.start();
// Register all controllers
Stimulus.register("navbar", navbarController);

1944
static/js/vendors/stimulus.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
/*
FUNCTIONS
*/
/**
* Connect to WebSockets server (SocialNetworkConsumer)
* @param {string} url - WebSockets server url
* @return {WebSocket}
*/
export function connect(url=`${document.body.dataset.scheme === 'http' ? 'ws' : 'wss'}://${ document.body.dataset.host }/ws/social-network/`) {
window.myWebSocket = new WebSocket(url);
return window.myWebSocket;
}
/**
* Send data to WebSockets server
* @param {string} message
* @param {WebSocket} webSocket
* @return {void}
*/
export function sendData(message, webSocket) {
webSocket.send(JSON.stringify(message));
}
/*
EVENTS
*/
/**
* On WebSockets server connection
* @param {WebSocket} webSocket
* @return {void}
*/
export function startEvents(webSocket=window.myWebSocket) {
// Event when a new message is received by WebSockets
webSocket.addEventListener("message", (event) => {
// Parse the data received
const data = JSON.parse(event.data);
// Renders the HTML received from the Consumer
const newFragment = document.createRange().createContextualFragment(data.html);
const target = document.querySelector(data.selector);
if (data.append) {
target.appendChild(newFragment);
} else {
target.replaceChildren(newFragment);
}
// Update URL
history.pushState({}, '', data.url)
});
}