├── home
├── __init__.py
├── apps.py
├── views
│ ├── __init__.py
│ ├── jinja.py
│ ├── stream.py
│ └── sse.py
├── templates
│ ├── chooser.html
│ ├── home
│ │ ├── _item.html
│ │ ├── home.html
│ │ └── home_sse.html
│ ├── shell.html
│ └── shell_htmx.html
└── recommendations.py
├── stream
├── __init__.py
├── asgi.py
├── wsgi.py
├── urls.py
└── settings.py
├── requirements.in
├── static
├── images
│ ├── offer.jpg
│ ├── avatar.png
│ ├── complete.png
│ ├── methods.png
│ ├── banner-bg.jpg
│ ├── favicon
│ │ ├── favicon.ico
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── apple-touch-icon.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── site.webmanifest
│ │ └── about.txt
│ ├── products
│ │ ├── product1.jpg
│ │ ├── product10.jpg
│ │ ├── product11.jpg
│ │ ├── product12.jpg
│ │ ├── product2.jpg
│ │ ├── product3.jpg
│ │ ├── product4.jpg
│ │ ├── product5.jpg
│ │ ├── product6.jpg
│ │ ├── product7.jpg
│ │ ├── product8.jpg
│ │ └── product9.jpg
│ ├── category
│ │ ├── category-1.jpg
│ │ ├── category-2.jpg
│ │ ├── category-3.jpg
│ │ ├── category-4.jpg
│ │ ├── category-5.jpg
│ │ └── category-6.jpg
│ ├── icons
│ │ ├── bed.svg
│ │ ├── phone.svg
│ │ ├── office.svg
│ │ ├── bed-2.svg
│ │ ├── terrace.svg
│ │ ├── delivery-van.svg
│ │ ├── sofa.svg
│ │ ├── restaurant.svg
│ │ ├── outdoor-cafe.svg
│ │ ├── money-back.svg
│ │ └── service-hours.svg
│ └── logo.svg
└── js
│ ├── htmx.1.9.4_dist_ext_sse.js
│ └── htmx.1.9.4.min.js
├── requirements.txt
├── manage.py
├── README.md
├── jinja
├── _item.html
├── home.html
└── shell.html
└── .gitignore
/home/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/stream/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.in:
--------------------------------------------------------------------------------
1 | django
2 | uvicorn
3 | jinja2
4 |
--------------------------------------------------------------------------------
/static/images/offer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/offer.jpg
--------------------------------------------------------------------------------
/static/images/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/avatar.png
--------------------------------------------------------------------------------
/static/images/complete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/complete.png
--------------------------------------------------------------------------------
/static/images/methods.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/methods.png
--------------------------------------------------------------------------------
/static/images/banner-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/banner-bg.jpg
--------------------------------------------------------------------------------
/static/images/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/favicon/favicon.ico
--------------------------------------------------------------------------------
/static/images/products/product1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/products/product1.jpg
--------------------------------------------------------------------------------
/static/images/products/product10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/products/product10.jpg
--------------------------------------------------------------------------------
/static/images/products/product11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/products/product11.jpg
--------------------------------------------------------------------------------
/static/images/products/product12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/products/product12.jpg
--------------------------------------------------------------------------------
/static/images/products/product2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/products/product2.jpg
--------------------------------------------------------------------------------
/static/images/products/product3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/products/product3.jpg
--------------------------------------------------------------------------------
/static/images/products/product4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/products/product4.jpg
--------------------------------------------------------------------------------
/static/images/products/product5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/products/product5.jpg
--------------------------------------------------------------------------------
/static/images/products/product6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/products/product6.jpg
--------------------------------------------------------------------------------
/static/images/products/product7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/products/product7.jpg
--------------------------------------------------------------------------------
/static/images/products/product8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/products/product8.jpg
--------------------------------------------------------------------------------
/static/images/products/product9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/products/product9.jpg
--------------------------------------------------------------------------------
/static/images/category/category-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/category/category-1.jpg
--------------------------------------------------------------------------------
/static/images/category/category-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/category/category-2.jpg
--------------------------------------------------------------------------------
/static/images/category/category-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/category/category-3.jpg
--------------------------------------------------------------------------------
/static/images/category/category-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/category/category-4.jpg
--------------------------------------------------------------------------------
/static/images/category/category-5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/category/category-5.jpg
--------------------------------------------------------------------------------
/static/images/category/category-6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/category/category-6.jpg
--------------------------------------------------------------------------------
/static/images/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/static/images/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/static/images/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/static/images/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/static/images/favicon/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyHAT-stack/web-async-patterns/HEAD/static/images/favicon/android-chrome-512x512.png
--------------------------------------------------------------------------------
/home/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class HomeConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "home"
7 |
--------------------------------------------------------------------------------
/home/views/__init__.py:
--------------------------------------------------------------------------------
1 | # Create your views here.
2 | from django.shortcuts import render
3 |
4 |
5 | def chooser(request):
6 | return render(request, 'chooser.html')
7 |
--------------------------------------------------------------------------------
/home/templates/chooser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/static/images/favicon/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/static/images/favicon/about.txt:
--------------------------------------------------------------------------------
1 | This favicon was generated using the following font:
2 |
3 | - Font Title: Roboto
4 | - Font Author: Copyright 2011 Google Inc. All Rights Reserved.
5 | - Font Source: http://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmWUlvAx05IsDqlA.ttf
6 | - Font License: Apache License, version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html))
7 |
--------------------------------------------------------------------------------
/stream/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for stream project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.asgi import get_asgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "stream.settings")
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/stream/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for stream project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "stream.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/home/recommendations.py:
--------------------------------------------------------------------------------
1 | customized_recommendations = [
2 | dict(name='Comfy Chair', discount_price=745.00, normal_price=800.00, review_count=1550, img='product1.jpg'),
3 | dict(name='Bed King Size', discount_price=1055.00, normal_price=1599.99, review_count=720, img='product4.jpg'),
4 | dict(name='Lounge pairs', discount_price=350.00, normal_price=499.99, review_count=952, img='product2.jpg'),
5 | dict(name='Air mattress', discount_price=189.99, normal_price=250.00, review_count=153, img='product3.jpg'),
6 | ]
7 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile with Python 3.11
3 | # by the following command:
4 | #
5 | # pip-compile requirements.in
6 | #
7 | asgiref==3.6.0
8 | # via django
9 | click==8.1.3
10 | # via uvicorn
11 | django==4.2.1
12 | # via -r requirements.in
13 | h11==0.14.0
14 | # via uvicorn
15 | jinja2==3.1.2
16 | # via -r requirements.in
17 | markupsafe==2.1.3
18 | # via jinja2
19 | sqlparse==0.4.4
20 | # via django
21 | uvicorn==0.22.0
22 | # via -r requirements.in
23 |
--------------------------------------------------------------------------------
/home/views/jinja.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from django.http import StreamingHttpResponse
4 | from django.template.loader import get_template
5 |
6 | from home.recommendations import customized_recommendations
7 |
8 |
9 | async def stream_homepage_content():
10 | for item in customized_recommendations:
11 | # Faking an expensive database query or slow API
12 | await asyncio.sleep(.7)
13 | yield item
14 |
15 |
16 | async def index(request):
17 | template = get_template('home.html')
18 | return StreamingHttpResponse(
19 | template.template.generate_async({
20 | 'recommendations': stream_homepage_content()
21 | })
22 | )
23 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "stream.settings")
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == "__main__":
22 | main()
23 |
--------------------------------------------------------------------------------
/home/views/stream.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from django.http import StreamingHttpResponse
4 | from django.template.loader import render_to_string
5 |
6 | from home.recommendations import customized_recommendations
7 |
8 |
9 | async def stream_homepage_content(request):
10 | pre_shell, post_shell = render_to_string('home/home.html', request=request).split('')
11 | yield pre_shell
12 | for item in customized_recommendations:
13 | await asyncio.sleep(.7) # Faking an expensive database query or slow API
14 | yield render_to_string('home/_item.html', dict(recommendation=item))
15 | yield post_shell
16 |
17 |
18 | async def index(request):
19 | return StreamingHttpResponse(stream_homepage_content(request))
20 |
--------------------------------------------------------------------------------
/home/views/sse.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import random
3 | import re
4 |
5 | from django.http import StreamingHttpResponse
6 | from django.shortcuts import render
7 | from django.template.loader import render_to_string
8 |
9 | from home.recommendations import customized_recommendations
10 |
11 |
12 | def index(request):
13 | return render(request, 'home/home_sse.html')
14 |
15 |
16 | async def sse_recommendation():
17 | recommendations = []
18 | for item in customized_recommendations:
19 | await asyncio.sleep(.7)
20 | content = render_to_string('home/_item.html', dict(recommendation=item))
21 | recommendations.append(
22 | re.sub('\n', '', content)
23 | )
24 | all_recommendations = ''.join(recommendations)
25 | yield f'data: {all_recommendations}\n\n'
26 |
27 |
28 | def handle_sse(request):
29 | return StreamingHttpResponse(streaming_content=sse_recommendation(), content_type='text/event-stream')
30 |
--------------------------------------------------------------------------------
/static/images/icons/bed.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/stream/urls.py:
--------------------------------------------------------------------------------
1 | """stream URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/4.1/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.conf import settings
17 | from django.conf.urls.static import static
18 | from django.contrib import admin
19 | from django.urls import path
20 |
21 | import home.views
22 | import home.views.stream
23 | import home.views.sse
24 | import home.views.jinja
25 |
26 | urlpatterns = [
27 | path('', home.views.chooser),
28 | path('stream', home.views.stream.index, name='index'),
29 | path('via-sse', home.views.sse.index, name='index_sse'),
30 | path('recommend-sse', home.views.sse.handle_sse, name='index_sse'),
31 | path('jinja', home.views.jinja.index, name='index_jinja'),
32 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
33 |
--------------------------------------------------------------------------------
/static/images/icons/phone.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/static/images/icons/office.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Proof of concept: Streaming HTML with Python
2 |
3 | Inspired by Taylor Hunt's incredible explanation of how he turned [a slow Fortune 20 webapp into a snappy experience, even on a cheap Android phone](https://dev.to/tigt/making-the-worlds-fastest-website-and-other-mistakes-56na), I made this repo to show the current capability of Django to render streaming HTML.
4 |
5 | This technique is best used to improve the perceptual performance for expensive database queries or slow API calls. The idea is that the user could start seeing the page come together while the query or calls are happening. Rendering of the page will pause once it hits the block that is waiting on the data, but being able to see the page render should make the user feel as though the website is performing faster.
6 |
7 | Referencing Taylor's blog post:
8 |
9 | > Both of [these pages](https://assets.codepen.io/183091/HTML+streaming+vs.+non.mp4) show search results in 2.5 seconds. But they sure don't _feel_ the same.
10 |
11 | This concept shows how a recommendation engine takes some time to recommend four products, based for the current user.
12 |
13 | ## Viewing the concept
14 |
15 | Open a terminal at the root of this project and type the following:
16 |
17 | ```shell
18 | python -m venv .venv --prompt stream_python
19 | # If in Windows:
20 | .venv/Scripts/activate
21 | # otherwise
22 | source .venv/bin/
23 | # Then
24 | pip install -r requirements.txt
25 | uvicorn stream.asgi:application --reload
26 | ```
27 |
28 | You can then click the link in the terminal or go to http://127.0.0.1:8000 to view the page.
29 |
30 |
31 | ## Thanks
32 |
33 | Thanks to https://github.com/fajar7xx/ecommerce-template-tailwind-1 for the HTML template.
34 |
--------------------------------------------------------------------------------
/static/images/icons/bed-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
--------------------------------------------------------------------------------
/static/images/icons/terrace.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/jinja/_item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
17 |
18 |
19 |
20 | {{ recommendation.name }}
21 |
22 |
23 |
${{ recommendation.discount_price }}
24 |
${{ recommendation.normal_price }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
({{ recommendation.review_count }})
35 |
36 |
37 |
Add
39 | to cart
40 |
41 |
--------------------------------------------------------------------------------
/home/templates/home/_item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
17 |
18 |
19 |
20 | {{ recommendation.name }}
21 |
22 |
23 |
${{ recommendation.discount_price | floatformat }}
24 |
${{ recommendation.normal_price | floatformat }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
({{ recommendation.review_count }})
35 |
36 |
37 |
Add
39 | to cart
40 |
41 |
--------------------------------------------------------------------------------
/static/images/icons/delivery-van.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/home/templates/home/home.html:
--------------------------------------------------------------------------------
1 | {% extends "shell.html" %}
2 |
3 | {% block main %}
4 | {% csrf_token %}
5 |
6 |
7 |
8 |
9 | best collection for
home decoration
10 |
11 |
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aperiam
12 | accusantium perspiciatis, sapiente
13 | magni eos dolorum ex quos dolores odio
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |

27 |
28 |
Free Shipping
29 |
Order over $200
30 |
31 |
32 |
33 |

34 |
35 |
Money Returns
36 |
30 days money returns
37 |
38 |
39 |
40 |

41 |
42 |
24/7 Support
43 |
Customer support
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
recommended for you
53 |
54 | {% for item in recommendations %}
55 | {% include 'home/_item.html' with recommendation=item %}
56 | {% endfor %}
57 |
58 |
59 |
60 |
61 |
62 | {% endblock %}
63 |
--------------------------------------------------------------------------------
/jinja/home.html:
--------------------------------------------------------------------------------
1 | {% extends "shell.html" %}
2 |
3 | {% block main %}
4 |
5 |
6 |
7 |
8 |
9 | best collection for
home decoration
10 |
11 |
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aperiam
12 | accusantium perspiciatis, sapiente
13 | magni eos dolorum ex quos dolores odio
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |

27 |
28 |
Free Shipping
29 |
Order over $200
30 |
31 |
32 |
33 |

34 |
35 |
Money Returns
36 |
30 days money returns
37 |
38 |
39 |
40 |

41 |
42 |
24/7 Support
43 |
Customer support
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
recommended for you
53 |
54 | {# {{ recommendations }}#}
55 | {% for recommendation in recommendations %}
56 | {% include '_item.html' %}
57 | {% endfor %}
58 |
59 |
60 |
61 |
62 |
63 | {% endblock %}
64 |
--------------------------------------------------------------------------------
/static/images/icons/sofa.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
60 |
--------------------------------------------------------------------------------
/static/images/icons/restaurant.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/home/templates/home/home_sse.html:
--------------------------------------------------------------------------------
1 | {% extends "shell_htmx.html" %}
2 |
3 | {% block main %}
4 |
5 |
6 |
7 |
8 |
9 | best collection for
home decoration
10 |
11 |
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aperiam
12 | accusantium perspiciatis, sapiente
13 | magni eos dolorum ex quos dolores odio
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |

27 |
28 |
Free Shipping
29 |
Order over $200
30 |
31 |
32 |
33 |

34 |
35 |
Money Returns
36 |
30 days money returns
37 |
38 |
39 |
40 |

41 |
42 |
24/7 Support
43 |
Customer support
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
recommended for you
53 |
57 | {% for item in recommendations %}
58 | {% include 'home/_item.html' with recommendation=item %}
59 | {% empty %}
60 |
66 | {% endfor %}
67 |
68 |
69 |
70 |
71 | {% endblock %}
72 |
--------------------------------------------------------------------------------
/stream/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for stream project.
3 |
4 | Generated by 'django-admin startproject' using Django 4.1.4.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/4.1/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/4.1/ref/settings/
11 | """
12 |
13 | from pathlib import Path
14 |
15 | from jinja2 import FileSystemLoader
16 |
17 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
18 | BASE_DIR = Path(__file__).resolve().parent.parent
19 |
20 | # Quick-start development settings - unsuitable for production
21 | # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
22 |
23 | # SECURITY WARNING: keep the secret key used in production secret!
24 | SECRET_KEY = "django-insecure-v2xyf4$&45iz^1-ltvlw66hs0n)chyqd!4wxv%d06g8773elh("
25 |
26 | # SECURITY WARNING: don't run with debug turned on in production!
27 | DEBUG = True
28 |
29 | ALLOWED_HOSTS = []
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | "django.contrib.admin",
35 | "django.contrib.auth",
36 | "django.contrib.contenttypes",
37 | "django.contrib.sessions",
38 | "django.contrib.messages",
39 | "django.contrib.staticfiles",
40 | 'home',
41 | ]
42 |
43 | MIDDLEWARE = [
44 | "django.middleware.security.SecurityMiddleware",
45 | "django.contrib.sessions.middleware.SessionMiddleware",
46 | "django.middleware.common.CommonMiddleware",
47 | "django.middleware.csrf.CsrfViewMiddleware",
48 | "django.contrib.auth.middleware.AuthenticationMiddleware",
49 | "django.contrib.messages.middleware.MessageMiddleware",
50 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
51 | ]
52 |
53 | ROOT_URLCONF = "stream.urls"
54 |
55 | TEMPLATES = [
56 | {
57 | "BACKEND": "django.template.backends.django.DjangoTemplates",
58 | "DIRS": [],
59 | "APP_DIRS": True,
60 | "OPTIONS": {
61 | "context_processors": [
62 | "django.template.context_processors.debug",
63 | "django.template.context_processors.request",
64 | "django.contrib.auth.context_processors.auth",
65 | "django.contrib.messages.context_processors.messages",
66 | ],
67 | },
68 | },
69 | {
70 | "BACKEND": "django.template.backends.jinja2.Jinja2",
71 | "DIRS": [
72 | BASE_DIR / 'jinja',
73 | ],
74 | 'OPTIONS': {
75 | 'enable_async': True,
76 | 'loader': FileSystemLoader(BASE_DIR / 'jinja')
77 | }
78 | },
79 | ]
80 |
81 | WSGI_APPLICATION = "stream.wsgi.application"
82 |
83 | # Database
84 | # https://docs.djangoproject.com/en/4.1/ref/settings/#databases
85 |
86 | # Password validation
87 | # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
88 |
89 | # Internationalization
90 | # https://docs.djangoproject.com/en/4.1/topics/i18n/
91 |
92 | LANGUAGE_CODE = "en-us"
93 |
94 | TIME_ZONE = "UTC"
95 |
96 | USE_I18N = True
97 |
98 | USE_TZ = True
99 |
100 | # Static files (CSS, JavaScript, Images)
101 | # https://docs.djangoproject.com/en/4.1/howto/static-files/
102 |
103 | STATIC_URL = "static/"
104 |
105 | STATIC_ROOT = BASE_DIR / "static"
106 |
107 | # Default primary key field type
108 | # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
109 |
110 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
111 |
--------------------------------------------------------------------------------
/static/images/logo.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/static/images/icons/outdoor-cafe.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .idea/
3 | ### Python template
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | share/python-wheels/
27 | *.egg-info/
28 | .installed.cfg
29 | *.egg
30 | MANIFEST
31 |
32 | # PyInstaller
33 | # Usually these files are written by a python script from a template
34 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
35 | *.manifest
36 | *.spec
37 |
38 | # Installer logs
39 | pip-log.txt
40 | pip-delete-this-directory.txt
41 |
42 | # Unit test / coverage reports
43 | htmlcov/
44 | .tox/
45 | .nox/
46 | .coverage
47 | .coverage.*
48 | .cache
49 | nosetests.xml
50 | coverage.xml
51 | *.cover
52 | *.py,cover
53 | .hypothesis/
54 | .pytest_cache/
55 | cover/
56 |
57 | # Translations
58 | *.mo
59 | *.pot
60 |
61 | # Django stuff:
62 | *.log
63 | local_settings.py
64 | db.sqlite3
65 | db.sqlite3-journal
66 |
67 | # Flask stuff:
68 | instance/
69 | .webassets-cache
70 |
71 | # Scrapy stuff:
72 | .scrapy
73 |
74 | # Sphinx documentation
75 | docs/_build/
76 |
77 | # PyBuilder
78 | .pybuilder/
79 | target/
80 |
81 | # Jupyter Notebook
82 | .ipynb_checkpoints
83 |
84 | # IPython
85 | profile_default/
86 | ipython_config.py
87 |
88 | # pyenv
89 | # For a library or package, you might want to ignore these files since the code is
90 | # intended to run in multiple environments; otherwise, check them in:
91 | # .python-version
92 |
93 | # pipenv
94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
97 | # install all needed dependencies.
98 | #Pipfile.lock
99 |
100 | # poetry
101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
102 | # This is especially recommended for binary packages to ensure reproducibility, and is more
103 | # commonly ignored for libraries.
104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
105 | #poetry.lock
106 |
107 | # pdm
108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
109 | #pdm.lock
110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
111 | # in version control.
112 | # https://pdm.fming.dev/#use-with-ide
113 | .pdm.toml
114 |
115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
116 | __pypackages__/
117 |
118 | # Celery stuff
119 | celerybeat-schedule
120 | celerybeat.pid
121 |
122 | # SageMath parsed files
123 | *.sage.py
124 |
125 | # Environments
126 | .env
127 | .venv
128 | env/
129 | venv/
130 | ENV/
131 | env.bak/
132 | venv.bak/
133 |
134 | # Spyder project settings
135 | .spyderproject
136 | .spyproject
137 |
138 | # Rope project settings
139 | .ropeproject
140 |
141 | # mkdocs documentation
142 | /site
143 |
144 | # mypy
145 | .mypy_cache/
146 | .dmypy.json
147 | dmypy.json
148 |
149 | # Pyre type checker
150 | .pyre/
151 |
152 | # pytype static type analyzer
153 | .pytype/
154 |
155 | # Cython debug symbols
156 | cython_debug/
157 |
158 | # PyCharm
159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
161 | # and can be added to the global gitignore or merged into this file. For a more nuclear
162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
163 | #.idea/
164 |
165 |
--------------------------------------------------------------------------------
/static/images/icons/money-back.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/static/js/htmx.1.9.4_dist_ext_sse.js:
--------------------------------------------------------------------------------
1 | /*
2 | Server Sent Events Extension
3 | ============================
4 | This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
5 |
6 | */
7 |
8 | (function(){
9 |
10 | /** @type {import("../htmx").HtmxInternalApi} */
11 | var api;
12 |
13 | htmx.defineExtension("sse", {
14 |
15 | /**
16 | * Init saves the provided reference to the internal HTMX API.
17 | *
18 | * @param {import("../htmx").HtmxInternalApi} api
19 | * @returns void
20 | */
21 | init: function(apiRef) {
22 | // store a reference to the internal API.
23 | api = apiRef;
24 |
25 | // set a function in the public API for creating new EventSource objects
26 | if (htmx.createEventSource == undefined) {
27 | htmx.createEventSource = createEventSource;
28 | }
29 | },
30 |
31 | /**
32 | * onEvent handles all events passed to this extension.
33 | *
34 | * @param {string} name
35 | * @param {Event} evt
36 | * @returns void
37 | */
38 | onEvent: function(name, evt) {
39 |
40 | switch (name) {
41 |
42 | // Try to remove remove an EventSource when elements are removed
43 | case "htmx:beforeCleanupElement":
44 | var internalData = api.getInternalData(evt.target)
45 | if (internalData.sseEventSource) {
46 | internalData.sseEventSource.close();
47 | }
48 | return;
49 |
50 | // Try to create EventSources when elements are processed
51 | case "htmx:afterProcessNode":
52 | createEventSourceOnElement(evt.target);
53 | }
54 | }
55 | });
56 |
57 | ///////////////////////////////////////////////
58 | // HELPER FUNCTIONS
59 | ///////////////////////////////////////////////
60 |
61 |
62 | /**
63 | * createEventSource is the default method for creating new EventSource objects.
64 | * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
65 | *
66 | * @param {string} url
67 | * @returns EventSource
68 | */
69 | function createEventSource(url) {
70 | return new EventSource(url, {withCredentials:true});
71 | }
72 |
73 | function splitOnWhitespace(trigger) {
74 | return trigger.trim().split(/\s+/);
75 | }
76 |
77 | function getLegacySSEURL(elt) {
78 | var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
79 | if (legacySSEValue) {
80 | var values = splitOnWhitespace(legacySSEValue);
81 | for (var i = 0; i < values.length; i++) {
82 | var value = values[i].split(/:(.+)/);
83 | if (value[0] === "connect") {
84 | return value[1];
85 | }
86 | }
87 | }
88 | }
89 |
90 | function getLegacySSESwaps(elt) {
91 | var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
92 | var returnArr = [];
93 | if (legacySSEValue) {
94 | var values = splitOnWhitespace(legacySSEValue);
95 | for (var i = 0; i < values.length; i++) {
96 | var value = values[i].split(/:(.+)/);
97 | if (value[0] === "swap") {
98 | returnArr.push(value[1]);
99 | }
100 | }
101 | }
102 | return returnArr;
103 | }
104 |
105 | /**
106 | * createEventSourceOnElement creates a new EventSource connection on the provided element.
107 | * If a usable EventSource already exists, then it is returned. If not, then a new EventSource
108 | * is created and stored in the element's internalData.
109 | * @param {HTMLElement} elt
110 | * @param {number} retryCount
111 | * @returns {EventSource | null}
112 | */
113 | function createEventSourceOnElement(elt, retryCount) {
114 |
115 | if (elt == null) {
116 | return null;
117 | }
118 |
119 | var internalData = api.getInternalData(elt);
120 |
121 | // get URL from element's attribute
122 | var sseURL = api.getAttributeValue(elt, "sse-connect");
123 |
124 |
125 | if (sseURL == undefined) {
126 | var legacyURL = getLegacySSEURL(elt)
127 | if (legacyURL) {
128 | sseURL = legacyURL;
129 | } else {
130 | return null;
131 | }
132 | }
133 |
134 | // Connect to the EventSource
135 | var source = htmx.createEventSource(sseURL);
136 | internalData.sseEventSource = source;
137 |
138 | // Create event handlers
139 | source.onerror = function (err) {
140 |
141 | // Log an error event
142 | api.triggerErrorEvent(elt, "htmx:sseError", {error:err, source:source});
143 |
144 | // If parent no longer exists in the document, then clean up this EventSource
145 | if (maybeCloseSSESource(elt)) {
146 | return;
147 | }
148 |
149 | // Otherwise, try to reconnect the EventSource
150 | if (source.readyState === EventSource.CLOSED) {
151 | retryCount = retryCount || 0;
152 | var timeout = Math.random() * (2 ^ retryCount) * 500;
153 | window.setTimeout(function() {
154 | createEventSourceOnElement(elt, Math.min(7, retryCount+1));
155 | }, timeout);
156 | }
157 | };
158 |
159 | source.onopen = function (evt) {
160 | api.triggerEvent(elt, "htmx:sseOpen", {source: source});
161 | }
162 |
163 | // Add message handlers for every `sse-swap` attribute
164 | queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
165 |
166 | var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
167 | if (sseSwapAttr) {
168 | var sseEventNames = sseSwapAttr.split(",");
169 | } else {
170 | var sseEventNames = getLegacySSESwaps(child);
171 | }
172 |
173 | for (var i = 0 ; i < sseEventNames.length ; i++) {
174 | var sseEventName = sseEventNames[i].trim();
175 | var listener = function(event) {
176 |
177 | // If the parent is missing then close SSE and remove listener
178 | if (maybeCloseSSESource(elt)) {
179 | source.removeEventListener(sseEventName, listener);
180 | return;
181 | }
182 |
183 | // swap the response into the DOM and trigger a notification
184 | swap(child, event.data);
185 | api.triggerEvent(elt, "htmx:sseMessage", event);
186 | };
187 |
188 | // Register the new listener
189 | api.getInternalData(elt).sseEventListener = listener;
190 | source.addEventListener(sseEventName, listener);
191 | }
192 | });
193 |
194 | // Add message handlers for every `hx-trigger="sse:*"` attribute
195 | queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
196 |
197 | var sseEventName = api.getAttributeValue(child, "hx-trigger");
198 | if (sseEventName == null) {
199 | return;
200 | }
201 |
202 | // Only process hx-triggers for events with the "sse:" prefix
203 | if (sseEventName.slice(0, 4) != "sse:") {
204 | return;
205 | }
206 |
207 | var listener = function(event) {
208 |
209 | // If parent is missing, then close SSE and remove listener
210 | if (maybeCloseSSESource(elt)) {
211 | source.removeEventListener(sseEventName, listener);
212 | return;
213 | }
214 |
215 | // Trigger events to be handled by the rest of htmx
216 | htmx.trigger(child, sseEventName, event);
217 | htmx.trigger(child, "htmx:sseMessage", event);
218 | }
219 |
220 | // Register the new listener
221 | api.getInternalData(elt).sseEventListener = listener;
222 | source.addEventListener(sseEventName.slice(4), listener);
223 | });
224 | }
225 |
226 | /**
227 | * maybeCloseSSESource confirms that the parent element still exists.
228 | * If not, then any associated SSE source is closed and the function returns true.
229 | *
230 | * @param {HTMLElement} elt
231 | * @returns boolean
232 | */
233 | function maybeCloseSSESource(elt) {
234 | if (!api.bodyContains(elt)) {
235 | var source = api.getInternalData(elt).sseEventSource;
236 | if (source != undefined) {
237 | source.close();
238 | // source = null
239 | return true;
240 | }
241 | }
242 | return false;
243 | }
244 |
245 | /**
246 | * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
247 | *
248 | * @param {HTMLElement} elt
249 | * @param {string} attributeName
250 | */
251 | function queryAttributeOnThisOrChildren(elt, attributeName) {
252 |
253 | var result = [];
254 |
255 | // If the parent element also contains the requested attribute, then add it to the results too.
256 | if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-sse")) {
257 | result.push(elt);
258 | }
259 |
260 | // Search all child nodes that match the requested attribute
261 | elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [hx-sse], [data-hx-sse]").forEach(function(node) {
262 | result.push(node);
263 | });
264 |
265 | return result;
266 | }
267 |
268 | /**
269 | * @param {HTMLElement} elt
270 | * @param {string} content
271 | */
272 | function swap(elt, content) {
273 |
274 | api.withExtensions(elt, function(extension) {
275 | content = extension.transformResponse(content, null, elt);
276 | });
277 |
278 | var swapSpec = api.getSwapSpecification(elt);
279 | var target = api.getTarget(elt);
280 | var settleInfo = api.makeSettleInfo(elt);
281 |
282 | api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
283 |
284 | settleInfo.elts.forEach(function (elt) {
285 | if (elt.classList) {
286 | elt.classList.add(htmx.config.settlingClass);
287 | }
288 | api.triggerEvent(elt, 'htmx:beforeSettle');
289 | });
290 |
291 | // Handle settle tasks (with delay if requested)
292 | if (swapSpec.settleDelay > 0) {
293 | setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
294 | } else {
295 | doSettle(settleInfo)();
296 | }
297 | }
298 |
299 | /**
300 | * doSettle mirrors much of the functionality in htmx that
301 | * settles elements after their content has been swapped.
302 | * TODO: this should be published by htmx, and not duplicated here
303 | * @param {import("../htmx").HtmxSettleInfo} settleInfo
304 | * @returns () => void
305 | */
306 | function doSettle(settleInfo) {
307 |
308 | return function() {
309 | settleInfo.tasks.forEach(function (task) {
310 | task.call();
311 | });
312 |
313 | settleInfo.elts.forEach(function (elt) {
314 | if (elt.classList) {
315 | elt.classList.remove(htmx.config.settlingClass);
316 | }
317 | api.triggerEvent(elt, 'htmx:afterSettle');
318 | });
319 | }
320 | }
321 |
322 | })();
--------------------------------------------------------------------------------
/jinja/shell.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Example Stream
9 |
10 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
46 |
129 |
130 |
131 |
132 |
133 |
134 |
183 |
184 |
185 |
186 |
235 |
236 |
237 | {% block main %} {% endblock %}
238 |
239 |
240 |
308 |
309 |
310 |
311 |
312 |
313 |
© TailCommerce - All Right Reserved
314 |
315 |

316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
--------------------------------------------------------------------------------
/home/templates/shell.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Example Stream
9 |
10 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
46 |
129 |
130 |
131 |
132 |
133 |
134 |
183 |
184 |
185 |
186 |
235 |
236 |
237 | {% block main %} {% endblock %}
238 |
239 |
240 |
308 |
309 |
310 |
311 |
312 |
313 |
© TailCommerce - All Right Reserved
314 |
315 |

316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
--------------------------------------------------------------------------------
/home/templates/shell_htmx.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Example Stream
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
22 |
47 |
130 |
131 |
132 |
133 |
134 |
135 |
184 |
185 |
186 |
187 |
236 |
237 |
238 | {% block main %} {% endblock %}
239 |
240 |
241 |
309 |
310 |
311 |
312 |
313 |
314 |
© TailCommerce - All Right Reserved
315 |
316 |

317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
--------------------------------------------------------------------------------
/static/images/icons/service-hours.svg:
--------------------------------------------------------------------------------
1 |
28 |
--------------------------------------------------------------------------------
/static/js/htmx.1.9.4.min.js:
--------------------------------------------------------------------------------
1 | (function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var G={onLoad:t,process:Nt,on:le,off:ue,trigger:oe,ajax:xr,find:b,findAll:f,closest:d,values:function(e,t){var r=er(e,t||"post");return r.values},remove:U,addClass:B,removeClass:n,toggleClass:V,takeClass:j,defineExtension:Cr,removeExtension:Rr,logAll:X,logNone:F,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"]},parseInterval:v,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=G.config.wsBinaryType;return t},version:"1.9.4"};var C={addTriggerHandler:bt,bodyContains:re,canAccessLocalStorage:M,findThisElement:he,filterValues:ar,hasAttribute:o,getAttributeValue:Z,getClosestAttributeValue:Y,getClosestMatch:c,getExpressionVars:gr,getHeaders:ir,getInputValues:er,getInternalData:ee,getSwapSpecification:sr,getTriggerSpecs:Ge,getTarget:de,makeFragment:l,mergeObjects:ne,makeSettleInfo:S,oobSwap:me,querySelectorExt:ie,selectAndSwap:De,settleImmediately:Wt,shouldCancel:Qe,triggerEvent:oe,triggerErrorEvent:ae,withExtensions:w};var R=["get","post","put","delete","patch"];var O=R.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function v(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function J(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function Z(e,t){return J(e,t)||J(e,"data-"+t)}function u(e){return e.parentElement}function K(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function T(e,t,r){var n=Z(t,r);var i=Z(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function Y(t,r){var n=null;c(t,function(e){return n=T(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function q(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=K().createDocumentFragment()}return i}function H(e){return e.match(/"+e+"",0);return r.querySelector("template").content}else{var n=q(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i("",1);case"col":return i("",2);case"tr":return i("",2);case"td":case"th":return i("",3);case"script":return i(""+e+"
",1);default:return i(e,0)}}}function Q(e){if(e){e()}}function L(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function A(e){return L(e,"Function")}function N(e){return L(e,"Object")}function ee(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function I(e){var t=[];if(e){for(var r=0;r=0}function re(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return K().body.contains(e.getRootNode().host)}else{return K().body.contains(e)}}function P(e){return e.trim().split(/\s+/)}function ne(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function y(e){try{return JSON.parse(e)}catch(e){x(e);return null}}function M(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function D(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return hr(K().body,function(){return eval(e)})}function t(t){var e=G.on("htmx:load",function(e){t(e.detail.elt)});return e}function X(){G.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function F(){G.logger=null}function b(e,t){if(t){return e.querySelector(t)}else{return b(K(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(K(),e)}}function U(e,t){e=s(e);if(t){setTimeout(function(){U(e);e=null},t)}else{e.parentElement.removeChild(e)}}function B(e,t,r){e=s(e);if(r){setTimeout(function(){B(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function V(e,t){e=s(e);e.classList.toggle(t)}function j(e,t){e=s(e);te(e.parentElement.children,function(e){n(e,t)});B(e,t)}function d(e,t){e=s(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function r(e){var t=e.trim();if(t.startsWith("<")&&t.endsWith("/>")){return t.substring(1,t.length-2)}else{return t}}function W(e,t){if(t.indexOf("closest ")===0){return[d(e,r(t.substr(8)))]}else if(t.indexOf("find ")===0){return[b(e,r(t.substr(5)))]}else if(t.indexOf("next ")===0){return[_(e,r(t.substr(5)))]}else if(t.indexOf("previous ")===0){return[z(e,r(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else{return K().querySelectorAll(r(t))}}var _=function(e,t){var r=K().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ie(e,t){if(t){return W(e,t)[0]}else{return W(K().body,e)[0]}}function s(e){if(L(e,"String")){return b(e)}else{return e}}function $(e,t,r){if(A(t)){return{target:K().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function le(t,r,n){Tr(function(){var e=$(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=A(r);return e?r:n}function ue(t,r,n){Tr(function(){var e=$(t,r,n);e.target.removeEventListener(e.event,e.listener)});return A(r)?r:n}var fe=K().createElement("output");function ce(e,t){var r=Y(e,t);if(r){if(r==="this"){return[he(e,t)]}else{var n=W(e,r);if(n.length===0){x('The selector "'+r+'" on '+t+" returned no matches!");return[fe]}else{return n}}}}function he(e,t){return c(e,function(e){return Z(e,t)!=null})}function de(e){var t=Y(e,"hx-target");if(t){if(t==="this"){return he(e,"hx-target")}else{return ie(e,t)}}else{var r=ee(e);if(r.boosted){return K().body}else{return e}}}function ve(e){var t=G.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=K().querySelectorAll(t);if(r){te(r,function(e){var t;var r=i.cloneNode(true);t=K().createDocumentFragment();t.appendChild(r);if(!pe(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!oe(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){Pe(o,e,e,t,a)}te(a.elts,function(e){oe(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);ae(K().body,"htmx:oobErrorNoTarget",{content:i})}return e}function xe(e,t,r){var n=Y(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();ge(e,i);s.tasks.push(function(){ge(e,a)})}}})}function we(e){return function(){n(e,G.config.addedClass);Nt(e);St(e);Se(e);oe(e,"htmx:load")}}function Se(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){be(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;B(i,G.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(we(i))}}}function Ee(e,t){var r=0;while(r-1){var t=e.replace(/