diff --git a/assets/css/main.css b/assets/css/main.css index 8c6d1d5..18a53c7 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -17,6 +17,7 @@ body { color: var(--color-white); line-height: 1.5; scroll-behavior: smooth; + margin-top: 6rem; } /* Components */ @@ -42,7 +43,7 @@ a, .link { padding-block: .3rem; } -.link:hover { +a:hover, .link:hover { text-decoration: underline; } @@ -69,6 +70,10 @@ a, .link { object-position: center; } +.block-center { + margin-inline: auto; +} + .button { display: inline-block; min-width: 4rem; @@ -85,6 +90,28 @@ a, .link { .button:hover { border-color: var(--color-brown); box-shadow: 0 5px 0 var(--color-brown); + text-decoration: none; +} + +.header { + position: fixed; + inset-inline: 0; + top: 0; + border-bottom: 2px solid var(--color-brown); + background-color: var(--color-black); + overflow-x: auto; +} + +.header .nav__list { + flex-wrap: nowrap; + align-items: center; + justify-content: flex-end; +} + +@media (width < 600px) { + .header .nav__list { + justify-content: flex-start; + } } .nav__list { @@ -92,97 +119,186 @@ a, .link { flex-wrap: wrap; list-style: none; padding: 0; + margin: 0; } -.code__block { - background-color: var(--color-gray); - color: var(--color-black); - padding: 1rem; - border-radius: .5rem; - font-family: 'Fira Code', monospace; - overflow-x: auto; -} - -.code__line { - color: var(--color-brown); - font-family: 'Fira Code', monospace; -} - -.details { - margin-block: 1rem; - border: 2px solid var(--color-brown); -} - - -.details__title { - border-bottom: 2px solid var(--color-brown); -} - -.details__summary { - background-color: var(--color-brown); - color: var(--color-white); - padding: 1rem; - list-style-type: "🐱 "; -} - - -.details[open] > .details__summary { - list-style-type: "😺 "; -} - -.details__content { - padding: 1rem; -} - -/* Hero */ - -.hero__hgroup { - display: grid; - grid-template-columns: repeat(2, 1fr); - grid-template-rows: 1fr auto 1fr; - grid-template-areas: - "title image" - "subtitle image" - "nav image"; - grid-gap: 1rem; -} - -@media (width < 600px) { - .hero__hgroup { - grid-template-columns: 1fr; - grid-template-rows: repeat(3, auto); - grid-template-areas: - "title" - "image" - "subtitle" - "nav"; + .code__block { + background-color: var(--color-gray); + color: var(--color-black); + padding: 1rem; + border-radius: .5rem; + font-family: 'Fira Code', monospace; + overflow-x: auto; } -} -.hero__logo { - grid-area: image; -} + .code__line { + color: var(--color-brown); + font-family: 'Fira Code', monospace; + } -.hero__title { - grid-area: title; - align-self: end; -} + .details { + margin-block: 1rem; + border: 2px solid var(--color-brown); + } -.hero__subtitle { - grid-area: subtitle; -} -.nav-docs { - grid-area: nav; -} + .details__title { + border-bottom: 2px solid var(--color-brown); + } -/* Home */ -.nav-home__list { + .details__summary { + background-color: var(--color-brown); + color: var(--color-white); + padding: 1rem; + list-style-type: "🐱 "; + } + + + .details[open] > .details__summary { + list-style-type: "😺 "; + } + + .details__content { + padding: 1rem; + } + + /* Hero */ + + .hero__hgroup { + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: 1fr auto 1fr; + grid-template-areas: + "title image" + "subtitle image" + "nav image"; + grid-gap: 1rem; + } + + @media (width < 600px) { + .hero__hgroup { + grid-template-columns: 1fr; + grid-template-rows: repeat(3, auto); + grid-template-areas: + "title" + "image" + "subtitle" + "nav"; + } + } + + .hero__logo { + grid-area: image; + } + + .hero__title { + grid-area: title; + align-self: end; + } + + .hero__subtitle { + grid-area: subtitle; + } + + .nav-docs { + grid-area: nav; + } + + /* Home */ + .nav-home__list { justify-content: center; gap: var(--gap-l); -} + } -/* Footer */ -.footer { - text-align: center; -} + /* Footer */ + .footer { + text-align: center; + } + + /* ------- '.one' classes used by 'one-ox' org backend ------- */ + + .one-hl { + font-family: 'Fira Mono', monospace; + font-size: 80%; + border-radius: 6px; + } + + .one-hl-inline { + background: #31424a; + padding: 0.2em 0.4em; + margin: 0; + white-space: break-spaces; + } + + .one-hl-block { + background: #161f22; + color: #c5c5c5; + display: block; + overflow: auto; + padding: 16px; + line-height: 1.45; + } + + .one-blockquote { + background: #202d31; + border-left: 0.3em solid #31424a; + margin: 0px auto 16px; + padding: 1em 1em; + width: 90%; + } + + .one-blockquote > p:last-child { + margin-bottom: 0; + } + + .one-hl-results { + background: #202d31 ; + border-left: 2px solid #c5c5c5; + display: block; + margin: auto; + padding: 0.5em 1em; + overflow: auto; + width: 98%; + } + + .one-hl-negation-char { color: #ff6c60} /* font-lock-negation-char-face */ + .one-hl-warning { color: #fd971f} /* font-lock-warning-face */ + .one-hl-variable-name { color: #fd971f} /* font-lock-variable-name-face */ + .one-hl-doc { color: #d3b2a1} /* font-lock-doc-face */ + .one-hl-doc-string { color: #d3b2a1} /* font-lock-doc-string-face */ + .one-hl-string { color: #d3b2a1} /* font-lock-string-face */ + .one-hl-function-name { color: #02d2da} /* font-lock-function-name-face */ + .one-hl-builtin { color: #b2a1d3} /* font-lock-builtin-face */ + .one-hl-type { color: #457f8b} /* font-lock-type-face */ + .one-hl-keyword { color: #f92672} /* font-lock-keyword-face */ + .one-hl-preprocessor { color: #f92672} /* font-lock-preprocessor-face */ + .one-hl-comment-delimiter { color: #8c8c8c} /* font-lock-comment-delimiter-face */ + .one-hl-comment { color: #8c8c8c} /* font-lock-comment-face */ + .one-hl-constant { color: #f5ebb6} /* font-lock-constant-face */ + .one-hl-reference { color: #f5ebb6} /* font-lock-reference-face */ + .one-hl-regexp-grouping-backslash { color: #966046} /* font-lock-regexp-grouping-backslash */ + .one-hl-regexp-grouping-construct { color: #aa86ee} /* font-lock-regexp-grouping-construct */ + .one-hl-number { color: #eedc82} /* font-lock-number-face */ + + .one-hl-sh-quoted-exec { color: #62bd9c} /* sh-quoted-exec */ + + .one-hl-ta-colon-keyword {color: #62b5e0;} /* ta-colon-keyword-face */ + + + .one-hl-org-code { color: #dedede; background: #31424a; } + .one-hl-org-block { color: #c5c5c5 ; background: #31424a; } + .one-hl-org-block-begin-line { color: #c3957e; } + .one-hl-org-block-end-line { color: #c3957e; } + .one-hl-org-meta-line { color: #8c8c8c;} + .one-hl-org-quote { color: #c5c5c5} + .one-hl-org-drawer { color: #d3b2a1; font-size: 0.9em; } + .one-hl-org-special-keyword { color: #c3957e; font-size: 0.9em; } + .one-hl-org-property-value { color: #d2934a; font-size: 0.9em; } + .one-hl-org-level-1 { font-size: 1.7em; text-decoration: underline; } + .one-hl-org-level-2 { font-size: 1.4em; text-decoration: underline; } + .one-hl-org-level-3 { font-size: 1.2em; text-decoration: underline; } + .one-hl-org-level-4 { font-size: 1.1em; text-decoration: underline; } + .one-hl-org-level-5 { font-size: 1.0em; text-decoration: underline; } + .one-hl-org-level-6 { font-size: 1.0em; text-decoration: underline; } + .one-hl-org-level-8 { font-size: 1.0em; text-decoration: underline; } + .one-hl-org-level-8 { font-size: 1.0em; text-decoration: underline; } diff --git a/assets/img/quickstart/minimal-template.webp b/assets/img/quickstart/minimal-template.webp new file mode 100644 index 0000000..e17514d Binary files /dev/null and b/assets/img/quickstart/minimal-template.webp differ diff --git a/one.org b/one.org index f55a923..7faac1a 100644 --- a/one.org +++ b/one.org @@ -34,6 +34,291 @@ Are you ready to create your first Realtime SPA? Let's go to the [[#/docs/quicks :DESCRIPTION: Get started with Django LiveView the easy way. :END: +All the steps are applied in a [[https://github.com/Django-LiveView/minimal-template][minimalist template]]. + +** 1. Install Django + +Install Django, create a project and an app. + +** 2. Install LiveView + +Install django-liveview with ~pip~. + +#+BEGIN_SRC sh +pip install django-liveview +#+END_SRC + +** 3. Modify the configuration + +Add ~liveview~ to your installed ~INSTALLED_APPS~. + +#+BEGIN_SRC python +INSTALLED_APPS = [ + "daphne", + "channels", + "liveview", +] +#+END_SRC + +Then indicate in which previously created App you want to implement LiveView. + +#+BEGIN_SRC python +LIVEVIEW_APPS = ["website"] +#+END_SRC + +** 4. Migration + +Execute the migrations so that the LiveView tables are generated. + +#+BEGIN_SRC python +python manage.py migrate +#+END_SRC + +** 5. ASGI + +Modify the ASGI file, ~asgi.py~ to add the LiveView routing. In this example it is assumed that settings.py is inside core, in your case it may be different. + +#+BEGIN_SRC python +import os +import django + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") +django.setup() + +from channels.auth import AuthMiddlewareStack +from django.core.asgi import get_asgi_application +from channels.security.websocket import AllowedHostsOriginValidator +from channels.routing import ProtocolTypeRouter, URLRouter +from django.urls import re_path +from liveview.consumers import LiveViewConsumer + + +application = ProtocolTypeRouter( + { + # Django's ASGI application to handle traditional HTTP requests + "http": get_asgi_application(), + # WebSocket handler + "websocket": AuthMiddlewareStack( + AllowedHostsOriginValidator( + URLRouter([re_path(r"^ws/liveview/$", LiveViewConsumer.as_asgi())]) + ) + ), + } +) +#+END_SRC + +** 6. Create your first Action + +Place where the functions and logic of the business logic are stored. We will start by creating an action to generate a random number and print it. + +Create inside your App a folder called ~actions~, here will go all the actions for each page. Now we will create inside the folder a file named ~home.py~. + +#+BEGIN_SRC python +# my-app/actions/home.py +from liveview.context_processors import get_global_context +from core import settings +from liveview.utils import ( + get_html, + update_active_nav, + enable_lang, + loading, +) +from channels.db import database_sync_to_async +from django.templatetags.static import static +from django.urls import reverse +from django.utils.translation import gettext as _ +from random import randint + +template = "pages/home.html" + +# Database + +# Functions + +async def get_context(consumer=None): + context = get_global_context(consumer=consumer) + # Update context + context.update( + { + "url": settings.DOMAIN_URL + reverse("home"), + "title": _("Home") + " | Home", + "meta": { + "description": _("Home page of the website"), + "image": f"{settings.DOMAIN_URL}{static('img/seo/og-image.jpg')}", + }, + "active_nav": "home", + "page": template, + } + ) + return context + + +@enable_lang +@loading +async def send_page(consumer, client_data, lang=None): + # Nav + await update_active_nav(consumer, "home") + # Main + my_context = await get_context(consumer=consumer) + html = await get_html(template, my_context) + data = { + "action": client_data["action"], + "selector": "#main", + "html": html, + } + data.update(my_context) + await consumer.send_html(data) + +async def random_number(consumer, client_data, lang=None): + my_context = await get_context(consumer=consumer) + data = { + "action": client_data["action"], + "selector": "#output-random-number", + "html": randint(0, 10), + } + data.update(my_context) + await consumer.send_html(data) +#+END_SRC + +There are several points in the above code to keep in mind. + +- ~template~ is the name of the template that will be rendered. +- ~get_context()~ is a function that returns a dictionary with the context of the page. +- ~send_page()~ is the function that will be executed when the page is loaded. +- ~random_number()~ is the function that will be executed when the button is clicked. + +** 7. Create the base template + +Now we will create the base template, which will be the one that will be rendered when the page is loaded. + +Create a folder called ~templates~, or use your template folder, inside your App and inside it create another folder called ~layouts~. Now create a file called ~base.html~. + +#+BEGIN_SRC html +{# my-app/templates/layouts/base.html #} +{% load static i18n %} +{% get_current_language as CURRENT_LANGUAGE %} + + + + {{ title }} + + + + + + +
+
+
+
+
+ {% include 'components/header.html' %} +
+
{% include page %}
+ +
+ + +#+END_SRC + +In the future we will define ~main.js~, a minimal JavaScript to connect the events and the WebSockets client. + +** 8. Create the page template + +We will create the home page template, which will be the one that will be rendered when the page is loaded. + +Create a folder called ~pages~ in your template folder and inside it create a file called ~home.html~. + +#+BEGIN_SRC html +{# my-app/templates/pages/home.html #} +{% load static %} + +
+

+ +

+

+
+#+END_SRC + +As you can see, we have defined a button to launch the action of generating the random number (~button~) and the place where we will print the result (~output-random-number~). + +** 9. Create frontend + +Now we are going to create the frontend, the part where we will manage the JavaScript events and invoke the actions. + +Download [[https://github.com/Django-LiveView/assets/archive/refs/heads/main.zip][assets]] and unzip it in your static folder. You will be left with the following route: ~/static/js/~. + +** 10. Create View + +We will create the view that will render the page for the first time (like Server Side Rendering). The rest of the times will be rendered dynamically (like Single Page Application). + +In a normal Django application we would create a view, ~views.py~, similar to the following: + +#+BEGIN_SRC python +# my-app/views.py +from django.shortcuts import render + +# Create your views here. +def home(request): + return render(request, "pages/home.html") +#+END_SRC + +With LiveView, on the other hand, you will have the following structure. + +#+BEGIN_SRC python +# my-app/views.py +from django.shortcuts import render +from .actions.home import get_context as get_home_context + +from liveview.utils import get_html + +async def home(request): + return render(request, "layouts/base.html", await get_home_context()) +#+END_SRC + +** 11. Create URL + +Finally, we will create the URL that will render the page. + +#+BEGIN_SRC python +# my-app/urls.py +from django.urls import path + +from .views import home + +urlpatterns = [ + path("", home, name="home"), +] +#+END_SRC + +** 12. Run the server + +Run the server. + +#+BEGIN_SRC sh +python manage.py runserver +#+END_SRC + +And open the browser at ~http://localhost:8000/~. You should see the home page with a button that generates a random number. + +#+ATTR_HTML: :class block-center +[[#/img/quickstart/minimal-template.webp][Random number]] + * Source code :PROPERTIES: :ONE: one-custom-default-doc diff --git a/onerc.el b/onerc.el index 00a231c..f3e7d8c 100644 --- a/onerc.el +++ b/onerc.el @@ -24,6 +24,18 @@ (:link (@ :rel "stylesheet" :type "text/css" :href "https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css")) (:link (@ :rel "stylesheet" :type "text/css" :href "/css/main.css"))) (:body + (:header.header + (:div.container + (:nav.nav-main + (:ul.nav__list.nav-main__list + (:li.nav-main__item + (:a.button.nav-main__link (@ :href "/docs/quickstart/") "Docs")) + (:li.nav-main__item + (:a.button.nav-main__link (@ :href "/tutorials/") "Tutorials")) + (:li.nav-main__item + (:a.button.nav-main__link (@ :href "https://github.com/Django-LiveView/" :target "_blank") "Source code")) + (:li.nav-main__item + (:a.button.nav-main__link (@ :href "https://django-liveview-demo.andros.dev/" :target "_blank") "Demo")))))) ,tree-content (:footer.footer (:p "Created with ❤️ by " (:a.link (@ :href "https://andros.dev/" :target "_blank") "Andros Fenollosa")) @@ -49,15 +61,6 @@ (:h1.hero__title "Django LiveView") (:h2.hero__subtitle "Framework for creating Realtime SPAs using HTML over the Wire technology") (:img.image.hero__logo (@ :alt "pet" :src "img/pet.webp"))))) - (:nav.nav-home - (:div.container - (:ul.nav__list.nav-home__list - (:li.nav-home__item - (:a.button.nav-home__link (@ :href "/docs/quickstart/") "Docs")) - (:li.nav-home__item - (:a.button.nav-home__link (@ :href "/school/make-a-blog/") "Tutorials")) - (:li.nav-home__item - (:a.button.nav-home__link (@ :href "https://django-liveview-demo.andros.dev/" :target "_blank") "Demo"))))) (:section (:div.container ,content))))))) @@ -71,7 +74,7 @@ 'one-ox nil)) (website-name (one-default-website-name pages)) (nav (one-default-nav path pages))) -(render-layout-html + (render-layout-html title description (jack-html `(:main.main