├── .circleci └── config.yml ├── .gitignore ├── .isort.cfg ├── .pre-commit-config.yaml ├── LICENSE ├── Makefile ├── README.md ├── app ├── advanced_htmx │ ├── __init__.py │ ├── admin.py │ ├── ajax.py │ ├── apps.py │ ├── constants.py │ ├── consumers.py │ ├── forms.py │ ├── handlers.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_car.py │ │ └── __init__.py │ ├── models.py │ ├── routing.py │ ├── urls.py │ └── views.py ├── core │ └── __init__.py ├── manage.py ├── myproject │ ├── __init__.py │ ├── asgi.py │ ├── context_processors.py │ ├── settings │ │ ├── base.py │ │ ├── development.py │ │ └── production.py │ ├── static │ │ ├── css │ │ │ ├── font-awesome.min.css │ │ │ ├── table.css │ │ │ ├── tailwind-input.css │ │ │ └── todo_list.css │ │ ├── fonts │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ └── js │ │ │ ├── flowbite.min.js │ │ │ ├── htmx_ext_ws.js │ │ │ └── todo_list.js │ ├── urls.py │ └── wsgi.py ├── templates │ ├── advanced_htmx │ │ ├── base.html │ │ ├── car_create.html │ │ ├── chat.html │ │ ├── mini_partials │ │ │ ├── car_create_form_field.html │ │ │ ├── chat_messages_container.html │ │ │ ├── product_random.html │ │ │ ├── product_random_event.html │ │ │ └── products_counter.html │ │ ├── partials │ │ │ ├── car_create.html │ │ │ ├── chat_messages_form.html │ │ │ ├── footer.html │ │ │ ├── header.html │ │ │ ├── success_car_create.html │ │ │ ├── table_product_item.html │ │ │ └── table_products_list.html │ │ └── table.html │ ├── base.html │ ├── partials │ │ ├── task_item.html │ │ └── tasks_list.html │ └── todo_list.html ├── tests │ ├── .coveragerc │ ├── conftest.py │ ├── fixtures │ │ ├── __init__.py │ │ └── todo_tracker.py │ ├── pytest.ini │ ├── test_ajax │ │ └── test_ajax_todo_list.py │ └── test_views │ │ └── test_todo_list.py └── todo_tracker │ ├── __init__.py │ ├── admin.py │ ├── ajax │ ├── __init__.py │ └── todo_list.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── models.py │ ├── urls.py │ └── views │ ├── __init__.py │ └── todo_list.py ├── docker-compose-circleci.yml ├── docker-compose.yml ├── docker └── Dockerfile ├── example.env ├── requirements ├── base.in ├── base.txt ├── dev.in └── dev.txt ├── screenshots ├── car_create_screen.png ├── chat_screen.png ├── sample_app_view.png └── table_screen.png └── tailwindcss ├── package-lock.json ├── package.json ├── postcss.config.js └── tailwind.config.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | # orbs: 4 | 5 | ####################################### 6 | ## ANCHORS 7 | ####################################### 8 | 9 | # build-and-push-image: &build-and-push-image 10 | # deploy_ecs: &deploy_ecs 11 | 12 | ####################################### 13 | ## EXECUTORS 14 | ####################################### 15 | executors: 16 | default: 17 | working_directory: ~/repo 18 | docker: 19 | - image: cimg/base:2023.09 20 | environment: 21 | DJANGO_SETTINGS_MODULE: myproject.settings.development 22 | SECRET_KEY: secret 23 | DEBUG: True 24 | DB_NAME: mydatabase 25 | DB_USER: postgres 26 | DB_PASSWORD: postgres 27 | DB_HOST: db 28 | DB_PORT: 5432 29 | 30 | ####################################### 31 | ## JOBS 32 | ####################################### 33 | jobs: 34 | checkout_code: 35 | executor: default 36 | resource_class: small 37 | steps: 38 | - checkout 39 | - persist_to_workspace: 40 | root: ~/repo 41 | paths: 42 | - . 43 | 44 | docker_run_tests: 45 | executor: default 46 | working_directory: ~/repo 47 | resource_class: medium 48 | steps: 49 | - attach_workspace: 50 | at: ~/repo 51 | - setup_remote_docker: 52 | docker_layer_caching: true 53 | - run: 54 | name: run tests and run coverage 55 | command: | 56 | env > .env 57 | make circleci-up 58 | make circleci-tests 59 | mkdir test-results && docker cp $(docker ps -qf "name=myproject"):/src/test-results/junit.xml ./test-results/junit.xml 60 | - store_test_results: 61 | path: test-results 62 | - store_artifacts: 63 | path: test-results 64 | 65 | ####################################### 66 | ## WORKFLOWS 67 | ####################################### 68 | workflows: 69 | version: 2 70 | build-and-deploy: 71 | jobs: 72 | - checkout_code 73 | - docker_run_tests: 74 | requires: 75 | - checkout_code 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # Local files 163 | .DS_Store 164 | node_modules 165 | .vscode 166 | tailwind-output.css 167 | htmx.min.js 168 | media/ 169 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | src_paths = app 3 | known_third_party = distutils,django,faker,pytest 4 | force_single_line = True 5 | line_length = 120 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: "docs|node_modules|migrations|.git|.tox" 2 | default_stages: [commit] 3 | fail_fast: true 4 | 5 | repos: 6 | - repo: https://github.com/PyCQA/autoflake 7 | rev: v2.2.1 8 | hooks: 9 | - id: autoflake 10 | args: ["--in-place", "--remove-all-unused-imports"] 11 | - repo: https://github.com/astral-sh/ruff-pre-commit 12 | rev: v0.0.291 13 | hooks: 14 | - id: ruff 15 | args: [--line-length, "120", "--fix"] 16 | - repo: https://github.com/PyCQA/isort 17 | rev: 5.12.0 18 | hooks: 19 | - id: isort 20 | - repo: https://github.com/PyCQA/bandit 21 | rev: 1.7.5 22 | hooks: 23 | - id: bandit 24 | args: ["--exclude=tests"] 25 | - repo: https://github.com/psf/black 26 | rev: 23.9.1 27 | hooks: 28 | - id: black 29 | args: [--line-length, "120"] 30 | language_version: python3.11 31 | - repo: https://github.com/pre-commit/pre-commit-hooks 32 | rev: v4.4.0 33 | hooks: 34 | - id: trailing-whitespace 35 | - id: check-merge-conflict 36 | - id: check-yaml 37 | - id: detect-private-key 38 | - id: end-of-file-fixer 39 | - id: check-added-large-files 40 | - id: no-commit-to-branch 41 | args: [--branch, main] 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sunscrapers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: update-deps build up down attach bash shell migrate migrations collectstatic run-build run tests circleci-up circleci-tests 2 | update-deps: 3 | pip install -U pip && pip install pip-tools 4 | 5 | pip-compile requirements/base.in 6 | pip-compile requirements/dev.in 7 | 8 | REQUIREMENTS_FILE = dev.txt 9 | build: 10 | docker compose build --build-arg="REQUIREMENTS_FILE=$(REQUIREMENTS_FILE)" 11 | 12 | up: 13 | make build 14 | docker compose up 15 | 16 | down: 17 | docker compose down 18 | 19 | attach: 20 | docker attach myproject 21 | 22 | bash: 23 | docker exec -it myproject bash 24 | 25 | shell: 26 | docker exec -it myproject python manage.py shell_plus --ipython 27 | 28 | migrate: 29 | docker exec -it myproject python manage.py migrate 30 | 31 | migrations: 32 | docker exec -it myproject python manage.py makemigrations 33 | 34 | collectstatic: 35 | docker exec -it myproject python manage.py collectstatic 36 | 37 | run-build: 38 | docker build . -f docker/Dockerfile -t myproject:latest --build-arg="REQUIREMENTS_FILE=$(REQUIREMENTS_FILE)" 39 | 40 | run: 41 | docker run --name myproject -p 8000:8000 --rm --env-file .env -it myproject:latest 42 | 43 | tests: 44 | docker exec -it myproject pytest -s --verbose 45 | 46 | circleci-up: 47 | make build 48 | docker compose -f docker-compose-circleci.yml up -d 49 | 50 | circleci-tests: 51 | docker exec -it myproject pytest -v --cov=./ --cov-config=tests/.coveragerc --junitxml=/src/test-results/junit.xml 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django project with HTMX and Tailwind CSS 2 | 3 | This Django project demonstrates the use of HTMX and Tailwind CSS to create interactive web applications. 4 | ![Sample app screenshot](screenshots/sample_app_view.png) 5 | 6 | ## Getting Started 7 | 8 | ### Prerequisites 9 | 10 | Before you can run this application, make sure you have the following prerequisites installed: 11 | 12 | - Python 3.x 13 | - Docker and Docker Compose 14 | - pip-tools 15 | 16 | ### Running the Application 17 | 18 | To launch the application, execute the following command: 19 | 20 | ```bash 21 | make up 22 | ``` 23 | 24 | ## Available Commands (using Make) 25 | 26 | Here are some handy `make` commands to manage and interact with the project: 27 | 28 | - **`make update-deps`**: Update Python dependencies using pip-tools. 29 | 30 | - **`make up`**: Run the application using Docker Compose. 31 | 32 | - **`make down`**: Stop the running application. 33 | 34 | - **`make build`**: Manually build the Django project using Docker. 35 | 36 | - **`make attach`**: Attach to the `myproject` Docker container (enter into the Django project container). 37 | 38 | - **`make bash`**: Open a bash shell inside the `myproject` Docker container. 39 | 40 | - **`make shell`**: Open a Python shell with IPython inside the `myproject` Docker container. 41 | 42 | - **`make migrate`**: Run database migrations inside the `myproject` Docker container. 43 | 44 | - **`make migrations`**: Generate database migration files inside the `myproject` Docker container. 45 | 46 | - **`make collectstatic`**: Collect static files inside the `myproject` Docker container. 47 | 48 | - **`make run-build`**: Build a Docker image for development purposes. 49 | 50 | - **`make run`**: Run the `myproject` Docker container with environment variables from .env. 51 | 52 | Feel free to use these commands to streamline your development workflow and interact with the project effortlessly. 53 | 54 | ## Tests 55 | 56 | To launch written tests, run application and execute the following command: 57 | 58 | ```bash 59 | make tests 60 | ``` 61 | 62 | ## Advanced Usage with HTMX 63 | 64 | This app showcases the advanced capabilities of HTMX, making your web application more dynamic and user-friendly. We've implemented several advanced features that leverage the power of HTMX: 65 | 66 | ### 1. Interactive Tables with Pagination 67 | 68 | ![Table with Pagination](screenshots/table_screen.png) 69 | 70 | Here you'll find interactive tables with features such as: 71 | 72 | - **hx-swap-oob:** We use the `hx-swap-oob` attribute to update specific parts of the page without reloading the entire page, enhancing the user experience. 73 | 74 | - **hx-trigger="load":** The `hx-trigger="load"` attribute is employed for automatic initialization of requests as soon as the page loads. This preloads data and ensures a seamless user experience. 75 | 76 | - **Pagination:** We've integrated pagination with HTMX, enabling users to navigate through data efficiently. 77 | 78 | ### 2. File Uploads with Django Forms 79 | 80 | ![File Upload with Django Forms](screenshots/car_create_screen.png) 81 | 82 | We've streamlined file uploads in your Django application using HTMX. Our project demonstrates how to easily handle file uploads with HTML forms and the `hx-encoding` attribute set to "multipart/form-data" This feature works seamlessly with Django forms, making it a breeze to implement file upload functionality. 83 | 84 | ### 3. Real-Time Chat with Django Channels 85 | 86 | ![Real-Time Chat with Django Channels](screenshots/chat_screen.png) 87 | 88 | Take your communication to the next level with real-time chat functionality powered by Django Channels. Our chat feature showcases the integration of WebSockets and HTMX, enabling instantaneous messaging and interactions between users. 89 | 90 | These advanced HTMX features enhance the overall user experience, making your web application more dynamic, interactive, and efficient. Explore our project to see these features in action and gain insights into how to implement them in your own applications. 91 | -------------------------------------------------------------------------------- /app/advanced_htmx/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/app/advanced_htmx/__init__.py -------------------------------------------------------------------------------- /app/advanced_htmx/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from advanced_htmx.models import Car 4 | from advanced_htmx.models import Category 5 | from advanced_htmx.models import Product 6 | 7 | 8 | @admin.register(Category) 9 | class CategoryAdmin(admin.ModelAdmin): 10 | list_display = ("name",) 11 | 12 | 13 | @admin.register(Product) 14 | class ProductAdmin(admin.ModelAdmin): 15 | list_display = ("name", "color", "category", "price") 16 | 17 | 18 | @admin.register(Car) 19 | class CarAdmin(admin.ModelAdmin): 20 | list_display = ("name", "price") 21 | search_fields = ("name",) 22 | -------------------------------------------------------------------------------- /app/advanced_htmx/ajax.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.shortcuts import render 3 | from django.template.response import TemplateResponse 4 | from django.views.generic import CreateView 5 | from django.views.generic import FormView 6 | from django.views.generic import TemplateView 7 | from django.views.generic.list import ListView 8 | 9 | from advanced_htmx.constants import HTMXTriggers 10 | from advanced_htmx.forms import ChatMessageForm 11 | from advanced_htmx.handlers import ChatHandler 12 | from advanced_htmx.handlers import ChatMessage 13 | from advanced_htmx.models import Car 14 | from advanced_htmx.models import Product 15 | 16 | 17 | class TableProductsListAjax(ListView): 18 | template_name = "advanced_htmx/partials/table_products_list.html" 19 | 20 | model = Product 21 | queryset = Product.objects.select_related("category").order_by("id") 22 | paginate_by = 10 23 | 24 | def get(self, request, *args, **kwargs) -> TemplateResponse: 25 | response: TemplateResponse = super().get(request, *args, **kwargs) 26 | response["HX-Trigger"] = HTMXTriggers.products_list_page_changed.value 27 | return response 28 | 29 | 30 | class RandomProductAjax(TemplateView): 31 | template_name = "advanced_htmx/mini_partials/product_random.html" 32 | 33 | def get_context_data(self, *args, **kwargs): 34 | ctx = super().get_context_data(*args, **kwargs) 35 | ctx["random_product"] = Product.objects.order_by("?").first() 36 | return ctx 37 | 38 | 39 | class RandomProductEventAjax(RandomProductAjax): 40 | template_name = "advanced_htmx/mini_partials/product_random_event.html" 41 | 42 | 43 | class CarCreateAjax(CreateView): 44 | model = Car 45 | fields = ("name", "price", "photo") 46 | template_name = "advanced_htmx/partials/car_create.html" 47 | success_template_name = "advanced_htmx/partials/success_car_create.html" 48 | object = None 49 | 50 | def form_valid(self, request, form): 51 | return render( 52 | self.request, 53 | self.success_template_name, 54 | ) 55 | 56 | def post(self, request, *args, **kwargs): 57 | form = self.get_form() 58 | if form.is_valid(): 59 | return self.form_valid(request, form) 60 | else: 61 | return self.form_invalid(form) 62 | 63 | 64 | class ChatAjax(FormView): 65 | template_name = "advanced_htmx/partials/chat_messages_form.html" 66 | http_method_names = ["post"] 67 | form_class = ChatMessageForm 68 | 69 | def form_valid(self, form): 70 | message: ChatMessage = ChatMessage( 71 | signature=form.cleaned_data["signature"], 72 | text=form.cleaned_data["text"], 73 | ) 74 | ChatHandler.save_message(message=message) 75 | 76 | return HttpResponse(status=204) 77 | -------------------------------------------------------------------------------- /app/advanced_htmx/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AdvancedHtmxConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "advanced_htmx" 7 | -------------------------------------------------------------------------------- /app/advanced_htmx/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | # Websockets 4 | CHAT_WS_GROUP_NAME = "chatroom" 5 | 6 | 7 | class EventType: 8 | chat_message = "chat_message" 9 | 10 | 11 | class HTMXTriggers(Enum): 12 | products_list_page_changed = "products-list-page-changed" 13 | -------------------------------------------------------------------------------- /app/advanced_htmx/consumers.py: -------------------------------------------------------------------------------- 1 | # chat/consumers.py 2 | from typing import Dict 3 | 4 | from asgiref.sync import async_to_sync 5 | from channels.generic.websocket import WebsocketConsumer 6 | from django.template.loader import get_template 7 | 8 | from advanced_htmx.constants import CHAT_WS_GROUP_NAME 9 | 10 | 11 | class ChatConsumer(WebsocketConsumer): 12 | def connect(self): 13 | async_to_sync(self.channel_layer.group_add)( 14 | CHAT_WS_GROUP_NAME, 15 | self.channel_name, 16 | ) 17 | 18 | self.accept() 19 | 20 | def disconnect(self, close_code): 21 | async_to_sync(self.channel_layer.group_discard)( 22 | CHAT_WS_GROUP_NAME, 23 | self.channel_name, 24 | ) 25 | 26 | def chat_message(self, event: Dict[str, str]): 27 | html = get_template( 28 | "advanced_htmx/mini_partials/chat_messages_container.html", 29 | ).render( 30 | context={ 31 | "messages": [ 32 | event["message"], 33 | ] 34 | } 35 | ) 36 | 37 | self.send(text_data=html) 38 | -------------------------------------------------------------------------------- /app/advanced_htmx/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class ChatMessageForm(forms.Form): 5 | signature = forms.CharField() 6 | text = forms.CharField() 7 | -------------------------------------------------------------------------------- /app/advanced_htmx/handlers.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict 2 | from dataclasses import dataclass 3 | from typing import Dict 4 | from typing import List 5 | 6 | from asgiref.sync import async_to_sync 7 | from channels.layers import get_channel_layer 8 | 9 | from advanced_htmx.constants import CHAT_WS_GROUP_NAME 10 | from advanced_htmx.constants import EventType 11 | 12 | 13 | @dataclass 14 | class ChatMessage: 15 | signature: str 16 | text: str 17 | 18 | def asdict(self): 19 | return asdict(self) 20 | 21 | 22 | @dataclass 23 | class EventChatMessage: 24 | type: EventType 25 | message: ChatMessage 26 | 27 | def asdict(self): 28 | return asdict(self) 29 | 30 | 31 | tmp_messages_storage: List[ChatMessage] = [None for _ in range(10)] # Store only ten latest messages 32 | 33 | 34 | class ChatHandler: 35 | @classmethod 36 | def save_message(cls, message: ChatMessage): 37 | tmp_messages_storage.pop(0) 38 | tmp_messages_storage.append(message) 39 | 40 | cls.propagate_message(message=message) 41 | 42 | @classmethod 43 | def get_messages(cls) -> List[ChatMessage]: 44 | return [msg for msg in tmp_messages_storage if msg] 45 | 46 | @classmethod 47 | def get_messages_data(cls) -> List[Dict[str, str]]: 48 | return [msg.asdict() for msg in cls.get_messages()] 49 | 50 | @classmethod 51 | def propagate_message(cls, message: ChatMessage): 52 | channel_layer = get_channel_layer() 53 | async_to_sync(channel_layer.group_send)( 54 | CHAT_WS_GROUP_NAME, 55 | EventChatMessage( 56 | type=EventType.chat_message, 57 | message=message, 58 | ).asdict(), 59 | ) 60 | -------------------------------------------------------------------------------- /app/advanced_htmx/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-10-24 07:31 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Category", 15 | fields=[ 16 | ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 17 | ("name", models.CharField(max_length=100, unique=True)), 18 | ], 19 | ), 20 | migrations.CreateModel( 21 | name="Product", 22 | fields=[ 23 | ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 24 | ("name", models.CharField(max_length=200, unique=True)), 25 | ( 26 | "color", 27 | models.CharField( 28 | choices=[ 29 | ("White", "White"), 30 | ("Silver", "Silver"), 31 | ("Yellow", "Yellow"), 32 | ("Blue", "Blue"), 33 | ("Red", "Red"), 34 | ("Green", "Green"), 35 | ("Black", "Black"), 36 | ("Other", "Other"), 37 | ], 38 | default="Other", 39 | max_length=10, 40 | ), 41 | ), 42 | ("price", models.DecimalField(decimal_places=2, max_digits=10)), 43 | ( 44 | "category", 45 | models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="advanced_htmx.category"), 46 | ), 47 | ], 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /app/advanced_htmx/migrations/0002_car.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-10-25 09:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('advanced_htmx', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Car', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('name', models.CharField(max_length=255, unique=True)), 18 | ('price', models.DecimalField(decimal_places=2, max_digits=10)), 19 | ('photo', models.ImageField(upload_to='cars')), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /app/advanced_htmx/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/app/advanced_htmx/migrations/__init__.py -------------------------------------------------------------------------------- /app/advanced_htmx/models.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from django.core.validators import MinValueValidator 4 | from django.db import models 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | 8 | class ColorChoices(models.TextChoices): 9 | WHITE = "White", _("White") 10 | SILVER = "Silver", _("Silver") 11 | YELLOW = "Yellow", _("Yellow") 12 | BLUE = "Blue", _("Blue") 13 | RED = "Red", _("Red") 14 | GREEN = "Green", _("Green") 15 | BLACK = "Black", _("Black") 16 | OTHER = "Other", _("Other") 17 | 18 | 19 | class Category(models.Model): 20 | name = models.CharField(max_length=100, unique=True, blank=False, null=False) 21 | 22 | def __str__(self): 23 | return self.name 24 | 25 | 26 | class Product(models.Model): 27 | name = models.CharField(max_length=200, unique=True, blank=False, null=False) 28 | color = models.CharField( 29 | max_length=10, 30 | choices=ColorChoices.choices, 31 | default=ColorChoices.OTHER.value, 32 | ) 33 | category = models.ForeignKey(Category, on_delete=models.CASCADE) 34 | price = models.DecimalField(max_digits=10, decimal_places=2) 35 | 36 | def __str__(self): 37 | return self.name 38 | 39 | 40 | class Car(models.Model): 41 | name = models.CharField(max_length=255, unique=True, blank=False, null=False) 42 | price = models.DecimalField( 43 | max_digits=10, 44 | decimal_places=2, 45 | validators=[MinValueValidator(Decimal("0.00"))], 46 | ) 47 | photo = models.ImageField(upload_to="cars") 48 | 49 | def __str__(self): 50 | return self.name 51 | -------------------------------------------------------------------------------- /app/advanced_htmx/routing.py: -------------------------------------------------------------------------------- 1 | # chat/routing.py 2 | from django.urls import path 3 | 4 | from advanced_htmx.consumers import ChatConsumer 5 | 6 | websocket_urlpatterns = [ 7 | path(r"ws/chatroom", ChatConsumer.as_asgi()), 8 | ] 9 | -------------------------------------------------------------------------------- /app/advanced_htmx/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from advanced_htmx.ajax import CarCreateAjax 4 | from advanced_htmx.ajax import ChatAjax 5 | from advanced_htmx.ajax import RandomProductAjax 6 | from advanced_htmx.ajax import RandomProductEventAjax 7 | from advanced_htmx.ajax import TableProductsListAjax 8 | from advanced_htmx.views import CarCreateView 9 | from advanced_htmx.views import ChatView 10 | from advanced_htmx.views import TableView 11 | 12 | app_name = "advanced-htmx" 13 | urlpatterns = [ 14 | # Ajax 15 | path("ajax/products", TableProductsListAjax.as_view(), name="table-products-list-ajax"), 16 | path("ajax/product/random", RandomProductAjax.as_view(), name="product-random-ajax"), 17 | path("ajax/product/random/event", RandomProductEventAjax.as_view(), name="product-random-event-ajax"), 18 | path("ajax/car", CarCreateAjax.as_view(), name="car-create-ajax"), 19 | path("ajax/chat", ChatAjax.as_view(), name="chat-ajax"), 20 | # Views 21 | path("", TableView.as_view(), name="table"), 22 | path("car", CarCreateView.as_view(), name="car-create"), 23 | path("chat", ChatView.as_view(), name="chat"), 24 | ] 25 | -------------------------------------------------------------------------------- /app/advanced_htmx/views.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from typing import Dict 3 | 4 | from django.views.generic import TemplateView 5 | from django.views.generic.list import ListView 6 | 7 | from advanced_htmx.handlers import ChatHandler 8 | from advanced_htmx.models import Product 9 | 10 | 11 | class TableView(ListView): 12 | model = Product 13 | template_name = "advanced_htmx/table.html" 14 | 15 | paginate_by = 10 16 | queryset = Product.objects.select_related("category").order_by("id") 17 | 18 | 19 | class CarCreateView(TemplateView): 20 | template_name = "advanced_htmx/car_create.html" 21 | 22 | 23 | class ChatView(TemplateView): 24 | template_name = "advanced_htmx/chat.html" 25 | 26 | def get_context_data(self, *args, **kwargs): 27 | ctx: Dict[str, Any] = super().get_context_data(*args, **kwargs) 28 | ctx["messages"] = ChatHandler.get_messages_data() 29 | 30 | return ctx 31 | -------------------------------------------------------------------------------- /app/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/app/core/__init__.py -------------------------------------------------------------------------------- /app/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", "myproject.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 | -------------------------------------------------------------------------------- /app/myproject/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/app/myproject/__init__.py -------------------------------------------------------------------------------- /app/myproject/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for myproject 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.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from channels.routing import ProtocolTypeRouter 13 | from channels.routing import URLRouter 14 | from channels.security.websocket import AllowedHostsOriginValidator 15 | from django.core.asgi import get_asgi_application 16 | 17 | from advanced_htmx.routing import websocket_urlpatterns 18 | 19 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") 20 | 21 | django_asgi_app = get_asgi_application() 22 | 23 | application = ProtocolTypeRouter( 24 | { 25 | "http": django_asgi_app, 26 | "websocket": AllowedHostsOriginValidator(URLRouter(websocket_urlpatterns)), 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /app/myproject/context_processors.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from django import template 4 | 5 | from advanced_htmx.constants import HTMXTriggers 6 | 7 | register = template.Library() 8 | 9 | 10 | def extra_template_variables(request): 11 | htmx_triggers: Dict[str, str] = {trigger.name: trigger.value for trigger in HTMXTriggers} 12 | return {"htmx_triggers": htmx_triggers} 13 | -------------------------------------------------------------------------------- /app/myproject/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for myproject project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = os.environ.get("SECRET_KEY") 25 | 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = False 29 | 30 | ALLOWED_HOSTS = [] 31 | 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | "daphne", # Third party 37 | "django.contrib.admin", 38 | "django.contrib.auth", 39 | "django.contrib.contenttypes", 40 | "django.contrib.sessions", 41 | "django.contrib.messages", 42 | "django.contrib.staticfiles", 43 | # Custom 44 | "myproject", 45 | "todo_tracker", 46 | "advanced_htmx", 47 | # Third party 48 | "channels", 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | "django.middleware.security.SecurityMiddleware", 53 | "django.contrib.sessions.middleware.SessionMiddleware", 54 | "django.middleware.common.CommonMiddleware", 55 | "django.middleware.csrf.CsrfViewMiddleware", 56 | "django.contrib.auth.middleware.AuthenticationMiddleware", 57 | "django.contrib.messages.middleware.MessageMiddleware", 58 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 59 | ] 60 | 61 | ROOT_URLCONF = "myproject.urls" 62 | 63 | TEMPLATES = [ 64 | { 65 | "BACKEND": "django.template.backends.django.DjangoTemplates", 66 | "DIRS": [ 67 | BASE_DIR / "templates", 68 | ], 69 | "APP_DIRS": True, 70 | "OPTIONS": { 71 | "context_processors": [ 72 | "django.template.context_processors.debug", 73 | "django.template.context_processors.request", 74 | "django.contrib.auth.context_processors.auth", 75 | "django.contrib.messages.context_processors.messages", 76 | "myproject.context_processors.extra_template_variables", 77 | ], 78 | }, 79 | }, 80 | ] 81 | 82 | ASGI_APPLICATION = "myproject.asgi.application" 83 | 84 | # Channels 85 | CHANNEL_LAYERS = { 86 | "default": { 87 | "BACKEND": "channels.layers.InMemoryChannelLayer", 88 | } 89 | } 90 | 91 | 92 | # Database 93 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 94 | 95 | DATABASES = { 96 | "default": { 97 | "ENGINE": "django.db.backends.postgresql", 98 | "NAME": os.environ.get("DB_NAME", default="mydatabase"), 99 | "USER": os.environ.get("DB_USER", default="postgres"), 100 | "PASSWORD": os.environ.get("DB_PASSWORD", default="postgres"), 101 | "HOST": os.environ.get("DB_HOST", default="db"), 102 | "PORT": os.environ.get("DB_PORT", default="5432"), 103 | } 104 | } 105 | 106 | 107 | # Password validation 108 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 109 | 110 | AUTH_PASSWORD_VALIDATORS = [ 111 | { 112 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 113 | }, 114 | { 115 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 116 | }, 117 | { 118 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 119 | }, 120 | { 121 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 122 | }, 123 | ] 124 | 125 | 126 | # Internationalization 127 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 128 | 129 | LANGUAGE_CODE = "en-us" 130 | 131 | TIME_ZONE = "UTC" 132 | 133 | USE_I18N = True 134 | 135 | USE_TZ = True 136 | 137 | # Media files 138 | MEDIA_ROOT = BASE_DIR / "media" 139 | 140 | # Static files (CSS, JavaScript, Images) 141 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 142 | 143 | STATICFILES_DIRS = [ 144 | BASE_DIR / "myproject" / "static", 145 | ] 146 | STATIC_ROOT = BASE_DIR / "static" 147 | STATIC_URL = "static/" 148 | 149 | # Default primary key field type 150 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 151 | 152 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 153 | -------------------------------------------------------------------------------- /app/myproject/settings/development.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | 3 | import os 4 | 5 | from distutils.util import strtobool 6 | 7 | from myproject.settings.base import * # noqa: F403 8 | 9 | DEBUG: bool = bool(strtobool(os.environ.get("DEBUG", default="True"))) 10 | 11 | ALLOWED_HOSTS = ["*"] 12 | 13 | INSTALLED_APPS.append("django_extensions") # noqa: F405 14 | -------------------------------------------------------------------------------- /app/myproject/settings/production.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | 3 | from myproject.settings.base import * # noqa 4 | -------------------------------------------------------------------------------- /app/myproject/static/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} 5 | -------------------------------------------------------------------------------- /app/myproject/static/css/table.css: -------------------------------------------------------------------------------- 1 | .htmx-swapping { 2 | opacity: 0; 3 | transition: opacity 50ms ease-out; 4 | } 5 | -------------------------------------------------------------------------------- /app/myproject/static/css/tailwind-input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /app/myproject/static/css/todo_list.css: -------------------------------------------------------------------------------- 1 | .strike_none{ 2 | text-decoration: none; 3 | } 4 | .selected_task{ 5 | background-color: #C2410C !important; 6 | } 7 | -------------------------------------------------------------------------------- /app/myproject/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/app/myproject/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /app/myproject/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/app/myproject/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /app/myproject/static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/app/myproject/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /app/myproject/static/js/flowbite.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("Flowbite",[],e):"object"==typeof exports?exports.Flowbite=e():t.Flowbite=e()}(self,(function(){return function(){"use strict";var t={647:function(t,e,i){i.r(e)},853:function(t,e,i){i.r(e),i.d(e,{afterMain:function(){return w},afterRead:function(){return y},afterWrite:function(){return k},applyStyles:function(){return H},arrow:function(){return Z},auto:function(){return a},basePlacements:function(){return c},beforeMain:function(){return b},beforeRead:function(){return _},beforeWrite:function(){return L},bottom:function(){return o},clippingParents:function(){return u},computeStyles:function(){return it},createPopper:function(){return Ht},createPopperBase:function(){return Pt},createPopperLite:function(){return Dt},detectOverflow:function(){return mt},end:function(){return d},eventListeners:function(){return ot},flip:function(){return yt},hide:function(){return wt},left:function(){return s},main:function(){return E},modifierPhases:function(){return O},offset:function(){return Lt},placements:function(){return g},popper:function(){return h},popperGenerator:function(){return Ct},popperOffsets:function(){return At},preventOverflow:function(){return kt},read:function(){return m},reference:function(){return f},right:function(){return r},start:function(){return l},top:function(){return n},variationPlacements:function(){return v},viewport:function(){return p},write:function(){return A}});var n="top",o="bottom",r="right",s="left",a="auto",c=[n,o,r,s],l="start",d="end",u="clippingParents",p="viewport",h="popper",f="reference",v=c.reduce((function(t,e){return t.concat([e+"-"+l,e+"-"+d])}),[]),g=[].concat(c,[a]).reduce((function(t,e){return t.concat([e,e+"-"+l,e+"-"+d])}),[]),_="beforeRead",m="read",y="afterRead",b="beforeMain",E="main",w="afterMain",L="beforeWrite",A="write",k="afterWrite",O=[_,m,y,b,E,w,L,A,k];function x(t){return t?(t.nodeName||"").toLowerCase():null}function I(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function T(t){return t instanceof I(t).Element||t instanceof Element}function C(t){return t instanceof I(t).HTMLElement||t instanceof HTMLElement}function P(t){return"undefined"!=typeof ShadowRoot&&(t instanceof I(t).ShadowRoot||t instanceof ShadowRoot)}var H={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},o=e.elements[t];C(o)&&x(o)&&(Object.assign(o.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?o.removeAttribute(t):o.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],o=e.attributes[t]||{},r=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});C(n)&&x(n)&&(Object.assign(n.style,r),Object.keys(o).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function D(t){return t.split("-")[0]}var j=Math.max,S=Math.min,z=Math.round;function M(){var t=navigator.userAgentData;return null!=t&&t.brands?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function q(){return!/^((?!chrome|android).)*safari/i.test(M())}function B(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),o=1,r=1;e&&C(t)&&(o=t.offsetWidth>0&&z(n.width)/t.offsetWidth||1,r=t.offsetHeight>0&&z(n.height)/t.offsetHeight||1);var s=(T(t)?I(t):window).visualViewport,a=!q()&&i,c=(n.left+(a&&s?s.offsetLeft:0))/o,l=(n.top+(a&&s?s.offsetTop:0))/r,d=n.width/o,u=n.height/r;return{width:d,height:u,top:l,right:c+d,bottom:l+u,left:c,x:c,y:l}}function R(t){var e=B(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function V(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&P(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function W(t){return I(t).getComputedStyle(t)}function F(t){return["table","td","th"].indexOf(x(t))>=0}function K(t){return((T(t)?t.ownerDocument:t.document)||window.document).documentElement}function N(t){return"html"===x(t)?t:t.assignedSlot||t.parentNode||(P(t)?t.host:null)||K(t)}function U(t){return C(t)&&"fixed"!==W(t).position?t.offsetParent:null}function X(t){for(var e=I(t),i=U(t);i&&F(i)&&"static"===W(i).position;)i=U(i);return i&&("html"===x(i)||"body"===x(i)&&"static"===W(i).position)?e:i||function(t){var e=/firefox/i.test(M());if(/Trident/i.test(M())&&C(t)&&"fixed"===W(t).position)return null;var i=N(t);for(P(i)&&(i=i.host);C(i)&&["html","body"].indexOf(x(i))<0;){var n=W(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Y(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function G(t,e,i){return j(t,S(e,i))}function J(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function Q(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}var Z={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,a=t.name,l=t.options,d=i.elements.arrow,u=i.modifiersData.popperOffsets,p=D(i.placement),h=Y(p),f=[s,r].indexOf(p)>=0?"height":"width";if(d&&u){var v=function(t,e){return J("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:Q(t,c))}(l.padding,i),g=R(d),_="y"===h?n:s,m="y"===h?o:r,y=i.rects.reference[f]+i.rects.reference[h]-u[h]-i.rects.popper[f],b=u[h]-i.rects.reference[h],E=X(d),w=E?"y"===h?E.clientHeight||0:E.clientWidth||0:0,L=y/2-b/2,A=v[_],k=w-g[f]-v[m],O=w/2-g[f]/2+L,x=G(A,O,k),I=h;i.modifiersData[a]=((e={})[I]=x,e.centerOffset=x-O,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&V(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function $(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,a=t.popperRect,c=t.placement,l=t.variation,u=t.offsets,p=t.position,h=t.gpuAcceleration,f=t.adaptive,v=t.roundOffsets,g=t.isFixed,_=u.x,m=void 0===_?0:_,y=u.y,b=void 0===y?0:y,E="function"==typeof v?v({x:m,y:b}):{x:m,y:b};m=E.x,b=E.y;var w=u.hasOwnProperty("x"),L=u.hasOwnProperty("y"),A=s,k=n,O=window;if(f){var x=X(i),T="clientHeight",C="clientWidth";if(x===I(i)&&"static"!==W(x=K(i)).position&&"absolute"===p&&(T="scrollHeight",C="scrollWidth"),c===n||(c===s||c===r)&&l===d)k=o,b-=(g&&x===O&&O.visualViewport?O.visualViewport.height:x[T])-a.height,b*=h?1:-1;if(c===s||(c===n||c===o)&&l===d)A=r,m-=(g&&x===O&&O.visualViewport?O.visualViewport.width:x[C])-a.width,m*=h?1:-1}var P,H=Object.assign({position:p},f&&tt),D=!0===v?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:z(e*n)/n||0,y:z(i*n)/n||0}}({x:m,y:b}):{x:m,y:b};return m=D.x,b=D.y,h?Object.assign({},H,((P={})[k]=L?"0":"",P[A]=w?"0":"",P.transform=(O.devicePixelRatio||1)<=1?"translate("+m+"px, "+b+"px)":"translate3d("+m+"px, "+b+"px, 0)",P)):Object.assign({},H,((e={})[k]=L?b+"px":"",e[A]=w?m+"px":"",e.transform="",e))}var it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,o=void 0===n||n,r=i.adaptive,s=void 0===r||r,a=i.roundOffsets,c=void 0===a||a,l={placement:D(e.placement),variation:$(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:o,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},l,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:s,roundOffsets:c})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},l,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:c})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}},nt={passive:!0};var ot={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,o=n.scroll,r=void 0===o||o,s=n.resize,a=void 0===s||s,c=I(e.elements.popper),l=[].concat(e.scrollParents.reference,e.scrollParents.popper);return r&&l.forEach((function(t){t.addEventListener("scroll",i.update,nt)})),a&&c.addEventListener("resize",i.update,nt),function(){r&&l.forEach((function(t){t.removeEventListener("scroll",i.update,nt)})),a&&c.removeEventListener("resize",i.update,nt)}},data:{}},rt={left:"right",right:"left",bottom:"top",top:"bottom"};function st(t){return t.replace(/left|right|bottom|top/g,(function(t){return rt[t]}))}var at={start:"end",end:"start"};function ct(t){return t.replace(/start|end/g,(function(t){return at[t]}))}function lt(t){var e=I(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function dt(t){return B(K(t)).left+lt(t).scrollLeft}function ut(t){var e=W(t),i=e.overflow,n=e.overflowX,o=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+o+n)}function pt(t){return["html","body","#document"].indexOf(x(t))>=0?t.ownerDocument.body:C(t)&&ut(t)?t:pt(N(t))}function ht(t,e){var i;void 0===e&&(e=[]);var n=pt(t),o=n===(null==(i=t.ownerDocument)?void 0:i.body),r=I(n),s=o?[r].concat(r.visualViewport||[],ut(n)?n:[]):n,a=e.concat(s);return o?a:a.concat(ht(N(s)))}function ft(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function vt(t,e,i){return e===p?ft(function(t,e){var i=I(t),n=K(t),o=i.visualViewport,r=n.clientWidth,s=n.clientHeight,a=0,c=0;if(o){r=o.width,s=o.height;var l=q();(l||!l&&"fixed"===e)&&(a=o.offsetLeft,c=o.offsetTop)}return{width:r,height:s,x:a+dt(t),y:c}}(t,i)):T(e)?function(t,e){var i=B(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):ft(function(t){var e,i=K(t),n=lt(t),o=null==(e=t.ownerDocument)?void 0:e.body,r=j(i.scrollWidth,i.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),s=j(i.scrollHeight,i.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),a=-n.scrollLeft+dt(t),c=-n.scrollTop;return"rtl"===W(o||i).direction&&(a+=j(i.clientWidth,o?o.clientWidth:0)-r),{width:r,height:s,x:a,y:c}}(K(t)))}function gt(t,e,i,n){var o="clippingParents"===e?function(t){var e=ht(N(t)),i=["absolute","fixed"].indexOf(W(t).position)>=0&&C(t)?X(t):t;return T(i)?e.filter((function(t){return T(t)&&V(t,i)&&"body"!==x(t)})):[]}(t):[].concat(e),r=[].concat(o,[i]),s=r[0],a=r.reduce((function(e,i){var o=vt(t,i,n);return e.top=j(o.top,e.top),e.right=S(o.right,e.right),e.bottom=S(o.bottom,e.bottom),e.left=j(o.left,e.left),e}),vt(t,s,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}function _t(t){var e,i=t.reference,a=t.element,c=t.placement,u=c?D(c):null,p=c?$(c):null,h=i.x+i.width/2-a.width/2,f=i.y+i.height/2-a.height/2;switch(u){case n:e={x:h,y:i.y-a.height};break;case o:e={x:h,y:i.y+i.height};break;case r:e={x:i.x+i.width,y:f};break;case s:e={x:i.x-a.width,y:f};break;default:e={x:i.x,y:i.y}}var v=u?Y(u):null;if(null!=v){var g="y"===v?"height":"width";switch(p){case l:e[v]=e[v]-(i[g]/2-a[g]/2);break;case d:e[v]=e[v]+(i[g]/2-a[g]/2)}}return e}function mt(t,e){void 0===e&&(e={});var i=e,s=i.placement,a=void 0===s?t.placement:s,l=i.strategy,d=void 0===l?t.strategy:l,v=i.boundary,g=void 0===v?u:v,_=i.rootBoundary,m=void 0===_?p:_,y=i.elementContext,b=void 0===y?h:y,E=i.altBoundary,w=void 0!==E&&E,L=i.padding,A=void 0===L?0:L,k=J("number"!=typeof A?A:Q(A,c)),O=b===h?f:h,x=t.rects.popper,I=t.elements[w?O:b],C=gt(T(I)?I:I.contextElement||K(t.elements.popper),g,m,d),P=B(t.elements.reference),H=_t({reference:P,element:x,strategy:"absolute",placement:a}),D=ft(Object.assign({},x,H)),j=b===h?D:P,S={top:C.top-j.top+k.top,bottom:j.bottom-C.bottom+k.bottom,left:C.left-j.left+k.left,right:j.right-C.right+k.right},z=t.modifiersData.offset;if(b===h&&z){var M=z[a];Object.keys(S).forEach((function(t){var e=[r,o].indexOf(t)>=0?1:-1,i=[n,o].indexOf(t)>=0?"y":"x";S[t]+=M[i]*e}))}return S}var yt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,d=t.name;if(!e.modifiersData[d]._skip){for(var u=i.mainAxis,p=void 0===u||u,h=i.altAxis,f=void 0===h||h,_=i.fallbackPlacements,m=i.padding,y=i.boundary,b=i.rootBoundary,E=i.altBoundary,w=i.flipVariations,L=void 0===w||w,A=i.allowedAutoPlacements,k=e.options.placement,O=D(k),x=_||(O===k||!L?[st(k)]:function(t){if(D(t)===a)return[];var e=st(t);return[ct(t),e,ct(e)]}(k)),I=[k].concat(x).reduce((function(t,i){return t.concat(D(i)===a?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,o=i.boundary,r=i.rootBoundary,s=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,d=void 0===l?g:l,u=$(n),p=u?a?v:v.filter((function(t){return $(t)===u})):c,h=p.filter((function(t){return d.indexOf(t)>=0}));0===h.length&&(h=p);var f=h.reduce((function(e,i){return e[i]=mt(t,{placement:i,boundary:o,rootBoundary:r,padding:s})[D(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}(e,{placement:i,boundary:y,rootBoundary:b,padding:m,flipVariations:L,allowedAutoPlacements:A}):i)}),[]),T=e.rects.reference,C=e.rects.popper,P=new Map,H=!0,j=I[0],S=0;S=0,R=B?"width":"height",V=mt(e,{placement:z,boundary:y,rootBoundary:b,altBoundary:E,padding:m}),W=B?q?r:s:q?o:n;T[R]>C[R]&&(W=st(W));var F=st(W),K=[];if(p&&K.push(V[M]<=0),f&&K.push(V[W]<=0,V[F]<=0),K.every((function(t){return t}))){j=z,H=!1;break}P.set(z,K)}if(H)for(var N=function(t){var e=I.find((function(e){var i=P.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return j=e,"break"},U=L?3:1;U>0;U--){if("break"===N(U))break}e.placement!==j&&(e.modifiersData[d]._skip=!0,e.placement=j,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function bt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Et(t){return[n,r,o,s].some((function(e){return t[e]>=0}))}var wt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,o=e.rects.popper,r=e.modifiersData.preventOverflow,s=mt(e,{elementContext:"reference"}),a=mt(e,{altBoundary:!0}),c=bt(s,n),l=bt(a,o,r),d=Et(c),u=Et(l);e.modifiersData[i]={referenceClippingOffsets:c,popperEscapeOffsets:l,isReferenceHidden:d,hasPopperEscaped:u},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":d,"data-popper-escaped":u})}};var Lt={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,o=t.name,a=i.offset,c=void 0===a?[0,0]:a,l=g.reduce((function(t,i){return t[i]=function(t,e,i){var o=D(t),a=[s,n].indexOf(o)>=0?-1:1,c="function"==typeof i?i(Object.assign({},e,{placement:t})):i,l=c[0],d=c[1];return l=l||0,d=(d||0)*a,[s,r].indexOf(o)>=0?{x:d,y:l}:{x:l,y:d}}(i,e.rects,c),t}),{}),d=l[e.placement],u=d.x,p=d.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=u,e.modifiersData.popperOffsets.y+=p),e.modifiersData[o]=l}};var At={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=_t({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}};var kt={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,a=t.name,c=i.mainAxis,d=void 0===c||c,u=i.altAxis,p=void 0!==u&&u,h=i.boundary,f=i.rootBoundary,v=i.altBoundary,g=i.padding,_=i.tether,m=void 0===_||_,y=i.tetherOffset,b=void 0===y?0:y,E=mt(e,{boundary:h,rootBoundary:f,padding:g,altBoundary:v}),w=D(e.placement),L=$(e.placement),A=!L,k=Y(w),O="x"===k?"y":"x",x=e.modifiersData.popperOffsets,I=e.rects.reference,T=e.rects.popper,C="function"==typeof b?b(Object.assign({},e.rects,{placement:e.placement})):b,P="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),H=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,z={x:0,y:0};if(x){if(d){var M,q="y"===k?n:s,B="y"===k?o:r,V="y"===k?"height":"width",W=x[k],F=W+E[q],K=W-E[B],N=m?-T[V]/2:0,U=L===l?I[V]:T[V],J=L===l?-T[V]:-I[V],Q=e.elements.arrow,Z=m&&Q?R(Q):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[q],it=tt[B],nt=G(0,I[V],Z[V]),ot=A?I[V]/2-N-nt-et-P.mainAxis:U-nt-et-P.mainAxis,rt=A?-I[V]/2+N+nt+it+P.mainAxis:J+nt+it+P.mainAxis,st=e.elements.arrow&&X(e.elements.arrow),at=st?"y"===k?st.clientTop||0:st.clientLeft||0:0,ct=null!=(M=null==H?void 0:H[k])?M:0,lt=W+rt-ct,dt=G(m?S(F,W+ot-ct-at):F,W,m?j(K,lt):K);x[k]=dt,z[k]=dt-W}if(p){var ut,pt="x"===k?n:s,ht="x"===k?o:r,ft=x[O],vt="y"===O?"height":"width",gt=ft+E[pt],_t=ft-E[ht],yt=-1!==[n,s].indexOf(w),bt=null!=(ut=null==H?void 0:H[O])?ut:0,Et=yt?gt:ft-I[vt]-T[vt]-bt+P.altAxis,wt=yt?ft+I[vt]+T[vt]-bt-P.altAxis:_t,Lt=m&&yt?function(t,e,i){var n=G(t,e,i);return n>i?i:n}(Et,ft,wt):G(m?Et:gt,ft,m?wt:_t);x[O]=Lt,z[O]=Lt-ft}e.modifiersData[a]=z}},requiresIfExists:["offset"]};function Ot(t,e,i){void 0===i&&(i=!1);var n,o,r=C(e),s=C(e)&&function(t){var e=t.getBoundingClientRect(),i=z(e.width)/t.offsetWidth||1,n=z(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=K(e),c=B(t,s,i),l={scrollLeft:0,scrollTop:0},d={x:0,y:0};return(r||!r&&!i)&&(("body"!==x(e)||ut(a))&&(l=(n=e)!==I(n)&&C(n)?{scrollLeft:(o=n).scrollLeft,scrollTop:o.scrollTop}:lt(n)),C(e)?((d=B(e,!0)).x+=e.clientLeft,d.y+=e.clientTop):a&&(d.x=dt(a))),{x:c.left+l.scrollLeft-d.x,y:c.top+l.scrollTop-d.y,width:c.width,height:c.height}}function xt(t){var e=new Map,i=new Set,n=[];function o(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&o(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||o(t)})),n}var It={placement:"bottom",modifiers:[],strategy:"absolute"};function Tt(){for(var t=arguments.length,e=new Array(t),i=0;i} messageQueue 164 | * @property {number} retryCount 165 | * @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state 166 | * @property {(message: string, sendElt: Element) => void} send 167 | * @property {(event: string, handler: Function) => void} addEventListener 168 | * @property {() => void} handleQueuedMessages 169 | * @property {() => void} init 170 | * @property {() => void} close 171 | */ 172 | /** 173 | * 174 | * @param socketElt 175 | * @param socketFunc 176 | * @returns {WebSocketWrapper} 177 | */ 178 | function createWebsocketWrapper(socketElt, socketFunc) { 179 | var wrapper = { 180 | socket: null, 181 | messageQueue: [], 182 | retryCount: 0, 183 | 184 | /** @type {Object} */ 185 | events: {}, 186 | 187 | addEventListener: function (event, handler) { 188 | if (this.socket) { 189 | this.socket.addEventListener(event, handler); 190 | } 191 | 192 | if (!this.events[event]) { 193 | this.events[event] = []; 194 | } 195 | 196 | this.events[event].push(handler); 197 | }, 198 | 199 | sendImmediately: function (message, sendElt) { 200 | if (!this.socket) { 201 | api.triggerErrorEvent() 202 | } 203 | if (sendElt && api.triggerEvent(sendElt, 'htmx:wsBeforeSend', { 204 | message: message, 205 | socketWrapper: this.publicInterface 206 | })) { 207 | this.socket.send(message); 208 | sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', { 209 | message: message, 210 | socketWrapper: this.publicInterface 211 | }) 212 | } 213 | }, 214 | 215 | send: function (message, sendElt) { 216 | if (this.socket.readyState !== this.socket.OPEN) { 217 | this.messageQueue.push({ message: message, sendElt: sendElt }); 218 | } else { 219 | this.sendImmediately(message, sendElt); 220 | } 221 | }, 222 | 223 | handleQueuedMessages: function () { 224 | while (this.messageQueue.length > 0) { 225 | var queuedItem = this.messageQueue[0] 226 | if (this.socket.readyState === this.socket.OPEN) { 227 | this.sendImmediately(queuedItem.message, queuedItem.sendElt); 228 | this.messageQueue.shift(); 229 | } else { 230 | break; 231 | } 232 | } 233 | }, 234 | 235 | init: function () { 236 | if (this.socket && this.socket.readyState === this.socket.OPEN) { 237 | // Close discarded socket 238 | this.socket.close() 239 | } 240 | 241 | // Create a new WebSocket and event handlers 242 | /** @type {WebSocket} */ 243 | var socket = socketFunc(); 244 | 245 | // The event.type detail is added for interface conformance with the 246 | // other two lifecycle events (open and close) so a single handler method 247 | // can handle them polymorphically, if required. 248 | api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } }); 249 | 250 | this.socket = socket; 251 | 252 | socket.onopen = function (e) { 253 | wrapper.retryCount = 0; 254 | api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface }); 255 | wrapper.handleQueuedMessages(); 256 | } 257 | 258 | socket.onclose = function (e) { 259 | // If socket should not be connected, stop further attempts to establish connection 260 | // If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause. 261 | if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) { 262 | var delay = getWebSocketReconnectDelay(wrapper.retryCount); 263 | setTimeout(function () { 264 | wrapper.retryCount += 1; 265 | wrapper.init(); 266 | }, delay); 267 | } 268 | 269 | // Notify client code that connection has been closed. Client code can inspect `event` field 270 | // to determine whether closure has been valid or abnormal 271 | api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface }) 272 | }; 273 | 274 | socket.onerror = function (e) { 275 | api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper }); 276 | maybeCloseWebSocketSource(socketElt); 277 | }; 278 | 279 | var events = this.events; 280 | Object.keys(events).forEach(function (k) { 281 | events[k].forEach(function (e) { 282 | socket.addEventListener(k, e); 283 | }) 284 | }); 285 | }, 286 | 287 | close: function () { 288 | this.socket.close() 289 | } 290 | } 291 | 292 | wrapper.init(); 293 | 294 | wrapper.publicInterface = { 295 | send: wrapper.send.bind(wrapper), 296 | sendImmediately: wrapper.sendImmediately.bind(wrapper), 297 | queue: wrapper.messageQueue 298 | }; 299 | 300 | return wrapper; 301 | } 302 | 303 | /** 304 | * ensureWebSocketSend attaches trigger handles to elements with 305 | * "ws-send" attribute 306 | * @param {HTMLElement} elt 307 | */ 308 | function ensureWebSocketSend(elt) { 309 | var legacyAttribute = api.getAttributeValue(elt, "hx-ws"); 310 | if (legacyAttribute && legacyAttribute !== 'send') { 311 | return; 312 | } 313 | 314 | var webSocketParent = api.getClosestMatch(elt, hasWebSocket) 315 | processWebSocketSend(webSocketParent, elt); 316 | } 317 | 318 | /** 319 | * hasWebSocket function checks if a node has webSocket instance attached 320 | * @param {HTMLElement} node 321 | * @returns {boolean} 322 | */ 323 | function hasWebSocket(node) { 324 | return api.getInternalData(node).webSocket != null; 325 | } 326 | 327 | /** 328 | * processWebSocketSend adds event listeners to the
element so that 329 | * messages can be sent to the WebSocket server when the form is submitted. 330 | * @param {HTMLElement} socketElt 331 | * @param {HTMLElement} sendElt 332 | */ 333 | function processWebSocketSend(socketElt, sendElt) { 334 | var nodeData = api.getInternalData(sendElt); 335 | var triggerSpecs = api.getTriggerSpecs(sendElt); 336 | triggerSpecs.forEach(function (ts) { 337 | api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) { 338 | if (maybeCloseWebSocketSource(socketElt)) { 339 | return; 340 | } 341 | 342 | /** @type {WebSocketWrapper} */ 343 | var socketWrapper = api.getInternalData(socketElt).webSocket; 344 | var headers = api.getHeaders(sendElt, api.getTarget(sendElt)); 345 | var results = api.getInputValues(sendElt, 'post'); 346 | var errors = results.errors; 347 | var rawParameters = results.values; 348 | var expressionVars = api.getExpressionVars(sendElt); 349 | var allParameters = api.mergeObjects(rawParameters, expressionVars); 350 | var filteredParameters = api.filterValues(allParameters, sendElt); 351 | 352 | var sendConfig = { 353 | parameters: filteredParameters, 354 | unfilteredParameters: allParameters, 355 | headers: headers, 356 | errors: errors, 357 | 358 | triggeringEvent: evt, 359 | messageBody: undefined, 360 | socketWrapper: socketWrapper.publicInterface 361 | }; 362 | 363 | if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) { 364 | return; 365 | } 366 | 367 | if (errors && errors.length > 0) { 368 | api.triggerEvent(elt, 'htmx:validation:halted', errors); 369 | return; 370 | } 371 | 372 | var body = sendConfig.messageBody; 373 | if (body === undefined) { 374 | var toSend = Object.assign({}, sendConfig.parameters); 375 | if (sendConfig.headers) 376 | toSend['HEADERS'] = headers; 377 | body = JSON.stringify(toSend); 378 | } 379 | 380 | socketWrapper.send(body, elt); 381 | 382 | if (api.shouldCancel(evt, elt)) { 383 | evt.preventDefault(); 384 | } 385 | }); 386 | }); 387 | } 388 | 389 | /** 390 | * getWebSocketReconnectDelay is the default easing function for WebSocket reconnects. 391 | * @param {number} retryCount // The number of retries that have already taken place 392 | * @returns {number} 393 | */ 394 | function getWebSocketReconnectDelay(retryCount) { 395 | 396 | /** @type {"full-jitter" | ((retryCount:number) => number)} */ 397 | var delay = htmx.config.wsReconnectDelay; 398 | if (typeof delay === 'function') { 399 | return delay(retryCount); 400 | } 401 | if (delay === 'full-jitter') { 402 | var exp = Math.min(retryCount, 6); 403 | var maxDelay = 1000 * Math.pow(2, exp); 404 | return maxDelay * Math.random(); 405 | } 406 | 407 | logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"'); 408 | } 409 | 410 | /** 411 | * maybeCloseWebSocketSource checks to the if the element that created the WebSocket 412 | * still exists in the DOM. If NOT, then the WebSocket is closed and this function 413 | * returns TRUE. If the element DOES EXIST, then no action is taken, and this function 414 | * returns FALSE. 415 | * 416 | * @param {*} elt 417 | * @returns 418 | */ 419 | function maybeCloseWebSocketSource(elt) { 420 | if (!api.bodyContains(elt)) { 421 | api.getInternalData(elt).webSocket.close(); 422 | return true; 423 | } 424 | return false; 425 | } 426 | 427 | /** 428 | * createWebSocket is the default method for creating new WebSocket objects. 429 | * it is hoisted into htmx.createWebSocket to be overridden by the user, if needed. 430 | * 431 | * @param {string} url 432 | * @returns WebSocket 433 | */ 434 | function createWebSocket(url) { 435 | var sock = new WebSocket(url, []); 436 | sock.binaryType = htmx.config.wsBinaryType; 437 | return sock; 438 | } 439 | 440 | /** 441 | * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT. 442 | * 443 | * @param {HTMLElement} elt 444 | * @param {string} attributeName 445 | */ 446 | function queryAttributeOnThisOrChildren(elt, attributeName) { 447 | 448 | var result = [] 449 | 450 | // If the parent element also contains the requested attribute, then add it to the results too. 451 | if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) { 452 | result.push(elt); 453 | } 454 | 455 | // Search all child nodes that match the requested attribute 456 | elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) { 457 | result.push(node) 458 | }) 459 | 460 | return result 461 | } 462 | 463 | /** 464 | * @template T 465 | * @param {T[]} arr 466 | * @param {(T) => void} func 467 | */ 468 | function forEach(arr, func) { 469 | if (arr) { 470 | for (var i = 0; i < arr.length; i++) { 471 | func(arr[i]); 472 | } 473 | } 474 | } 475 | 476 | })(); 477 | -------------------------------------------------------------------------------- /app/myproject/static/js/todo_list.js: -------------------------------------------------------------------------------- 1 | function checked(id) { 2 | var checked_green = document.getElementById("check" + id); 3 | checked_green.classList.toggle("selected_task"); 4 | var strike_unstrike = document.getElementById("strike" + id); 5 | strike_unstrike.classList.toggle("strike_none"); 6 | } 7 | -------------------------------------------------------------------------------- /app/myproject/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for myproject project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.conf import settings 18 | from django.conf.urls.static import static 19 | from django.contrib import admin 20 | from django.urls import include 21 | from django.urls import path 22 | 23 | app_name = "myproject" 24 | urlpatterns = [ 25 | # Admin 26 | path("admin/", admin.site.urls), 27 | # Apps 28 | # todo_tracker 29 | path("", include("todo_tracker.urls", namespace="todo-tracker")), 30 | # advanced_htmx 31 | path("advanced/", include("advanced_htmx.urls", namespace="advanced-htmx")), 32 | ] 33 | 34 | if settings.DEBUG: 35 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 36 | -------------------------------------------------------------------------------- /app/myproject/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for myproject 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.2/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", "myproject.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block title %}Example Web page{% endblock title %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% block head_extra %}{% endblock head_extra %} 20 | 21 | 22 | {% include "advanced_htmx/partials/header.html" %} 23 | {% block content %}{% endblock content %} 24 | {% include "advanced_htmx/partials/footer.html" %} 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/car_create.html: -------------------------------------------------------------------------------- 1 | {% extends "advanced_htmx/base.html" %} 2 | {% load static %} 3 | 4 | {% block head_extra %} 5 | 6 | {% endblock head_extra %} 7 | 8 | {% block content %} 9 | {% include "advanced_htmx/partials/car_create.html" %} 10 | {% endblock content %} 11 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/chat.html: -------------------------------------------------------------------------------- 1 | {% extends "advanced_htmx/base.html" %} 2 | {% load static %} 3 | 4 | {% block head_extra %} 5 | 6 | {% endblock head_extra %} 7 | 8 | {% block content %} 9 |
10 | {% include "advanced_htmx/mini_partials/chat_messages_container.html" %} 11 | {% include "advanced_htmx/partials/chat_messages_form.html" %} 12 |
13 | {% endblock content %} 14 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/mini_partials/car_create_form_field.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 17 | {% if errors %} 18 | {% for error in errors %} 19 |

20 | {{ error }} 21 |

22 | {% endfor %} 23 | {% endif %} 24 |
25 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/mini_partials/chat_messages_container.html: -------------------------------------------------------------------------------- 1 |
8 | {% for msg in messages %} 9 | 15 | {% endfor %} 16 |
17 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/mini_partials/product_random.html: -------------------------------------------------------------------------------- 1 |

11 | Random product every 10s: 12 | {% if random_product %} 13 | {{ random_product }} 14 | {% else %} 15 | Result loading... 16 | {% endif%} 17 |

18 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/mini_partials/product_random_event.html: -------------------------------------------------------------------------------- 1 |

11 | Random product when custom event(changed page): 12 | {% if random_product %} 13 | {{ random_product }} 14 | {% else %} 15 | Result loading... 16 | {% endif %} 17 |

18 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/mini_partials/products_counter.html: -------------------------------------------------------------------------------- 1 |

2 | Out of Band Element - Current Page: {{ page_number }} 3 |

4 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/partials/car_create.html: -------------------------------------------------------------------------------- 1 | 7 | {% comment %} Car name field {% endcomment %} 8 | {% include "advanced_htmx/mini_partials/car_create_form_field.html" with label_for="name" label="Car name" input_type="text" input_id="name" input_name="name" input_placeholder="Car" input_value=form.name.value input_required="true" errors=form.errors.name %} 9 | {% comment %} Car price field {% endcomment %} 10 | {% include "advanced_htmx/mini_partials/car_create_form_field.html" with label_for="price" label="Car price" input_type="number" input_id="price" input_name="price" input_value=form.price.value input_step=".01" input_required="true" errors=form.errors.price %} 11 | {% comment %} Car photo field {% endcomment %} 12 | {% include "advanced_htmx/mini_partials/car_create_form_field.html" with label_for="photo" label="Car photo" input_type="file" input_id="photo" input_name="photo" input_class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50focus:outline-none" errors=form.errors.photo %} 13 | 19 |
20 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/partials/chat_messages_form.html: -------------------------------------------------------------------------------- 1 |
7 |
8 | 9 | 19 | 36 |
37 |
38 | 39 | 48 |
49 |
50 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/partials/footer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Advanced HTMX 5 | 6 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/partials/header.html: -------------------------------------------------------------------------------- 1 |
2 | 68 |
69 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/partials/success_car_create.html: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/partials/table_product_item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ product.name }} 4 | 5 | {{ product.color.title }} 6 | {{ product.category.name }} 7 | ${{ product.price }} 8 | 9 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/partials/table_products_list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% if object_list %} 14 | {% for product in object_list %} 15 | {% include "advanced_htmx/partials/table_product_item.html" %} 16 | {% endfor %} 17 | {% else %} 18 | 19 | 30 | 31 | {% endif %} 32 | 33 |
Product nameColorCategoryPrice
20 |

21 | Nothing to show here. Go to the 22 | admin panel 27 | and add some products 28 |

29 |
34 |
35 | 36 | 72 |
73 | 74 | {% if request.headers.hx_request %} 75 | {{ page_obj.number }} 76 | {% endif %} 77 | -------------------------------------------------------------------------------- /app/templates/advanced_htmx/table.html: -------------------------------------------------------------------------------- 1 | {% extends "advanced_htmx/base.html" %} 2 | {% load static %} 3 | 4 | {% block head_extra %} 5 | 6 | {% endblock head_extra %} 7 | 8 | {% block content %} 9 |
10 | {% include "advanced_htmx/mini_partials/product_random_event.html" %} 11 | {% include "advanced_htmx/mini_partials/product_random.html" %} 12 | {% include "advanced_htmx/mini_partials/products_counter.html" with page_number=page_obj.number %} 13 |
14 | {% include "advanced_htmx/partials/table_products_list.html" %} 15 | {% endblock content %} 16 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block title %}Example Web page{% endblock title %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% block head_extra %}{% endblock head_extra %} 19 | 20 | 21 | {% block content %}{% endblock content %} 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/templates/partials/task_item.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 |
    4 |
    13 | 14 |
    15 | {{ task.value }} 18 |
    19 | {{ task.created_at|date:"g:i a" }}
    {{ task.created_at|date:"d/m/Y" }} 22 |
    23 |
    24 |
  • 25 | -------------------------------------------------------------------------------- /app/templates/partials/tasks_list.html: -------------------------------------------------------------------------------- 1 |
      2 |
    • 3 |
      4 |
      5 | 12 |
      13 | 18 |
      19 |
    • 20 | {% for task in object_list %} {% include "partials/task_item.html" with task=task %} {% endfor %} 21 |
    22 | -------------------------------------------------------------------------------- /app/templates/todo_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static%} 3 | 4 | {% block head_extra %} 5 | 6 | 7 | {% endblock head_extra %} 8 | {% block content %} 9 |
    10 |
    11 |
    12 |

    {% now "l d F" %}

    13 |

    {% now "h:i a" %}

    14 |
    15 |

    To-do List

    16 |
    17 |
    18 | {% for day in days %} 19 |

    {{ day|date:"D" }}

    20 | {% endfor %} 21 |
    22 |
    23 | {% for day in days %} 24 |

    {{ day|date:"d" }}

    25 | {% endfor %} 26 |
    27 |
    28 | {% include "partials/tasks_list.html" with object_list=task_list %} 29 |
    30 |
    31 | {% endblock content %} 32 | -------------------------------------------------------------------------------- /app/tests/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = tests/*,migrations/*,wsgi.py,asgi.py,manage.py 3 | -------------------------------------------------------------------------------- /app/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.fixtures.todo_tracker import * # noqa 4 | 5 | 6 | @pytest.fixture(scope="session", autouse=True) 7 | def db_setup(django_db_setup): 8 | ... 9 | 10 | 11 | @pytest.fixture(autouse=True) 12 | def get_db_access(db): 13 | ... 14 | -------------------------------------------------------------------------------- /app/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/app/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /app/tests/fixtures/todo_tracker.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | from faker import Faker 5 | 6 | from todo_tracker.models import Task 7 | 8 | fake = Faker() 9 | 10 | 11 | @pytest.fixture 12 | def fake_tasks(): 13 | tasks: List[Task] = [] 14 | 15 | for _ in range(10): 16 | task: Task = Task(value=fake.word()) 17 | tasks.append(task) 18 | 19 | Task.objects.bulk_create(tasks) 20 | return tasks 21 | -------------------------------------------------------------------------------- /app/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | 2 | [pytest] 3 | DJANGO_SETTINGS_MODULE=myproject.settings.development 4 | python_files = tests.py test_*.py *_tests.py 5 | -------------------------------------------------------------------------------- /app/tests/test_ajax/test_ajax_todo_list.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from typing import List 3 | from urllib.parse import urlencode 4 | 5 | from django.http.response import HttpResponse 6 | from django.template.response import TemplateResponse 7 | from django.test.client import Client 8 | from django.urls import reverse 9 | 10 | from todo_tracker.models import Task 11 | 12 | 13 | def test_create_todo_task(client: Client, fake_tasks: List[Task]): 14 | url: str = reverse("todo-tracker:ajax-task-create") 15 | 16 | data: Dict[str, str] = {"value": "Go outside!"} 17 | response: TemplateResponse = client.post( 18 | url, 19 | data=urlencode(data), 20 | content_type="application/x-www-form-urlencoded", 21 | ) 22 | 23 | assert response.status_code == 200 24 | 25 | task: Task = Task.objects.filter(value="Go outside!").first() 26 | assert task 27 | assert task.value in str(response.content) 28 | for fake_task in fake_tasks: 29 | assert fake_task.value in str(response.content) 30 | 31 | 32 | def test_delete_todo_task(client: Client, fake_tasks: List[Task]): 33 | original_tasks_count: int = Task.objects.count() 34 | url: str = reverse("todo-tracker:ajax-task-delete", kwargs={"task_id": fake_tasks[0].id}) 35 | 36 | response: HttpResponse = client.post(url) 37 | 38 | changed_tasks_count: int = Task.objects.count() 39 | assert response.status_code == 200 40 | assert changed_tasks_count == original_tasks_count - 1 41 | -------------------------------------------------------------------------------- /app/tests/test_views/test_todo_list.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from django.template.response import TemplateResponse 4 | from django.test.client import Client 5 | from django.urls import reverse 6 | 7 | from todo_tracker.models import Task 8 | 9 | 10 | def test_get_todo_list(client: Client, fake_tasks: List[Task]): 11 | url: str = reverse("todo-tracker:todo-list") 12 | response: TemplateResponse = client.get(url) 13 | 14 | assert response.status_code == 200 15 | for task in fake_tasks: 16 | assert task.value in response.rendered_content 17 | -------------------------------------------------------------------------------- /app/todo_tracker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/app/todo_tracker/__init__.py -------------------------------------------------------------------------------- /app/todo_tracker/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from todo_tracker.models import Task 4 | 5 | 6 | class TaskAdmin(admin.ModelAdmin): 7 | pass 8 | 9 | 10 | admin.site.register(Task, TaskAdmin) 11 | -------------------------------------------------------------------------------- /app/todo_tracker/ajax/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/app/todo_tracker/ajax/__init__.py -------------------------------------------------------------------------------- /app/todo_tracker/ajax/todo_list.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.shortcuts import render 3 | from django.views.generic import CreateView 4 | from django.views.generic import DeleteView 5 | from django.views.generic import ListView 6 | 7 | from todo_tracker.models import Task 8 | 9 | 10 | class AjaxTaskCreateView(CreateView, ListView): 11 | model = Task 12 | http_method_names = ["post"] 13 | fields = [ 14 | "value", 15 | ] 16 | template_name = "partials/tasks_list.html" 17 | 18 | paginate_by = 25 19 | object_list = Task.objects.order_by("-created_at") 20 | 21 | def get_success_url(self): 22 | # Not used, required by Django generic view 23 | return "/" 24 | 25 | def post(self, request, *args, **kwargs): 26 | super().post(request, *args, **kwargs) 27 | 28 | return render( 29 | request, 30 | self.template_name, 31 | context=self.get_context_data(), 32 | ) 33 | 34 | 35 | class AjaxTaskDeleteView(DeleteView): 36 | model = Task 37 | pk_url_kwarg = "task_id" 38 | http_method_names = ["post"] 39 | 40 | def get_success_url(self): 41 | # Not used, required by Django generic view 42 | return "/" 43 | 44 | def post(self, request, *args, **kwargs): 45 | super().post(request, *args, **kwargs) 46 | 47 | return HttpResponse() 48 | -------------------------------------------------------------------------------- /app/todo_tracker/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TodoTrackerConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "todo_tracker" 7 | -------------------------------------------------------------------------------- /app/todo_tracker/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-09-22 13:44 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Task", 15 | fields=[ 16 | ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 17 | ("value", models.CharField(max_length=255)), 18 | ("created_at", models.DateTimeField(auto_now_add=True)), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /app/todo_tracker/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/app/todo_tracker/migrations/__init__.py -------------------------------------------------------------------------------- /app/todo_tracker/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Task(models.Model): 5 | value = models.CharField(max_length=255) 6 | created_at = models.DateTimeField(auto_now_add=True) 7 | 8 | def __str__(self): 9 | return self.value 10 | -------------------------------------------------------------------------------- /app/todo_tracker/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from todo_tracker.ajax.todo_list import AjaxTaskCreateView 4 | from todo_tracker.ajax.todo_list import AjaxTaskDeleteView 5 | from todo_tracker.views.todo_list import TodoListView 6 | 7 | app_name = "todo-tracker" 8 | urlpatterns = [ 9 | path("", TodoListView.as_view(), name="todo-list"), 10 | # Ajax views 11 | path("ajax/todo-list", AjaxTaskCreateView.as_view(), name="ajax-task-create"), 12 | path("ajax/todo-list/", AjaxTaskDeleteView.as_view(), name="ajax-task-delete"), 13 | ] 14 | -------------------------------------------------------------------------------- /app/todo_tracker/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/app/todo_tracker/views/__init__.py -------------------------------------------------------------------------------- /app/todo_tracker/views/todo_list.py: -------------------------------------------------------------------------------- 1 | from django.views.generic.list import ListView 2 | 3 | from todo_tracker.models import Task 4 | 5 | 6 | class TodoListView(ListView): 7 | model = Task 8 | template_name = "todo_list.html" 9 | 10 | paginate_by = 25 11 | queryset = Task.objects.order_by("-created_at") 12 | -------------------------------------------------------------------------------- /docker-compose-circleci.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | x-base-app-conf: &base_app_conf 4 | env_file: .env 5 | stdin_open: true 6 | tty: true 7 | 8 | services: 9 | myproject: 10 | <<: *base_app_conf 11 | image: myproject:latest 12 | container_name: myproject 13 | restart: always 14 | build: 15 | context: . 16 | dockerfile: docker/Dockerfile 17 | depends_on: 18 | - db 19 | 20 | db: 21 | image: postgres:16.0-alpine 22 | container_name: db 23 | restart: always 24 | environment: 25 | - POSTGRES_DB=mydatabase 26 | - POSTGRES_USER=postgres 27 | - POSTGRES_PASSWORD=postgres 28 | ports: 29 | - "5432:5432" 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | x-base-app-conf: &base_app_conf 4 | env_file: .env 5 | stdin_open: true 6 | tty: true 7 | 8 | services: 9 | myproject: 10 | <<: *base_app_conf 11 | image: myproject:latest 12 | container_name: myproject 13 | restart: always 14 | build: 15 | context: . 16 | dockerfile: docker/Dockerfile 17 | ports: 18 | - "8000:8000" 19 | volumes: 20 | - "./app:/src/app" 21 | depends_on: 22 | - db 23 | - django-migrations 24 | 25 | # Apply Django migrations 26 | django-migrations: 27 | <<: *base_app_conf 28 | image: myproject:latest 29 | container_name: django-migrations 30 | command: python manage.py migrate 31 | restart: no 32 | build: 33 | context: . 34 | dockerfile: docker/Dockerfile 35 | volumes: 36 | - "./app:/src/app" 37 | depends_on: 38 | - db 39 | 40 | # Generating CSS output file for Tailwind CSS on save 41 | npm-watch: 42 | <<: *base_app_conf 43 | image: myproject:latest 44 | container_name: npm-watch 45 | working_dir: /src 46 | command: npm run watch:tailwindcss 47 | restart: always 48 | volumes: 49 | - "./app:/src/app" 50 | depends_on: 51 | - myproject 52 | # Copying JS file from htmx package 53 | htmx-js-generator: 54 | <<: *base_app_conf 55 | image: myproject:latest 56 | container_name: htmx-js-generator 57 | working_dir: /src 58 | command: npm run build:htmx 59 | restart: no 60 | volumes: 61 | - "./app:/src/app" 62 | depends_on: 63 | - myproject 64 | 65 | db: 66 | image: postgres:16.0-alpine 67 | command: ["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"] 68 | container_name: db 69 | restart: always 70 | environment: 71 | - POSTGRES_DB=mydatabase 72 | - POSTGRES_USER=postgres 73 | - POSTGRES_PASSWORD=postgres 74 | ports: 75 | - "5432:5432" 76 | volumes: 77 | - db:/var/lib/postgresql/data 78 | volumes: 79 | db: 80 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11.5-slim 2 | 3 | # Install Node.js 4 | RUN apt update && apt install -y nodejs npm 5 | 6 | # Install requirements 7 | ARG REQUIREMENTS_FILE=base.txt 8 | ADD requirements/${REQUIREMENTS_FILE} requirements.txt 9 | RUN pip install pip --upgrade && pip install -r requirements.txt 10 | 11 | # Copy project 12 | ADD /app /src/app 13 | ADD /tailwindcss/ /src 14 | 15 | # Install Tailwind CSS 16 | WORKDIR /src 17 | RUN npm install 18 | RUN npm run build:tailwindcss 19 | RUN npm run build:htmx 20 | 21 | # Set final workdir 22 | WORKDIR /src/app 23 | 24 | # Command to run when image started 25 | CMD python manage.py runserver 0.0.0.0:8000 26 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | DJANGO_SETTINGS_MODULE=myproject.settings.development 2 | SECRET_KEY=secret 3 | DEBUG=True 4 | DB_NAME=mydatabase 5 | DB_USER=postgres 6 | DB_PASSWORD=postgres 7 | DB_HOST=db 8 | DB_PORT=5432 9 | -------------------------------------------------------------------------------- /requirements/base.in: -------------------------------------------------------------------------------- 1 | Django==4.2.5 2 | markdown==3.4.4 3 | Pillow==10.1.0 4 | 5 | psycopg2-binary==2.9.7 6 | channels[daphne]==4.0.0 7 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile requirements/base.in 6 | # 7 | asgiref==3.7.2 8 | # via 9 | # channels 10 | # daphne 11 | # django 12 | attrs==23.1.0 13 | # via 14 | # automat 15 | # service-identity 16 | # twisted 17 | autobahn==23.6.2 18 | # via daphne 19 | automat==22.10.0 20 | # via twisted 21 | cffi==1.16.0 22 | # via cryptography 23 | channels[daphne]==4.0.0 24 | # via 25 | # -r requirements/base.in 26 | # channels 27 | constantly==15.1.0 28 | # via twisted 29 | cryptography==41.0.5 30 | # via 31 | # autobahn 32 | # pyopenssl 33 | # service-identity 34 | daphne==4.0.0 35 | # via channels 36 | django==4.2.5 37 | # via 38 | # -r requirements/base.in 39 | # channels 40 | hyperlink==21.0.0 41 | # via 42 | # autobahn 43 | # twisted 44 | idna==3.4 45 | # via 46 | # hyperlink 47 | # twisted 48 | incremental==22.10.0 49 | # via twisted 50 | markdown==3.4.4 51 | # via -r requirements/base.in 52 | pillow==10.1.0 53 | # via -r requirements/base.in 54 | psycopg2-binary==2.9.7 55 | # via -r requirements/base.in 56 | pyasn1==0.5.0 57 | # via 58 | # pyasn1-modules 59 | # service-identity 60 | pyasn1-modules==0.3.0 61 | # via service-identity 62 | pycparser==2.21 63 | # via cffi 64 | pyopenssl==23.3.0 65 | # via twisted 66 | service-identity==23.1.0 67 | # via twisted 68 | six==1.16.0 69 | # via automat 70 | sqlparse==0.4.4 71 | # via django 72 | twisted[tls]==23.8.0 73 | # via 74 | # daphne 75 | # twisted 76 | txaio==23.1.1 77 | # via autobahn 78 | typing-extensions==4.8.0 79 | # via twisted 80 | zope-interface==6.1 81 | # via twisted 82 | 83 | # The following packages are considered to be unsafe in a requirements file: 84 | # setuptools 85 | -------------------------------------------------------------------------------- /requirements/dev.in: -------------------------------------------------------------------------------- 1 | -r base.in 2 | 3 | django-extensions==3.2.3 4 | 5 | pytest==7.4.2 6 | pytest-cov==4.1.0 7 | pytest-django==4.5.2 8 | Faker 9 | 10 | ipdb 11 | ipython 12 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile requirements/dev.in 6 | # 7 | appnope==0.1.3 8 | # via ipython 9 | asgiref==3.7.2 10 | # via 11 | # channels 12 | # daphne 13 | # django 14 | asttokens==2.4.0 15 | # via stack-data 16 | attrs==23.1.0 17 | # via 18 | # automat 19 | # service-identity 20 | # twisted 21 | autobahn==23.6.2 22 | # via daphne 23 | automat==22.10.0 24 | # via twisted 25 | backcall==0.2.0 26 | # via ipython 27 | cffi==1.16.0 28 | # via cryptography 29 | channels[daphne]==4.0.0 30 | # via 31 | # -r requirements/base.in 32 | # channels 33 | constantly==15.1.0 34 | # via twisted 35 | coverage[toml]==7.3.2 36 | # via 37 | # coverage 38 | # pytest-cov 39 | cryptography==41.0.5 40 | # via 41 | # autobahn 42 | # pyopenssl 43 | # service-identity 44 | daphne==4.0.0 45 | # via channels 46 | decorator==5.1.1 47 | # via 48 | # ipdb 49 | # ipython 50 | django==4.2.5 51 | # via 52 | # -r requirements/base.in 53 | # channels 54 | # django-extensions 55 | django-extensions==3.2.3 56 | # via -r requirements/dev.in 57 | executing==1.2.0 58 | # via stack-data 59 | faker==19.6.2 60 | # via -r requirements/dev.in 61 | hyperlink==21.0.0 62 | # via 63 | # autobahn 64 | # twisted 65 | idna==3.4 66 | # via 67 | # hyperlink 68 | # twisted 69 | incremental==22.10.0 70 | # via twisted 71 | iniconfig==2.0.0 72 | # via pytest 73 | ipdb==0.13.13 74 | # via -r requirements/dev.in 75 | ipython==8.15.0 76 | # via 77 | # -r requirements/dev.in 78 | # ipdb 79 | jedi==0.19.0 80 | # via ipython 81 | markdown==3.4.4 82 | # via -r requirements/base.in 83 | matplotlib-inline==0.1.6 84 | # via ipython 85 | packaging==23.1 86 | # via pytest 87 | parso==0.8.3 88 | # via jedi 89 | pexpect==4.8.0 90 | # via ipython 91 | pickleshare==0.7.5 92 | # via ipython 93 | pillow==10.1.0 94 | # via -r requirements/base.in 95 | pluggy==1.3.0 96 | # via pytest 97 | prompt-toolkit==3.0.39 98 | # via ipython 99 | psycopg2-binary==2.9.7 100 | # via -r requirements/base.in 101 | ptyprocess==0.7.0 102 | # via pexpect 103 | pure-eval==0.2.2 104 | # via stack-data 105 | pyasn1==0.5.0 106 | # via 107 | # pyasn1-modules 108 | # service-identity 109 | pyasn1-modules==0.3.0 110 | # via service-identity 111 | pycparser==2.21 112 | # via cffi 113 | pygments==2.16.1 114 | # via ipython 115 | pyopenssl==23.3.0 116 | # via twisted 117 | pytest==7.4.2 118 | # via 119 | # -r requirements/dev.in 120 | # pytest-cov 121 | # pytest-django 122 | pytest-cov==4.1.0 123 | # via -r requirements/dev.in 124 | pytest-django==4.5.2 125 | # via -r requirements/dev.in 126 | python-dateutil==2.8.2 127 | # via faker 128 | service-identity==23.1.0 129 | # via twisted 130 | six==1.16.0 131 | # via 132 | # asttokens 133 | # automat 134 | # python-dateutil 135 | sqlparse==0.4.4 136 | # via django 137 | stack-data==0.6.2 138 | # via ipython 139 | traitlets==5.10.0 140 | # via 141 | # ipython 142 | # matplotlib-inline 143 | twisted[tls]==23.8.0 144 | # via 145 | # daphne 146 | # twisted 147 | txaio==23.1.1 148 | # via autobahn 149 | typing-extensions==4.8.0 150 | # via twisted 151 | wcwidth==0.2.6 152 | # via prompt-toolkit 153 | zope-interface==6.1 154 | # via twisted 155 | 156 | # The following packages are considered to be unsafe in a requirements file: 157 | # setuptools 158 | -------------------------------------------------------------------------------- /screenshots/car_create_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/screenshots/car_create_screen.png -------------------------------------------------------------------------------- /screenshots/chat_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/screenshots/chat_screen.png -------------------------------------------------------------------------------- /screenshots/sample_app_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/screenshots/sample_app_view.png -------------------------------------------------------------------------------- /screenshots/table_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunscrapers/django-htmx-tailwindcss/893b86d87bb6c217403935005a2f0b1aaca80f14/screenshots/table_screen.png -------------------------------------------------------------------------------- /tailwindcss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "autoprefixer": "^10.4.16", 4 | "postcss": "^8.4.31", 5 | "postcss-cli": "^10.1.0", 6 | "tailwindcss": "3.3.3", 7 | "htmx.org": "1.9.6", 8 | "cssnano": "6.0.1", 9 | "npm-watch": "0.11.0" 10 | }, 11 | "watch": { 12 | "build:tailwindcss": { 13 | "patterns": [ 14 | "./app/myproject/static/**/*.{html,js}", 15 | "./app/templates/**/*.{html,js}" 16 | ], 17 | "extensions": "html,js", 18 | "quiet": false 19 | } 20 | }, 21 | "scripts": { 22 | "build:tailwindcss": "postcss ./app/myproject/static/css/tailwind-input.css -o ./app/myproject/static/css/tailwind-output.css", 23 | "build:htmx": "cp node_modules/htmx.org/dist/htmx.min.js ./app/myproject/static/js/htmx.min.js", 24 | "watch:tailwindcss": "npm-watch build:tailwindcss" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tailwindcss/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | cssnano: {}, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tailwindcss/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/myproject/static/**/*.{html,js}", 5 | "./app/templates/**/*.{html,js}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | --------------------------------------------------------------------------------