Add simpletodo htmx example app
This commit is contained in:
parent
15bea892e4
commit
db291b50c0
|
|
@ -82,6 +82,7 @@ THIRD_PARTY_APPS = [
|
|||
"rest_framework.authtoken",
|
||||
"corsheaders",
|
||||
"drf_spectacular",
|
||||
"django_htmx",
|
||||
]
|
||||
|
||||
LOCAL_APPS = [
|
||||
|
|
@ -145,6 +146,7 @@ MIDDLEWARE = [
|
|||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.common.BrokenLinkEmailsMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"django_htmx.middleware.HtmxMiddleware",
|
||||
"vbv_lernwelt.core.middleware.auth.AuthenticationRequiredMiddleware",
|
||||
"vbv_lernwelt.core.middleware.security.SecurityRequestResponseLoggingMiddleware",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,26 +11,15 @@ from rest_framework.authtoken.views import obtain_auth_token
|
|||
|
||||
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
||||
|
||||
# fmt: off
|
||||
urlpatterns = [
|
||||
path(
|
||||
"",
|
||||
django_view_authentication_exempt(
|
||||
TemplateView.as_view(template_name="pages/home.html")
|
||||
),
|
||||
name="home",
|
||||
),
|
||||
path(
|
||||
"about/", TemplateView.as_view(template_name="pages/about.html"), name="about"
|
||||
),
|
||||
path("", django_view_authentication_exempt(TemplateView.as_view(template_name="pages/home.html")), name="home"),
|
||||
path("about/", TemplateView.as_view(template_name="pages/about.html"), name="about"),
|
||||
# Django Admin, use {% url 'admin:index' %}
|
||||
path(settings.ADMIN_URL, admin.site.urls),
|
||||
# Your stuff: custom urls includes go here
|
||||
path(
|
||||
"login/",
|
||||
django_view_authentication_exempt(
|
||||
auth_views.LoginView.as_view(template_name="core/login.html")
|
||||
),
|
||||
),
|
||||
path("login/", django_view_authentication_exempt(auth_views.LoginView.as_view(template_name="core/login.html"))),
|
||||
path("todo/", include("vbv_lernwelt.simpletodo.urls")),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
if settings.DEBUG:
|
||||
# Static file serving when using Gunicorn + Uvicorn for local web socket development
|
||||
|
|
@ -43,12 +32,9 @@ urlpatterns += [
|
|||
# DRF auth token
|
||||
path("auth-token/", obtain_auth_token),
|
||||
path("api/schema/", SpectacularAPIView.as_view(), name="api-schema"),
|
||||
path(
|
||||
"api/docs/",
|
||||
SpectacularSwaggerView.as_view(url_name="api-schema"),
|
||||
name="api-docs",
|
||||
),
|
||||
path("api/docs/", SpectacularSwaggerView.as_view(url_name="api-schema"), name="api-docs",),
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
if settings.DEBUG:
|
||||
# This allows the error pages to be debugged during development, just visit
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export VBV_DATABASE_URL='postgres://vbv_lernwelt@localhost:5432/vbv_lernwelt'
|
||||
#export VBV_DJANGO_LOGGING_CONF=VBV_DJANGO_LOGGING_CONF_CONSOLE_COLOR
|
||||
export VBV_DJANGO_LOGGING_CONF=VBV_DJANGO_LOGGING_CONF_CONSOLE_COLOR
|
||||
export VBV_DJANGO_DEBUG=True
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ django==3.2.12
|
|||
# django-cors-headers
|
||||
# django-debug-toolbar
|
||||
# django-extensions
|
||||
# django-htmx
|
||||
# django-model-utils
|
||||
# django-redis
|
||||
# django-stubs
|
||||
|
|
@ -83,6 +84,8 @@ django-debug-toolbar==3.2.4
|
|||
# via -r requirements-dev.in
|
||||
django-extensions==3.1.5
|
||||
# via -r requirements-dev.in
|
||||
django-htmx==1.8.0
|
||||
# via -r requirements.in
|
||||
django-model-utils==4.2.0
|
||||
# via -r requirements.in
|
||||
django-redis==5.2.0
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ djangorestframework # https://github.com/encode/django-rest-framework
|
|||
django-cors-headers # https://github.com/adamchainz/django-cors-headers
|
||||
# DRF-spectacular for api documentation
|
||||
drf-spectacular
|
||||
django-htmx
|
||||
|
||||
psycopg2-binary
|
||||
gunicorn
|
||||
|
|
|
|||
|
|
@ -30,12 +30,15 @@ django==3.2.12
|
|||
# via
|
||||
# -r requirements.in
|
||||
# django-cors-headers
|
||||
# django-htmx
|
||||
# django-model-utils
|
||||
# django-redis
|
||||
# djangorestframework
|
||||
# drf-spectacular
|
||||
django-cors-headers==3.11.0
|
||||
# via -r requirements.in
|
||||
django-htmx==1.8.0
|
||||
# via -r requirements.in
|
||||
django-model-utils==4.2.0
|
||||
# via -r requirements.in
|
||||
django-redis==5.2.0
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from django.contrib.auth import get_user_model
|
|||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
|
||||
class SimpleUserFactory(factory.django.DjangoModelFactory):
|
||||
class UserFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = get_user_model()
|
||||
django_get_or_create = ("username",)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,39 @@
|
|||
# Register your models here.
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from .models import SimpleTask, SimpleList
|
||||
|
||||
|
||||
@admin.register(SimpleList)
|
||||
class SimpleListAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"title",
|
||||
"user",
|
||||
"created",
|
||||
]
|
||||
list_filter = [
|
||||
"user",
|
||||
]
|
||||
search_fields = [
|
||||
"title",
|
||||
]
|
||||
|
||||
|
||||
@admin.register(SimpleTask)
|
||||
class SimpleTaskAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = "deadline"
|
||||
list_display = [
|
||||
"title",
|
||||
"deadline",
|
||||
"created",
|
||||
"list",
|
||||
"done",
|
||||
]
|
||||
list_filter = [
|
||||
"list",
|
||||
"done",
|
||||
]
|
||||
search_fields = [
|
||||
"title",
|
||||
"text",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class SimpletodoConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'vbv_lernwelt.simpletodo'
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "vbv_lernwelt.simpletodo"
|
||||
|
|
|
|||
|
|
@ -18,32 +18,88 @@ class Migration(migrations.Migration):
|
|||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SimpleList',
|
||||
name="SimpleList",
|
||||
fields=[
|
||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"created",
|
||||
model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="created",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modified",
|
||||
model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=255)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SimpleTask',
|
||||
name="SimpleTask",
|
||||
fields=[
|
||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
|
||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('text', models.TextField(blank=True, default='')),
|
||||
('done', models.BooleanField(default=False)),
|
||||
('deadline', models.DateTimeField(blank=True, null=True)),
|
||||
('list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='simpletodo.simplelist')),
|
||||
(
|
||||
"created",
|
||||
model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="created",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modified",
|
||||
model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=255)),
|
||||
("text", models.TextField(blank=True, default="")),
|
||||
("done", models.BooleanField(default=False)),
|
||||
("deadline", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"list",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="simpletodo.simplelist",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ class SimpleList(TimeStampedModel):
|
|||
title = models.CharField(max_length=255)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} ({self.user})"
|
||||
|
||||
|
||||
class SimpleTask(TimeStampedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
from rest_framework import serializers
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
|
||||
from vbv_lernwelt.simpletodo.models import SimpleTask, SimpleList
|
||||
|
||||
|
||||
class SimpleTaskSerializer(ModelSerializer):
|
||||
list_title = serializers.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
model = SimpleTask
|
||||
fields = ['id', 'title', 'text', 'done', 'deadline', 'list_title', ]
|
||||
|
||||
def create(self, validated_data):
|
||||
user = validated_data.pop('user', None)
|
||||
if user is None:
|
||||
raise serializers.ValidationError('User is required')
|
||||
|
||||
list_title = validated_data.pop('list_title')
|
||||
simple_list, _ = SimpleList.objects.get_or_create(title=list_title, user=user)
|
||||
|
||||
validated_data['list'] = simple_list
|
||||
return super().create(validated_data)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container mx-auto">
|
||||
<h1>Hello Todos</h1>
|
||||
|
||||
{% for list in simple_lists %}
|
||||
{% include "simpletodo/partials/simple_list.html" with list=list%}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<form class="flex mt-4" action="/todo/api/tasks/" method="POST">
|
||||
{% csrf_token %}
|
||||
<input class="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-gray-darker"
|
||||
type="text"
|
||||
name="title"
|
||||
maxlength="100"
|
||||
required
|
||||
placeholder="Add Todo"
|
||||
>
|
||||
<input type="hidden" name="list_title" value="{{ list.title }}">
|
||||
|
||||
<input
|
||||
type="submit"
|
||||
value="Add"
|
||||
hx-post="/todo/api/tasks/"
|
||||
hx-trigger="submit"
|
||||
hx-target="#parent-div"
|
||||
hx-swap="outerHTML"
|
||||
class="flex-no-shrink p-2 border-2 rounded text-blue-500 border-blue-500 hover:text-white hover:bg-blue-500"
|
||||
>
|
||||
</form>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<div class="h-100 w-full flex items-center justify-center bg-blue-100 font-sans">
|
||||
|
||||
<div class="bg-white rounded shadow p-6 m-4 w-full lg:w-3/5 md:w-3/4">
|
||||
<div class="mb-4">
|
||||
<h2 class="text-gray-darkest">{{ list.title }}</h2>
|
||||
{% include "simpletodo/partials/add_task_form.html" with task=task %}
|
||||
</div>
|
||||
{% for task in list.simpletask_set.all %}
|
||||
{% include "simpletodo/partials/task.html" with task=task %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<div class="task">
|
||||
<div class="flex mb-4 items-center">
|
||||
{% if task.done %}
|
||||
<p class="flex-auto text-blue-500 line-through">
|
||||
{% else %}
|
||||
<p class="flex-auto text-blue-900">
|
||||
{% endif %}
|
||||
{{ task.title }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
hx-post="/todo/api/tasks/{{ task.id }}/toggle_done/"
|
||||
hx-swap="outerHTML swap:0.5s"
|
||||
hx-target="closest .task"
|
||||
{% if task.done %}
|
||||
class="flex-no-shrink p-2 ml-4 mr-2 border-2 rounded hover:text-white text-gray-500 border-gray-500 hover:bg-gray-500"
|
||||
{% else %}
|
||||
class="flex-no-shrink p-2 ml-4 mr-2 border-2 rounded hover:text-white text-green-500 border-green-500 hover:bg-green-500"
|
||||
{% endif %}
|
||||
>
|
||||
{% if task.done %}
|
||||
Not Done
|
||||
{% else %}
|
||||
Done
|
||||
{% endif %}
|
||||
</button>
|
||||
<button
|
||||
hx-delete="/todo/api/tasks/{{ task.id }}/"
|
||||
hx-swap="outerHTML swap:0.5s"
|
||||
hx-target="closest .task"
|
||||
class="flex-no-shrink p-2 ml-2 border-2 rounded text-red-500 border-red-500 hover:text-white hover:bg-red-500">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
from django.test import TestCase
|
||||
|
||||
from vbv_lernwelt.core.tests.factories import UserFactory
|
||||
from vbv_lernwelt.simpletodo.models import SimpleTask
|
||||
from vbv_lernwelt.simpletodo.serializers import SimpleTaskSerializer
|
||||
|
||||
|
||||
class SimpleTaskSerializerTestCase(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user = UserFactory()
|
||||
|
||||
def test_serializer(self):
|
||||
serializer = SimpleTaskSerializer(data={
|
||||
'title': 'Test',
|
||||
'list_title': 'Todos',
|
||||
})
|
||||
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(user=self.user)
|
||||
|
||||
task = SimpleTask.objects.first()
|
||||
|
||||
self.assertEqual(task.title, 'Test')
|
||||
self.assertEqual(task.list.title, 'Todos')
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
from django.conf.urls import url, include
|
||||
from django.urls import path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from . import views
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'tasks', views.SimpleTaskViewSet, basename='tasks')
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.index, name="index"),
|
||||
url(r'^api/', include(router.urls)),
|
||||
]
|
||||
|
|
@ -1,3 +1,85 @@
|
|||
from django.shortcuts import render
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.renderers import TemplateHTMLRenderer, JSONRenderer
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Create your views here.
|
||||
from vbv_lernwelt.simpletodo.models import SimpleList, SimpleTask
|
||||
from vbv_lernwelt.simpletodo.serializers import SimpleTaskSerializer
|
||||
|
||||
|
||||
def index(request):
|
||||
simple_lists = SimpleList.objects.filter(user=request.user)
|
||||
return render(request, 'simpletodo/index.html', {
|
||||
'simple_lists': simple_lists
|
||||
})
|
||||
|
||||
|
||||
class SimpleTaskViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = SimpleTaskSerializer
|
||||
renderer_classes = [TemplateHTMLRenderer, JSONRenderer]
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
return SimpleTask.objects.filter(list__user=user)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
|
||||
if request.accepted_renderer.format == 'html':
|
||||
serializer.is_valid(raise_exception=True)
|
||||
else:
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
serializer.save(user=request.user)
|
||||
|
||||
if request.accepted_renderer.format == 'html':
|
||||
return redirect("/todo/")
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
instance.delete()
|
||||
|
||||
if request.htmx:
|
||||
return HttpResponse(status=200, content='')
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(detail=True, methods=['post',])
|
||||
def toggle_done(self, request, pk=None):
|
||||
task = self.get_object()
|
||||
task.done = not task.done
|
||||
task.save()
|
||||
|
||||
if request.htmx:
|
||||
return render(request, 'simpletodo/partials/task.html', {
|
||||
'task': task
|
||||
})
|
||||
|
||||
return Response(self.get_serializer(task), status=status.HTTP_200_OK)
|
||||
|
||||
#
|
||||
# def get_category_from_request(self, request):
|
||||
# cat_name = request.query_params.get('cat_name')
|
||||
# category_obj = None
|
||||
#
|
||||
# if cat_name:
|
||||
# category_obj = VideoCategory.objects.filter(category__iexact=cat_name).first()
|
||||
# if not category_obj:
|
||||
# category_obj = VideoCategory.objects.first()
|
||||
#
|
||||
# return category_obj
|
||||
#
|
||||
# @action(detail=False, methods=['get'])
|
||||
# def form(self, request):
|
||||
# category_obj = self.get_category_from_request(request)
|
||||
# return Response(template_name='videos/partials/video_form.html', data={'category': category_obj})
|
||||
#
|
||||
# @action(detail=False, methods=['get'])
|
||||
# def cancel(self, request):
|
||||
# category_obj = self.get_category_from_request(request)
|
||||
# return Response(template_name='videos/partials/show_add_form.html', data={'category': category_obj})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
.task.htmx-swapping {
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease-out;
|
||||
}
|
||||
|
|
@ -20,6 +20,9 @@
|
|||
<!-- Le javascript
|
||||
================================================== -->
|
||||
{# Placed at the top of the document so pages load faster with defer #}
|
||||
<script defer src="https://unpkg.com/htmx.org@1.6.1"></script>
|
||||
|
||||
|
||||
{% block javascript %}
|
||||
<!-- Your stuff: Third-party javascript libraries go here -->
|
||||
<!-- place project specific Javascript in this file -->
|
||||
|
|
@ -57,115 +60,19 @@
|
|||
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto">
|
||||
<div class="flex justify-between flex-col md:flex-row">
|
||||
<img class="w-full md:w-2/4" src="https://www.thezebra.com/insurance-news/wp-content/uploads/2016/01/Tree-fallen-on-car-1024x682.jpeg"/>
|
||||
<div class="w-full md:w-2/4 flex flex-col justify-center p-4 md:p-16">
|
||||
<h2 class="text-xl md:text-3xl font-bold">Machen Sie hier eine Selbstevaluation</h2>
|
||||
<p class="my-4 text-xl">Hier steht noch etwas mehr Text</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto bg-blue-100 border-t-2 border-gray-900">
|
||||
<div class="p-8 flex flex-col md:flex-row">
|
||||
|
||||
<div class="w-full md:w-1/2 xl:w-1/3 px-4">
|
||||
<div class="bg-white rounded-lg overflow-hidden mb-10 shadow-md">
|
||||
<img
|
||||
src="https://cdn.tailgrids.com/1.0/assets/images/cards/card-01/image-01.jpg"
|
||||
alt="image"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="p-8 sm:p-9 md:p-7 xl:p-9 text-center">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="
|
||||
inline-block
|
||||
py-2
|
||||
px-7
|
||||
border border-[#E5E7EB]
|
||||
rounded-full
|
||||
text-base text-body-color
|
||||
font-medium
|
||||
hover:border-primary hover:bg-primary hover:text-white
|
||||
transition
|
||||
"
|
||||
>
|
||||
Kurs X
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full md:w-1/2 xl:w-1/3 px-4">
|
||||
<div class="bg-white rounded-lg overflow-hidden mb-10 shadow-md">
|
||||
<img
|
||||
src="https://cdn.tailgrids.com/1.0/assets/images/cards/card-01/image-02.jpg"
|
||||
alt="image"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="p-8 sm:p-9 md:p-7 xl:p-9 text-center">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="
|
||||
inline-block
|
||||
py-2
|
||||
px-7
|
||||
border border-[#E5E7EB]
|
||||
rounded-full
|
||||
text-base text-body-color
|
||||
font-medium
|
||||
hover:border-primary hover:bg-primary hover:text-white
|
||||
transition
|
||||
"
|
||||
>
|
||||
Kurs Y
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full md:w-1/2 xl:w-1/3 px-4">
|
||||
<div class="bg-white rounded-lg overflow-hidden mb-10 shadow-md">
|
||||
<img
|
||||
src="https://cdn.tailgrids.com/1.0/assets/images/cards/card-01/image-03.jpg"
|
||||
alt="image"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="p-8 sm:p-9 md:p-7 xl:p-9 text-center">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="
|
||||
inline-block
|
||||
py-2
|
||||
px-7
|
||||
border border-[#E5E7EB]
|
||||
rounded-full
|
||||
text-base text-body-color
|
||||
font-medium
|
||||
hover:border-primary hover:bg-primary hover:text-white
|
||||
transition
|
||||
"
|
||||
>
|
||||
Kurs Z
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="p-8 text-right">weitere Kurse entdecken und buchen</p>
|
||||
|
||||
<div class="border-t-2 border-blue-400 m-8 p-8"></div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
{% block modal %}{% endblock modal %}
|
||||
|
||||
<script>
|
||||
document.body.addEventListener('htmx:configRequest', (event) => {
|
||||
event.detail.headers['X-CSRFToken'] = '{{ csrf_token }}';
|
||||
})
|
||||
</script>
|
||||
|
||||
{% block inline_javascript %}
|
||||
|
||||
{% comment %}
|
||||
Script tags with only code, no src (defer by default). To run
|
||||
with a "defer" so that you run inline code:
|
||||
|
|
|
|||
|
|
@ -1 +1,108 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto">
|
||||
<div class="flex justify-between flex-col md:flex-row">
|
||||
<img class="w-full md:w-2/4"
|
||||
src="https://www.thezebra.com/insurance-news/wp-content/uploads/2016/01/Tree-fallen-on-car-1024x682.jpeg"/>
|
||||
<div class="w-full md:w-2/4 flex flex-col justify-center p-4 md:p-16">
|
||||
<h2 class="text-xl md:text-3xl font-bold">Machen Sie hier eine Selbstevaluation</h2>
|
||||
<p class="my-4 text-xl">Hier steht noch etwas mehr Text</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto bg-blue-100 border-t-2 border-gray-900">
|
||||
<div class="p-8 flex flex-col md:flex-row">
|
||||
|
||||
<div class="w-full md:w-1/2 xl:w-1/3 px-4">
|
||||
<div class="bg-white rounded-lg overflow-hidden mb-10 shadow-md">
|
||||
<img
|
||||
src="https://cdn.tailgrids.com/1.0/assets/images/cards/card-01/image-01.jpg"
|
||||
alt="image"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="p-8 sm:p-9 md:p-7 xl:p-9 text-center">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="
|
||||
inline-block
|
||||
py-2
|
||||
px-7
|
||||
border border-[#E5E7EB]
|
||||
rounded-full
|
||||
text-base text-body-color
|
||||
font-medium
|
||||
hover:border-primary hover:bg-primary hover:text-white
|
||||
transition
|
||||
"
|
||||
>
|
||||
Kurs X
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full md:w-1/2 xl:w-1/3 px-4">
|
||||
<div class="bg-white rounded-lg overflow-hidden mb-10 shadow-md">
|
||||
<img
|
||||
src="https://cdn.tailgrids.com/1.0/assets/images/cards/card-01/image-02.jpg"
|
||||
alt="image"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="p-8 sm:p-9 md:p-7 xl:p-9 text-center">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="
|
||||
inline-block
|
||||
py-2
|
||||
px-7
|
||||
border border-[#E5E7EB]
|
||||
rounded-full
|
||||
text-base text-body-color
|
||||
font-medium
|
||||
hover:border-primary hover:bg-primary hover:text-white
|
||||
transition
|
||||
"
|
||||
>
|
||||
Kurs Y
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full md:w-1/2 xl:w-1/3 px-4">
|
||||
<div class="bg-white rounded-lg overflow-hidden mb-10 shadow-md">
|
||||
<img
|
||||
src="https://cdn.tailgrids.com/1.0/assets/images/cards/card-01/image-03.jpg"
|
||||
alt="image"
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="p-8 sm:p-9 md:p-7 xl:p-9 text-center">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="
|
||||
inline-block
|
||||
py-2
|
||||
px-7
|
||||
border border-[#E5E7EB]
|
||||
rounded-full
|
||||
text-base text-body-color
|
||||
font-medium
|
||||
hover:border-primary hover:bg-primary hover:text-white
|
||||
transition
|
||||
"
|
||||
>
|
||||
Kurs Z
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="p-8 text-right">weitere Kurse entdecken und buchen</p>
|
||||
|
||||
<div class="border-t-2 border-blue-400 m-8 p-8"></div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Reference in New Issue