From 825d642738f925b0dcc7da0dd105237bb25dedae Mon Sep 17 00:00:00 2001 From: Andros Fenollosa Date: Thu, 29 Feb 2024 17:08:43 +0100 Subject: [PATCH] Add nav --- assets/css/main.css | 284 +++++++++++++------ assets/img/quickstart/minimal-template.webp | Bin 0 -> 5852 bytes one.org | 285 ++++++++++++++++++++ onerc.el | 23 +- 4 files changed, 498 insertions(+), 94 deletions(-) create mode 100644 assets/img/quickstart/minimal-template.webp 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 0000000000000000000000000000000000000000..e17514d090568cf1cc7a42573ddfa4e13f832d2a GIT binary patch literal 5852 zcmeHKiC+`f7QR7HTo7$taCs0S?j)HkKoSY4Y{rtxrXu1pnM^Q}jVvsJ5}ylIuz;c< zqM#r?>jHv^N|DwDP#-9YTU9_QT3XydK?TXX6BccM_PsygdHG%Dn{&T&?z!KbJ9FbF z@bs)51HfXBxju`0IDta|0QRsCA%O+}1a59VStjrnm}JQ?xyBR$K(5fL{k-Oq1D6Dm z2R#Kt010+Wz(O&NYL1VOH^KgVYrhS-u-&8*u|CiFTdRc_*I)pU$dEA`Q>nEOuY}k! zLaQQh8pNZpRj3%^qY&GxVS*6fCG4Ue+(O_GBQ`E-lsdrA4X$kzw39gLVtL*gMA|K0s@Y1mu<%<2q= zw#fht+5y0aB!i)Izrpa~0009X0PvIIqrWl>0L~j|Px@%H%LKr3BLJr^ezb{F0Vs|G z!0Mg~Rinmv2EhL&5(xmUhXAlx0>IbTVf`%a(G89f;~@JE00B@d?z+Tdfp`=H^g<%!%PO;rT;>X}%@&MD5#i|kzzjJ## z*46QBbuFr-GPO1@r+dLVk9Yd04b6+MyS*)b6I-5nl{$hR&^_lvW^3X5 z`e*g|mMzx$wdC#{?^rLtd28rcaCKjZqM;phv$~2q>pK#EsechW(6zGLH#V-l@ZExr zyw(*D%e3e8WeKr}f%M^O$I{L+gW(X}VTI_$?UfaFK@|s8O6v6+p3ai(j7D~H;<%iI z$f`+&rM4*(?Y0+(#|>*5yw0^1t0OfJq-MojUF?7KlCQV-%C?*GYuPFgy@XsbGC%pr z#)UPFchZpNOL31wMyISqtOgdAAFQ%;epx0=YTcJ~*r#>qiMvuHaefs;Sv<`*?{>@k z9R7i4gEPK*XkmwyOD9>gJnzS3*mi662ZG9-CCRl<@~1nudYm0UuOi_u$@TIDR{?+LYNha{|iWxkuPj?1snd90j>^U5+ja zEj;@$^hoen*^8HY$%Ok~ACE_eHhdpfoiTgXDQ&LhnLr&S%qML4X0tQy5lOQwn$9$@ zD~#oRUobfm%iC~49w%%{JyH7d*S*DSGiNqtt=zquma!#zD$|Y=Yj`i2J zer|;}9*0*9n?K;BMfJJwW?$O4Z&^`S%hdTTl9~ni*1u=RWNx!Qp1aAaXk+P9>kSU3 zbIuiHn2A;Cn<)1(kfz+0y9xIiWUUmJG2^X*8#lFeEl53YG9bDrlQa5A*ZDXQocGN6 za_(^9St)Dp9^LVUPXa&av-IXEmt8{>62@}Y)#m7FGuP@wC&nlRmgWZ?ia6!rSrgZ% z2h|;wR+!p$6;$o9V=ap9u1AmCyg#xzyL_mxZ*$e{x|`h{P8SVtABb!~vUqt0dCJ`N z{!Izm&Xr%KALLJb_Y~xo6se;N^JPul8!^jmS*a~P%NyFGV#AZr{OIoV;TCg!8$?HL zS($V{-0tyeMR|8~>d1JTy@gv&jpu9XNANHGH1V>*bI3LAqQpsC_(|3m?H&BL%&YWt zJYsn2{Pwi9$@%BqTVp@q{BlLW+Qmc62G~BI@z_F~uwJ?= zBkudsD;)bS3Ua!dR&D=FT6~vs%?XL=mRM3{k>k<3gcblH=YJI+)aE%)%JJq;ndHjTG8)eCci&r9lczb!*t#dipF}odM(e|=eNwz_^|M|$=H$vI; z+nlSWt*`IfZOU;tE79#wz8D<#R`2-WAF*mD?;ji<9$udP{OL&N^y0R3bF*oGJW4*Y zbKSVq?tc$F+Ws;u^2JqwSc8y95>+tYcAcF zU%zek)BA_d-Spp)9bc1zyaD6zsfH+0@P}iQFQht&Do>wk-amFEdS~m-^5R+g%d15r z*NKPGe1hOvh-7L!vJ53b*oz;*5fjn?<7aI%#9u)ShmCza z$;^07fcF!6aSFuKdO5_YgF!L@7Vw$@I}>8!@bLre(zu?v&OuBxbK~DUh=}NYS1*Ck zhs>lg5E=`fNSFwd#bY2mgh6IF@enQ#VL>)2>tjYlq(JEMau@*4u8}Gf3&pi$5gsB{ z@F_1!$|z*1m`_>kAVh>JH++?J-WoOTzvkNjY)vS}6;oVY$j*_xNV!UmYf*BfT&B?Q zBKZ_ltQ6rqhzT>DLN-#gp?nH4m77wHli4&j4WW8OO2e5H7(*?V@ci68dn3R*K4q0w ztK!k=5fKrz2o_DL4xux+TrM49(wR&uWKcCx3N0E*RcI#lMR12lK($n*l`0ivA|fhM z>a=_coWCbAxk@PP53JD8jA_y^rJNp#s^|lY>jM<)*~D`_one#wk9A-h130T zjZ&w^aF1|Yp`Fy{PCSTyZ(4=#G?tBW7AWy8sq+*^CiV%?q;b06Z z2N83qY&OoKa@ZUvD$@aRa$s?ojuHn(UmSr#qeT@MPQ-zcX;K&jVPGOef+AEdOTwnI zMF>t6IXK`{oXr+V99a%*CP&=ozd$X86^F_`&4-91hH*q(oFx|F9I6Ax!KiGO2xjDj zF{vUav6#!i8K@ZJP{@Sdyg5DsK7~m`dT)JXs8*s>%lQ;Z;ly!p>}4#%)f!ksL@47%qL9TH&l3hhVL?dcXb4V^z(pzwnGl`_bB79| zMAgBF#uf40)HteDssoft8K2U>Cws<(a8HO)E$WVHaX3Z)77ggzqB9XD4}oid0`P=N zu~ZWEzo^76G(OLHQVpCos&_~E<6*r>CM6r|nTKLMd1}yb9ID@Agg=WEAvoOUu-1Ak z($73k5f|mMISe)x>J94wlZaqFIAT;Mlr3SPECz$c!utF*N{KcCRpWC)pfpecSX#!n z5_y{ONlfqKMXbUJnV|+$20}#`eHu9SYCwni(}}IzvlRMYS7{vkWZU=A;k$%@{jVk$ k@|X5SfiDVtQQ(ULUljPF!2h2DAAj873V70qfInUS0~aw_jQ{`u literal 0 HcmV?d00001 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 %}
+
+ {% include 'components/footer.html' %} +
+
+ + +#+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