├── .dockerignore
├── .github
└── workflows
│ └── fly.yml
├── .gitignore
├── Dockerfile
├── README.md
├── analytics
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models.py
├── templates
│ └── analytics.html
├── tests.py
├── urls.py
└── views.py
├── core
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
├── entrypoint.sh
├── fixtures
└── notes.json
├── fly.toml
├── htmx_messages
├── __init__.py
├── apps.py
├── middleware.py
├── static
│ └── toasts.js
└── templates
│ └── toasts.html
├── manage.py
├── note
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_alter_note_title.py
│ ├── 0003_remove_note_completed_note_completed_at.py
│ └── __init__.py
├── models.py
├── templates
│ └── note
│ │ └── home.html
├── tests.py
├── urls.py
└── views.py
├── package-lock.json
├── package.json
├── postcss.config.js
├── requirements.txt
├── screenshots
├── desktop_dark.png
├── desktop_light.png
├── mobile_dark.png
├── mobile_dark_create.png
├── mobile_dark_edit.png
├── mobile_dark_menu.png
├── mobile_dark_profile.png
└── mobile_light.png
├── static
├── css
│ ├── custom
│ │ └── custom.css
│ └── tailwind
│ │ ├── main.css
│ │ └── main.min.css
├── favicon.ico
├── js
│ ├── alpine.min.js
│ └── htmx.min.js
└── note_logo.png
├── tailwind.config.js
├── templates
└── base.html
└── user
├── __init__.py
├── admin.py
├── apps.py
├── migrations
├── 0001_initial.py
├── 0002_noteuser_email_confirmed.py
├── 0003_noteuser_is_dummy.py
└── __init__.py
├── models.py
├── templates
├── login.html
├── profile.html
└── register.html
├── tests.py
├── urls.py
└── views.py
/.dockerignore:
--------------------------------------------------------------------------------
1 | fly.toml
2 | .git/
3 | *.sqlite3
4 | .env
5 | screenshots/
6 |
--------------------------------------------------------------------------------
/.github/workflows/fly.yml:
--------------------------------------------------------------------------------
1 | name: Fly Deploy
2 | on:
3 | push:
4 | branches:
5 | - master
6 | - develop
7 | jobs:
8 | deploy:
9 | name: Deploy app
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: superfly/flyctl-actions/setup-flyctl@master
14 | - run: flyctl deploy --remote-only
15 | env:
16 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite3
2 | .vscode
3 | __pycache__
4 | .env
5 | .pwa
6 | node_modules
7 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG PYTHON_VERSION=3.11-slim-bookworm
2 |
3 | FROM python:${PYTHON_VERSION}
4 |
5 | ENV PYTHONDONTWRITEBYTECODE 1
6 | ENV PYTHONUNBUFFERED 1
7 |
8 | RUN mkdir -p /code
9 |
10 | WORKDIR /code
11 |
12 | # install psycopg2 dependencies
13 | RUN apt-get update && apt-get install -y \
14 | libpq-dev \
15 | gcc \
16 | && rm -rf /var/lib/apt/lists/*
17 |
18 | COPY requirements.txt /tmp/requirements.txt
19 |
20 | RUN set -ex && \
21 | pip install --upgrade pip && \
22 | pip install -r /tmp/requirements.txt && \
23 | rm -rf /root/.cache/
24 |
25 | COPY . /code
26 |
27 | RUN ["chmod", "+x", "./entrypoint.sh"]
28 |
29 | EXPOSE 8000
30 |
31 | # run entrypoint.sh
32 | ENTRYPOINT ["./entrypoint.sh"]
33 |
34 | CMD ["gunicorn", "--bind", ":8000", "--workers", "2", "core.wsgi"]
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Just Notes
2 |
3 | ## Simple Notes App
4 |
5 | Light Theme | Dark Theme
6 | :-------------------------:|:-------------------------:
7 |  | 
8 |
9 |
10 | ## Deployed with [Fly.io](https//:fly.io) - [just-notes.fly.dev](https://just-notes.fly.dev/)
11 |
12 | ### Dummy Credentials:
13 | ```
14 | email: dummy@mail.com
15 | password: dummypassword
16 | ```
17 |
18 | ### Created using Django, HTMX, Alpine.js, TailwindCSS
19 |
20 | ### Functionality:
21 |
22 | - [x] Register/Login
23 | - [x] Profile Page:
24 | - [x] Profile Details update
25 | - [x] Home (Notes) Page:
26 | - [x] List All Notes
27 | - [x] All Notes counter
28 | - [x] Create a new Note
29 | - [x] Update Note
30 | - [x] Delete Note
31 | - [x] Bulk Actions on Notes:
32 | - [x] Selected Notes counter
33 | - [x] Select All Notes option
34 | - [x] Bulk Delete selected Notes
35 | - [x] Bulk Change 'Completed' Status on selected Notes
36 | - [x] Pagination
37 | - [x] Live Search
38 | - [x] Filter by `is_completed` status
39 | - [ ] Analytics Page (WIP):
40 | - [x] Base Analytics about Completed Notes per Month
41 | - [ ] ...
42 | - [x] Toasts Application to show Django Messages with HTMX
43 | - [x] Auto-dismissed Toasts
44 | - [x] Manual Toast dismiss
45 | - [x] Dark/Light mode:
46 | - [x] Manual switching
47 | - [x] Detecting system theme switching
48 |
49 | ### Stack
50 | - `Django` for the backend server
51 | - `TailwingCSS` for frontend styling
52 | - `HTMX` to make a frontend dynamic and interactive like a "reactive" apps
53 | - `Alpine.js` for state management and frontend interactivity
54 | - `JavaScript` for minor but important for usability(interactivity) things: theme switching, toasts, etc
55 |
56 | #### Steps to run locally:
57 | 1. create a virtual environment
58 | 2. install requirements:
59 | ```pip install -r requirements.txt```
60 | 3. run migrations:
61 | ```python manage.py migrate```
62 | 4. start server:
63 | ```python manage.py runserver```
64 |
--------------------------------------------------------------------------------
/analytics/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/analytics/__init__.py
--------------------------------------------------------------------------------
/analytics/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/analytics/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class AnalyticsConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'analytics'
7 |
--------------------------------------------------------------------------------
/analytics/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/analytics/migrations/__init__.py
--------------------------------------------------------------------------------
/analytics/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/analytics/templates/analytics.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block content %}
4 |
5 |
Your Analytics about completed Notes
6 |
7 |
8 | {% endblock content %}
9 |
10 | {% block additional_javascript %}
11 |
12 |
13 |
82 | {% endblock additional_javascript %}
--------------------------------------------------------------------------------
/analytics/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/analytics/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from . import views
4 |
5 | urlpatterns = [
6 | path('analytics/', views.analytics_home, name='analytics_home_view'),
7 | path('analytics/get_data/', views.get_analytics_data, name='analytics_get_data'),
8 | ]
9 |
--------------------------------------------------------------------------------
/analytics/views.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 |
3 | from django.shortcuts import render, redirect
4 | from django.contrib import messages
5 | from django.db.models.functions import TruncMonth
6 | from django.core.serializers.json import DjangoJSONEncoder
7 | from django.db.models import Count, F
8 | from django.http.response import JsonResponse
9 | from django.utils import timezone
10 |
11 | from note.models import Note
12 |
13 |
14 | def jsonify_queryset(object,fields):
15 | return simplejson.dumps(list(object.values(*fields.split(','))))
16 |
17 |
18 | # Create your views here.
19 | def analytics_home(request):
20 | template_name = 'analytics.html'
21 | context = {}
22 | user = request.user
23 | if not user.is_authenticated:
24 | messages.add_message(
25 | request,
26 | messages.ERROR,
27 | 'You need to log in first!'
28 | )
29 | return redirect('login_view')
30 | return render(request, template_name, context)
31 |
32 |
33 | def get_analytics_data(request):
34 | user = request.user
35 | current_date = timezone.now().date()
36 | user_register_date = user.date_joined.date()
37 |
38 | completed_notes_counts = []
39 | date = user_register_date
40 | date_interval_dates = []
41 | while date <= current_date:
42 | next_month = (date + timedelta(days=31)).replace(day=1)
43 | notes_completed_count = Note.objects.filter(
44 | author=user,
45 | completed_at__date__gte=date,
46 | completed_at__date__lt=next_month
47 | ).count()
48 | month_name = date.strftime('%B')
49 | completed_notes_counts.append(notes_completed_count)
50 | date_interval_dates.append((month_name, date.year))
51 | date = next_month
52 |
53 | max_completed = max(completed_notes_counts)
54 | prepared_chart_data = {
55 | 'labels': date_interval_dates,
56 | 'values': completed_notes_counts,
57 | 'maxValueY': max_completed + 3
58 | }
59 | chart_data = prepared_chart_data
60 | return JsonResponse(data=chart_data, safe=False)
61 |
--------------------------------------------------------------------------------
/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/core/__init__.py
--------------------------------------------------------------------------------
/core/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for core 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 django.core.asgi import get_asgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/core/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for core project.
3 |
4 | Generated by 'django-admin startproject' using Django 4.2.
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 | from pathlib import Path
14 | import environ
15 | from django.contrib.messages import constants as messages
16 | from django.core.management.utils import get_random_secret_key
17 |
18 | env = environ.Env(
19 | # set casting, default value
20 | DEBUG=(bool, False)
21 | )
22 |
23 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
24 | BASE_DIR = Path(__file__).resolve().parent.parent
25 |
26 | # Take environment variables from .env file
27 | environ.Env.read_env(BASE_DIR / '.env')
28 |
29 | # Quick-start development settings - unsuitable for production
30 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
31 |
32 | # SECURITY WARNING: keep the secret key used in production secret!
33 | SECRET_KEY = env.str("SECRET_KEY", default=get_random_secret_key())
34 |
35 | # SECURITY WARNING: don't run with debug turned on in production!
36 | DEBUG = env('DEBUG')
37 |
38 | ALLOWED_HOSTS = ['localhost', '127.0.0.1', '.fly.dev']
39 |
40 | CSRF_TRUSTED_ORIGINS = ['https://just-notes.fly.dev', 'https://*.127.0.0.1']
41 |
42 | # Application definition
43 |
44 | INSTALLED_APPS = [
45 | 'django.contrib.admin',
46 | 'django.contrib.auth',
47 | 'django.contrib.contenttypes',
48 | 'django.contrib.sessions',
49 | 'django.contrib.messages',
50 | 'whitenoise.runserver_nostatic',
51 | 'django.contrib.staticfiles',
52 | # third-party apps
53 | 'django_htmx',
54 | # apps
55 | 'htmx_messages',
56 | 'user',
57 | 'note',
58 | 'analytics',
59 | ]
60 |
61 | MIDDLEWARE = [
62 | 'django.middleware.security.SecurityMiddleware',
63 | 'django.contrib.sessions.middleware.SessionMiddleware',
64 | 'django.middleware.common.CommonMiddleware',
65 | 'django.middleware.csrf.CsrfViewMiddleware',
66 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
67 | 'django.contrib.messages.middleware.MessageMiddleware',
68 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
69 | 'django_htmx.middleware.HtmxMiddleware',
70 | 'htmx_messages.middleware.HtmxMessageMiddleware',
71 | 'whitenoise.middleware.WhiteNoiseMiddleware',
72 | ]
73 |
74 | ROOT_URLCONF = 'core.urls'
75 |
76 | TEMPLATES = [
77 | {
78 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
79 | 'DIRS': ['templates'],
80 | 'APP_DIRS': True,
81 | 'OPTIONS': {
82 | 'context_processors': [
83 | 'django.template.context_processors.debug',
84 | 'django.template.context_processors.request',
85 | 'django.contrib.auth.context_processors.auth',
86 | 'django.contrib.messages.context_processors.messages',
87 | ],
88 | },
89 | },
90 | ]
91 |
92 | WSGI_APPLICATION = 'core.wsgi.application'
93 |
94 |
95 | # Database
96 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases
97 | DATABASES = {
98 | # read os.environ['DATABASE_URL']
99 | 'default': env.db()
100 | }
101 |
102 | # Password validation
103 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
104 |
105 | AUTH_PASSWORD_VALIDATORS = [
106 | {
107 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
108 | },
109 | {
110 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
111 | },
112 | {
113 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
114 | },
115 | {
116 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
117 | },
118 | ]
119 |
120 |
121 | # Internationalization
122 | # https://docs.djangoproject.com/en/4.2/topics/i18n/
123 |
124 | LANGUAGE_CODE = 'en-us'
125 |
126 | TIME_ZONE = 'UTC'
127 |
128 | USE_I18N = True
129 |
130 | USE_TZ = True
131 |
132 |
133 | # Static files (CSS, JavaScript, Images)
134 | # https://docs.djangoproject.com/en/4.2/howto/static-files/
135 |
136 | STATIC_URL = 'static/'
137 | STATICFILES_DIRS = [BASE_DIR / "static"]
138 | STATIC_ROOT = BASE_DIR / 'staticfiles'
139 |
140 | STORAGES = {
141 | # ...
142 | "staticfiles": {
143 | "BACKEND": "whitenoise.storage.CompressedStaticFilesStorage",
144 | },
145 | }
146 |
147 | # Media files
148 | MEDIA_ROOT = BASE_DIR / 'media'
149 | MEDIA_URL = '/media/'
150 |
151 | # Default primary key field type
152 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
153 |
154 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
155 |
156 | # Custom User Model
157 | AUTH_USER_MODEL = 'user.NoteUser'
158 |
159 |
160 | # Messages config
161 | MESSAGE_TAGS = {
162 | messages.DEBUG: 'alert-secondary',
163 | messages.INFO: 'alert-info',
164 | messages.SUCCESS: 'success',
165 | messages.WARNING: 'warning',
166 | messages.ERROR: 'danger',
167 | }
168 |
--------------------------------------------------------------------------------
/core/urls.py:
--------------------------------------------------------------------------------
1 | """
2 | URL configuration for core 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.contrib import admin
18 | from django.urls import path, include
19 |
20 | urlpatterns = [
21 | path('admin/', admin.site.urls),
22 |
23 | path('account/', include('user.urls')),
24 | path('', include('note.urls')),
25 | path('', include('analytics.urls')),
26 | ]
27 |
--------------------------------------------------------------------------------
/core/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for core 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', 'core.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | python manage.py collectstatic --no-input --clear
4 |
5 | # gunicorn -w 2 -b 0.0.0.0:8000 code/core.wsgi:application
6 |
7 | exec "$@"
--------------------------------------------------------------------------------
/fixtures/notes.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "model": "note.note",
4 | "fields": {
5 | "title": "This is first note Updated",
6 | "content": "test",
7 | "author": 3,
8 | "completed_at": null,
9 | "created_at": "2023-07-12T17:20:58.808Z",
10 | "updated_at": "2023-07-12T23:24:16.445Z"
11 | }
12 | },
13 | {
14 | "model": "note.note",
15 | "fields": {
16 | "title": "This is second note (completed_at)",
17 | "content": "test completed_at",
18 | "author": 3,
19 | "completed_at": null,
20 | "created_at": "2023-07-12T17:21:10.641Z",
21 | "updated_at": "2023-07-12T22:51:32.516Z"
22 | }
23 | },
24 | {
25 | "model": "note.note",
26 | "fields": {
27 | "title": "This is first note Updated",
28 | "content": "test",
29 | "author": 3,
30 | "completed_at": null,
31 | "created_at": "2023-07-12T17:20:58.808Z",
32 | "updated_at": "2023-07-12T23:24:16.445Z"
33 | }
34 | },
35 | {
36 | "model": "note.note",
37 | "fields": {
38 | "title": "This is second note (completed_at)",
39 | "content": "test completed_at",
40 | "author": 3,
41 | "completed_at": null,
42 | "created_at": "2023-07-12T17:21:10.641Z",
43 | "updated_at": "2023-07-12T22:51:32.516Z"
44 | }
45 | },
46 | {
47 | "model": "note.note",
48 | "fields": {
49 | "title": "This is first note Updated",
50 | "content": "test",
51 | "author": 3,
52 | "completed_at": null,
53 | "created_at": "2023-07-12T17:20:58.808Z",
54 | "updated_at": "2023-07-12T23:24:16.445Z"
55 | }
56 | },
57 | {
58 | "model": "note.note",
59 | "fields": {
60 | "title": "This is second note (completed_at)",
61 | "content": "test completed_at",
62 | "author": 3,
63 | "completed_at": null,
64 | "created_at": "2023-07-12T17:21:10.641Z",
65 | "updated_at": "2023-07-12T22:51:32.516Z"
66 | }
67 | },
68 | {
69 | "model": "note.note",
70 | "fields": {
71 | "title": "This is first note Updated",
72 | "content": "test",
73 | "author": 3,
74 | "completed_at": null,
75 | "created_at": "2023-07-12T17:20:58.808Z",
76 | "updated_at": "2023-07-12T23:24:16.445Z"
77 | }
78 | },
79 | {
80 | "model": "note.note",
81 | "fields": {
82 | "title": "This is second note (completed_at)",
83 | "content": "test completed_at",
84 | "author": 3,
85 | "completed_at": null,
86 | "created_at": "2023-07-12T17:21:10.641Z",
87 | "updated_at": "2023-07-12T22:51:32.516Z"
88 | }
89 | },
90 | {
91 | "model": "note.note",
92 | "fields": {
93 | "title": "This is first note Updated",
94 | "content": "test",
95 | "author": 3,
96 | "completed_at": null,
97 | "created_at": "2023-07-12T17:20:58.808Z",
98 | "updated_at": "2023-07-12T23:24:16.445Z"
99 | }
100 | },
101 | {
102 | "model": "note.note",
103 | "fields": {
104 | "title": "This is second note (completed_at)",
105 | "content": "test completed_at",
106 | "author": 3,
107 | "completed_at": null,
108 | "created_at": "2023-07-12T17:21:10.641Z",
109 | "updated_at": "2023-07-12T22:51:32.516Z"
110 | }
111 | },
112 | {
113 | "model": "note.note",
114 | "fields": {
115 | "title": "This is first note Updated",
116 | "content": "test",
117 | "author": 3,
118 | "completed_at": null,
119 | "created_at": "2023-07-12T17:20:58.808Z",
120 | "updated_at": "2023-07-12T23:24:16.445Z"
121 | }
122 | },
123 | {
124 | "model": "note.note",
125 | "fields": {
126 | "title": "This is second note (completed_at)",
127 | "content": "test completed_at",
128 | "author": 3,
129 | "completed_at": null,
130 | "created_at": "2023-07-12T17:21:10.641Z",
131 | "updated_at": "2023-07-12T22:51:32.516Z"
132 | }
133 | },
134 | {
135 | "model": "note.note",
136 | "fields": {
137 | "title": "This is first note Updated",
138 | "content": "test",
139 | "author": 3,
140 | "completed_at": null,
141 | "created_at": "2023-07-12T17:20:58.808Z",
142 | "updated_at": "2023-07-12T23:24:16.445Z"
143 | }
144 | },
145 | {
146 | "model": "note.note",
147 | "fields": {
148 | "title": "This is second note (completed_at)",
149 | "content": "test completed_at",
150 | "author": 3,
151 | "completed_at": null,
152 | "created_at": "2023-07-12T17:21:10.641Z",
153 | "updated_at": "2023-07-12T22:51:32.516Z"
154 | }
155 | },
156 | {
157 | "model": "note.note",
158 | "fields": {
159 | "title": "This is first note Updated",
160 | "content": "test",
161 | "author": 3,
162 | "completed_at": null,
163 | "created_at": "2023-07-12T17:20:58.808Z",
164 | "updated_at": "2023-07-12T23:24:16.445Z"
165 | }
166 | },
167 | {
168 | "model": "note.note",
169 | "fields": {
170 | "title": "This is first note Updated",
171 | "content": "test",
172 | "author": 3,
173 | "completed_at": null,
174 | "created_at": "2023-07-12T17:20:58.808Z",
175 | "updated_at": "2023-07-12T23:24:16.445Z"
176 | }
177 | },
178 | {
179 | "model": "note.note",
180 | "fields": {
181 | "title": "This is first note Updated",
182 | "content": "test",
183 | "author": 3,
184 | "completed_at": null,
185 | "created_at": "2023-07-12T17:20:58.808Z",
186 | "updated_at": "2023-07-12T23:24:16.445Z"
187 | }
188 | },
189 | {
190 | "model": "note.note",
191 | "fields": {
192 | "title": "This is first note Updated",
193 | "content": "test",
194 | "author": 3,
195 | "completed_at": null,
196 | "created_at": "2023-07-12T17:20:58.808Z",
197 | "updated_at": "2023-07-12T23:24:16.445Z"
198 | }
199 | },
200 | {
201 | "model": "note.note",
202 | "fields": {
203 | "title": "This is first note Updated",
204 | "content": "test",
205 | "author": 3,
206 | "completed_at": null,
207 | "created_at": "2023-07-12T17:20:58.808Z",
208 | "updated_at": "2023-07-12T23:24:16.445Z"
209 | }
210 | },
211 | {
212 | "model": "note.note",
213 | "fields": {
214 | "title": "This is first note Updated",
215 | "content": "test",
216 | "author": 3,
217 | "completed_at": null,
218 | "created_at": "2023-07-12T17:20:58.808Z",
219 | "updated_at": "2023-07-12T23:24:16.445Z"
220 | }
221 | },
222 | {
223 | "model": "note.note",
224 | "fields": {
225 | "title": "This is first note Updated",
226 | "content": "test",
227 | "author": 3,
228 | "completed_at": null,
229 | "created_at": "2023-07-12T17:20:58.808Z",
230 | "updated_at": "2023-07-12T23:24:16.445Z"
231 | }
232 | },
233 | {
234 | "model": "note.note",
235 | "fields": {
236 | "title": "This is first note Updated",
237 | "content": "test",
238 | "author": 3,
239 | "completed_at": null,
240 | "created_at": "2023-07-12T17:20:58.808Z",
241 | "updated_at": "2023-07-12T23:24:16.445Z"
242 | }
243 | },
244 | {
245 | "model": "note.note",
246 | "fields": {
247 | "title": "This is first note Updated",
248 | "content": "test",
249 | "author": 3,
250 | "completed_at": null,
251 | "created_at": "2023-07-12T17:20:58.808Z",
252 | "updated_at": "2023-07-12T23:24:16.445Z"
253 | }
254 | },
255 | {
256 | "model": "note.note",
257 | "fields": {
258 | "title": "This is first note Updated",
259 | "content": "test",
260 | "author": 3,
261 | "completed_at": null,
262 | "created_at": "2023-07-12T17:20:58.808Z",
263 | "updated_at": "2023-07-12T23:24:16.445Z"
264 | }
265 | },
266 | {
267 | "model": "note.note",
268 | "fields": {
269 | "title": "This is first note Updated",
270 | "content": "test",
271 | "author": 3,
272 | "completed_at": null,
273 | "created_at": "2023-07-12T17:20:58.808Z",
274 | "updated_at": "2023-07-12T23:24:16.445Z"
275 | }
276 | },
277 | {
278 | "model": "note.note",
279 | "fields": {
280 | "title": "This is first note Updated",
281 | "content": "test",
282 | "author": 3,
283 | "completed_at": null,
284 | "created_at": "2023-07-12T17:20:58.808Z",
285 | "updated_at": "2023-07-12T23:24:16.445Z"
286 | }
287 | },
288 | {
289 | "model": "note.note",
290 | "fields": {
291 | "title": "This is first note Updated",
292 | "content": "test",
293 | "author": 3,
294 | "completed_at": null,
295 | "created_at": "2023-07-12T17:20:58.808Z",
296 | "updated_at": "2023-07-12T23:24:16.445Z"
297 | }
298 | },
299 | {
300 | "model": "note.note",
301 | "fields": {
302 | "title": "This is first note Updated",
303 | "content": "test",
304 | "author": 3,
305 | "completed_at": null,
306 | "created_at": "2023-07-12T17:20:58.808Z",
307 | "updated_at": "2023-07-12T23:24:16.445Z"
308 | }
309 | },
310 | {
311 | "model": "note.note",
312 | "fields": {
313 | "title": "This is first note Updated",
314 | "content": "test",
315 | "author": 3,
316 | "completed_at": null,
317 | "created_at": "2023-07-12T17:20:58.808Z",
318 | "updated_at": "2023-07-12T23:24:16.445Z"
319 | }
320 | }
321 | ]
322 |
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | # fly.toml app configuration file generated for just-notes on 2023-07-21T17:58:15+03:00
2 | #
3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file.
4 | #
5 |
6 | app = "just-notes"
7 | primary_region = "waw"
8 | console_command = "/code/manage.py shell"
9 |
10 | [deploy]
11 | release_command = "python manage.py migrate"
12 |
13 | [env]
14 | PORT = "8000"
15 |
16 | [http_service]
17 | internal_port = 8000
18 | force_https = true
19 | auto_stop_machines = true
20 | auto_start_machines = true
21 | min_machines_running = 0
22 | processes = ["app"]
23 |
24 | [[statics]]
25 | guest_path = "/code/staticfiles"
26 | url_prefix = "/static/"
27 |
--------------------------------------------------------------------------------
/htmx_messages/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/htmx_messages/__init__.py
--------------------------------------------------------------------------------
/htmx_messages/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class HtmxMessagesConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'htmx_messages'
7 |
--------------------------------------------------------------------------------
/htmx_messages/middleware.py:
--------------------------------------------------------------------------------
1 | import json
2 | from django.contrib.messages import get_messages
3 | from django.http import HttpRequest, HttpResponse
4 | from django.utils.deprecation import MiddlewareMixin
5 |
6 |
7 | class HtmxMessageMiddleware(MiddlewareMixin):
8 | """
9 | Middleware that moves messages into the HX-Trigger header when request is made with HTMX
10 | """
11 |
12 | def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
13 |
14 | # The HX-Request header indicates that the request was made with HTMX
15 | if "HX-Request" not in request.headers:
16 | return response
17 |
18 | # Ignore redirections because HTMX cannot read the headers
19 | if 300 <= response.status_code < 400:
20 | return response
21 |
22 | # Extract the messages
23 | messages = [
24 | {"message": message.message, "tags": message.tags}
25 | for message in get_messages(request)
26 | ]
27 | if not messages:
28 | return response
29 |
30 | # Get the existing HX-Trigger that could have been defined by the view
31 | hx_trigger = response.headers.get("HX-Trigger")
32 |
33 | if hx_trigger is None:
34 | # If the HX-Trigger is not set, start with an empty object
35 | hx_trigger = {}
36 | elif hx_trigger.startswith("{"):
37 | # If the HX-Trigger uses the object syntax, parse the object
38 | hx_trigger = json.loads(hx_trigger)
39 | else:
40 | # If the HX-Trigger uses the string syntax, convert to the object syntax
41 | hx_trigger = {hx_trigger: True}
42 |
43 | # Add the messages array in the HX-Trigger object
44 | hx_trigger["messages"] = messages
45 |
46 | # Add or update the HX-Trigger
47 | response.headers["HX-Trigger"] = json.dumps(hx_trigger)
48 |
49 | return response
--------------------------------------------------------------------------------
/htmx_messages/static/toasts.js:
--------------------------------------------------------------------------------
1 | ;(function () {
2 | const toastOptions = { delay: 5000 }
3 |
4 | function createToast(message) {
5 | // Clone the template
6 | const element = htmx.find("[data-toast-template]").cloneNode(true)
7 | const success_icon_svg = ''
8 | const warning_icon_svg = ''
9 | const danger_icon_svg = ''
10 | const succes_icon_classes = ' text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200'
11 | const warning_icon_classes = ' text-yellow-500 bg-yellow-100 rounded-lg dark:bg-yellow-700 dark:text-yellow-200'
12 | const danger_icon_classes = ' text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200'
13 |
14 | // Remove the data-toast-template attribute
15 | delete element.dataset.toastTemplate
16 |
17 | // Set the CSS class
18 | element.id += message.tags
19 | element.classList.remove('hidden');
20 | element.classList.add('flex');
21 |
22 | // Set the text
23 | htmx.find(element, "[data-toast-body]").innerText = message.message
24 | if (message.tags === 'success') {
25 | element.classList += ' border-green-500'
26 | htmx.find(element, "[data-icon-container]").innerHTML = success_icon_svg
27 | htmx.find(element, "[data-icon-container]").classList += succes_icon_classes
28 | } else if (message.tags === 'warning') {
29 | element.classList += ' border-yellow-500'
30 | htmx.find(element, "[data-icon-container]").innerHTML = warning_icon_svg
31 | htmx.find(element, "[data-icon-container]").classList += warning_icon_classes
32 | } else if (message.tags === 'danger') {
33 | element.classList += ' border-red-500'
34 | htmx.find(element, "[data-icon-container]").innerHTML = danger_icon_svg
35 | htmx.find(element, "[data-icon-container]").classList += danger_icon_classes
36 | }
37 | // Add the new element to the container
38 | htmx.find("[data-toast-container]").appendChild(element)
39 |
40 | // Show the toast
41 |
42 | }
43 |
44 | htmx.on("messages", (event) => {
45 | event.detail.value.forEach(createToast);
46 | htmx.findAll(".toast:not([data-toast-template])").forEach((element) => {
47 | setTimeout(function() {
48 | removeFadeOut(element, 1000)
49 | }, 5000)
50 | })
51 | })
52 | })()
53 |
54 | function removeFadeOut( el, speed ) {
55 | var seconds = speed/1000;
56 | el.style.transition = "opacity "+seconds+"s ease";
57 |
58 | el.style.opacity = 0;
59 | setTimeout(function() {
60 | el.parentNode.removeChild(el);
61 | }, speed);
62 | }
63 |
64 | function removeToast(target) {
65 | let toastElement = target.closest('.toast');
66 | removeFadeOut(toastElement, 1000);
67 | }
68 |
--------------------------------------------------------------------------------
/htmx_messages/templates/toasts.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | Check icon
7 |
8 |
9 |
17 |
18 | {% for message in messages %}
19 |
55 | {% endfor %}
56 |
57 |
--------------------------------------------------------------------------------
/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', 'core.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 |
--------------------------------------------------------------------------------
/note/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/note/__init__.py
--------------------------------------------------------------------------------
/note/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import Note
4 |
5 | # Register your models here.
6 | class NoteAdmin(admin.ModelAdmin):
7 | list_display = ['id', 'title', 'author', 'completed_at', 'updated_at']
8 | readonly_fields = ['created_at', 'updated_at']
9 |
10 | admin.site.register(Note, NoteAdmin)
11 |
--------------------------------------------------------------------------------
/note/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class NoteConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'note'
7 |
--------------------------------------------------------------------------------
/note/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-07-12 16:59
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Note',
19 | fields=[
20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('title', models.CharField(max_length=255)),
22 | ('content', models.TextField(blank=True, null=True)),
23 | ('completed', models.BooleanField(default=False)),
24 | ('created_at', models.DateTimeField(auto_now_add=True)),
25 | ('updated_at', models.DateTimeField(auto_now=True)),
26 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to=settings.AUTH_USER_MODEL)),
27 | ],
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/note/migrations/0002_alter_note_title.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-07-13 10:35
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('note', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='note',
15 | name='title',
16 | field=models.CharField(blank=True, max_length=255, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/note/migrations/0003_remove_note_completed_note_completed_at.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2 on 2023-07-23 19:04
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('note', '0002_alter_note_title'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='note',
15 | name='completed',
16 | ),
17 | migrations.AddField(
18 | model_name='note',
19 | name='completed_at',
20 | field=models.DateTimeField(blank=True, default=None, help_text="None if the note isn't completed. Contain datetime when the note was completed", null=True),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/note/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/note/migrations/__init__.py
--------------------------------------------------------------------------------
/note/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from user.models import NoteUser
4 |
5 |
6 | # Create your models here.
7 | class Note(models.Model):
8 | title = models.CharField(max_length=255, blank=True, null=True)
9 | content = models.TextField(blank=True, null=True)
10 | author = models.ForeignKey(NoteUser, on_delete=models.CASCADE,
11 | related_name='notes')
12 | completed_at = models.DateTimeField(help_text="None if the note isn't completed. Contain datetime when the note was completed", blank=True, null=True, default=None)
13 | created_at = models.DateTimeField(auto_now_add=True)
14 | updated_at = models.DateTimeField(auto_now=True)
15 |
16 | def __str__(self):
17 | return f'{self.id}: {self.title}'
18 |
19 | @property
20 | def is_completed(self):
21 | '''check if note completed'''
22 | if self.completed_at is not None:
23 | return True
24 | return False
25 |
--------------------------------------------------------------------------------
/note/templates/note/home.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block title %}
4 | Home |
5 | {% endblock title %}
6 |
7 | {% block additional_css %}
8 | {% endblock additional_css %}
9 |
10 | {% block content %}
11 |
13 |
14 | Your Notes
15 |
16 |
23 |
35 |
36 |
37 |
40 |
41 |
42 |
43 |
61 |
62 |
63 | Total: {{ notes.paginator.count }}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
77 |
78 |
79 | Selected:
80 |
81 |
82 |
97 |
112 |
119 |
120 |
121 |
122 |
169 |
170 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
280 |
343 |
344 |
345 |
346 |
347 |
348 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 | {% endblock content %}
404 |
405 | {% block additional_javascript %}
406 |
515 |
525 |
526 | {% endblock additional_javascript %}
--------------------------------------------------------------------------------
/note/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/note/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from . import views
4 |
5 | urlpatterns = [
6 | path('', views.home, name='home_view'),
7 | path('note/', views.note_view, name='note_view'),
8 | path('notes/', views.bulk_notes_view, name='bulk_notes_view'),
9 | path('note//', views.delete_note_view,
10 | name='delete_note_view'),
11 | ]
12 |
--------------------------------------------------------------------------------
/note/views.py:
--------------------------------------------------------------------------------
1 | from time import sleep
2 | from django_htmx.http import push_url
3 | from django.shortcuts import render, redirect
4 | from django.contrib import messages
5 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
6 | from django.utils import timezone
7 | from django.urls import reverse
8 | from django.db.models import Q
9 |
10 | from note.models import Note
11 |
12 |
13 | def get_paginated_query(query, request):
14 | '''
15 | helper function to generate paginated query
16 | '''
17 | paginator = Paginator(query, 19)
18 | page = request.GET.get('page', 1)
19 | try:
20 | sleep(0.1)
21 | query = paginator.page(page)
22 | except EmptyPage:
23 | messages.add_message(request, messages.ERROR, 'Bad page number...')
24 | query = paginator.page(paginator.num_pages)
25 | except PageNotAnInteger:
26 | query = paginator.page(1)
27 | # add context
28 | # config pagination
29 | page_range = paginator.get_elided_page_range(number=query.number,
30 | on_each_side=3,
31 | on_ends=1)
32 | return query, page_range
33 |
34 |
35 | # Create your views here.
36 | def home(request):
37 | '''
38 | Home page
39 | '''
40 | if not request.user.is_authenticated:
41 | messages.add_message(
42 | request,
43 | messages.ERROR,
44 | 'You need to log in first!'
45 | )
46 | return redirect('login_view')
47 | context = {}
48 | # get all notes for user
49 | notes = Note.objects.filter(
50 | author=request.user).order_by('-updated_at')
51 | # if search param in url
52 | query = request.GET.get('search', False)
53 | if query:
54 | sleep(0.1)
55 | notes = notes.filter(Q(title__icontains=query) | Q(content__icontains=query))
56 | # filter by is_completed status
57 | uncompleted = request.GET.get('uncompleted', False)
58 | if uncompleted == 'on':
59 | sleep(0.1)
60 | notes = notes.filter(completed_at__isnull=True)
61 | # get pagination
62 | context['notes'], context['page_range'] = get_paginated_query(notes, request)
63 |
64 | context['search'] = query
65 | context['uncompleted'] = uncompleted
66 | # template
67 | template_name = 'note/home.html'
68 | return render(request, template_name, context)
69 |
70 |
71 | def note_view(request):
72 | '''
73 | Update single Note
74 | '''
75 | context = {}
76 | template_name = 'note/home.html'
77 | if request.htmx:
78 | data = request.POST
79 | # update Note
80 | if data['note_action'] == 'update':
81 | try:
82 | note = Note.objects.get(id=int(data.get('note_id')))
83 | except (Note.DoesNotExist, Note.MultipleObjectsReturned):
84 | messages.add_message(request, messages.ERROR,
85 | 'Something went wrong...')
86 | # if Note exist - update it
87 | else:
88 | if note:
89 | note.title = data.get('title')
90 | note.content = data.get('content')
91 | # set completed status
92 | if (data.get('completed', None) is not None
93 | and data.get('completed')) == 'on':
94 | note.completed_at = timezone.now()
95 | else:
96 | note.completed = None
97 | note.save()
98 | messages.add_message(request, messages.SUCCESS,
99 | 'Your Note updated!')
100 | # create new Note
101 | if data['note_action'] == 'create':
102 | if (
103 | (data['title'] is None or len(data['title']) < 1)
104 | and (data['content'] is None or len(data['content']) < 1)
105 | ):
106 | messages.add_message(request, messages.ERROR,
107 | 'Title or Content is required!')
108 | # if at least title or content provided - create Note
109 | else:
110 | new_note = Note.objects.create(title=data['title'],
111 | content=data['content'],
112 | author=request.user)
113 | # set completed status
114 | if (data.get('completed', None) is not None
115 | and data.get('completed')) == 'on':
116 | new_note.completed_at = timezone.now()
117 | new_note.save()
118 | messages.add_message(request, messages.SUCCESS,
119 | 'New Note added!')
120 | # return all notes for user
121 | notes = Note.objects.filter(
122 | author=request.user
123 | ).order_by('-updated_at')
124 | # get pagination
125 | context['notes'], context['page_range'] = get_paginated_query(notes, request)
126 |
127 | response = render(request, template_name, context)
128 | return push_url(response, reverse('home_view'))
129 |
130 |
131 | def delete_note_view(request, note_id):
132 | '''
133 | Delete single Note
134 | '''
135 | context = {}
136 | template_name = 'note/home.html'
137 | # delete Note
138 | if request.method == 'DELETE':
139 | try:
140 | note = Note.objects.get(id=note_id)
141 | note.delete()
142 | messages.add_message(request, messages.WARNING,
143 | 'Your Note was deleted!')
144 | except (Note.DoesNotExist, Note.MultipleObjectsReturned):
145 | messages.add_message(request, messages.ERROR,
146 | 'Something went wrong...')
147 | # return all notes for user
148 | notes = Note.objects.filter(
149 | author=request.user
150 | ).order_by('-updated_at')
151 | # get pagination
152 | context['notes'], context['page_range'] = get_paginated_query(notes, request)
153 | response = render(request, template_name, context)
154 | return push_url(response, reverse('home_view'))
155 |
156 |
157 | def bulk_notes_view(request):
158 | '''
159 | Bulk Notes actions
160 | '''
161 | context = {}
162 | template_name = 'note/home.html'
163 | if request.htmx:
164 | # get selected Notes ids
165 | selected_notes_ids_raw = request.POST.get('selected-notes-ids')
166 | selected_notes_ids = selected_notes_ids_raw.split(',')
167 | # get nptes action
168 | action = request.POST.get('bulk-notes-action')
169 | # bulk delete Notes
170 | if action == 'bulk_delete':
171 | for note_id in selected_notes_ids:
172 | try:
173 | note = Note.objects.get(id=note_id)
174 | note.delete()
175 | except (Note.DoesNotExist, Note.MultipleObjectsReturned):
176 | messages.add_message(request, messages.ERROR,
177 | 'Something went wrong...')
178 | messages.add_message(request, messages.WARNING,
179 | 'Notes deleted!')
180 | # bulk change completes status for Notes
181 | else:
182 | for note_id in selected_notes_ids:
183 | try:
184 | note = Note.objects.get(id=note_id)
185 | if note.is_completed:
186 | note.completed_at = None
187 | else:
188 | note.completed_at = timezone.now()
189 | note.save()
190 | except (Note.DoesNotExist, Note.MultipleObjectsReturned):
191 | messages.add_message(request, messages.ERROR,
192 | 'Something went wrong...')
193 | messages.add_message(request, messages.SUCCESS,
194 | 'Notes updated!')
195 | # return all notes for user
196 | notes = Note.objects.filter(
197 | author=request.user
198 | ).order_by('-updated_at')
199 | # get pagination
200 | context['notes'], context['page_range'] = get_paginated_query(notes, request)
201 | response = render(request, template_name, context)
202 | return push_url(response, reverse('home_view'))
203 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": {
3 | "build": {
4 | "patterns": [
5 | "./templates/**/*",
6 | "./**/templates/**/*"
7 | ],
8 | "extensions": "html",
9 | "quiet": false
10 | }
11 | },
12 | "scripts": {
13 | "build": "postcss static/css/tailwind/main.css -o static/css/tailwind/main.min.css",
14 | "dev": "postcss static/css/tailwind/main.css -o static/css/tailwind/main.min.css --watch",
15 | "watch": "npm-watch"
16 | },
17 | "devDependencies": {
18 | "autoprefixer": "^10.4.14",
19 | "postcss": "^8.4.27",
20 | "postcss-cli": "^10.1.0",
21 | "tailwindcss": "^3.3.3"
22 | },
23 | "dependencies": {
24 | "npm-watch": "^0.11.0"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | }
6 | }
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==4.2
2 | django-htmx==1.15.0
3 | gunicorn==21.2.0
4 | whitenoise==6.5.0
5 | psycopg2==2.9.6
6 | django-environ==0.10.0
7 |
--------------------------------------------------------------------------------
/screenshots/desktop_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/screenshots/desktop_dark.png
--------------------------------------------------------------------------------
/screenshots/desktop_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/screenshots/desktop_light.png
--------------------------------------------------------------------------------
/screenshots/mobile_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/screenshots/mobile_dark.png
--------------------------------------------------------------------------------
/screenshots/mobile_dark_create.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/screenshots/mobile_dark_create.png
--------------------------------------------------------------------------------
/screenshots/mobile_dark_edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/screenshots/mobile_dark_edit.png
--------------------------------------------------------------------------------
/screenshots/mobile_dark_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/screenshots/mobile_dark_menu.png
--------------------------------------------------------------------------------
/screenshots/mobile_dark_profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/screenshots/mobile_dark_profile.png
--------------------------------------------------------------------------------
/screenshots/mobile_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/screenshots/mobile_light.png
--------------------------------------------------------------------------------
/static/css/custom/custom.css:
--------------------------------------------------------------------------------
1 | [x-cloak] { display: none !important; }
2 |
3 | .invalid-feedback {
4 | display: none;
5 | }
6 | .invalid-feedback.show {
7 | color: red;
8 | display: block;
9 | }
10 |
11 | .theme-light {
12 | --text: #080703;
13 | --text-secondary: #201c0b;
14 | --text-minor: #545454;
15 | --text-light: #f0f0f0;
16 | --text-dark: #080703;
17 | --background: #fafafa;
18 | --background-secondary: #ececec;
19 | --primary: #85b03b;
20 | --secondary: #bac0e8;
21 | --secondary-dark: #959aba;
22 | --accent: #5d36a1;
23 | }
24 | .theme-dark {
25 | --text: #fcf7f7;
26 | --text-secondary: #dfdfdf;
27 | --text-minor: #bdbdbd;
28 | --text-light: #f0f0f0;
29 | --text-dark: #080703;
30 | --background: #2e2c54;
31 | --background-secondary: #242247;
32 | --primary: #85b03b;
33 | --secondary: #393760;
34 | --secondary-dark: #4d4b70;
35 | --accent: #8871e6;
36 | }
37 | body {
38 | font-family: 'Exo 2', sans-serif;
39 | }
40 |
41 | .error-message {
42 | color:red;
43 | }
44 | input.validation-error {
45 | box-shadow: 0 0 3px #CC0000;
46 | }
47 | input.valid {
48 | box-shadow: 0 0 3px #36cc00;
49 | }
--------------------------------------------------------------------------------
/static/css/tailwind/main.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/static/css/tailwind/main.min.css:
--------------------------------------------------------------------------------
1 | /*
2 | ! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com
3 | *//*
4 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
5 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
6 | */
7 |
8 | *,
9 | ::before,
10 | ::after {
11 | box-sizing: border-box; /* 1 */
12 | border-width: 0; /* 2 */
13 | border-style: solid; /* 2 */
14 | border-color: #e5e7eb; /* 2 */
15 | }
16 |
17 | ::before,
18 | ::after {
19 | --tw-content: '';
20 | }
21 |
22 | /*
23 | 1. Use a consistent sensible line-height in all browsers.
24 | 2. Prevent adjustments of font size after orientation changes in iOS.
25 | 3. Use a more readable tab size.
26 | 4. Use the user's configured `sans` font-family by default.
27 | 5. Use the user's configured `sans` font-feature-settings by default.
28 | 6. Use the user's configured `sans` font-variation-settings by default.
29 | */
30 |
31 | html {
32 | line-height: 1.5; /* 1 */
33 | -webkit-text-size-adjust: 100%; /* 2 */
34 | -moz-tab-size: 4; /* 3 */
35 | -o-tab-size: 4;
36 | tab-size: 4; /* 3 */
37 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */
38 | font-feature-settings: normal; /* 5 */
39 | font-variation-settings: normal; /* 6 */
40 | }
41 |
42 | /*
43 | 1. Remove the margin in all browsers.
44 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
45 | */
46 |
47 | body {
48 | margin: 0; /* 1 */
49 | line-height: inherit; /* 2 */
50 | }
51 |
52 | /*
53 | 1. Add the correct height in Firefox.
54 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
55 | 3. Ensure horizontal rules are visible by default.
56 | */
57 |
58 | hr {
59 | height: 0; /* 1 */
60 | color: inherit; /* 2 */
61 | border-top-width: 1px; /* 3 */
62 | }
63 |
64 | /*
65 | Add the correct text decoration in Chrome, Edge, and Safari.
66 | */
67 |
68 | abbr:where([title]) {
69 | -webkit-text-decoration: underline dotted;
70 | text-decoration: underline dotted;
71 | }
72 |
73 | /*
74 | Remove the default font size and weight for headings.
75 | */
76 |
77 | h1,
78 | h2,
79 | h3,
80 | h4,
81 | h5,
82 | h6 {
83 | font-size: inherit;
84 | font-weight: inherit;
85 | }
86 |
87 | /*
88 | Reset links to optimize for opt-in styling instead of opt-out.
89 | */
90 |
91 | a {
92 | color: inherit;
93 | text-decoration: inherit;
94 | }
95 |
96 | /*
97 | Add the correct font weight in Edge and Safari.
98 | */
99 |
100 | b,
101 | strong {
102 | font-weight: bolder;
103 | }
104 |
105 | /*
106 | 1. Use the user's configured `mono` font family by default.
107 | 2. Correct the odd `em` font sizing in all browsers.
108 | */
109 |
110 | code,
111 | kbd,
112 | samp,
113 | pre {
114 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */
115 | font-size: 1em; /* 2 */
116 | }
117 |
118 | /*
119 | Add the correct font size in all browsers.
120 | */
121 |
122 | small {
123 | font-size: 80%;
124 | }
125 |
126 | /*
127 | Prevent `sub` and `sup` elements from affecting the line height in all browsers.
128 | */
129 |
130 | sub,
131 | sup {
132 | font-size: 75%;
133 | line-height: 0;
134 | position: relative;
135 | vertical-align: baseline;
136 | }
137 |
138 | sub {
139 | bottom: -0.25em;
140 | }
141 |
142 | sup {
143 | top: -0.5em;
144 | }
145 |
146 | /*
147 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
148 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
149 | 3. Remove gaps between table borders by default.
150 | */
151 |
152 | table {
153 | text-indent: 0; /* 1 */
154 | border-color: inherit; /* 2 */
155 | border-collapse: collapse; /* 3 */
156 | }
157 |
158 | /*
159 | 1. Change the font styles in all browsers.
160 | 2. Remove the margin in Firefox and Safari.
161 | 3. Remove default padding in all browsers.
162 | */
163 |
164 | button,
165 | input,
166 | optgroup,
167 | select,
168 | textarea {
169 | font-family: inherit; /* 1 */
170 | font-feature-settings: inherit; /* 1 */
171 | font-variation-settings: inherit; /* 1 */
172 | font-size: 100%; /* 1 */
173 | font-weight: inherit; /* 1 */
174 | line-height: inherit; /* 1 */
175 | color: inherit; /* 1 */
176 | margin: 0; /* 2 */
177 | padding: 0; /* 3 */
178 | }
179 |
180 | /*
181 | Remove the inheritance of text transform in Edge and Firefox.
182 | */
183 |
184 | button,
185 | select {
186 | text-transform: none;
187 | }
188 |
189 | /*
190 | 1. Correct the inability to style clickable types in iOS and Safari.
191 | 2. Remove default button styles.
192 | */
193 |
194 | button,
195 | [type='button'],
196 | [type='reset'],
197 | [type='submit'] {
198 | -webkit-appearance: button; /* 1 */
199 | background-color: transparent; /* 2 */
200 | background-image: none; /* 2 */
201 | }
202 |
203 | /*
204 | Use the modern Firefox focus style for all focusable elements.
205 | */
206 |
207 | :-moz-focusring {
208 | outline: auto;
209 | }
210 |
211 | /*
212 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
213 | */
214 |
215 | :-moz-ui-invalid {
216 | box-shadow: none;
217 | }
218 |
219 | /*
220 | Add the correct vertical alignment in Chrome and Firefox.
221 | */
222 |
223 | progress {
224 | vertical-align: baseline;
225 | }
226 |
227 | /*
228 | Correct the cursor style of increment and decrement buttons in Safari.
229 | */
230 |
231 | ::-webkit-inner-spin-button,
232 | ::-webkit-outer-spin-button {
233 | height: auto;
234 | }
235 |
236 | /*
237 | 1. Correct the odd appearance in Chrome and Safari.
238 | 2. Correct the outline style in Safari.
239 | */
240 |
241 | [type='search'] {
242 | -webkit-appearance: textfield; /* 1 */
243 | outline-offset: -2px; /* 2 */
244 | }
245 |
246 | /*
247 | Remove the inner padding in Chrome and Safari on macOS.
248 | */
249 |
250 | ::-webkit-search-decoration {
251 | -webkit-appearance: none;
252 | }
253 |
254 | /*
255 | 1. Correct the inability to style clickable types in iOS and Safari.
256 | 2. Change font properties to `inherit` in Safari.
257 | */
258 |
259 | ::-webkit-file-upload-button {
260 | -webkit-appearance: button; /* 1 */
261 | font: inherit; /* 2 */
262 | }
263 |
264 | /*
265 | Add the correct display in Chrome and Safari.
266 | */
267 |
268 | summary {
269 | display: list-item;
270 | }
271 |
272 | /*
273 | Removes the default spacing and border for appropriate elements.
274 | */
275 |
276 | blockquote,
277 | dl,
278 | dd,
279 | h1,
280 | h2,
281 | h3,
282 | h4,
283 | h5,
284 | h6,
285 | hr,
286 | figure,
287 | p,
288 | pre {
289 | margin: 0;
290 | }
291 |
292 | fieldset {
293 | margin: 0;
294 | padding: 0;
295 | }
296 |
297 | legend {
298 | padding: 0;
299 | }
300 |
301 | ol,
302 | ul,
303 | menu {
304 | list-style: none;
305 | margin: 0;
306 | padding: 0;
307 | }
308 |
309 | /*
310 | Reset default styling for dialogs.
311 | */
312 | dialog {
313 | padding: 0;
314 | }
315 |
316 | /*
317 | Prevent resizing textareas horizontally by default.
318 | */
319 |
320 | textarea {
321 | resize: vertical;
322 | }
323 |
324 | /*
325 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
326 | 2. Set the default placeholder color to the user's configured gray 400 color.
327 | */
328 |
329 | input::-moz-placeholder, textarea::-moz-placeholder {
330 | opacity: 1; /* 1 */
331 | color: #9ca3af; /* 2 */
332 | }
333 |
334 | input::placeholder,
335 | textarea::placeholder {
336 | opacity: 1; /* 1 */
337 | color: #9ca3af; /* 2 */
338 | }
339 |
340 | /*
341 | Set the default cursor for buttons.
342 | */
343 |
344 | button,
345 | [role="button"] {
346 | cursor: pointer;
347 | }
348 |
349 | /*
350 | Make sure disabled buttons don't get the pointer cursor.
351 | */
352 | :disabled {
353 | cursor: default;
354 | }
355 |
356 | /*
357 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
358 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
359 | This can trigger a poorly considered lint error in some tools but is included by design.
360 | */
361 |
362 | img,
363 | svg,
364 | video,
365 | canvas,
366 | audio,
367 | iframe,
368 | embed,
369 | object {
370 | display: block; /* 1 */
371 | vertical-align: middle; /* 2 */
372 | }
373 |
374 | /*
375 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
376 | */
377 |
378 | img,
379 | video {
380 | max-width: 100%;
381 | height: auto;
382 | }
383 |
384 | /* Make elements with the HTML hidden attribute stay hidden by default */
385 | [hidden] {
386 | display: none;
387 | }
388 |
389 | *, ::before, ::after {
390 | --tw-border-spacing-x: 0;
391 | --tw-border-spacing-y: 0;
392 | --tw-translate-x: 0;
393 | --tw-translate-y: 0;
394 | --tw-rotate: 0;
395 | --tw-skew-x: 0;
396 | --tw-skew-y: 0;
397 | --tw-scale-x: 1;
398 | --tw-scale-y: 1;
399 | --tw-pan-x: ;
400 | --tw-pan-y: ;
401 | --tw-pinch-zoom: ;
402 | --tw-scroll-snap-strictness: proximity;
403 | --tw-gradient-from-position: ;
404 | --tw-gradient-via-position: ;
405 | --tw-gradient-to-position: ;
406 | --tw-ordinal: ;
407 | --tw-slashed-zero: ;
408 | --tw-numeric-figure: ;
409 | --tw-numeric-spacing: ;
410 | --tw-numeric-fraction: ;
411 | --tw-ring-inset: ;
412 | --tw-ring-offset-width: 0px;
413 | --tw-ring-offset-color: #fff;
414 | --tw-ring-color: rgb(59 130 246 / 0.5);
415 | --tw-ring-offset-shadow: 0 0 #0000;
416 | --tw-ring-shadow: 0 0 #0000;
417 | --tw-shadow: 0 0 #0000;
418 | --tw-shadow-colored: 0 0 #0000;
419 | --tw-blur: ;
420 | --tw-brightness: ;
421 | --tw-contrast: ;
422 | --tw-grayscale: ;
423 | --tw-hue-rotate: ;
424 | --tw-invert: ;
425 | --tw-saturate: ;
426 | --tw-sepia: ;
427 | --tw-drop-shadow: ;
428 | --tw-backdrop-blur: ;
429 | --tw-backdrop-brightness: ;
430 | --tw-backdrop-contrast: ;
431 | --tw-backdrop-grayscale: ;
432 | --tw-backdrop-hue-rotate: ;
433 | --tw-backdrop-invert: ;
434 | --tw-backdrop-opacity: ;
435 | --tw-backdrop-saturate: ;
436 | --tw-backdrop-sepia: ;
437 | }
438 |
439 | ::backdrop {
440 | --tw-border-spacing-x: 0;
441 | --tw-border-spacing-y: 0;
442 | --tw-translate-x: 0;
443 | --tw-translate-y: 0;
444 | --tw-rotate: 0;
445 | --tw-skew-x: 0;
446 | --tw-skew-y: 0;
447 | --tw-scale-x: 1;
448 | --tw-scale-y: 1;
449 | --tw-pan-x: ;
450 | --tw-pan-y: ;
451 | --tw-pinch-zoom: ;
452 | --tw-scroll-snap-strictness: proximity;
453 | --tw-gradient-from-position: ;
454 | --tw-gradient-via-position: ;
455 | --tw-gradient-to-position: ;
456 | --tw-ordinal: ;
457 | --tw-slashed-zero: ;
458 | --tw-numeric-figure: ;
459 | --tw-numeric-spacing: ;
460 | --tw-numeric-fraction: ;
461 | --tw-ring-inset: ;
462 | --tw-ring-offset-width: 0px;
463 | --tw-ring-offset-color: #fff;
464 | --tw-ring-color: rgb(59 130 246 / 0.5);
465 | --tw-ring-offset-shadow: 0 0 #0000;
466 | --tw-ring-shadow: 0 0 #0000;
467 | --tw-shadow: 0 0 #0000;
468 | --tw-shadow-colored: 0 0 #0000;
469 | --tw-blur: ;
470 | --tw-brightness: ;
471 | --tw-contrast: ;
472 | --tw-grayscale: ;
473 | --tw-hue-rotate: ;
474 | --tw-invert: ;
475 | --tw-saturate: ;
476 | --tw-sepia: ;
477 | --tw-drop-shadow: ;
478 | --tw-backdrop-blur: ;
479 | --tw-backdrop-brightness: ;
480 | --tw-backdrop-contrast: ;
481 | --tw-backdrop-grayscale: ;
482 | --tw-backdrop-hue-rotate: ;
483 | --tw-backdrop-invert: ;
484 | --tw-backdrop-opacity: ;
485 | --tw-backdrop-saturate: ;
486 | --tw-backdrop-sepia: ;
487 | }
488 | .sr-only {
489 | position: absolute;
490 | width: 1px;
491 | height: 1px;
492 | padding: 0;
493 | margin: -1px;
494 | overflow: hidden;
495 | clip: rect(0, 0, 0, 0);
496 | white-space: nowrap;
497 | border-width: 0;
498 | }
499 | .static {
500 | position: static;
501 | }
502 | .fixed {
503 | position: fixed;
504 | }
505 | .absolute {
506 | position: absolute;
507 | }
508 | .relative {
509 | position: relative;
510 | }
511 | .inset-0 {
512 | inset: 0px;
513 | }
514 | .bottom-\[2vh\] {
515 | bottom: 2vh;
516 | }
517 | .left-0 {
518 | left: 0px;
519 | }
520 | .right-0 {
521 | right: 0px;
522 | }
523 | .right-2 {
524 | right: 0.5rem;
525 | }
526 | .right-2\.5 {
527 | right: 0.625rem;
528 | }
529 | .right-\[5vw\] {
530 | right: 5vw;
531 | }
532 | .top-0 {
533 | top: 0px;
534 | }
535 | .top-3 {
536 | top: 0.75rem;
537 | }
538 | .top-4 {
539 | top: 1rem;
540 | }
541 | .z-20 {
542 | z-index: 20;
543 | }
544 | .z-40 {
545 | z-index: 40;
546 | }
547 | .z-50 {
548 | z-index: 50;
549 | }
550 | .order-2 {
551 | order: 2;
552 | }
553 | .order-3 {
554 | order: 3;
555 | }
556 | .order-4 {
557 | order: 4;
558 | }
559 | .-mx-1 {
560 | margin-left: -0.25rem;
561 | margin-right: -0.25rem;
562 | }
563 | .-mx-1\.5 {
564 | margin-left: -0.375rem;
565 | margin-right: -0.375rem;
566 | }
567 | .-my-1 {
568 | margin-top: -0.25rem;
569 | margin-bottom: -0.25rem;
570 | }
571 | .-my-1\.5 {
572 | margin-top: -0.375rem;
573 | margin-bottom: -0.375rem;
574 | }
575 | .mx-\[5vw\] {
576 | margin-left: 5vw;
577 | margin-right: 5vw;
578 | }
579 | .mx-auto {
580 | margin-left: auto;
581 | margin-right: auto;
582 | }
583 | .my-4 {
584 | margin-top: 1rem;
585 | margin-bottom: 1rem;
586 | }
587 | .mb-2 {
588 | margin-bottom: 0.5rem;
589 | }
590 | .mb-3 {
591 | margin-bottom: 0.75rem;
592 | }
593 | .mb-4 {
594 | margin-bottom: 1rem;
595 | }
596 | .mb-5 {
597 | margin-bottom: 1.25rem;
598 | }
599 | .ml-2 {
600 | margin-left: 0.5rem;
601 | }
602 | .ml-3 {
603 | margin-left: 0.75rem;
604 | }
605 | .ml-auto {
606 | margin-left: auto;
607 | }
608 | .mr-3 {
609 | margin-right: 0.75rem;
610 | }
611 | .mt-1 {
612 | margin-top: 0.25rem;
613 | }
614 | .mt-1\.5 {
615 | margin-top: 0.375rem;
616 | }
617 | .mt-10 {
618 | margin-top: 2.5rem;
619 | }
620 | .mt-2 {
621 | margin-top: 0.5rem;
622 | }
623 | .mt-4 {
624 | margin-top: 1rem;
625 | }
626 | .mt-5 {
627 | margin-top: 1.25rem;
628 | }
629 | .block {
630 | display: block;
631 | }
632 | .flex {
633 | display: flex;
634 | }
635 | .inline-flex {
636 | display: inline-flex;
637 | }
638 | .grid {
639 | display: grid;
640 | }
641 | .hidden {
642 | display: none;
643 | }
644 | .h-10 {
645 | height: 2.5rem;
646 | }
647 | .h-3 {
648 | height: 0.75rem;
649 | }
650 | .h-4 {
651 | height: 1rem;
652 | }
653 | .h-5 {
654 | height: 1.25rem;
655 | }
656 | .h-6 {
657 | height: 1.5rem;
658 | }
659 | .h-8 {
660 | height: 2rem;
661 | }
662 | .h-\[calc\(100\%-1rem\)\] {
663 | height: calc(100% - 1rem);
664 | }
665 | .h-full {
666 | height: 100%;
667 | }
668 | .max-h-full {
669 | max-height: 100%;
670 | }
671 | .min-h-\[80vh\] {
672 | min-height: 80vh;
673 | }
674 | .w-1\/3 {
675 | width: 33.333333%;
676 | }
677 | .w-1\/4 {
678 | width: 25%;
679 | }
680 | .w-10 {
681 | width: 2.5rem;
682 | }
683 | .w-11 {
684 | width: 2.75rem;
685 | }
686 | .w-16 {
687 | width: 4rem;
688 | }
689 | .w-2\/3 {
690 | width: 66.666667%;
691 | }
692 | .w-3 {
693 | width: 0.75rem;
694 | }
695 | .w-3\/4 {
696 | width: 75%;
697 | }
698 | .w-4 {
699 | width: 1rem;
700 | }
701 | .w-5 {
702 | width: 1.25rem;
703 | }
704 | .w-6 {
705 | width: 1.5rem;
706 | }
707 | .w-8 {
708 | width: 2rem;
709 | }
710 | .w-\[90vw\] {
711 | width: 90vw;
712 | }
713 | .w-auto {
714 | width: auto;
715 | }
716 | .w-fit {
717 | width: -moz-fit-content;
718 | width: fit-content;
719 | }
720 | .w-full {
721 | width: 100%;
722 | }
723 | .max-w-2xl {
724 | max-width: 42rem;
725 | }
726 | .max-w-md {
727 | max-width: 28rem;
728 | }
729 | .flex-shrink-0 {
730 | flex-shrink: 0;
731 | }
732 | .transform {
733 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
734 | }
735 | @keyframes spin {
736 |
737 | to {
738 | transform: rotate(360deg);
739 | }
740 | }
741 | .animate-spin {
742 | animation: spin 1s linear infinite;
743 | }
744 | .cursor-pointer {
745 | cursor: pointer;
746 | }
747 | .resize-none {
748 | resize: none;
749 | }
750 | .appearance-none {
751 | -webkit-appearance: none;
752 | -moz-appearance: none;
753 | appearance: none;
754 | }
755 | .grid-flow-col {
756 | grid-auto-flow: column;
757 | }
758 | .grid-cols-1 {
759 | grid-template-columns: repeat(1, minmax(0, 1fr));
760 | }
761 | .flex-col {
762 | flex-direction: column;
763 | }
764 | .flex-wrap {
765 | flex-wrap: wrap;
766 | }
767 | .items-start {
768 | align-items: flex-start;
769 | }
770 | .items-center {
771 | align-items: center;
772 | }
773 | .justify-center {
774 | justify-content: center;
775 | }
776 | .justify-between {
777 | justify-content: space-between;
778 | }
779 | .gap-1 {
780 | gap: 0.25rem;
781 | }
782 | .gap-2 {
783 | gap: 0.5rem;
784 | }
785 | .gap-3 {
786 | gap: 0.75rem;
787 | }
788 | .space-x-2 > :not([hidden]) ~ :not([hidden]) {
789 | --tw-space-x-reverse: 0;
790 | margin-right: calc(0.5rem * var(--tw-space-x-reverse));
791 | margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
792 | }
793 | .space-x-4 > :not([hidden]) ~ :not([hidden]) {
794 | --tw-space-x-reverse: 0;
795 | margin-right: calc(1rem * var(--tw-space-x-reverse));
796 | margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
797 | }
798 | .space-y-2 > :not([hidden]) ~ :not([hidden]) {
799 | --tw-space-y-reverse: 0;
800 | margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
801 | margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
802 | }
803 | .space-y-3 > :not([hidden]) ~ :not([hidden]) {
804 | --tw-space-y-reverse: 0;
805 | margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
806 | margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
807 | }
808 | .space-y-5 > :not([hidden]) ~ :not([hidden]) {
809 | --tw-space-y-reverse: 0;
810 | margin-top: calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));
811 | margin-bottom: calc(1.25rem * var(--tw-space-y-reverse));
812 | }
813 | .space-y-6 > :not([hidden]) ~ :not([hidden]) {
814 | --tw-space-y-reverse: 0;
815 | margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
816 | margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
817 | }
818 | .self-start {
819 | align-self: flex-start;
820 | }
821 | .self-center {
822 | align-self: center;
823 | }
824 | .overflow-y-auto {
825 | overflow-y: auto;
826 | }
827 | .overflow-x-hidden {
828 | overflow-x: hidden;
829 | }
830 | .rounded {
831 | border-radius: 0.25rem;
832 | }
833 | .rounded-full {
834 | border-radius: 9999px;
835 | }
836 | .rounded-lg {
837 | border-radius: 0.5rem;
838 | }
839 | .rounded-l-none {
840 | border-top-left-radius: 0px;
841 | border-bottom-left-radius: 0px;
842 | }
843 | .rounded-r-none {
844 | border-top-right-radius: 0px;
845 | border-bottom-right-radius: 0px;
846 | }
847 | .border {
848 | border-width: 1px;
849 | }
850 | .border-2 {
851 | border-width: 2px;
852 | }
853 | .border-\[--secondary\] {
854 | border-color: var(--secondary);
855 | }
856 | .border-gray-300 {
857 | --tw-border-opacity: 1;
858 | border-color: rgb(209 213 219 / var(--tw-border-opacity));
859 | }
860 | .border-green-500 {
861 | --tw-border-opacity: 1;
862 | border-color: rgb(34 197 94 / var(--tw-border-opacity));
863 | }
864 | .border-red-500 {
865 | --tw-border-opacity: 1;
866 | border-color: rgb(239 68 68 / var(--tw-border-opacity));
867 | }
868 | .border-yellow-500 {
869 | --tw-border-opacity: 1;
870 | border-color: rgb(234 179 8 / var(--tw-border-opacity));
871 | }
872 | .bg-\[--background-secondary\] {
873 | background-color: var(--background-secondary);
874 | }
875 | .bg-\[--background\] {
876 | background-color: var(--background);
877 | }
878 | .bg-\[--primary\] {
879 | background-color: var(--primary);
880 | }
881 | .bg-\[--secondary\] {
882 | background-color: var(--secondary);
883 | }
884 | .bg-gray-50 {
885 | --tw-bg-opacity: 1;
886 | background-color: rgb(249 250 251 / var(--tw-bg-opacity));
887 | }
888 | .bg-gray-900 {
889 | --tw-bg-opacity: 1;
890 | background-color: rgb(17 24 39 / var(--tw-bg-opacity));
891 | }
892 | .bg-green-100 {
893 | --tw-bg-opacity: 1;
894 | background-color: rgb(220 252 231 / var(--tw-bg-opacity));
895 | }
896 | .bg-red-100 {
897 | --tw-bg-opacity: 1;
898 | background-color: rgb(254 226 226 / var(--tw-bg-opacity));
899 | }
900 | .bg-red-900 {
901 | --tw-bg-opacity: 1;
902 | background-color: rgb(127 29 29 / var(--tw-bg-opacity));
903 | }
904 | .bg-yellow-100 {
905 | --tw-bg-opacity: 1;
906 | background-color: rgb(254 249 195 / var(--tw-bg-opacity));
907 | }
908 | .bg-opacity-50 {
909 | --tw-bg-opacity: 0.5;
910 | }
911 | .stroke-\[--accent\] {
912 | stroke: var(--accent);
913 | }
914 | .stroke-green-700 {
915 | stroke: #15803d;
916 | }
917 | .stroke-red-500 {
918 | stroke: #ef4444;
919 | }
920 | .p-1 {
921 | padding: 0.25rem;
922 | }
923 | .p-1\.5 {
924 | padding: 0.375rem;
925 | }
926 | .p-2 {
927 | padding: 0.5rem;
928 | }
929 | .p-2\.5 {
930 | padding: 0.625rem;
931 | }
932 | .p-3 {
933 | padding: 0.75rem;
934 | }
935 | .p-4 {
936 | padding: 1rem;
937 | }
938 | .p-6 {
939 | padding: 1.5rem;
940 | }
941 | .px-2 {
942 | padding-left: 0.5rem;
943 | padding-right: 0.5rem;
944 | }
945 | .px-4 {
946 | padding-left: 1rem;
947 | padding-right: 1rem;
948 | }
949 | .px-6 {
950 | padding-left: 1.5rem;
951 | padding-right: 1.5rem;
952 | }
953 | .px-\[10vw\] {
954 | padding-left: 10vw;
955 | padding-right: 10vw;
956 | }
957 | .px-\[2vw\] {
958 | padding-left: 2vw;
959 | padding-right: 2vw;
960 | }
961 | .px-\[3vw\] {
962 | padding-left: 3vw;
963 | padding-right: 3vw;
964 | }
965 | .py-2 {
966 | padding-top: 0.5rem;
967 | padding-bottom: 0.5rem;
968 | }
969 | .py-4 {
970 | padding-top: 1rem;
971 | padding-bottom: 1rem;
972 | }
973 | .py-6 {
974 | padding-top: 1.5rem;
975 | padding-bottom: 1.5rem;
976 | }
977 | .pb-5 {
978 | padding-bottom: 1.25rem;
979 | }
980 | .pt-10 {
981 | padding-top: 2.5rem;
982 | }
983 | .text-center {
984 | text-align: center;
985 | }
986 | .text-2xl {
987 | font-size: 1.5rem;
988 | line-height: 2rem;
989 | }
990 | .text-3xl {
991 | font-size: 1.875rem;
992 | line-height: 2.25rem;
993 | }
994 | .text-\[min\(10vw\2c 30px\)\] {
995 | font-size: min(10vw,30px);
996 | }
997 | .text-lg {
998 | font-size: 1.125rem;
999 | line-height: 1.75rem;
1000 | }
1001 | .text-sm {
1002 | font-size: 0.875rem;
1003 | line-height: 1.25rem;
1004 | }
1005 | .text-xl {
1006 | font-size: 1.25rem;
1007 | line-height: 1.75rem;
1008 | }
1009 | .text-xs {
1010 | font-size: 0.75rem;
1011 | line-height: 1rem;
1012 | }
1013 | .font-bold {
1014 | font-weight: 700;
1015 | }
1016 | .font-extrabold {
1017 | font-weight: 800;
1018 | }
1019 | .font-light {
1020 | font-weight: 300;
1021 | }
1022 | .font-medium {
1023 | font-weight: 500;
1024 | }
1025 | .font-normal {
1026 | font-weight: 400;
1027 | }
1028 | .font-semibold {
1029 | font-weight: 600;
1030 | }
1031 | .tracking-tight {
1032 | letter-spacing: -0.025em;
1033 | }
1034 | .text-\[--accent\] {
1035 | color: var(--accent);
1036 | }
1037 | .text-\[--text-dark\] {
1038 | color: var(--text-dark);
1039 | }
1040 | .text-\[--text-light\] {
1041 | color: var(--text-light);
1042 | }
1043 | .text-\[--text-minor\] {
1044 | color: var(--text-minor);
1045 | }
1046 | .text-\[--text-secondary\] {
1047 | color: var(--text-secondary);
1048 | }
1049 | .text-\[--text\] {
1050 | color: var(--text);
1051 | }
1052 | .text-green-500 {
1053 | --tw-text-opacity: 1;
1054 | color: rgb(34 197 94 / var(--tw-text-opacity));
1055 | }
1056 | .text-red-500 {
1057 | --tw-text-opacity: 1;
1058 | color: rgb(239 68 68 / var(--tw-text-opacity));
1059 | }
1060 | .text-yellow-500 {
1061 | --tw-text-opacity: 1;
1062 | color: rgb(234 179 8 / var(--tw-text-opacity));
1063 | }
1064 | .placeholder-gray-600::-moz-placeholder {
1065 | --tw-placeholder-opacity: 1;
1066 | color: rgb(75 85 99 / var(--tw-placeholder-opacity));
1067 | }
1068 | .placeholder-gray-600::placeholder {
1069 | --tw-placeholder-opacity: 1;
1070 | color: rgb(75 85 99 / var(--tw-placeholder-opacity));
1071 | }
1072 | .opacity-0 {
1073 | opacity: 0;
1074 | }
1075 | .shadow {
1076 | --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
1077 | --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
1078 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1079 | }
1080 | .transition {
1081 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
1082 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
1083 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
1084 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1085 | transition-duration: 150ms;
1086 | }
1087 | .duration-200 {
1088 | transition-duration: 200ms;
1089 | }
1090 | .ease-in-out {
1091 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1092 | }
1093 | .before\:content-\[\'Show_All\'\]::before {
1094 | --tw-content: 'Show All';
1095 | content: var(--tw-content);
1096 | }
1097 | .after\:absolute::after {
1098 | content: var(--tw-content);
1099 | position: absolute;
1100 | }
1101 | .after\:left-\[2px\]::after {
1102 | content: var(--tw-content);
1103 | left: 2px;
1104 | }
1105 | .after\:top-0::after {
1106 | content: var(--tw-content);
1107 | top: 0px;
1108 | }
1109 | .after\:top-0\.5::after {
1110 | content: var(--tw-content);
1111 | top: 0.125rem;
1112 | }
1113 | .after\:h-5::after {
1114 | content: var(--tw-content);
1115 | height: 1.25rem;
1116 | }
1117 | .after\:w-5::after {
1118 | content: var(--tw-content);
1119 | width: 1.25rem;
1120 | }
1121 | .after\:rounded-full::after {
1122 | content: var(--tw-content);
1123 | border-radius: 9999px;
1124 | }
1125 | .after\:border-white::after {
1126 | content: var(--tw-content);
1127 | --tw-border-opacity: 1;
1128 | border-color: rgb(255 255 255 / var(--tw-border-opacity));
1129 | }
1130 | .after\:bg-\[--secondary\]::after {
1131 | content: var(--tw-content);
1132 | background-color: var(--secondary);
1133 | }
1134 | .after\:bg-white::after {
1135 | content: var(--tw-content);
1136 | --tw-bg-opacity: 1;
1137 | background-color: rgb(255 255 255 / var(--tw-bg-opacity));
1138 | }
1139 | .after\:transition-all::after {
1140 | content: var(--tw-content);
1141 | transition-property: all;
1142 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1143 | transition-duration: 150ms;
1144 | }
1145 | .after\:content-\[\'\'\]::after {
1146 | --tw-content: '';
1147 | content: var(--tw-content);
1148 | }
1149 | .checked\:opacity-100:checked {
1150 | opacity: 1;
1151 | }
1152 | .hover\:origin-top:hover {
1153 | transform-origin: top;
1154 | }
1155 | .hover\:translate-y-\[-3px\]:hover {
1156 | --tw-translate-y: -3px;
1157 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1158 | }
1159 | .hover\:cursor-default:hover {
1160 | cursor: default;
1161 | }
1162 | .hover\:cursor-pointer:hover {
1163 | cursor: pointer;
1164 | }
1165 | .hover\:cursor-text:hover {
1166 | cursor: text;
1167 | }
1168 | .hover\:bg-\[--secondary-dark\]:hover {
1169 | background-color: var(--secondary-dark);
1170 | }
1171 | .hover\:text-\[--accent\]:hover {
1172 | color: var(--accent);
1173 | }
1174 | .hover\:text-gray-900:hover {
1175 | --tw-text-opacity: 1;
1176 | color: rgb(17 24 39 / var(--tw-text-opacity));
1177 | }
1178 | .focus\:outline-none:focus {
1179 | outline: 2px solid transparent;
1180 | outline-offset: 2px;
1181 | }
1182 | .focus\:ring-2:focus {
1183 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
1184 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
1185 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
1186 | }
1187 | .focus\:ring-blue-300:focus {
1188 | --tw-ring-opacity: 1;
1189 | --tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity));
1190 | }
1191 | .focus\:ring-gray-300:focus {
1192 | --tw-ring-opacity: 1;
1193 | --tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity));
1194 | }
1195 | .focus-visible\:outline-none:focus-visible {
1196 | outline: 2px solid transparent;
1197 | outline-offset: 2px;
1198 | }
1199 | .disabled\:cursor-not-allowed:disabled {
1200 | cursor: not-allowed;
1201 | }
1202 | .disabled\:text-gray-600:disabled {
1203 | --tw-text-opacity: 1;
1204 | color: rgb(75 85 99 / var(--tw-text-opacity));
1205 | }
1206 | .group:hover .group-hover\:opacity-100 {
1207 | opacity: 1;
1208 | }
1209 | .peer:checked ~ .peer-checked\:border-\[--accent\] {
1210 | border-color: var(--accent);
1211 | }
1212 | .peer:checked ~ .peer-checked\:bg-\[--accent\] {
1213 | background-color: var(--accent);
1214 | }
1215 | .peer:checked ~ .peer-checked\:text-\[--text\] {
1216 | color: var(--text);
1217 | }
1218 | .peer:checked ~ .peer-checked\:before\:content-\[\'Show_Uncompleted\'\]::before {
1219 | --tw-content: 'Show Uncompleted';
1220 | content: var(--tw-content);
1221 | }
1222 | .peer:checked ~ .peer-checked\:after\:translate-x-full::after {
1223 | content: var(--tw-content);
1224 | --tw-translate-x: 100%;
1225 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1226 | }
1227 | .peer:checked ~ .peer-checked\:after\:border-white::after {
1228 | content: var(--tw-content);
1229 | --tw-border-opacity: 1;
1230 | border-color: rgb(255 255 255 / var(--tw-border-opacity));
1231 | }
1232 | .peer:checked ~ .peer-checked\:after\:bg-white::after {
1233 | content: var(--tw-content);
1234 | --tw-bg-opacity: 1;
1235 | background-color: rgb(255 255 255 / var(--tw-bg-opacity));
1236 | }
1237 | @media (prefers-color-scheme: dark) {
1238 |
1239 | .dark\:border-gray-500 {
1240 | --tw-border-opacity: 1;
1241 | border-color: rgb(107 114 128 / var(--tw-border-opacity));
1242 | }
1243 |
1244 | .dark\:bg-gray-600 {
1245 | --tw-bg-opacity: 1;
1246 | background-color: rgb(75 85 99 / var(--tw-bg-opacity));
1247 | }
1248 |
1249 | .dark\:bg-green-800 {
1250 | --tw-bg-opacity: 1;
1251 | background-color: rgb(22 101 52 / var(--tw-bg-opacity));
1252 | }
1253 |
1254 | .dark\:bg-red-800 {
1255 | --tw-bg-opacity: 1;
1256 | background-color: rgb(153 27 27 / var(--tw-bg-opacity));
1257 | }
1258 |
1259 | .dark\:bg-yellow-700 {
1260 | --tw-bg-opacity: 1;
1261 | background-color: rgb(161 98 7 / var(--tw-bg-opacity));
1262 | }
1263 |
1264 | .dark\:bg-opacity-80 {
1265 | --tw-bg-opacity: 0.8;
1266 | }
1267 |
1268 | .dark\:text-green-200 {
1269 | --tw-text-opacity: 1;
1270 | color: rgb(187 247 208 / var(--tw-text-opacity));
1271 | }
1272 |
1273 | .dark\:text-red-200 {
1274 | --tw-text-opacity: 1;
1275 | color: rgb(254 202 202 / var(--tw-text-opacity));
1276 | }
1277 |
1278 | .dark\:text-yellow-200 {
1279 | --tw-text-opacity: 1;
1280 | color: rgb(254 240 138 / var(--tw-text-opacity));
1281 | }
1282 |
1283 | .dark\:ring-offset-gray-800 {
1284 | --tw-ring-offset-color: #1f2937;
1285 | }
1286 |
1287 | .dark\:hover\:text-white:hover {
1288 | --tw-text-opacity: 1;
1289 | color: rgb(255 255 255 / var(--tw-text-opacity));
1290 | }
1291 |
1292 | .dark\:focus\:ring-blue-600:focus {
1293 | --tw-ring-opacity: 1;
1294 | --tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity));
1295 | }
1296 |
1297 | .dark\:focus\:ring-offset-gray-800:focus {
1298 | --tw-ring-offset-color: #1f2937;
1299 | }
1300 | }
1301 | @media not all and (min-width: 768px) {
1302 |
1303 | .max-md\:justify-center {
1304 | justify-content: center;
1305 | }
1306 | }
1307 | @media (min-width: 640px) {
1308 |
1309 | .sm\:right-\[30vw\] {
1310 | right: 30vw;
1311 | }
1312 |
1313 | .sm\:ml-10 {
1314 | margin-left: 2.5rem;
1315 | }
1316 |
1317 | .sm\:ml-3 {
1318 | margin-left: 0.75rem;
1319 | }
1320 |
1321 | .sm\:mt-0 {
1322 | margin-top: 0px;
1323 | }
1324 |
1325 | .sm\:w-\[40vw\] {
1326 | width: 40vw;
1327 | }
1328 |
1329 | .sm\:grid-cols-2 {
1330 | grid-template-columns: repeat(2, minmax(0, 1fr));
1331 | }
1332 |
1333 | .sm\:flex-row {
1334 | flex-direction: row;
1335 | }
1336 |
1337 | .sm\:items-center {
1338 | align-items: center;
1339 | }
1340 |
1341 | .sm\:px-\[20vw\] {
1342 | padding-left: 20vw;
1343 | padding-right: 20vw;
1344 | }
1345 | }
1346 | @media (min-width: 768px) {
1347 |
1348 | .md\:inset-0 {
1349 | inset: 0px;
1350 | }
1351 |
1352 | .md\:order-1 {
1353 | order: 1;
1354 | }
1355 |
1356 | .md\:order-2 {
1357 | order: 2;
1358 | }
1359 |
1360 | .md\:order-3 {
1361 | order: 3;
1362 | }
1363 |
1364 | .md\:order-4 {
1365 | order: 4;
1366 | }
1367 |
1368 | .md\:ml-0 {
1369 | margin-left: 0px;
1370 | }
1371 |
1372 | .md\:mt-0 {
1373 | margin-top: 0px;
1374 | }
1375 |
1376 | .md\:flex {
1377 | display: flex;
1378 | }
1379 |
1380 | .md\:hidden {
1381 | display: none;
1382 | }
1383 |
1384 | .md\:w-1\/4 {
1385 | width: 25%;
1386 | }
1387 |
1388 | .md\:w-2\/4 {
1389 | width: 50%;
1390 | }
1391 |
1392 | .md\:w-auto {
1393 | width: auto;
1394 | }
1395 |
1396 | .md\:w-fit {
1397 | width: -moz-fit-content;
1398 | width: fit-content;
1399 | }
1400 |
1401 | .md\:grid-cols-3 {
1402 | grid-template-columns: repeat(3, minmax(0, 1fr));
1403 | }
1404 |
1405 | .md\:flex-row {
1406 | flex-direction: row;
1407 | }
1408 |
1409 | .md\:space-x-4 > :not([hidden]) ~ :not([hidden]) {
1410 | --tw-space-x-reverse: 0;
1411 | margin-right: calc(1rem * var(--tw-space-x-reverse));
1412 | margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
1413 | }
1414 |
1415 | .md\:space-y-0 > :not([hidden]) ~ :not([hidden]) {
1416 | --tw-space-y-reverse: 0;
1417 | margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
1418 | margin-bottom: calc(0px * var(--tw-space-y-reverse));
1419 | }
1420 |
1421 | .md\:p-4 {
1422 | padding: 1rem;
1423 | }
1424 |
1425 | .md\:px-\[30vw\] {
1426 | padding-left: 30vw;
1427 | padding-right: 30vw;
1428 | }
1429 |
1430 | .md\:text-2xl {
1431 | font-size: 1.5rem;
1432 | line-height: 2rem;
1433 | }
1434 | }
1435 | @media (min-width: 1024px) {
1436 |
1437 | .lg\:ml-2 {
1438 | margin-left: 0.5rem;
1439 | }
1440 |
1441 | .lg\:block {
1442 | display: block;
1443 | }
1444 |
1445 | .lg\:px-8 {
1446 | padding-left: 2rem;
1447 | padding-right: 2rem;
1448 | }
1449 |
1450 | .lg\:px-\[5vw\] {
1451 | padding-left: 5vw;
1452 | padding-right: 5vw;
1453 | }
1454 | }
1455 | @media (min-width: 1280px) {
1456 |
1457 | .xl\:grid-cols-4 {
1458 | grid-template-columns: repeat(4, minmax(0, 1fr));
1459 | }
1460 | }
1461 | @media (min-width: 1536px) {
1462 |
1463 | .\32xl\:grid-cols-5 {
1464 | grid-template-columns: repeat(5, minmax(0, 1fr));
1465 | }
1466 | }
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/olksndrdevhub/just-notes-htmx/598805beb63f550535b041e41744e3a04c8a1ac1/static/favicon.ico
--------------------------------------------------------------------------------
/static/js/alpine.min.js:
--------------------------------------------------------------------------------
1 | (()=>{var Xe=!1,Ze=!1,V=[],Qe=-1;function Kt(e){En(e)}function En(e){V.includes(e)||V.push(e),Sn()}function ye(e){let t=V.indexOf(e);t!==-1&&t>Qe&&V.splice(t,1)}function Sn(){!Ze&&!Xe&&(Xe=!0,queueMicrotask(An))}function An(){Xe=!1,Ze=!0;for(let e=0;ee.effect(t,{scheduler:r=>{tt?Kt(r):r()}}),et=e.raw}function rt(e){D=e}function Ht(e){let t=()=>{};return[n=>{let i=D(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),$(i))},i},()=>{t()}]}var qt=[],Ut=[],Wt=[];function Gt(e){Wt.push(e)}function be(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Ut.push(t))}function Jt(e){qt.push(e)}function Yt(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function nt(e,t){!e._x_attributeCleanups||Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}var ot=new MutationObserver(it),st=!1;function se(){ot.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),st=!0}function at(){On(),ot.disconnect(),st=!1}var ae=[],ct=!1;function On(){ae=ae.concat(ot.takeRecords()),ae.length&&!ct&&(ct=!0,queueMicrotask(()=>{Tn(),ct=!1}))}function Tn(){it(ae),ae.length=0}function h(e){if(!st)return e();at();let t=e();return se(),t}var lt=!1,ve=[];function Xt(){lt=!0}function Zt(){lt=!1,it(ve),ve=[]}function it(e){if(lt){ve=ve.concat(e);return}let t=[],r=[],n=new Map,i=new Map;for(let o=0;os.nodeType===1&&t.push(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.push(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{nt(s,o)}),n.forEach((o,s)=>{qt.forEach(a=>a(s,o))});for(let o of r)if(!t.includes(o)&&(Ut.forEach(s=>s(o)),o._x_cleanups))for(;o._x_cleanups.length;)o._x_cleanups.pop()();t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.includes(o)||!o.isConnected||(delete o._x_ignoreSelf,delete o._x_ignore,Wt.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function we(e){return F(L(e))}function N(e,t,r){return e._x_dataStack=[t,...L(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function L(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?L(e.host):e.parentNode?L(e.parentNode):[]}function F(e){let t=new Proxy({},{ownKeys:()=>Array.from(new Set(e.flatMap(r=>Object.keys(r)))),has:(r,n)=>e.some(i=>i.hasOwnProperty(n)),get:(r,n)=>(e.find(i=>{if(i.hasOwnProperty(n)){let o=Object.getOwnPropertyDescriptor(i,n);if(o.get&&o.get._x_alreadyBound||o.set&&o.set._x_alreadyBound)return!0;if((o.get||o.set)&&o.enumerable){let s=o.get,a=o.set,c=o;s=s&&s.bind(t),a=a&&a.bind(t),s&&(s._x_alreadyBound=!0),a&&(a._x_alreadyBound=!0),Object.defineProperty(i,n,{...c,get:s,set:a})}return!0}return!1})||{})[n],set:(r,n,i)=>{let o=e.find(s=>s.hasOwnProperty(n));return o?o[n]=i:e[e.length-1][n]=i,!0}});return t}function Ee(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Se(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>Cn(n,i),s=>ut(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function Cn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function ut(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),ut(e[t[0]],t.slice(1),r)}}var Qt={};function y(e,t){Qt[e]=t}function ce(e,t){return Object.entries(Qt).forEach(([r,n])=>{let i=null;function o(){if(i)return i;{let[s,a]=ft(t);return i={interceptor:Se,...s},be(t,a),i}}Object.defineProperty(e,`$${r}`,{get(){return n(t,o())},enumerable:!1})}),e}function er(e,t,r,...n){try{return r(...n)}catch(i){X(i,e,t)}}function X(e,t,r=void 0){Object.assign(e,{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message}
2 |
3 | ${r?'Expression: "'+r+`"
4 |
5 | `:""}`,t),setTimeout(()=>{throw e},0)}var Ae=!0;function Oe(e){let t=Ae;Ae=!1;let r=e();return Ae=t,r}function R(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return tr(...e)}var tr=dt;function rr(e){tr=e}function dt(e,t){let r={};ce(r,e);let n=[r,...L(e)],i=typeof t=="function"?Rn(n,t):Mn(n,t,e);return er.bind(null,e,t,i)}function Rn(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(F([n,...e]),i);Te(r,o)}}var pt={};function Nn(e,t){if(pt[e])return pt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(async()=>{ ${e} })()`:e,o=(()=>{try{return new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`)}catch(s){return X(s,t,e),Promise.resolve()}})();return pt[e]=o,o}function Mn(e,t,r){let n=Nn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=F([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>X(l,r,t));n.finished?(Te(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Te(i,l,a,s,r)}).catch(l=>X(l,r,t)).finally(()=>n.result=void 0)}}}function Te(e,t,r,n,i){if(Ae&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Te(e,s,r,n)).catch(s=>X(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var mt="x-";function O(e=""){return mt+e}function nr(e){mt=e}var ht={};function p(e,t){return ht[e]=t,{before(r){if(!ht[r]){console.warn("Cannot find directive `${directive}`. `${name}` will use the default order of execution");return}let n=H.indexOf(r);H.splice(n>=0?n:H.indexOf("DEFAULT"),0,e)}}}function le(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=_t(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(ir((o,s)=>n[o]=s)).filter(or).map(In(n,r)).sort(Dn).map(o=>Pn(e,o))}function _t(e){return Array.from(e).map(ir()).filter(t=>!or(t))}var gt=!1,ue=new Map,sr=Symbol();function ar(e){gt=!0;let t=Symbol();sr=t,ue.set(t,[]);let r=()=>{for(;ue.get(t).length;)ue.get(t).shift()();ue.delete(t)},n=()=>{gt=!1,r()};e(r),n()}function ft(e){let t=[],r=a=>t.push(a),[n,i]=Ht(e);return t.push(i),[{Alpine:j,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:R.bind(R,e)},()=>t.forEach(a=>a())]}function Pn(e,t){let r=()=>{},n=ht[t.type]||r,[i,o]=ft(e);Yt(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),gt?ue.get(sr).push(n):n())};return s.runCleanups=o,s}var Ce=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Re=e=>e;function ir(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=cr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var cr=[];function Z(e){cr.push(e)}function or({name:e}){return lr().test(e)}var lr=()=>new RegExp(`^${mt}([^:^.]+)\\b`);function In(e,t){return({name:r,value:n})=>{let i=r.match(lr()),o=r.match(/:([a-zA-Z0-9\-:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var xt="DEFAULT",H=["ignore","ref","data","id","bind","init","for","model","modelable","transition","show","if",xt,"teleport"];function Dn(e,t){let r=H.indexOf(e.type)===-1?xt:e.type,n=H.indexOf(t.type)===-1?xt:t.type;return H.indexOf(r)-H.indexOf(n)}function q(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function T(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>T(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)T(n,t,!1),n=n.nextElementSibling}function S(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var ur=!1;function dr(){ur&&S("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),ur=!0,document.body||S("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 | {% block additional_head_links %}{% endblock additional_head_links %}
24 | {% block additional_css %}{% endblock additional_css %}
25 |
26 |
31 |
150 |
151 | {% block content %}
152 | {% endblock content %}
153 |
154 |
169 | {% include "toasts.html" %}
170 |
171 |
174 |
175 |
176 |
224 |
225 | {% block additional_javascript %}{% endblock additional_javascript %}
226 |
227 |