├── .gitignore ├── .prettierrc ├── README.md ├── app ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── config ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── dashboard.png ├── landing.png ├── manage.py ├── package-lock.json ├── package.json ├── requirements.txt ├── static ├── branding │ ├── 1.png │ └── 2.png ├── css │ ├── dashboard │ │ └── custom.css │ ├── output.css │ └── public │ │ └── custom.css └── libraries │ └── tailwind │ └── input.css ├── tailwind.config.js ├── templates ├── account │ ├── _layout.html │ ├── email │ │ ├── email_confirmation_message.txt │ │ └── email_confirmation_subject.txt │ ├── email_confirm.html │ ├── email_confirmed.html │ ├── login.html │ ├── password_change.html │ ├── password_reset.html │ ├── password_reset_confirm.html │ ├── password_reset_done.html │ ├── password_reset_from_key.html │ ├── password_reset_from_key_done.html │ └── signup.html ├── dashboard.html ├── index.html └── layout │ ├── dashboard │ ├── _breadcrumb.html │ ├── _footer.html │ ├── _header.html │ ├── _sidenav.html │ └── layout.html │ └── public │ ├── _footer.html │ ├── _header.html │ └── layout.html └── users ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── managers.py ├── migrations ├── 0001_initial.py ├── 0002_user_is_custom_admin.py ├── 0003_userprofile.py ├── 0004_rename_userprofile_enduserprofile_and_more.py ├── 0005_enduser_staff_alter_user_options_user_gender_and_more.py └── __init__.py ├── models.py ├── signals.py ├── tests.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/django,node,visualstudiocode,macos,virtualenv,python,windows,linux 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=django,node,visualstudiocode,macos,virtualenv,python,windows,linux 3 | 4 | ### Django ### 5 | *.log 6 | *.pot 7 | *.pyc 8 | __pycache__/ 9 | local_settings.py 10 | db.sqlite3 11 | db.sqlite3-journal 12 | media 13 | 14 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 15 | # in your Git repository. Update and uncomment the following line accordingly. 16 | # /staticfiles/ 17 | 18 | ### Django.Python Stack ### 19 | # Byte-compiled / optimized / DLL files 20 | *.py[cod] 21 | *$py.class 22 | 23 | # C extensions 24 | *.so 25 | 26 | # Distribution / packaging 27 | .Python 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | share/python-wheels/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | MANIFEST 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .nox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage.xml 65 | *.cover 66 | *.py,cover 67 | .hypothesis/ 68 | .pytest_cache/ 69 | cover/ 70 | 71 | # Translations 72 | *.mo 73 | 74 | # Django stuff: 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | .pybuilder/ 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | # For a library or package, you might want to ignore these files since the code is 99 | # intended to run in multiple environments; otherwise, check them in: 100 | # .python-version 101 | 102 | # pipenv 103 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 104 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 105 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 106 | # install all needed dependencies. 107 | #Pipfile.lock 108 | 109 | # poetry 110 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 111 | # This is especially recommended for binary packages to ensure reproducibility, and is more 112 | # commonly ignored for libraries. 113 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 114 | #poetry.lock 115 | 116 | # pdm 117 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 118 | #pdm.lock 119 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 120 | # in version control. 121 | # https://pdm.fming.dev/#use-with-ide 122 | .pdm.toml 123 | 124 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 125 | __pypackages__/ 126 | 127 | # Celery stuff 128 | celerybeat-schedule 129 | celerybeat.pid 130 | 131 | # SageMath parsed files 132 | *.sage.py 133 | 134 | # Environments 135 | .env 136 | .venv 137 | env/ 138 | venv/ 139 | ENV/ 140 | env.bak/ 141 | venv.bak/ 142 | 143 | # Spyder project settings 144 | .spyderproject 145 | .spyproject 146 | 147 | # Rope project settings 148 | .ropeproject 149 | 150 | # mkdocs documentation 151 | /site 152 | 153 | # mypy 154 | .mypy_cache/ 155 | .dmypy.json 156 | dmypy.json 157 | 158 | # Pyre type checker 159 | .pyre/ 160 | 161 | # pytype static type analyzer 162 | .pytype/ 163 | 164 | # Cython debug symbols 165 | cython_debug/ 166 | 167 | # PyCharm 168 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 169 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 170 | # and can be added to the global gitignore or merged into this file. For a more nuclear 171 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 172 | #.idea/ 173 | 174 | ### Linux ### 175 | *~ 176 | 177 | # temporary files which can be created if a process still has a handle open of a deleted file 178 | .fuse_hidden* 179 | 180 | # KDE directory preferences 181 | .directory 182 | 183 | # Linux trash folder which might appear on any partition or disk 184 | .Trash-* 185 | 186 | # .nfs files are created when an open file is removed but is still being accessed 187 | .nfs* 188 | 189 | ### macOS ### 190 | # General 191 | .DS_Store 192 | .AppleDouble 193 | .LSOverride 194 | 195 | # Icon must end with two \r 196 | Icon 197 | 198 | 199 | # Thumbnails 200 | ._* 201 | 202 | # Files that might appear in the root of a volume 203 | .DocumentRevisions-V100 204 | .fseventsd 205 | .Spotlight-V100 206 | .TemporaryItems 207 | .Trashes 208 | .VolumeIcon.icns 209 | .com.apple.timemachine.donotpresent 210 | 211 | # Directories potentially created on remote AFP share 212 | .AppleDB 213 | .AppleDesktop 214 | Network Trash Folder 215 | Temporary Items 216 | .apdisk 217 | 218 | ### macOS Patch ### 219 | # iCloud generated files 220 | *.icloud 221 | 222 | ### Node ### 223 | # Logs 224 | logs 225 | npm-debug.log* 226 | yarn-debug.log* 227 | yarn-error.log* 228 | lerna-debug.log* 229 | .pnpm-debug.log* 230 | 231 | # Diagnostic reports (https://nodejs.org/api/report.html) 232 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 233 | 234 | # Runtime data 235 | pids 236 | *.pid 237 | *.seed 238 | *.pid.lock 239 | 240 | # Directory for instrumented libs generated by jscoverage/JSCover 241 | lib-cov 242 | 243 | # Coverage directory used by tools like istanbul 244 | coverage 245 | *.lcov 246 | 247 | # nyc test coverage 248 | .nyc_output 249 | 250 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 251 | .grunt 252 | 253 | # Bower dependency directory (https://bower.io/) 254 | bower_components 255 | 256 | # node-waf configuration 257 | .lock-wscript 258 | 259 | # Compiled binary addons (https://nodejs.org/api/addons.html) 260 | build/Release 261 | 262 | # Dependency directories 263 | node_modules/ 264 | jspm_packages/ 265 | 266 | # Snowpack dependency directory (https://snowpack.dev/) 267 | web_modules/ 268 | 269 | # TypeScript cache 270 | *.tsbuildinfo 271 | 272 | # Optional npm cache directory 273 | .npm 274 | 275 | # Optional eslint cache 276 | .eslintcache 277 | 278 | # Optional stylelint cache 279 | .stylelintcache 280 | 281 | # Microbundle cache 282 | .rpt2_cache/ 283 | .rts2_cache_cjs/ 284 | .rts2_cache_es/ 285 | .rts2_cache_umd/ 286 | 287 | # Optional REPL history 288 | .node_repl_history 289 | 290 | # Output of 'npm pack' 291 | *.tgz 292 | 293 | # Yarn Integrity file 294 | .yarn-integrity 295 | 296 | # dotenv environment variable files 297 | .env.development.local 298 | .env.test.local 299 | .env.production.local 300 | .env.local 301 | 302 | # parcel-bundler cache (https://parceljs.org/) 303 | .parcel-cache 304 | 305 | # Next.js build output 306 | .next 307 | out 308 | 309 | # Nuxt.js build / generate output 310 | .nuxt 311 | dist 312 | 313 | # Gatsby files 314 | .cache/ 315 | # Comment in the public line in if your project uses Gatsby and not Next.js 316 | # https://nextjs.org/blog/next-9-1#public-directory-support 317 | # public 318 | 319 | # vuepress build output 320 | .vuepress/dist 321 | 322 | # vuepress v2.x temp and cache directory 323 | .temp 324 | 325 | # Docusaurus cache and generated files 326 | .docusaurus 327 | 328 | # Serverless directories 329 | .serverless/ 330 | 331 | # FuseBox cache 332 | .fusebox/ 333 | 334 | # DynamoDB Local files 335 | .dynamodb/ 336 | 337 | # TernJS port file 338 | .tern-port 339 | 340 | # Stores VSCode versions used for testing VSCode extensions 341 | .vscode-test 342 | 343 | # yarn v2 344 | .yarn/cache 345 | .yarn/unplugged 346 | .yarn/build-state.yml 347 | .yarn/install-state.gz 348 | .pnp.* 349 | 350 | ### Node Patch ### 351 | # Serverless Webpack directories 352 | .webpack/ 353 | 354 | # Optional stylelint cache 355 | 356 | # SvelteKit build / generate output 357 | .svelte-kit 358 | 359 | ### Python ### 360 | # Byte-compiled / optimized / DLL files 361 | 362 | # C extensions 363 | 364 | # Distribution / packaging 365 | 366 | # PyInstaller 367 | # Usually these files are written by a python script from a template 368 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 369 | 370 | # Installer logs 371 | 372 | # Unit test / coverage reports 373 | 374 | # Translations 375 | 376 | # Django stuff: 377 | 378 | # Flask stuff: 379 | 380 | # Scrapy stuff: 381 | 382 | # Sphinx documentation 383 | 384 | # PyBuilder 385 | 386 | # Jupyter Notebook 387 | 388 | # IPython 389 | 390 | # pyenv 391 | # For a library or package, you might want to ignore these files since the code is 392 | # intended to run in multiple environments; otherwise, check them in: 393 | # .python-version 394 | 395 | # pipenv 396 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 397 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 398 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 399 | # install all needed dependencies. 400 | 401 | # poetry 402 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 403 | # This is especially recommended for binary packages to ensure reproducibility, and is more 404 | # commonly ignored for libraries. 405 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 406 | 407 | # pdm 408 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 409 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 410 | # in version control. 411 | # https://pdm.fming.dev/#use-with-ide 412 | 413 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 414 | 415 | # Celery stuff 416 | 417 | # SageMath parsed files 418 | 419 | # Environments 420 | 421 | # Spyder project settings 422 | 423 | # Rope project settings 424 | 425 | # mkdocs documentation 426 | 427 | # mypy 428 | 429 | # Pyre type checker 430 | 431 | # pytype static type analyzer 432 | 433 | # Cython debug symbols 434 | 435 | # PyCharm 436 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 437 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 438 | # and can be added to the global gitignore or merged into this file. For a more nuclear 439 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 440 | 441 | ### Python Patch ### 442 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 443 | poetry.toml 444 | 445 | # ruff 446 | .ruff_cache/ 447 | 448 | # LSP config files 449 | pyrightconfig.json 450 | 451 | ### VirtualEnv ### 452 | # Virtualenv 453 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 454 | [Bb]in 455 | [Ii]nclude 456 | [Ll]ib 457 | [Ll]ib64 458 | [Ll]ocal 459 | [Ss]cripts 460 | pyvenv.cfg 461 | pip-selfcheck.json 462 | 463 | ### VisualStudioCode ### 464 | .vscode/* 465 | !.vscode/settings.json 466 | !.vscode/tasks.json 467 | !.vscode/launch.json 468 | !.vscode/extensions.json 469 | !.vscode/*.code-snippets 470 | 471 | # Local History for Visual Studio Code 472 | .history/ 473 | 474 | # Built Visual Studio Code Extensions 475 | *.vsix 476 | 477 | ### VisualStudioCode Patch ### 478 | # Ignore all local history of files 479 | .history 480 | .ionide 481 | 482 | ### Windows ### 483 | # Windows thumbnail cache files 484 | Thumbs.db 485 | Thumbs.db:encryptable 486 | ehthumbs.db 487 | ehthumbs_vista.db 488 | 489 | # Dump file 490 | *.stackdump 491 | 492 | # Folder config file 493 | [Dd]esktop.ini 494 | 495 | # Recycle Bin used on file shares 496 | $RECYCLE.BIN/ 497 | 498 | # Windows Installer files 499 | *.cab 500 | *.msi 501 | *.msix 502 | *.msm 503 | *.msp 504 | 505 | # Windows shortcuts 506 | *.lnk 507 | 508 | # End of https://www.toptal.com/developers/gitignore/api/django,node,visualstudiocode,macos,virtualenv,python,windows,linux 509 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "prettier-plugin-tailwindcss" 4 | ] 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Starter 2 | 3 | ![landingpage](./landing.png) 4 | ![dashboardpage](./dashboard.png) 5 | 6 | 7 | ## Resources Used 8 | 9 | https://learndjango.com/tutorials/django-custom-user-model#custom-user-model 10 | 11 | https://learndjango.com/tutorials/django-allauth-tutorial#django-allauth 12 | 13 | https://github.com/django-crispy-forms/crispy-tailwind?tab=readme-ov-file 14 | 15 | 16 | ## Resources thinking 17 | 18 | https://resend.com/python 19 | 20 | https://uploadthing.com/ 21 | 22 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisDevCode-Technologies/django-starter/51b6655bda5cc5539c716ee7945d2b8bd161187d/app/__init__.py -------------------------------------------------------------------------------- /app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AppConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'app' 7 | -------------------------------------------------------------------------------- /app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisDevCode-Technologies/django-starter/51b6655bda5cc5539c716ee7945d2b8bd161187d/app/migrations/__init__.py -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /app/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import landing_page, dashboard_landing_page 3 | 4 | app_name = 'app' 5 | urlpatterns = [ 6 | path('', landing_page, name="landing_page"), 7 | path('dashboard/', dashboard_landing_page, name="dashboard_landing_page"), 8 | ] 9 | -------------------------------------------------------------------------------- /app/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.contrib.auth.decorators import login_required 3 | 4 | def landing_page(request): 5 | return render(request, "index.html") 6 | 7 | # protected view 8 | @login_required 9 | def dashboard_landing_page(request): 10 | return render(request, "dashboard.html") -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisDevCode-Technologies/django-starter/51b6655bda5cc5539c716ee7945d2b8bd161187d/config/__init__.py -------------------------------------------------------------------------------- /config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for config 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/5.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for config project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.1.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.1/ref/settings/ 11 | """ 12 | import os 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-9g4_@r*l&bw(ngafy$%q4u@zvm(t))-i-f^99h@kk3i6zuo(+s' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | "app", 42 | "users.apps.UsersConfig", 43 | 44 | "allauth", 45 | "allauth.account", 46 | "crispy_forms", 47 | "crispy_tailwind", 48 | ] 49 | 50 | MIDDLEWARE = [ 51 | 'django.middleware.security.SecurityMiddleware', 52 | 'django.contrib.sessions.middleware.SessionMiddleware', 53 | 'django.middleware.common.CommonMiddleware', 54 | 'django.middleware.csrf.CsrfViewMiddleware', 55 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 56 | 'django.contrib.messages.middleware.MessageMiddleware', 57 | 'allauth.account.middleware.AccountMiddleware', 58 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 59 | ] 60 | 61 | ROOT_URLCONF = 'config.urls' 62 | 63 | TEMPLATES = [ 64 | { 65 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 66 | 'DIRS': [os.path.join(BASE_DIR, "templates")], 67 | 'APP_DIRS': True, 68 | 'OPTIONS': { 69 | 'context_processors': [ 70 | 'django.template.context_processors.debug', 71 | 'django.template.context_processors.request', 72 | 'django.contrib.auth.context_processors.auth', 73 | 'django.contrib.messages.context_processors.messages', 74 | ], 75 | }, 76 | }, 77 | ] 78 | 79 | WSGI_APPLICATION = 'config.wsgi.application' 80 | 81 | 82 | # Database 83 | # https://docs.djangoproject.com/en/5.1/ref/settings/#databases 84 | 85 | DATABASES = { 86 | 'default': { 87 | 'ENGINE': 'django.db.backends.sqlite3', 88 | 'NAME': BASE_DIR / 'db.sqlite3', 89 | } 90 | } 91 | 92 | 93 | # Password validation 94 | # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators 95 | 96 | AUTH_PASSWORD_VALIDATORS = [ 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 108 | }, 109 | ] 110 | 111 | AUTH_USER_MODEL = "users.User" 112 | ACCOUNT_USER_MODEL_USERNAME_FIELD = None 113 | # Username is not required 114 | ACCOUNT_USERNAME_REQUIRED = False 115 | # Use email for authentication 116 | ACCOUNT_AUTHENTICATION_METHOD = "email" 117 | ACCOUNT_EMAIL_REQUIRED = True 118 | ACCOUNT_EMAIL_VERIFICATION = "mandatory" 119 | AUTHENTICATION_BACKENDS = [ 120 | # Needed to login by username in Django admin, regardless of `allauth` 121 | 'django.contrib.auth.backends.ModelBackend', 122 | # `allauth` specific authentication methods, such as login by email 123 | 'allauth.account.auth_backends.AuthenticationBackend', 124 | ] 125 | 126 | SITE_ID = 1 127 | 128 | # https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url 129 | LOGIN_REDIRECT_URL = "app:dashboard_landing_page" 130 | 131 | # https://django-allauth.readthedocs.io/en/latest/views.html#logout-account-logout 132 | ACCOUNT_LOGOUT_REDIRECT_URL = "app:landing_page" 133 | 134 | 135 | #email configs 136 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 137 | 138 | # Internationalization 139 | # https://docs.djangoproject.com/en/5.1/topics/i18n/ 140 | 141 | LANGUAGE_CODE = 'en-us' 142 | TIME_ZONE = 'Africa/Nairobi' 143 | USE_I18N = True 144 | USE_TZ = True 145 | 146 | 147 | # Static files (CSS, JavaScript, Images) 148 | # https://docs.djangoproject.com/en/5.1/howto/static-files/ 149 | 150 | STATIC_URL = 'static/' 151 | STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] 152 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") 153 | 154 | # media 155 | MEDIA_URL = "/media/" 156 | MEDIA_ROOT = os.path.join(BASE_DIR, "media") 157 | 158 | # Crispy configs 159 | CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind" 160 | CRISPY_TEMPLATE_PACK = "tailwind" 161 | 162 | # Default primary key field type 163 | # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field 164 | 165 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 166 | -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.conf import settings 3 | from django.conf.urls.static import static 4 | from django.urls import path, include 5 | 6 | urlpatterns = [ 7 | path('', include('app.urls')), 8 | path('admin/', admin.site.urls), 9 | path('accounts/', include('allauth.urls')), 10 | ] 11 | 12 | if settings.DEBUG: 13 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 14 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 15 | 16 | 17 | SITE_NAME = "Django Stater Template" 18 | admin.site.site_header = SITE_NAME 19 | admin.site.index_title = SITE_NAME + "Dashboard" 20 | admin.site.site_title = SITE_NAME + "Dashboard" -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config 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/5.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisDevCode-Technologies/django-starter/51b6655bda5cc5539c716ee7945d2b8bd161187d/dashboard.png -------------------------------------------------------------------------------- /landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisDevCode-Technologies/django-starter/51b6655bda5cc5539c716ee7945d2b8bd161187d/landing.png -------------------------------------------------------------------------------- /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', 'config.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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "tailwindcss -i static/libraries/tailwind/input.css -o static/css/output.css --watch", 4 | "build": "tailwindcss -i static/libraries/tailwind/input.css -o static/css/output.css" 5 | }, 6 | "devDependencies": { 7 | "prettier": "^3.4.2", 8 | "prettier-plugin-tailwindcss": "^0.6.9", 9 | "tailwindcss": "^3.4.17" 10 | }, 11 | "dependencies": { 12 | "preline": "^2.6.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.8.1 2 | crispy-tailwind==1.0.3 3 | Django==5.1.4 4 | django-allauth==65.3.1 5 | django-crispy-forms==2.3 6 | sqlparse==0.5.3 7 | -------------------------------------------------------------------------------- /static/branding/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisDevCode-Technologies/django-starter/51b6655bda5cc5539c716ee7945d2b8bd161187d/static/branding/1.png -------------------------------------------------------------------------------- /static/branding/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisDevCode-Technologies/django-starter/51b6655bda5cc5539c716ee7945d2b8bd161187d/static/branding/2.png -------------------------------------------------------------------------------- /static/css/dashboard/custom.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisDevCode-Technologies/django-starter/51b6655bda5cc5539c716ee7945d2b8bd161187d/static/css/dashboard/custom.css -------------------------------------------------------------------------------- /static/css/output.css: -------------------------------------------------------------------------------- 1 | *, ::before, ::after { 2 | --tw-border-spacing-x: 0; 3 | --tw-border-spacing-y: 0; 4 | --tw-translate-x: 0; 5 | --tw-translate-y: 0; 6 | --tw-rotate: 0; 7 | --tw-skew-x: 0; 8 | --tw-skew-y: 0; 9 | --tw-scale-x: 1; 10 | --tw-scale-y: 1; 11 | --tw-pan-x: ; 12 | --tw-pan-y: ; 13 | --tw-pinch-zoom: ; 14 | --tw-scroll-snap-strictness: proximity; 15 | --tw-gradient-from-position: ; 16 | --tw-gradient-via-position: ; 17 | --tw-gradient-to-position: ; 18 | --tw-ordinal: ; 19 | --tw-slashed-zero: ; 20 | --tw-numeric-figure: ; 21 | --tw-numeric-spacing: ; 22 | --tw-numeric-fraction: ; 23 | --tw-ring-inset: ; 24 | --tw-ring-offset-width: 0px; 25 | --tw-ring-offset-color: #fff; 26 | --tw-ring-color: rgb(59 130 246 / 0.5); 27 | --tw-ring-offset-shadow: 0 0 #0000; 28 | --tw-ring-shadow: 0 0 #0000; 29 | --tw-shadow: 0 0 #0000; 30 | --tw-shadow-colored: 0 0 #0000; 31 | --tw-blur: ; 32 | --tw-brightness: ; 33 | --tw-contrast: ; 34 | --tw-grayscale: ; 35 | --tw-hue-rotate: ; 36 | --tw-invert: ; 37 | --tw-saturate: ; 38 | --tw-sepia: ; 39 | --tw-drop-shadow: ; 40 | --tw-backdrop-blur: ; 41 | --tw-backdrop-brightness: ; 42 | --tw-backdrop-contrast: ; 43 | --tw-backdrop-grayscale: ; 44 | --tw-backdrop-hue-rotate: ; 45 | --tw-backdrop-invert: ; 46 | --tw-backdrop-opacity: ; 47 | --tw-backdrop-saturate: ; 48 | --tw-backdrop-sepia: ; 49 | --tw-contain-size: ; 50 | --tw-contain-layout: ; 51 | --tw-contain-paint: ; 52 | --tw-contain-style: ; 53 | } 54 | 55 | ::backdrop { 56 | --tw-border-spacing-x: 0; 57 | --tw-border-spacing-y: 0; 58 | --tw-translate-x: 0; 59 | --tw-translate-y: 0; 60 | --tw-rotate: 0; 61 | --tw-skew-x: 0; 62 | --tw-skew-y: 0; 63 | --tw-scale-x: 1; 64 | --tw-scale-y: 1; 65 | --tw-pan-x: ; 66 | --tw-pan-y: ; 67 | --tw-pinch-zoom: ; 68 | --tw-scroll-snap-strictness: proximity; 69 | --tw-gradient-from-position: ; 70 | --tw-gradient-via-position: ; 71 | --tw-gradient-to-position: ; 72 | --tw-ordinal: ; 73 | --tw-slashed-zero: ; 74 | --tw-numeric-figure: ; 75 | --tw-numeric-spacing: ; 76 | --tw-numeric-fraction: ; 77 | --tw-ring-inset: ; 78 | --tw-ring-offset-width: 0px; 79 | --tw-ring-offset-color: #fff; 80 | --tw-ring-color: rgb(59 130 246 / 0.5); 81 | --tw-ring-offset-shadow: 0 0 #0000; 82 | --tw-ring-shadow: 0 0 #0000; 83 | --tw-shadow: 0 0 #0000; 84 | --tw-shadow-colored: 0 0 #0000; 85 | --tw-blur: ; 86 | --tw-brightness: ; 87 | --tw-contrast: ; 88 | --tw-grayscale: ; 89 | --tw-hue-rotate: ; 90 | --tw-invert: ; 91 | --tw-saturate: ; 92 | --tw-sepia: ; 93 | --tw-drop-shadow: ; 94 | --tw-backdrop-blur: ; 95 | --tw-backdrop-brightness: ; 96 | --tw-backdrop-contrast: ; 97 | --tw-backdrop-grayscale: ; 98 | --tw-backdrop-hue-rotate: ; 99 | --tw-backdrop-invert: ; 100 | --tw-backdrop-opacity: ; 101 | --tw-backdrop-saturate: ; 102 | --tw-backdrop-sepia: ; 103 | --tw-contain-size: ; 104 | --tw-contain-layout: ; 105 | --tw-contain-paint: ; 106 | --tw-contain-style: ; 107 | } 108 | 109 | /* 110 | ! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com 111 | */ 112 | 113 | /* 114 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 115 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 116 | */ 117 | 118 | *, 119 | ::before, 120 | ::after { 121 | box-sizing: border-box; 122 | /* 1 */ 123 | border-width: 0; 124 | /* 2 */ 125 | border-style: solid; 126 | /* 2 */ 127 | border-color: #e5e7eb; 128 | /* 2 */ 129 | } 130 | 131 | ::before, 132 | ::after { 133 | --tw-content: ''; 134 | } 135 | 136 | /* 137 | 1. Use a consistent sensible line-height in all browsers. 138 | 2. Prevent adjustments of font size after orientation changes in iOS. 139 | 3. Use a more readable tab size. 140 | 4. Use the user's configured `sans` font-family by default. 141 | 5. Use the user's configured `sans` font-feature-settings by default. 142 | 6. Use the user's configured `sans` font-variation-settings by default. 143 | 7. Disable tap highlights on iOS 144 | */ 145 | 146 | html, 147 | :host { 148 | line-height: 1.5; 149 | /* 1 */ 150 | -webkit-text-size-adjust: 100%; 151 | /* 2 */ 152 | -moz-tab-size: 4; 153 | /* 3 */ 154 | -o-tab-size: 4; 155 | tab-size: 4; 156 | /* 3 */ 157 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 158 | /* 4 */ 159 | font-feature-settings: normal; 160 | /* 5 */ 161 | font-variation-settings: normal; 162 | /* 6 */ 163 | -webkit-tap-highlight-color: transparent; 164 | /* 7 */ 165 | } 166 | 167 | /* 168 | 1. Remove the margin in all browsers. 169 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 170 | */ 171 | 172 | body { 173 | margin: 0; 174 | /* 1 */ 175 | line-height: inherit; 176 | /* 2 */ 177 | } 178 | 179 | /* 180 | 1. Add the correct height in Firefox. 181 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 182 | 3. Ensure horizontal rules are visible by default. 183 | */ 184 | 185 | hr { 186 | height: 0; 187 | /* 1 */ 188 | color: inherit; 189 | /* 2 */ 190 | border-top-width: 1px; 191 | /* 3 */ 192 | } 193 | 194 | /* 195 | Add the correct text decoration in Chrome, Edge, and Safari. 196 | */ 197 | 198 | abbr:where([title]) { 199 | -webkit-text-decoration: underline dotted; 200 | text-decoration: underline dotted; 201 | } 202 | 203 | /* 204 | Remove the default font size and weight for headings. 205 | */ 206 | 207 | h1, 208 | h2, 209 | h3, 210 | h4, 211 | h5, 212 | h6 { 213 | font-size: inherit; 214 | font-weight: inherit; 215 | } 216 | 217 | /* 218 | Reset links to optimize for opt-in styling instead of opt-out. 219 | */ 220 | 221 | a { 222 | color: inherit; 223 | text-decoration: inherit; 224 | } 225 | 226 | /* 227 | Add the correct font weight in Edge and Safari. 228 | */ 229 | 230 | b, 231 | strong { 232 | font-weight: bolder; 233 | } 234 | 235 | /* 236 | 1. Use the user's configured `mono` font-family by default. 237 | 2. Use the user's configured `mono` font-feature-settings by default. 238 | 3. Use the user's configured `mono` font-variation-settings by default. 239 | 4. Correct the odd `em` font sizing in all browsers. 240 | */ 241 | 242 | code, 243 | kbd, 244 | samp, 245 | pre { 246 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 247 | /* 1 */ 248 | font-feature-settings: normal; 249 | /* 2 */ 250 | font-variation-settings: normal; 251 | /* 3 */ 252 | font-size: 1em; 253 | /* 4 */ 254 | } 255 | 256 | /* 257 | Add the correct font size in all browsers. 258 | */ 259 | 260 | small { 261 | font-size: 80%; 262 | } 263 | 264 | /* 265 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 266 | */ 267 | 268 | sub, 269 | sup { 270 | font-size: 75%; 271 | line-height: 0; 272 | position: relative; 273 | vertical-align: baseline; 274 | } 275 | 276 | sub { 277 | bottom: -0.25em; 278 | } 279 | 280 | sup { 281 | top: -0.5em; 282 | } 283 | 284 | /* 285 | 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) 286 | 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) 287 | 3. Remove gaps between table borders by default. 288 | */ 289 | 290 | table { 291 | text-indent: 0; 292 | /* 1 */ 293 | border-color: inherit; 294 | /* 2 */ 295 | border-collapse: collapse; 296 | /* 3 */ 297 | } 298 | 299 | /* 300 | 1. Change the font styles in all browsers. 301 | 2. Remove the margin in Firefox and Safari. 302 | 3. Remove default padding in all browsers. 303 | */ 304 | 305 | button, 306 | input, 307 | optgroup, 308 | select, 309 | textarea { 310 | font-family: inherit; 311 | /* 1 */ 312 | font-feature-settings: inherit; 313 | /* 1 */ 314 | font-variation-settings: inherit; 315 | /* 1 */ 316 | font-size: 100%; 317 | /* 1 */ 318 | font-weight: inherit; 319 | /* 1 */ 320 | line-height: inherit; 321 | /* 1 */ 322 | letter-spacing: inherit; 323 | /* 1 */ 324 | color: inherit; 325 | /* 1 */ 326 | margin: 0; 327 | /* 2 */ 328 | padding: 0; 329 | /* 3 */ 330 | } 331 | 332 | /* 333 | Remove the inheritance of text transform in Edge and Firefox. 334 | */ 335 | 336 | button, 337 | select { 338 | text-transform: none; 339 | } 340 | 341 | /* 342 | 1. Correct the inability to style clickable types in iOS and Safari. 343 | 2. Remove default button styles. 344 | */ 345 | 346 | button, 347 | input:where([type='button']), 348 | input:where([type='reset']), 349 | input:where([type='submit']) { 350 | -webkit-appearance: button; 351 | /* 1 */ 352 | background-color: transparent; 353 | /* 2 */ 354 | background-image: none; 355 | /* 2 */ 356 | } 357 | 358 | /* 359 | Use the modern Firefox focus style for all focusable elements. 360 | */ 361 | 362 | :-moz-focusring { 363 | outline: auto; 364 | } 365 | 366 | /* 367 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 368 | */ 369 | 370 | :-moz-ui-invalid { 371 | box-shadow: none; 372 | } 373 | 374 | /* 375 | Add the correct vertical alignment in Chrome and Firefox. 376 | */ 377 | 378 | progress { 379 | vertical-align: baseline; 380 | } 381 | 382 | /* 383 | Correct the cursor style of increment and decrement buttons in Safari. 384 | */ 385 | 386 | ::-webkit-inner-spin-button, 387 | ::-webkit-outer-spin-button { 388 | height: auto; 389 | } 390 | 391 | /* 392 | 1. Correct the odd appearance in Chrome and Safari. 393 | 2. Correct the outline style in Safari. 394 | */ 395 | 396 | [type='search'] { 397 | -webkit-appearance: textfield; 398 | /* 1 */ 399 | outline-offset: -2px; 400 | /* 2 */ 401 | } 402 | 403 | /* 404 | Remove the inner padding in Chrome and Safari on macOS. 405 | */ 406 | 407 | ::-webkit-search-decoration { 408 | -webkit-appearance: none; 409 | } 410 | 411 | /* 412 | 1. Correct the inability to style clickable types in iOS and Safari. 413 | 2. Change font properties to `inherit` in Safari. 414 | */ 415 | 416 | ::-webkit-file-upload-button { 417 | -webkit-appearance: button; 418 | /* 1 */ 419 | font: inherit; 420 | /* 2 */ 421 | } 422 | 423 | /* 424 | Add the correct display in Chrome and Safari. 425 | */ 426 | 427 | summary { 428 | display: list-item; 429 | } 430 | 431 | /* 432 | Removes the default spacing and border for appropriate elements. 433 | */ 434 | 435 | blockquote, 436 | dl, 437 | dd, 438 | h1, 439 | h2, 440 | h3, 441 | h4, 442 | h5, 443 | h6, 444 | hr, 445 | figure, 446 | p, 447 | pre { 448 | margin: 0; 449 | } 450 | 451 | fieldset { 452 | margin: 0; 453 | padding: 0; 454 | } 455 | 456 | legend { 457 | padding: 0; 458 | } 459 | 460 | ol, 461 | ul, 462 | menu { 463 | list-style: none; 464 | margin: 0; 465 | padding: 0; 466 | } 467 | 468 | /* 469 | Reset default styling for dialogs. 470 | */ 471 | 472 | dialog { 473 | padding: 0; 474 | } 475 | 476 | /* 477 | Prevent resizing textareas horizontally by default. 478 | */ 479 | 480 | textarea { 481 | resize: vertical; 482 | } 483 | 484 | /* 485 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 486 | 2. Set the default placeholder color to the user's configured gray 400 color. 487 | */ 488 | 489 | input::-moz-placeholder, textarea::-moz-placeholder { 490 | opacity: 1; 491 | /* 1 */ 492 | color: #9ca3af; 493 | /* 2 */ 494 | } 495 | 496 | input::placeholder, 497 | textarea::placeholder { 498 | opacity: 1; 499 | /* 1 */ 500 | color: #9ca3af; 501 | /* 2 */ 502 | } 503 | 504 | /* 505 | Set the default cursor for buttons. 506 | */ 507 | 508 | button, 509 | [role="button"] { 510 | cursor: pointer; 511 | } 512 | 513 | /* 514 | Make sure disabled buttons don't get the pointer cursor. 515 | */ 516 | 517 | :disabled { 518 | cursor: default; 519 | } 520 | 521 | /* 522 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 523 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 524 | This can trigger a poorly considered lint error in some tools but is included by design. 525 | */ 526 | 527 | img, 528 | svg, 529 | video, 530 | canvas, 531 | audio, 532 | iframe, 533 | embed, 534 | object { 535 | display: block; 536 | /* 1 */ 537 | vertical-align: middle; 538 | /* 2 */ 539 | } 540 | 541 | /* 542 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 543 | */ 544 | 545 | img, 546 | video { 547 | max-width: 100%; 548 | height: auto; 549 | } 550 | 551 | /* Make elements with the HTML hidden attribute stay hidden by default */ 552 | 553 | [hidden]:where(:not([hidden="until-found"])) { 554 | display: none; 555 | } 556 | 557 | .sr-only { 558 | position: absolute; 559 | width: 1px; 560 | height: 1px; 561 | padding: 0; 562 | margin: -1px; 563 | overflow: hidden; 564 | clip: rect(0, 0, 0, 0); 565 | white-space: nowrap; 566 | border-width: 0; 567 | } 568 | 569 | .pointer-events-none { 570 | pointer-events: none; 571 | } 572 | 573 | .invisible { 574 | visibility: hidden; 575 | } 576 | 577 | .static { 578 | position: static; 579 | } 580 | 581 | .fixed { 582 | position: fixed; 583 | } 584 | 585 | .absolute { 586 | position: absolute; 587 | } 588 | 589 | .relative { 590 | position: relative; 591 | } 592 | 593 | .sticky { 594 | position: sticky; 595 | } 596 | 597 | .inset-x-0 { 598 | left: 0px; 599 | right: 0px; 600 | } 601 | 602 | .inset-y-0 { 603 | top: 0px; 604 | bottom: 0px; 605 | } 606 | 607 | .end-0 { 608 | inset-inline-end: 0px; 609 | } 610 | 611 | .start-0 { 612 | inset-inline-start: 0px; 613 | } 614 | 615 | .top-0 { 616 | top: 0px; 617 | } 618 | 619 | .z-10 { 620 | z-index: 10; 621 | } 622 | 623 | .z-20 { 624 | z-index: 20; 625 | } 626 | 627 | .z-50 { 628 | z-index: 50; 629 | } 630 | 631 | .z-\[48\] { 632 | z-index: 48; 633 | } 634 | 635 | .z-\[60\] { 636 | z-index: 60; 637 | } 638 | 639 | .-m-1\.5 { 640 | margin: -0.375rem; 641 | } 642 | 643 | .mx-1 { 644 | margin-left: 0.25rem; 645 | margin-right: 0.25rem; 646 | } 647 | 648 | .mx-3 { 649 | margin-left: 0.75rem; 650 | margin-right: 0.75rem; 651 | } 652 | 653 | .mx-auto { 654 | margin-left: auto; 655 | margin-right: auto; 656 | } 657 | 658 | .mb-6 { 659 | margin-bottom: 1.5rem; 660 | } 661 | 662 | .me-5 { 663 | margin-inline-end: 1.25rem; 664 | } 665 | 666 | .ms-2 { 667 | margin-inline-start: 0.5rem; 668 | } 669 | 670 | .ms-3 { 671 | margin-inline-start: 0.75rem; 672 | } 673 | 674 | .ms-auto { 675 | margin-inline-start: auto; 676 | } 677 | 678 | .mt-0\.5 { 679 | margin-top: 0.125rem; 680 | } 681 | 682 | .mt-1 { 683 | margin-top: 0.25rem; 684 | } 685 | 686 | .mt-2 { 687 | margin-top: 0.5rem; 688 | } 689 | 690 | .mt-4 { 691 | margin-top: 1rem; 692 | } 693 | 694 | .mt-5 { 695 | margin-top: 1.25rem; 696 | } 697 | 698 | .mt-6 { 699 | margin-top: 1.5rem; 700 | } 701 | 702 | .mt-8 { 703 | margin-top: 2rem; 704 | } 705 | 706 | .mt-auto { 707 | margin-top: auto; 708 | } 709 | 710 | .block { 711 | display: block; 712 | } 713 | 714 | .inline-block { 715 | display: inline-block; 716 | } 717 | 718 | .flex { 719 | display: flex; 720 | } 721 | 722 | .inline-flex { 723 | display: inline-flex; 724 | } 725 | 726 | .table { 727 | display: table; 728 | } 729 | 730 | .grid { 731 | display: grid; 732 | } 733 | 734 | .hidden { 735 | display: none; 736 | } 737 | 738 | .size-2\.5 { 739 | width: 0.625rem; 740 | height: 0.625rem; 741 | } 742 | 743 | .size-3 { 744 | width: 0.75rem; 745 | height: 0.75rem; 746 | } 747 | 748 | .size-3\.5 { 749 | width: 0.875rem; 750 | height: 0.875rem; 751 | } 752 | 753 | .size-4 { 754 | width: 1rem; 755 | height: 1rem; 756 | } 757 | 758 | .size-5 { 759 | width: 1.25rem; 760 | height: 1.25rem; 761 | } 762 | 763 | .size-6 { 764 | width: 1.5rem; 765 | height: 1.5rem; 766 | } 767 | 768 | .size-7 { 769 | width: 1.75rem; 770 | height: 1.75rem; 771 | } 772 | 773 | .size-8 { 774 | width: 2rem; 775 | height: 2rem; 776 | } 777 | 778 | .size-\[38px\] { 779 | width: 38px; 780 | height: 38px; 781 | } 782 | 783 | .size-px { 784 | width: 1px; 785 | height: 1px; 786 | } 787 | 788 | .h-1\.5 { 789 | height: 0.375rem; 790 | } 791 | 792 | .h-auto { 793 | height: auto; 794 | } 795 | 796 | .h-full { 797 | height: 100%; 798 | } 799 | 800 | .h-px { 801 | height: 1px; 802 | } 803 | 804 | .max-h-full { 805 | max-height: 100%; 806 | } 807 | 808 | .min-h-\[410px\] { 809 | min-height: 410px; 810 | } 811 | 812 | .min-h-screen { 813 | min-height: 100vh; 814 | } 815 | 816 | .w-28 { 817 | width: 7rem; 818 | } 819 | 820 | .w-72 { 821 | width: 18rem; 822 | } 823 | 824 | .w-\[260px\] { 825 | width: 260px; 826 | } 827 | 828 | .w-full { 829 | width: 100%; 830 | } 831 | 832 | .min-w-28 { 833 | min-width: 7rem; 834 | } 835 | 836 | .min-w-60 { 837 | min-width: 15rem; 838 | } 839 | 840 | .min-w-full { 841 | min-width: 100%; 842 | } 843 | 844 | .max-w-2xl { 845 | max-width: 42rem; 846 | } 847 | 848 | .max-w-3xl { 849 | max-width: 48rem; 850 | } 851 | 852 | .max-w-7xl { 853 | max-width: 80rem; 854 | } 855 | 856 | .max-w-\[85rem\] { 857 | max-width: 85rem; 858 | } 859 | 860 | .max-w-md { 861 | max-width: 28rem; 862 | } 863 | 864 | .flex-none { 865 | flex: none; 866 | } 867 | 868 | .flex-shrink-0 { 869 | flex-shrink: 0; 870 | } 871 | 872 | .shrink-0 { 873 | flex-shrink: 0; 874 | } 875 | 876 | .grow { 877 | flex-grow: 1; 878 | } 879 | 880 | .basis-full { 881 | flex-basis: 100%; 882 | } 883 | 884 | .-translate-x-full { 885 | --tw-translate-x: -100%; 886 | 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)); 887 | } 888 | 889 | .transform { 890 | 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)); 891 | } 892 | 893 | .grid-cols-1 { 894 | grid-template-columns: repeat(1, minmax(0, 1fr)); 895 | } 896 | 897 | .flex-row { 898 | flex-direction: row; 899 | } 900 | 901 | .flex-col { 902 | flex-direction: column; 903 | } 904 | 905 | .flex-wrap { 906 | flex-wrap: wrap; 907 | } 908 | 909 | .items-center { 910 | align-items: center; 911 | } 912 | 913 | .justify-end { 914 | justify-content: flex-end; 915 | } 916 | 917 | .justify-center { 918 | justify-content: center; 919 | } 920 | 921 | .justify-between { 922 | justify-content: space-between; 923 | } 924 | 925 | .gap-1 { 926 | gap: 0.25rem; 927 | } 928 | 929 | .gap-3 { 930 | gap: 0.75rem; 931 | } 932 | 933 | .gap-4 { 934 | gap: 1rem; 935 | } 936 | 937 | .gap-5 { 938 | gap: 1.25rem; 939 | } 940 | 941 | .gap-x-0 { 942 | -moz-column-gap: 0px; 943 | column-gap: 0px; 944 | } 945 | 946 | .gap-x-1 { 947 | -moz-column-gap: 0.25rem; 948 | column-gap: 0.25rem; 949 | } 950 | 951 | .gap-x-2 { 952 | -moz-column-gap: 0.5rem; 953 | column-gap: 0.5rem; 954 | } 955 | 956 | .gap-x-3 { 957 | -moz-column-gap: 0.75rem; 958 | column-gap: 0.75rem; 959 | } 960 | 961 | .gap-x-3\.5 { 962 | -moz-column-gap: 0.875rem; 963 | column-gap: 0.875rem; 964 | } 965 | 966 | .gap-y-4 { 967 | row-gap: 1rem; 968 | } 969 | 970 | .space-x-2 > :not([hidden]) ~ :not([hidden]) { 971 | --tw-space-x-reverse: 0; 972 | margin-right: calc(0.5rem * var(--tw-space-x-reverse)); 973 | margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); 974 | } 975 | 976 | .space-y-0\.5 > :not([hidden]) ~ :not([hidden]) { 977 | --tw-space-y-reverse: 0; 978 | margin-top: calc(0.125rem * calc(1 - var(--tw-space-y-reverse))); 979 | margin-bottom: calc(0.125rem * var(--tw-space-y-reverse)); 980 | } 981 | 982 | .space-y-1 > :not([hidden]) ~ :not([hidden]) { 983 | --tw-space-y-reverse: 0; 984 | margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); 985 | margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); 986 | } 987 | 988 | .space-y-4 > :not([hidden]) ~ :not([hidden]) { 989 | --tw-space-y-reverse: 0; 990 | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); 991 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); 992 | } 993 | 994 | .space-y-6 > :not([hidden]) ~ :not([hidden]) { 995 | --tw-space-y-reverse: 0; 996 | margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); 997 | margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); 998 | } 999 | 1000 | .divide-y > :not([hidden]) ~ :not([hidden]) { 1001 | --tw-divide-y-reverse: 0; 1002 | border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); 1003 | border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); 1004 | } 1005 | 1006 | .divide-gray-200 > :not([hidden]) ~ :not([hidden]) { 1007 | --tw-divide-opacity: 1; 1008 | border-color: rgb(229 231 235 / var(--tw-divide-opacity, 1)); 1009 | } 1010 | 1011 | .self-center { 1012 | align-self: center; 1013 | } 1014 | 1015 | .overflow-hidden { 1016 | overflow: hidden; 1017 | } 1018 | 1019 | .overflow-visible { 1020 | overflow: visible; 1021 | } 1022 | 1023 | .overflow-x-auto { 1024 | overflow-x: auto; 1025 | } 1026 | 1027 | .overflow-y-auto { 1028 | overflow-y: auto; 1029 | } 1030 | 1031 | .truncate { 1032 | overflow: hidden; 1033 | text-overflow: ellipsis; 1034 | white-space: nowrap; 1035 | } 1036 | 1037 | .whitespace-nowrap { 1038 | white-space: nowrap; 1039 | } 1040 | 1041 | .\!rounded-sm { 1042 | border-radius: 0.125rem !important; 1043 | } 1044 | 1045 | .rounded { 1046 | border-radius: 0.25rem; 1047 | } 1048 | 1049 | .rounded-full { 1050 | border-radius: 9999px; 1051 | } 1052 | 1053 | .rounded-lg { 1054 | border-radius: 0.5rem; 1055 | } 1056 | 1057 | .rounded-md { 1058 | border-radius: 0.375rem; 1059 | } 1060 | 1061 | .rounded-xl { 1062 | border-radius: 0.75rem; 1063 | } 1064 | 1065 | .rounded-t-lg { 1066 | border-top-left-radius: 0.5rem; 1067 | border-top-right-radius: 0.5rem; 1068 | } 1069 | 1070 | .border { 1071 | border-width: 1px; 1072 | } 1073 | 1074 | .border-y { 1075 | border-top-width: 1px; 1076 | border-bottom-width: 1px; 1077 | } 1078 | 1079 | .border-b { 1080 | border-bottom-width: 1px; 1081 | } 1082 | 1083 | .border-e { 1084 | border-inline-end-width: 1px; 1085 | } 1086 | 1087 | .border-t { 1088 | border-top-width: 1px; 1089 | } 1090 | 1091 | .border-gray-200 { 1092 | --tw-border-opacity: 1; 1093 | border-color: rgb(229 231 235 / var(--tw-border-opacity, 1)); 1094 | } 1095 | 1096 | .border-gray-300 { 1097 | --tw-border-opacity: 1; 1098 | border-color: rgb(209 213 219 / var(--tw-border-opacity, 1)); 1099 | } 1100 | 1101 | .border-transparent { 1102 | border-color: transparent; 1103 | } 1104 | 1105 | .bg-blue-500 { 1106 | --tw-bg-opacity: 1; 1107 | background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1)); 1108 | } 1109 | 1110 | .bg-blue-600 { 1111 | --tw-bg-opacity: 1; 1112 | background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1)); 1113 | } 1114 | 1115 | .bg-gray-100 { 1116 | --tw-bg-opacity: 1; 1117 | background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); 1118 | } 1119 | 1120 | .bg-gray-200 { 1121 | --tw-bg-opacity: 1; 1122 | background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1)); 1123 | } 1124 | 1125 | .bg-gray-50 { 1126 | --tw-bg-opacity: 1; 1127 | background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); 1128 | } 1129 | 1130 | .bg-gray-800 { 1131 | --tw-bg-opacity: 1; 1132 | background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1)); 1133 | } 1134 | 1135 | .bg-gray-900 { 1136 | --tw-bg-opacity: 1; 1137 | background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1)); 1138 | } 1139 | 1140 | .bg-red-100 { 1141 | --tw-bg-opacity: 1; 1142 | background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1)); 1143 | } 1144 | 1145 | .bg-teal-100 { 1146 | --tw-bg-opacity: 1; 1147 | background-color: rgb(204 251 241 / var(--tw-bg-opacity, 1)); 1148 | } 1149 | 1150 | .bg-white { 1151 | --tw-bg-opacity: 1; 1152 | background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); 1153 | } 1154 | 1155 | .bg-yellow-100 { 1156 | --tw-bg-opacity: 1; 1157 | background-color: rgb(254 249 195 / var(--tw-bg-opacity, 1)); 1158 | } 1159 | 1160 | .bg-gradient-to-tl { 1161 | background-image: linear-gradient(to top left, var(--tw-gradient-stops)); 1162 | } 1163 | 1164 | .from-blue-600 { 1165 | --tw-gradient-from: #2563eb var(--tw-gradient-from-position); 1166 | --tw-gradient-to: rgb(37 99 235 / 0) var(--tw-gradient-to-position); 1167 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); 1168 | } 1169 | 1170 | .to-violet-600 { 1171 | --tw-gradient-to: #7c3aed var(--tw-gradient-to-position); 1172 | } 1173 | 1174 | .bg-clip-text { 1175 | -webkit-background-clip: text; 1176 | background-clip: text; 1177 | } 1178 | 1179 | .p-1\.5 { 1180 | padding: 0.375rem; 1181 | } 1182 | 1183 | .p-2 { 1184 | padding: 0.5rem; 1185 | } 1186 | 1187 | .p-3 { 1188 | padding: 0.75rem; 1189 | } 1190 | 1191 | .p-4 { 1192 | padding: 1rem; 1193 | } 1194 | 1195 | .p-6 { 1196 | padding: 1.5rem; 1197 | } 1198 | 1199 | .p-8 { 1200 | padding: 2rem; 1201 | } 1202 | 1203 | .px-1\.5 { 1204 | padding-left: 0.375rem; 1205 | padding-right: 0.375rem; 1206 | } 1207 | 1208 | .px-2 { 1209 | padding-left: 0.5rem; 1210 | padding-right: 0.5rem; 1211 | } 1212 | 1213 | .px-2\.5 { 1214 | padding-left: 0.625rem; 1215 | padding-right: 0.625rem; 1216 | } 1217 | 1218 | .px-3 { 1219 | padding-left: 0.75rem; 1220 | padding-right: 0.75rem; 1221 | } 1222 | 1223 | .px-4 { 1224 | padding-left: 1rem; 1225 | padding-right: 1rem; 1226 | } 1227 | 1228 | .px-5 { 1229 | padding-left: 1.25rem; 1230 | padding-right: 1.25rem; 1231 | } 1232 | 1233 | .px-6 { 1234 | padding-left: 1.5rem; 1235 | padding-right: 1.5rem; 1236 | } 1237 | 1238 | .py-1 { 1239 | padding-top: 0.25rem; 1240 | padding-bottom: 0.25rem; 1241 | } 1242 | 1243 | .py-1\.5 { 1244 | padding-top: 0.375rem; 1245 | padding-bottom: 0.375rem; 1246 | } 1247 | 1248 | .py-10 { 1249 | padding-top: 2.5rem; 1250 | padding-bottom: 2.5rem; 1251 | } 1252 | 1253 | .py-2 { 1254 | padding-top: 0.5rem; 1255 | padding-bottom: 0.5rem; 1256 | } 1257 | 1258 | .py-2\.5 { 1259 | padding-top: 0.625rem; 1260 | padding-bottom: 0.625rem; 1261 | } 1262 | 1263 | .py-3 { 1264 | padding-top: 0.75rem; 1265 | padding-bottom: 0.75rem; 1266 | } 1267 | 1268 | .py-4 { 1269 | padding-top: 1rem; 1270 | padding-bottom: 1rem; 1271 | } 1272 | 1273 | .py-7 { 1274 | padding-top: 1.75rem; 1275 | padding-bottom: 1.75rem; 1276 | } 1277 | 1278 | .py-\[5px\] { 1279 | padding-top: 5px; 1280 | padding-bottom: 5px; 1281 | } 1282 | 1283 | .pb-10 { 1284 | padding-bottom: 2.5rem; 1285 | } 1286 | 1287 | .pe-1 { 1288 | padding-inline-end: 0.25rem; 1289 | } 1290 | 1291 | .pe-16 { 1292 | padding-inline-end: 4rem; 1293 | } 1294 | 1295 | .pe-3 { 1296 | padding-inline-end: 0.75rem; 1297 | } 1298 | 1299 | .pe-6 { 1300 | padding-inline-end: 1.5rem; 1301 | } 1302 | 1303 | .pe-8 { 1304 | padding-inline-end: 2rem; 1305 | } 1306 | 1307 | .ps-10 { 1308 | padding-inline-start: 2.5rem; 1309 | } 1310 | 1311 | .ps-3 { 1312 | padding-inline-start: 0.75rem; 1313 | } 1314 | 1315 | .ps-3\.5 { 1316 | padding-inline-start: 0.875rem; 1317 | } 1318 | 1319 | .ps-6 { 1320 | padding-inline-start: 1.5rem; 1321 | } 1322 | 1323 | .ps-8 { 1324 | padding-inline-start: 2rem; 1325 | } 1326 | 1327 | .pt-1 { 1328 | padding-top: 0.25rem; 1329 | } 1330 | 1331 | .pt-24 { 1332 | padding-top: 6rem; 1333 | } 1334 | 1335 | .pt-4 { 1336 | padding-top: 1rem; 1337 | } 1338 | 1339 | .text-center { 1340 | text-align: center; 1341 | } 1342 | 1343 | .text-start { 1344 | text-align: start; 1345 | } 1346 | 1347 | .text-end { 1348 | text-align: end; 1349 | } 1350 | 1351 | .align-middle { 1352 | vertical-align: middle; 1353 | } 1354 | 1355 | .font-mono { 1356 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 1357 | } 1358 | 1359 | .text-2xl { 1360 | font-size: 1.5rem; 1361 | line-height: 2rem; 1362 | } 1363 | 1364 | .text-4xl { 1365 | font-size: 2.25rem; 1366 | line-height: 2.5rem; 1367 | } 1368 | 1369 | .text-lg { 1370 | font-size: 1.125rem; 1371 | line-height: 1.75rem; 1372 | } 1373 | 1374 | .text-sm { 1375 | font-size: 0.875rem; 1376 | line-height: 1.25rem; 1377 | } 1378 | 1379 | .text-xl { 1380 | font-size: 1.25rem; 1381 | line-height: 1.75rem; 1382 | } 1383 | 1384 | .text-xs { 1385 | font-size: 0.75rem; 1386 | line-height: 1rem; 1387 | } 1388 | 1389 | .font-bold { 1390 | font-weight: 700; 1391 | } 1392 | 1393 | .font-medium { 1394 | font-weight: 500; 1395 | } 1396 | 1397 | .font-semibold { 1398 | font-weight: 600; 1399 | } 1400 | 1401 | .uppercase { 1402 | text-transform: uppercase; 1403 | } 1404 | 1405 | .leading-none { 1406 | line-height: 1; 1407 | } 1408 | 1409 | .tracking-wide { 1410 | letter-spacing: 0.025em; 1411 | } 1412 | 1413 | .text-black { 1414 | --tw-text-opacity: 1; 1415 | color: rgb(0 0 0 / var(--tw-text-opacity, 1)); 1416 | } 1417 | 1418 | .text-blue-500 { 1419 | --tw-text-opacity: 1; 1420 | color: rgb(59 130 246 / var(--tw-text-opacity, 1)); 1421 | } 1422 | 1423 | .text-blue-600 { 1424 | --tw-text-opacity: 1; 1425 | color: rgb(37 99 235 / var(--tw-text-opacity, 1)); 1426 | } 1427 | 1428 | .text-gray-300 { 1429 | --tw-text-opacity: 1; 1430 | color: rgb(209 213 219 / var(--tw-text-opacity, 1)); 1431 | } 1432 | 1433 | .text-gray-400 { 1434 | --tw-text-opacity: 1; 1435 | color: rgb(156 163 175 / var(--tw-text-opacity, 1)); 1436 | } 1437 | 1438 | .text-gray-500 { 1439 | --tw-text-opacity: 1; 1440 | color: rgb(107 114 128 / var(--tw-text-opacity, 1)); 1441 | } 1442 | 1443 | .text-gray-600 { 1444 | --tw-text-opacity: 1; 1445 | color: rgb(75 85 99 / var(--tw-text-opacity, 1)); 1446 | } 1447 | 1448 | .text-gray-700 { 1449 | --tw-text-opacity: 1; 1450 | color: rgb(55 65 81 / var(--tw-text-opacity, 1)); 1451 | } 1452 | 1453 | .text-gray-800 { 1454 | --tw-text-opacity: 1; 1455 | color: rgb(31 41 55 / var(--tw-text-opacity, 1)); 1456 | } 1457 | 1458 | .text-gray-900 { 1459 | --tw-text-opacity: 1; 1460 | color: rgb(17 24 39 / var(--tw-text-opacity, 1)); 1461 | } 1462 | 1463 | .text-green-600 { 1464 | --tw-text-opacity: 1; 1465 | color: rgb(22 163 74 / var(--tw-text-opacity, 1)); 1466 | } 1467 | 1468 | .text-red-600 { 1469 | --tw-text-opacity: 1; 1470 | color: rgb(220 38 38 / var(--tw-text-opacity, 1)); 1471 | } 1472 | 1473 | .text-red-800 { 1474 | --tw-text-opacity: 1; 1475 | color: rgb(153 27 27 / var(--tw-text-opacity, 1)); 1476 | } 1477 | 1478 | .text-teal-800 { 1479 | --tw-text-opacity: 1; 1480 | color: rgb(17 94 89 / var(--tw-text-opacity, 1)); 1481 | } 1482 | 1483 | .text-transparent { 1484 | color: transparent; 1485 | } 1486 | 1487 | .text-white { 1488 | --tw-text-opacity: 1; 1489 | color: rgb(255 255 255 / var(--tw-text-opacity, 1)); 1490 | } 1491 | 1492 | .text-yellow-800 { 1493 | --tw-text-opacity: 1; 1494 | color: rgb(133 77 14 / var(--tw-text-opacity, 1)); 1495 | } 1496 | 1497 | .decoration-2 { 1498 | text-decoration-thickness: 2px; 1499 | } 1500 | 1501 | .opacity-0 { 1502 | opacity: 0; 1503 | } 1504 | 1505 | .shadow-lg { 1506 | --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); 1507 | --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); 1508 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1509 | } 1510 | 1511 | .shadow-md { 1512 | --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); 1513 | --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); 1514 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1515 | } 1516 | 1517 | .shadow-sm { 1518 | --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 1519 | --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); 1520 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1521 | } 1522 | 1523 | .filter { 1524 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); 1525 | } 1526 | 1527 | .transition { 1528 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; 1529 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; 1530 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; 1531 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1532 | transition-duration: 150ms; 1533 | } 1534 | 1535 | .transition-\[height\] { 1536 | transition-property: height; 1537 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1538 | transition-duration: 150ms; 1539 | } 1540 | 1541 | .transition-\[opacity\2c margin\] { 1542 | transition-property: opacity,margin; 1543 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1544 | transition-duration: 150ms; 1545 | } 1546 | 1547 | .transition-all { 1548 | transition-property: all; 1549 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1550 | transition-duration: 150ms; 1551 | } 1552 | 1553 | .transition-opacity { 1554 | transition-property: opacity; 1555 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1556 | transition-duration: 150ms; 1557 | } 1558 | 1559 | .duration-300 { 1560 | transition-duration: 300ms; 1561 | } 1562 | 1563 | .\[--auto-close\:lg\] { 1564 | --auto-close: lg; 1565 | } 1566 | 1567 | .\[--placement\:bottom-right\] { 1568 | --placement: bottom-right; 1569 | } 1570 | 1571 | .before\:absolute::before { 1572 | content: var(--tw-content); 1573 | position: absolute; 1574 | } 1575 | 1576 | .before\:-top-4::before { 1577 | content: var(--tw-content); 1578 | top: -1rem; 1579 | } 1580 | 1581 | .before\:bottom-0\.5::before { 1582 | content: var(--tw-content); 1583 | bottom: 0.125rem; 1584 | } 1585 | 1586 | .before\:end-3::before { 1587 | content: var(--tw-content); 1588 | inset-inline-end: 0.75rem; 1589 | } 1590 | 1591 | .before\:start-0::before { 1592 | content: var(--tw-content); 1593 | inset-inline-start: 0px; 1594 | } 1595 | 1596 | .before\:start-1\/2::before { 1597 | content: var(--tw-content); 1598 | inset-inline-start: 50%; 1599 | } 1600 | 1601 | .before\:top-0::before { 1602 | content: var(--tw-content); 1603 | top: 0px; 1604 | } 1605 | 1606 | .before\:top-1\/2::before { 1607 | content: var(--tw-content); 1608 | top: 50%; 1609 | } 1610 | 1611 | .before\:-z-\[1\]::before { 1612 | content: var(--tw-content); 1613 | z-index: -1; 1614 | } 1615 | 1616 | .before\:size-full::before { 1617 | content: var(--tw-content); 1618 | width: 100%; 1619 | height: 100%; 1620 | } 1621 | 1622 | .before\:h-1::before { 1623 | content: var(--tw-content); 1624 | height: 0.25rem; 1625 | } 1626 | 1627 | .before\:h-4::before { 1628 | content: var(--tw-content); 1629 | height: 1rem; 1630 | } 1631 | 1632 | .before\:w-full::before { 1633 | content: var(--tw-content); 1634 | width: 100%; 1635 | } 1636 | 1637 | .before\:-translate-x-1\/2::before { 1638 | content: var(--tw-content); 1639 | --tw-translate-x: -50%; 1640 | 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)); 1641 | } 1642 | 1643 | .before\:-translate-y-1\/2::before { 1644 | content: var(--tw-content); 1645 | --tw-translate-y: -50%; 1646 | 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)); 1647 | } 1648 | 1649 | .before\:transform::before { 1650 | content: var(--tw-content); 1651 | 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)); 1652 | } 1653 | 1654 | .before\:bg-lime-400::before { 1655 | content: var(--tw-content); 1656 | --tw-bg-opacity: 1; 1657 | background-color: rgb(163 230 53 / var(--tw-bg-opacity, 1)); 1658 | } 1659 | 1660 | .before\:bg-\[url\(\'https\:\/\/preline\.co\/assets\/svg\/examples\/polygon-bg-element\.svg\'\)\]::before { 1661 | content: var(--tw-content); 1662 | background-image: url('https://preline.co/assets/svg/examples/polygon-bg-element.svg'); 1663 | } 1664 | 1665 | .before\:bg-cover::before { 1666 | content: var(--tw-content); 1667 | background-size: cover; 1668 | } 1669 | 1670 | .before\:bg-top::before { 1671 | content: var(--tw-content); 1672 | background-position: top; 1673 | } 1674 | 1675 | .before\:bg-no-repeat::before { 1676 | content: var(--tw-content); 1677 | background-repeat: no-repeat; 1678 | } 1679 | 1680 | .before\:text-black::before { 1681 | content: var(--tw-content); 1682 | --tw-text-opacity: 1; 1683 | color: rgb(0 0 0 / var(--tw-text-opacity, 1)); 1684 | } 1685 | 1686 | .before\:content-\[\'\/\'\]::before { 1687 | --tw-content: '/'; 1688 | content: var(--tw-content); 1689 | } 1690 | 1691 | .after\:absolute::after { 1692 | content: var(--tw-content); 1693 | position: absolute; 1694 | } 1695 | 1696 | .after\:-bottom-4::after { 1697 | content: var(--tw-content); 1698 | bottom: -1rem; 1699 | } 1700 | 1701 | .after\:start-0::after { 1702 | content: var(--tw-content); 1703 | inset-inline-start: 0px; 1704 | } 1705 | 1706 | .after\:h-4::after { 1707 | content: var(--tw-content); 1708 | height: 1rem; 1709 | } 1710 | 1711 | .after\:w-full::after { 1712 | content: var(--tw-content); 1713 | width: 100%; 1714 | } 1715 | 1716 | .last\:pe-0:last-child { 1717 | padding-inline-end: 0px; 1718 | } 1719 | 1720 | .last-of-type\:before\:hidden:last-of-type::before { 1721 | content: var(--tw-content); 1722 | display: none; 1723 | } 1724 | 1725 | .hover\:bg-blue-600:hover { 1726 | --tw-bg-opacity: 1; 1727 | background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1)); 1728 | } 1729 | 1730 | .hover\:bg-blue-700:hover { 1731 | --tw-bg-opacity: 1; 1732 | background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1)); 1733 | } 1734 | 1735 | .hover\:bg-gray-100:hover { 1736 | --tw-bg-opacity: 1; 1737 | background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); 1738 | } 1739 | 1740 | .hover\:bg-gray-50:hover { 1741 | --tw-bg-opacity: 1; 1742 | background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); 1743 | } 1744 | 1745 | .hover\:from-violet-600:hover { 1746 | --tw-gradient-from: #7c3aed var(--tw-gradient-from-position); 1747 | --tw-gradient-to: rgb(124 58 237 / 0) var(--tw-gradient-to-position); 1748 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); 1749 | } 1750 | 1751 | .hover\:to-blue-600:hover { 1752 | --tw-gradient-to: #2563eb var(--tw-gradient-to-position); 1753 | } 1754 | 1755 | .hover\:text-blue-600:hover { 1756 | --tw-text-opacity: 1; 1757 | color: rgb(37 99 235 / var(--tw-text-opacity, 1)); 1758 | } 1759 | 1760 | .hover\:text-gray-500:hover { 1761 | --tw-text-opacity: 1; 1762 | color: rgb(107 114 128 / var(--tw-text-opacity, 1)); 1763 | } 1764 | 1765 | .hover\:text-gray-600:hover { 1766 | --tw-text-opacity: 1; 1767 | color: rgb(75 85 99 / var(--tw-text-opacity, 1)); 1768 | } 1769 | 1770 | .hover\:underline:hover { 1771 | text-decoration-line: underline; 1772 | } 1773 | 1774 | .focus\:border-blue-500:focus { 1775 | --tw-border-opacity: 1; 1776 | border-color: rgb(59 130 246 / var(--tw-border-opacity, 1)); 1777 | } 1778 | 1779 | .focus\:border-transparent:focus { 1780 | border-color: transparent; 1781 | } 1782 | 1783 | .focus\:bg-blue-700:focus { 1784 | --tw-bg-opacity: 1; 1785 | background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1)); 1786 | } 1787 | 1788 | .focus\:bg-gray-100:focus { 1789 | --tw-bg-opacity: 1; 1790 | background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); 1791 | } 1792 | 1793 | .focus\:bg-gray-50:focus { 1794 | --tw-bg-opacity: 1; 1795 | background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); 1796 | } 1797 | 1798 | .focus\:from-violet-600:focus { 1799 | --tw-gradient-from: #7c3aed var(--tw-gradient-from-position); 1800 | --tw-gradient-to: rgb(124 58 237 / 0) var(--tw-gradient-to-position); 1801 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); 1802 | } 1803 | 1804 | .focus\:to-blue-600:focus { 1805 | --tw-gradient-to: #2563eb var(--tw-gradient-to-position); 1806 | } 1807 | 1808 | .focus\:text-blue-600:focus { 1809 | --tw-text-opacity: 1; 1810 | color: rgb(37 99 235 / var(--tw-text-opacity, 1)); 1811 | } 1812 | 1813 | .focus\:text-gray-500:focus { 1814 | --tw-text-opacity: 1; 1815 | color: rgb(107 114 128 / var(--tw-text-opacity, 1)); 1816 | } 1817 | 1818 | .focus\:underline:focus { 1819 | text-decoration-line: underline; 1820 | } 1821 | 1822 | .focus\:opacity-80:focus { 1823 | opacity: 0.8; 1824 | } 1825 | 1826 | .focus\:outline-none:focus { 1827 | outline: 2px solid transparent; 1828 | outline-offset: 2px; 1829 | } 1830 | 1831 | .focus\:ring:focus { 1832 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 1833 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color); 1834 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 1835 | } 1836 | 1837 | .focus\:ring-2:focus { 1838 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 1839 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); 1840 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 1841 | } 1842 | 1843 | .focus\:ring-blue-500:focus { 1844 | --tw-ring-opacity: 1; 1845 | --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1)); 1846 | } 1847 | 1848 | .focus\:ring-opacity-50:focus { 1849 | --tw-ring-opacity: 0.5; 1850 | } 1851 | 1852 | .focus\:ring-offset-2:focus { 1853 | --tw-ring-offset-width: 2px; 1854 | } 1855 | 1856 | .focus\:ring-offset-gray-50:focus { 1857 | --tw-ring-offset-color: #f9fafb; 1858 | } 1859 | 1860 | .disabled\:pointer-events-none:disabled { 1861 | pointer-events: none; 1862 | } 1863 | 1864 | .disabled\:opacity-50:disabled { 1865 | opacity: 0.5; 1866 | } 1867 | 1868 | .group:hover .group-hover\:rotate-6 { 1869 | --tw-rotate: 6deg; 1870 | 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)); 1871 | } 1872 | 1873 | .hs-dropdown.open > .hs-dropdown-open\:opacity-100 { 1874 | opacity: 1; 1875 | } 1876 | 1877 | .hs-dropdown.open > .hs-dropdown-toggle .hs-dropdown-open\:opacity-100 { 1878 | opacity: 1; 1879 | } 1880 | 1881 | .hs-dropdown.open > .hs-dropdown-menu > .hs-dropdown-open\:opacity-100 { 1882 | opacity: 1; 1883 | } 1884 | 1885 | .hs-dropdown-menu.open.hs-dropdown-open\:opacity-100 { 1886 | opacity: 1; 1887 | } 1888 | 1889 | .hs-tooltip.show .hs-tooltip-shown\:visible { 1890 | visibility: visible; 1891 | } 1892 | 1893 | .hs-tooltip.show .hs-tooltip-shown\:opacity-100 { 1894 | opacity: 1; 1895 | } 1896 | 1897 | .hs-tooltip-content.show.hs-tooltip-shown\:visible { 1898 | visibility: visible; 1899 | } 1900 | 1901 | .hs-tooltip-content.show.hs-tooltip-shown\:opacity-100 { 1902 | opacity: 1; 1903 | } 1904 | 1905 | .hs-accordion.active.hs-accordion-active\:block { 1906 | display: block; 1907 | } 1908 | 1909 | .hs-accordion.active.hs-accordion-active\:hidden { 1910 | display: none; 1911 | } 1912 | 1913 | .hs-accordion.active > .hs-accordion-active\:block { 1914 | display: block; 1915 | } 1916 | 1917 | .hs-accordion.active > .hs-accordion-active\:hidden { 1918 | display: none; 1919 | } 1920 | 1921 | .hs-accordion.active > .hs-accordion-toggle .hs-accordion-active\:block { 1922 | display: block; 1923 | } 1924 | 1925 | .hs-accordion.active > .hs-accordion-toggle .hs-accordion-active\:hidden { 1926 | display: none; 1927 | } 1928 | 1929 | .hs-accordion.active > .hs-accordion-heading > .hs-accordion-toggle .hs-accordion-active\:block { 1930 | display: block; 1931 | } 1932 | 1933 | .hs-accordion.active > .hs-accordion-heading > .hs-accordion-toggle .hs-accordion-active\:hidden { 1934 | display: none; 1935 | } 1936 | 1937 | .hs-accordion.active > .hs-accordion-toggle.hs-accordion-active\:block { 1938 | display: block; 1939 | } 1940 | 1941 | .hs-accordion.active > .hs-accordion-toggle.hs-accordion-active\:hidden { 1942 | display: none; 1943 | } 1944 | 1945 | .hs-accordion.active > .hs-accordion-heading > .hs-accordion-toggle.hs-accordion-active\:block { 1946 | display: block; 1947 | } 1948 | 1949 | .hs-accordion.active > .hs-accordion-heading > .hs-accordion-toggle.hs-accordion-active\:hidden { 1950 | display: none; 1951 | } 1952 | 1953 | .hs-accordion.active .hs-accordion-force-active.hs-accordion-active\:block { 1954 | display: block; 1955 | } 1956 | 1957 | .hs-accordion.active .hs-accordion-force-active.hs-accordion-active\:hidden { 1958 | display: none; 1959 | } 1960 | 1961 | .hs-collapse.open .hs-collapse-open\:block { 1962 | display: block; 1963 | } 1964 | 1965 | .hs-collapse.open .hs-collapse-open\:hidden { 1966 | display: none; 1967 | } 1968 | 1969 | .hs-collapse.open.hs-collapse-open\:block { 1970 | display: block; 1971 | } 1972 | 1973 | .hs-collapse.open.hs-collapse-open\:hidden { 1974 | display: none; 1975 | } 1976 | 1977 | .hs-collapse-toggle.open .hs-collapse-open\:block { 1978 | display: block; 1979 | } 1980 | 1981 | .hs-collapse-toggle.open .hs-collapse-open\:hidden { 1982 | display: none; 1983 | } 1984 | 1985 | .hs-collapse-toggle.open.hs-collapse-open\:block { 1986 | display: block; 1987 | } 1988 | 1989 | .hs-collapse-toggle.open.hs-collapse-open\:hidden { 1990 | display: none; 1991 | } 1992 | 1993 | .open.hs-overlay-open\:translate-x-0 { 1994 | --tw-translate-x: 0px; 1995 | 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)); 1996 | } 1997 | 1998 | .open .hs-overlay-open\:translate-x-0 { 1999 | --tw-translate-x: 0px; 2000 | 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)); 2001 | } 2002 | 2003 | @media (min-width: 640px) { 2004 | .sm\:grid-cols-2 { 2005 | grid-template-columns: repeat(2, minmax(0, 1fr)); 2006 | } 2007 | 2008 | .sm\:gap-6 { 2009 | gap: 1.5rem; 2010 | } 2011 | 2012 | .sm\:gap-x-3 { 2013 | -moz-column-gap: 0.75rem; 2014 | column-gap: 0.75rem; 2015 | } 2016 | 2017 | .sm\:space-y-6 > :not([hidden]) ~ :not([hidden]) { 2018 | --tw-space-y-reverse: 0; 2019 | margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); 2020 | margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); 2021 | } 2022 | 2023 | .sm\:p-6 { 2024 | padding: 1.5rem; 2025 | } 2026 | 2027 | .sm\:px-6 { 2028 | padding-left: 1.5rem; 2029 | padding-right: 1.5rem; 2030 | } 2031 | 2032 | .sm\:text-2xl { 2033 | font-size: 1.5rem; 2034 | line-height: 2rem; 2035 | } 2036 | } 2037 | 2038 | @media (min-width: 768px) { 2039 | .md\:order-2 { 2040 | order: 2; 2041 | } 2042 | 2043 | .md\:order-3 { 2044 | order: 3; 2045 | } 2046 | 2047 | .md\:col-span-3 { 2048 | grid-column: span 3 / span 3; 2049 | } 2050 | 2051 | .md\:col-span-6 { 2052 | grid-column: span 6 / span 6; 2053 | } 2054 | 2055 | .md\:mt-0 { 2056 | margin-top: 0px; 2057 | } 2058 | 2059 | .md\:block { 2060 | display: block; 2061 | } 2062 | 2063 | .md\:flex { 2064 | display: flex; 2065 | } 2066 | 2067 | .md\:grid { 2068 | display: grid; 2069 | } 2070 | 2071 | .md\:hidden { 2072 | display: none; 2073 | } 2074 | 2075 | .md\:w-auto { 2076 | width: auto; 2077 | } 2078 | 2079 | .md\:basis-auto { 2080 | flex-basis: auto; 2081 | } 2082 | 2083 | .md\:grid-cols-12 { 2084 | grid-template-columns: repeat(12, minmax(0, 1fr)); 2085 | } 2086 | 2087 | .md\:grid-cols-3 { 2088 | grid-template-columns: repeat(3, minmax(0, 1fr)); 2089 | } 2090 | 2091 | .md\:flex-row { 2092 | flex-direction: row; 2093 | } 2094 | 2095 | .md\:flex-nowrap { 2096 | flex-wrap: nowrap; 2097 | } 2098 | 2099 | .md\:items-center { 2100 | align-items: center; 2101 | } 2102 | 2103 | .md\:justify-start { 2104 | justify-content: flex-start; 2105 | } 2106 | 2107 | .md\:justify-center { 2108 | justify-content: center; 2109 | } 2110 | 2111 | .md\:justify-between { 2112 | justify-content: space-between; 2113 | } 2114 | 2115 | .md\:gap-x-3 { 2116 | -moz-column-gap: 0.75rem; 2117 | column-gap: 0.75rem; 2118 | } 2119 | 2120 | .md\:gap-x-7 { 2121 | -moz-column-gap: 1.75rem; 2122 | column-gap: 1.75rem; 2123 | } 2124 | 2125 | .md\:gap-y-0 { 2126 | row-gap: 0px; 2127 | } 2128 | 2129 | .md\:border-t-0 { 2130 | border-top-width: 0px; 2131 | } 2132 | 2133 | .md\:p-5 { 2134 | padding: 1.25rem; 2135 | } 2136 | 2137 | .md\:px-8 { 2138 | padding-left: 2rem; 2139 | padding-right: 2rem; 2140 | } 2141 | 2142 | .md\:ps-6 { 2143 | padding-inline-start: 1.5rem; 2144 | } 2145 | 2146 | .md\:pt-0 { 2147 | padding-top: 0px; 2148 | } 2149 | 2150 | .md\:text-start { 2151 | text-align: start; 2152 | } 2153 | 2154 | .md\:text-end { 2155 | text-align: end; 2156 | } 2157 | 2158 | .md\:text-5xl { 2159 | font-size: 3rem; 2160 | line-height: 1; 2161 | } 2162 | } 2163 | 2164 | @media (min-width: 1024px) { 2165 | .lg\:bottom-0 { 2166 | bottom: 0px; 2167 | } 2168 | 2169 | .lg\:end-auto { 2170 | inset-inline-end: auto; 2171 | } 2172 | 2173 | .lg\:me-0 { 2174 | margin-inline-end: 0px; 2175 | } 2176 | 2177 | .lg\:block { 2178 | display: block; 2179 | } 2180 | 2181 | .lg\:hidden { 2182 | display: none; 2183 | } 2184 | 2185 | .lg\:translate-x-0 { 2186 | --tw-translate-x: 0px; 2187 | 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)); 2188 | } 2189 | 2190 | .lg\:grid-cols-2 { 2191 | grid-template-columns: repeat(2, minmax(0, 1fr)); 2192 | } 2193 | 2194 | .lg\:grid-cols-4 { 2195 | grid-template-columns: repeat(4, minmax(0, 1fr)); 2196 | } 2197 | 2198 | .lg\:px-8 { 2199 | padding-left: 2rem; 2200 | padding-right: 2rem; 2201 | } 2202 | 2203 | .lg\:ps-3 { 2204 | padding-inline-start: 0.75rem; 2205 | } 2206 | 2207 | .lg\:ps-64 { 2208 | padding-inline-start: 16rem; 2209 | } 2210 | 2211 | .lg\:ps-\[260px\] { 2212 | padding-inline-start: 260px; 2213 | } 2214 | 2215 | .lg\:text-6xl { 2216 | font-size: 3.75rem; 2217 | line-height: 1; 2218 | } 2219 | } 2220 | 2221 | @media (min-width: 1280px) { 2222 | .xl\:ps-0 { 2223 | padding-inline-start: 0px; 2224 | } 2225 | } 2226 | 2227 | @media (prefers-color-scheme: dark) { 2228 | .dark\:divide-neutral-700 > :not([hidden]) ~ :not([hidden]) { 2229 | --tw-divide-opacity: 1; 2230 | border-color: rgb(64 64 64 / var(--tw-divide-opacity, 1)); 2231 | } 2232 | 2233 | .dark\:border { 2234 | border-width: 1px; 2235 | } 2236 | 2237 | .dark\:border-neutral-600 { 2238 | --tw-border-opacity: 1; 2239 | border-color: rgb(82 82 82 / var(--tw-border-opacity, 1)); 2240 | } 2241 | 2242 | .dark\:border-neutral-700 { 2243 | --tw-border-opacity: 1; 2244 | border-color: rgb(64 64 64 / var(--tw-border-opacity, 1)); 2245 | } 2246 | 2247 | .dark\:bg-gray-900 { 2248 | --tw-bg-opacity: 1; 2249 | background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1)); 2250 | } 2251 | 2252 | .dark\:bg-neutral-200 { 2253 | --tw-bg-opacity: 1; 2254 | background-color: rgb(229 229 229 / var(--tw-bg-opacity, 1)); 2255 | } 2256 | 2257 | .dark\:bg-neutral-700 { 2258 | --tw-bg-opacity: 1; 2259 | background-color: rgb(64 64 64 / var(--tw-bg-opacity, 1)); 2260 | } 2261 | 2262 | .dark\:bg-neutral-800 { 2263 | --tw-bg-opacity: 1; 2264 | background-color: rgb(38 38 38 / var(--tw-bg-opacity, 1)); 2265 | } 2266 | 2267 | .dark\:bg-neutral-900 { 2268 | --tw-bg-opacity: 1; 2269 | background-color: rgb(23 23 23 / var(--tw-bg-opacity, 1)); 2270 | } 2271 | 2272 | .dark\:bg-red-500\/10 { 2273 | background-color: rgb(239 68 68 / 0.1); 2274 | } 2275 | 2276 | .dark\:bg-teal-500\/10 { 2277 | background-color: rgb(20 184 166 / 0.1); 2278 | } 2279 | 2280 | .dark\:bg-transparent { 2281 | background-color: transparent; 2282 | } 2283 | 2284 | .dark\:bg-yellow-500\/10 { 2285 | background-color: rgb(234 179 8 / 0.1); 2286 | } 2287 | 2288 | .dark\:text-blue-500 { 2289 | --tw-text-opacity: 1; 2290 | color: rgb(59 130 246 / var(--tw-text-opacity, 1)); 2291 | } 2292 | 2293 | .dark\:text-gray-400 { 2294 | --tw-text-opacity: 1; 2295 | color: rgb(156 163 175 / var(--tw-text-opacity, 1)); 2296 | } 2297 | 2298 | .dark\:text-neutral-200 { 2299 | --tw-text-opacity: 1; 2300 | color: rgb(229 229 229 / var(--tw-text-opacity, 1)); 2301 | } 2302 | 2303 | .dark\:text-neutral-300 { 2304 | --tw-text-opacity: 1; 2305 | color: rgb(212 212 212 / var(--tw-text-opacity, 1)); 2306 | } 2307 | 2308 | .dark\:text-neutral-400 { 2309 | --tw-text-opacity: 1; 2310 | color: rgb(163 163 163 / var(--tw-text-opacity, 1)); 2311 | } 2312 | 2313 | .dark\:text-neutral-500 { 2314 | --tw-text-opacity: 1; 2315 | color: rgb(115 115 115 / var(--tw-text-opacity, 1)); 2316 | } 2317 | 2318 | .dark\:text-neutral-600 { 2319 | --tw-text-opacity: 1; 2320 | color: rgb(82 82 82 / var(--tw-text-opacity, 1)); 2321 | } 2322 | 2323 | .dark\:text-red-500 { 2324 | --tw-text-opacity: 1; 2325 | color: rgb(239 68 68 / var(--tw-text-opacity, 1)); 2326 | } 2327 | 2328 | .dark\:text-teal-500 { 2329 | --tw-text-opacity: 1; 2330 | color: rgb(20 184 166 / var(--tw-text-opacity, 1)); 2331 | } 2332 | 2333 | .dark\:text-white { 2334 | --tw-text-opacity: 1; 2335 | color: rgb(255 255 255 / var(--tw-text-opacity, 1)); 2336 | } 2337 | 2338 | .dark\:text-white\/60 { 2339 | color: rgb(255 255 255 / 0.6); 2340 | } 2341 | 2342 | .dark\:text-yellow-500 { 2343 | --tw-text-opacity: 1; 2344 | color: rgb(234 179 8 / var(--tw-text-opacity, 1)); 2345 | } 2346 | 2347 | .dark\:placeholder\:text-neutral-400::-moz-placeholder { 2348 | --tw-text-opacity: 1; 2349 | color: rgb(163 163 163 / var(--tw-text-opacity, 1)); 2350 | } 2351 | 2352 | .dark\:placeholder\:text-neutral-400::placeholder { 2353 | --tw-text-opacity: 1; 2354 | color: rgb(163 163 163 / var(--tw-text-opacity, 1)); 2355 | } 2356 | 2357 | .dark\:before\:bg-\[url\(\'https\:\/\/preline\.co\/assets\/svg\/examples-dark\/polygon-bg-element\.svg\'\)\]::before { 2358 | content: var(--tw-content); 2359 | background-image: url('https://preline.co/assets/svg/examples-dark/polygon-bg-element.svg'); 2360 | } 2361 | 2362 | .dark\:before\:text-white::before { 2363 | content: var(--tw-content); 2364 | --tw-text-opacity: 1; 2365 | color: rgb(255 255 255 / var(--tw-text-opacity, 1)); 2366 | } 2367 | 2368 | .dark\:checked\:border-blue-500:checked { 2369 | --tw-border-opacity: 1; 2370 | border-color: rgb(59 130 246 / var(--tw-border-opacity, 1)); 2371 | } 2372 | 2373 | .dark\:checked\:bg-blue-500:checked { 2374 | --tw-bg-opacity: 1; 2375 | background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1)); 2376 | } 2377 | 2378 | .dark\:hover\:bg-neutral-700:hover { 2379 | --tw-bg-opacity: 1; 2380 | background-color: rgb(64 64 64 / var(--tw-bg-opacity, 1)); 2381 | } 2382 | 2383 | .dark\:hover\:bg-neutral-800:hover { 2384 | --tw-bg-opacity: 1; 2385 | background-color: rgb(38 38 38 / var(--tw-bg-opacity, 1)); 2386 | } 2387 | 2388 | .dark\:hover\:bg-neutral-900:hover { 2389 | --tw-bg-opacity: 1; 2390 | background-color: rgb(23 23 23 / var(--tw-bg-opacity, 1)); 2391 | } 2392 | 2393 | .dark\:hover\:bg-white\/10:hover { 2394 | background-color: rgb(255 255 255 / 0.1); 2395 | } 2396 | 2397 | .dark\:hover\:text-blue-500:hover { 2398 | --tw-text-opacity: 1; 2399 | color: rgb(59 130 246 / var(--tw-text-opacity, 1)); 2400 | } 2401 | 2402 | .dark\:hover\:text-neutral-300:hover { 2403 | --tw-text-opacity: 1; 2404 | color: rgb(212 212 212 / var(--tw-text-opacity, 1)); 2405 | } 2406 | 2407 | .dark\:hover\:text-neutral-400:hover { 2408 | --tw-text-opacity: 1; 2409 | color: rgb(163 163 163 / var(--tw-text-opacity, 1)); 2410 | } 2411 | 2412 | .dark\:hover\:text-neutral-500:hover { 2413 | --tw-text-opacity: 1; 2414 | color: rgb(115 115 115 / var(--tw-text-opacity, 1)); 2415 | } 2416 | 2417 | .dark\:hover\:text-white:hover { 2418 | --tw-text-opacity: 1; 2419 | color: rgb(255 255 255 / var(--tw-text-opacity, 1)); 2420 | } 2421 | 2422 | .dark\:focus\:bg-neutral-700:focus { 2423 | --tw-bg-opacity: 1; 2424 | background-color: rgb(64 64 64 / var(--tw-bg-opacity, 1)); 2425 | } 2426 | 2427 | .dark\:focus\:bg-neutral-800:focus { 2428 | --tw-bg-opacity: 1; 2429 | background-color: rgb(38 38 38 / var(--tw-bg-opacity, 1)); 2430 | } 2431 | 2432 | .dark\:focus\:text-blue-500:focus { 2433 | --tw-text-opacity: 1; 2434 | color: rgb(59 130 246 / var(--tw-text-opacity, 1)); 2435 | } 2436 | 2437 | .dark\:focus\:text-neutral-300:focus { 2438 | --tw-text-opacity: 1; 2439 | color: rgb(212 212 212 / var(--tw-text-opacity, 1)); 2440 | } 2441 | 2442 | .dark\:focus\:text-neutral-500:focus { 2443 | --tw-text-opacity: 1; 2444 | color: rgb(115 115 115 / var(--tw-text-opacity, 1)); 2445 | } 2446 | 2447 | .dark\:focus\:ring-neutral-600:focus { 2448 | --tw-ring-opacity: 1; 2449 | --tw-ring-color: rgb(82 82 82 / var(--tw-ring-opacity, 1)); 2450 | } 2451 | 2452 | .dark\:focus\:ring-offset-gray-800:focus { 2453 | --tw-ring-offset-color: #1f2937; 2454 | } 2455 | 2456 | .dark\:focus\:ring-offset-gray-900:focus { 2457 | --tw-ring-offset-color: #111827; 2458 | } 2459 | } 2460 | 2461 | .\[\&\:\:-webkit-scrollbar-thumb\]\:rounded-full::-webkit-scrollbar-thumb { 2462 | border-radius: 9999px; 2463 | } 2464 | 2465 | .\[\&\:\:-webkit-scrollbar-thumb\]\:bg-gray-300::-webkit-scrollbar-thumb { 2466 | --tw-bg-opacity: 1; 2467 | background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1)); 2468 | } 2469 | 2470 | @media (prefers-color-scheme: dark) { 2471 | .dark\:\[\&\:\:-webkit-scrollbar-thumb\]\:bg-neutral-500::-webkit-scrollbar-thumb { 2472 | --tw-bg-opacity: 1; 2473 | background-color: rgb(115 115 115 / var(--tw-bg-opacity, 1)); 2474 | } 2475 | } 2476 | 2477 | .\[\&\:\:-webkit-scrollbar-track\]\:bg-gray-100::-webkit-scrollbar-track { 2478 | --tw-bg-opacity: 1; 2479 | background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); 2480 | } 2481 | 2482 | @media (prefers-color-scheme: dark) { 2483 | .dark\:\[\&\:\:-webkit-scrollbar-track\]\:bg-neutral-700::-webkit-scrollbar-track { 2484 | --tw-bg-opacity: 1; 2485 | background-color: rgb(64 64 64 / var(--tw-bg-opacity, 1)); 2486 | } 2487 | } 2488 | 2489 | .\[\&\:\:-webkit-scrollbar\]\:w-2::-webkit-scrollbar { 2490 | width: 0.5rem; 2491 | } -------------------------------------------------------------------------------- /static/css/public/custom.css: -------------------------------------------------------------------------------- 1 | main { 2 | min-height: 80vh; 3 | } -------------------------------------------------------------------------------- /static/libraries/tailwind/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./templates/**/*.html", 5 | "./**/templates/**/*.html", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [ 11 | require('preline/plugin'), 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /templates/account/_layout.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}Authentication{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | {% block content %} {% endblock %} 48 |
49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /templates/account/email/email_confirmation_message.txt: -------------------------------------------------------------------------------- 1 | Hello {{ user.email }}, 2 | 3 | Please verify your email by clicking the link below: 4 | 5 | {{ activate_url }} 6 | 7 | Thanks, 8 | Your Team -------------------------------------------------------------------------------- /templates/account/email/email_confirmation_subject.txt: -------------------------------------------------------------------------------- 1 | Verify Your Email Address -------------------------------------------------------------------------------- /templates/account/email_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "account/_layout.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |
6 |
7 |

Confirm Your Email

8 | 9 | {% if confirmation %} 10 |

11 | {% blocktrans with email=confirmation.email_address.email %} 12 | Please confirm that {{ email }} is your email address. 13 | {% endblocktrans %} 14 |

15 | 16 |
17 | {% csrf_token %} 18 | 21 |
22 | {% else %} 23 |

24 | {% trans "This email confirmation link is invalid or has expired." %} 25 |

26 |

27 | 28 | {% trans "Request a new confirmation email" %} 29 | 30 |

31 | {% endif %} 32 | 33 |

34 | 35 | {% trans "Back to Login" %} 36 | 37 |

38 |
39 |
40 | {% endblock %} -------------------------------------------------------------------------------- /templates/account/email_confirmed.html: -------------------------------------------------------------------------------- 1 | {% extends "account/_layout.html" %} 2 | {% block content %} 3 |
4 |
5 |

Email Verified

6 |

7 | Your email has been successfully verified. You can now 8 | log in. 9 |

10 |
11 |
12 | {% endblock %} -------------------------------------------------------------------------------- /templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends "account/_layout.html" %} 2 | {% load tailwind_filters %} 3 | {% block content %} 4 |

Log In

5 |
6 |
7 |
8 |

Log In

9 |
10 |
11 | {% csrf_token %} 12 | {{ form|crispy }} 13 | 18 |
19 |
20 |

21 | Don’t have an account? 22 | 23 | Sign up here 24 | 25 |

26 |

27 | 28 | Forgot your password? 29 | 30 |

31 |
32 |
33 |
34 | {% endblock %} -------------------------------------------------------------------------------- /templates/account/password_change.html: -------------------------------------------------------------------------------- 1 | {% extends "account/_layout.html" %} 2 | {% block content %} 3 |
4 |
5 |

Change Your Password

6 | 7 |
8 | {% csrf_token %} 9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 | Back to Login 31 |
32 |
33 |
34 |
35 | {% endblock %} -------------------------------------------------------------------------------- /templates/account/password_reset.html: -------------------------------------------------------------------------------- 1 | {% extends "account/_layout.html" %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block content %} 5 |
6 |
7 |

Reset Your Password

8 | 9 |

10 | Enter your email address below, and we’ll send you a link to reset your password. 11 |

12 | 13 |
14 | {% csrf_token %} 15 | {{ form|crispy }} 16 | 19 |
20 | 21 |

22 | Remembered your password? 23 | Log in 24 |

25 |
26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /templates/account/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "account/_layout.html" %} 2 | {% load tailwind_filters %} 3 | {% block content %} 4 |
5 |
6 |
7 |

Reset Your Password

8 |

9 | Enter your new password below. 10 |

11 |
12 |
13 | {% csrf_token %} 14 | {{ form|crispy }} 15 | 20 |
21 |
22 |

23 | Remembered your password? 24 | 25 | Log in here 26 | 27 |

28 |
29 |
30 |
31 | {% endblock %} -------------------------------------------------------------------------------- /templates/account/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/_layout.html" %} 2 | {% block content %} 3 |
4 |
5 |

Password Reset Instructions Sent

6 |

We’ve sent an email to {{ email }} with instructions on how to reset your password.

7 |

If you don't see the email in your inbox, check your spam folder or try again.

8 |

9 | Return to the login page 10 |

11 |
12 |
13 | {% endblock %} -------------------------------------------------------------------------------- /templates/account/password_reset_from_key.html: -------------------------------------------------------------------------------- 1 | {% extends "account/_layout.html" %} 2 | {% load tailwind_filters %} 3 | {% block content %} 4 |
5 |
6 |

7 | {% if token_fail %}Bad Token{% else %}Change Password{% endif %} 8 |

9 | 10 | {% if token_fail %} 11 |

12 | The password reset link was invalid. Perhaps it has already been used? 13 | Please request a 14 | new password reset. 15 |

16 | {% else %} 17 | {% if form %} 18 |
19 | {% csrf_token %} 20 |
21 | {{ form|crispy }} 22 |
23 | 26 |
27 | {% else %} 28 |

Your password is now changed.

29 | {% endif %} 30 | {% endif %} 31 |
32 |
33 | {% endblock %} -------------------------------------------------------------------------------- /templates/account/password_reset_from_key_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/_layout.html" %} 2 | {% load tailwind_filters %} 3 | {% block content %} 4 |
5 |
6 |

Password Change Successful

7 |

Your password has been successfully changed.

8 |

9 | 10 | Return to Login 11 | 12 |

13 |
14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /templates/account/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "account/_layout.html" %} 2 | {% load tailwind_filters %} 3 | {% block content %} 4 |

Sign Up

5 |
6 |
7 |
8 |

Sign Up

9 |
10 |
11 | {% csrf_token %} 12 | {{ form|crispy }} 13 | 18 |
19 |

20 | Already have an account? 21 | 22 | Log in here 23 | 24 |

25 |
26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout/public/layout.html" %} 2 | {% block content %} 3 | 4 | 5 |
8 |
9 | 10 |
11 |

14 | Django 15 | Starter 19 |

20 |
21 | 22 | 23 |
24 |

25 | Django Starter is a Django template to help you start your project without doing the same setup every time. Note that this is customized for my personal use, but you can use it as a base for your project. 26 |

27 |
28 | 29 | 30 |
31 | 36 | Clone Repository 37 | 49 | 50 | 51 | 52 | 79 |
80 | 81 | 82 |
83 | Styling: 86 | tailwind + preline 87 | 98 | 103 | Usage Guide 104 | 116 | 117 | 118 | 119 |
120 |
121 |
122 | 123 | 124 | {% endblock %} 125 | -------------------------------------------------------------------------------- /templates/layout/dashboard/_breadcrumb.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 12 | 13 | 14 | 15 |
    16 |
  1. 17 | Application Layout 18 | 19 | 20 | 21 |
  2. 22 |
  3. 23 | Dashboard 24 |
  4. 25 |
26 | 27 |
28 |
-------------------------------------------------------------------------------- /templates/layout/dashboard/_footer.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisDevCode-Technologies/django-starter/51b6655bda5cc5539c716ee7945d2b8bd161187d/templates/layout/dashboard/_footer.html -------------------------------------------------------------------------------- /templates/layout/dashboard/_header.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 |
5 | 324 |
325 | -------------------------------------------------------------------------------- /templates/layout/dashboard/_sidenav.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 529 | -------------------------------------------------------------------------------- /templates/layout/dashboard/layout.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | Django Starter Template | dashboard 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 41 | 42 | 43 | 47 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {% include 'layout/dashboard/_header.html' %} 61 | {% include 'layout/dashboard/_breadcrumb.html' %} 62 | {% include 'layout/dashboard/_sidenav.html' %} 63 | 64 |
65 |
66 | {% block content %} {% endblock %} 67 |
68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 269 | 467 | 468 | 469 | -------------------------------------------------------------------------------- /templates/layout/public/_footer.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 131 | -------------------------------------------------------------------------------- /templates/layout/public/_header.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 |
3 | 117 |
118 | -------------------------------------------------------------------------------- /templates/layout/public/layout.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | Django Starter Template 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {% include 'layout/public/_header.html' %} 47 |
48 | {% block content %} {% endblock %} 49 |
50 | {% include 'layout/public/_footer.html' %} 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'users.apps.UsersConfig' -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from .forms import UserCreationForm, UserChangeForm 6 | from .models import User, EndUserProfile, StaffUserProfile 7 | 8 | 9 | # Profile Inlines for User Admin 10 | class StaffUserProfileInline(admin.StackedInline): 11 | model = StaffUserProfile 12 | can_delete = False 13 | verbose_name_plural = "Staff Profile" 14 | 15 | 16 | class EndUserProfileInline(admin.StackedInline): 17 | model = EndUserProfile 18 | can_delete = False 19 | verbose_name_plural = "End User Profile" 20 | 21 | 22 | # Custom User Admin 23 | class CustomUserAdmin(UserAdmin): 24 | add_form = UserCreationForm 25 | form = UserChangeForm 26 | model = User 27 | 28 | # Displayed fields in the admin list view 29 | list_display = ( 30 | "email", 31 | "first_name", 32 | "last_name", 33 | "type", 34 | "gender", 35 | "is_staff", 36 | "is_active", 37 | "is_superuser", 38 | "is_verified", 39 | ) 40 | list_filter = ( 41 | "type", 42 | "gender", 43 | "is_verified", 44 | "is_active", 45 | "is_superuser", 46 | "is_staff", 47 | ) 48 | 49 | # Fieldsets for viewing/editing a user 50 | fieldsets = ( 51 | (_("Credentials"), {"fields": ("email", "password")}), 52 | (_("Personal Info"), {"fields": ("first_name", "last_name", "gender")}), 53 | ( 54 | _("Permissions"), 55 | {"fields": ("is_staff", "is_active", "is_verified", "is_superuser", "groups", "user_permissions")}, 56 | ), 57 | (_("User Type"), {"fields": ("type",)}), 58 | ) 59 | 60 | # Fieldsets for creating a new user 61 | add_fieldsets = ( 62 | ( 63 | None, 64 | { 65 | "classes": ("wide",), 66 | "fields": ( 67 | "email", 68 | "first_name", 69 | "last_name", 70 | "gender", 71 | "password1", 72 | "password2", 73 | "type", 74 | "is_staff", 75 | "is_active", 76 | "is_superuser", 77 | "groups", 78 | "user_permissions", 79 | ), 80 | }, 81 | ), 82 | ) 83 | 84 | search_fields = ("email", "first_name", "last_name") 85 | ordering = ("email",) 86 | 87 | # Dynamically include the correct profile inline 88 | def get_inlines(self, request, obj=None): 89 | """Return the correct profile inline based on the user type.""" 90 | if obj: 91 | if obj.type == User.Types.STAFF: 92 | return [StaffUserProfileInline] 93 | elif obj.type == User.Types.ENDUSER: 94 | return [EndUserProfileInline] 95 | return [] 96 | 97 | # Ensure profile inline exists in the admin panel 98 | def get_queryset(self, request): 99 | """Ensure profiles are loaded efficiently.""" 100 | queryset = super().get_queryset(request) 101 | return queryset.prefetch_related("staff_profile", "end_user_profile") 102 | 103 | 104 | # Register the custom User model and admin 105 | admin.site.register(User, CustomUserAdmin) 106 | 107 | 108 | # Admin for Staff and End User Profiles 109 | @admin.register(StaffUserProfile) 110 | class StaffUserProfileAdmin(admin.ModelAdmin): 111 | list_display = ("user",) 112 | search_fields = ("user__email", "user__first_name", "user__last_name") 113 | 114 | 115 | @admin.register(EndUserProfile) 116 | class EndUserProfileAdmin(admin.ModelAdmin): 117 | list_display = ("user",) 118 | search_fields = ("user__email", "user__first_name", "user__last_name") 119 | -------------------------------------------------------------------------------- /users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'users' 7 | 8 | def ready(self): 9 | import users.signals 10 | print("UsersConfig ready - signals imported") 11 | -------------------------------------------------------------------------------- /users/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import UserCreationForm, UserChangeForm 3 | from allauth.account.forms import ResetPasswordForm 4 | 5 | from .models import User 6 | 7 | 8 | class UserCreationForm(UserCreationForm): 9 | 10 | class Meta: 11 | model = User 12 | fields = ("email", "first_name", "last_name", "password1", "password2") 13 | 14 | 15 | class UserChangeForm(UserChangeForm): 16 | 17 | class Meta: 18 | model = User 19 | fields = ("email", "first_name", "last_name") 20 | 21 | 22 | 23 | class CustomResetPasswordForm(ResetPasswordForm): 24 | # Example: Add custom field to the form 25 | custom_field = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': 'Custom Field'})) 26 | 27 | def clean_custom_field(self): 28 | # Add custom validation logic for the custom field 29 | custom_value = self.cleaned_data.get('custom_field') 30 | if custom_value and len(custom_value) < 5: 31 | raise forms.ValidationError("Custom field must be at least 5 characters.") 32 | return custom_value 33 | -------------------------------------------------------------------------------- /users/managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.base_user import BaseUserManager 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class CustomUserManager(BaseUserManager): 6 | """ 7 | Custom user model manager where email is the unique identifiers 8 | for authentication instead of usernames. 9 | """ 10 | 11 | def create_user(self, email, password, **extra_fields): 12 | """ 13 | Create and save a user with the given email and password. 14 | """ 15 | # Add default for is_active 16 | extra_fields.setdefault('is_active', True) 17 | 18 | # Add email validation 19 | if not email: 20 | raise ValueError(_("The Email must be set")) 21 | email = self.normalize_email(email).lower() # Force lowercase 22 | 23 | user = self.model(email=email, **extra_fields) 24 | user.set_password(password) 25 | user.save() 26 | return user 27 | 28 | def create_superuser(self, email, password, **extra_fields): 29 | """ 30 | Create and save a SuperUser with the given email and password. 31 | """ 32 | extra_fields.setdefault("is_staff", True) 33 | extra_fields.setdefault("is_superuser", True) 34 | extra_fields.setdefault("is_active", True) 35 | extra_fields.setdefault("is_verified", True) 36 | 37 | if extra_fields.get("is_staff") is not True: 38 | raise ValueError(_("Superuser must have is_staff=True.")) 39 | if extra_fields.get("is_superuser") is not True: 40 | raise ValueError(_("Superuser must have is_superuser=True.")) 41 | if extra_fields.get("is_active") is not True: 42 | raise ValueError(_("Superuser must have is_active=True.")) 43 | return self.create_user(email, password, **extra_fields) -------------------------------------------------------------------------------- /users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.4 on 2024-12-29 22:49 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('auth', '0012_alter_user_first_name_max_length'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='User', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('password', models.CharField(max_length=128, verbose_name='password')), 21 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 22 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 23 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 24 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 25 | ('email', models.EmailField(db_index=True, max_length=254, unique=True, verbose_name='email address')), 26 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 27 | ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), 28 | ('is_verified', models.BooleanField(default=False, help_text='Designates whether this user has verified their email.')), 29 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now)), 30 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), 31 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), 32 | ], 33 | options={ 34 | 'verbose_name': 'user', 35 | 'verbose_name_plural': 'users', 36 | 'abstract': False, 37 | }, 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /users/migrations/0002_user_is_custom_admin.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.4 on 2024-12-29 23:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='is_custom_admin', 16 | field=models.BooleanField(default=False, help_text='Designates whether this user has dashboard access to the site.'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /users/migrations/0003_userprofile.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.4 on 2024-12-29 23:53 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('users', '0002_user_is_custom_admin'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='UserProfile', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 20 | ], 21 | options={ 22 | 'verbose_name': 'User Profile', 23 | 'verbose_name_plural': 'User Profiles', 24 | }, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /users/migrations/0004_rename_userprofile_enduserprofile_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.4 on 2025-01-21 12:13 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('users', '0003_userprofile'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameModel( 16 | old_name='UserProfile', 17 | new_name='EndUserProfile', 18 | ), 19 | migrations.AlterModelOptions( 20 | name='enduserprofile', 21 | options={'verbose_name': 'End User Profile', 'verbose_name_plural': 'End User Profiles'}, 22 | ), 23 | migrations.AddField( 24 | model_name='user', 25 | name='type', 26 | field=models.CharField(choices=[('STAFF', 'Staff'), ('ENDUSER', 'EndUser')], default='STAFF', max_length=50, verbose_name='User Type'), 27 | ), 28 | migrations.CreateModel( 29 | name='StaffUserProfile', 30 | fields=[ 31 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 33 | ], 34 | options={ 35 | 'verbose_name': 'Staff Profile', 36 | 'verbose_name_plural': 'Staff Profiles', 37 | }, 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /users/migrations/0005_enduser_staff_alter_user_options_user_gender_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.4 on 2025-02-03 08:27 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('users', '0004_rename_userprofile_enduserprofile_and_more'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='EndUser', 17 | fields=[ 18 | ], 19 | options={ 20 | 'proxy': True, 21 | 'indexes': [], 22 | 'constraints': [], 23 | }, 24 | bases=('users.user',), 25 | ), 26 | migrations.CreateModel( 27 | name='Staff', 28 | fields=[ 29 | ], 30 | options={ 31 | 'proxy': True, 32 | 'indexes': [], 33 | 'constraints': [], 34 | }, 35 | bases=('users.user',), 36 | ), 37 | migrations.AlterModelOptions( 38 | name='user', 39 | options={'verbose_name': 'User', 'verbose_name_plural': 'Users'}, 40 | ), 41 | migrations.AddField( 42 | model_name='user', 43 | name='gender', 44 | field=models.CharField(blank=True, choices=[('male', 'Male'), ('female', 'Female')], max_length=30, null=True, verbose_name='Gender'), 45 | ), 46 | migrations.AlterField( 47 | model_name='enduserprofile', 48 | name='user', 49 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='end_user_profile', to=settings.AUTH_USER_MODEL), 50 | ), 51 | migrations.AlterField( 52 | model_name='staffuserprofile', 53 | name='user', 54 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='staff_profile', to=settings.AUTH_USER_MODEL), 55 | ), 56 | migrations.AlterField( 57 | model_name='user', 58 | name='type', 59 | field=models.CharField(choices=[('STAFF', 'Staff'), ('ENDUSER', 'End User')], default='STAFF', max_length=50, verbose_name='User Type'), 60 | ), 61 | ] 62 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisDevCode-Technologies/django-starter/51b6655bda5cc5539c716ee7945d2b8bd161187d/users/migrations/__init__.py -------------------------------------------------------------------------------- /users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser, PermissionsMixin 2 | from django.db import models 3 | from django.db.models.signals import post_save 4 | from django.dispatch import receiver 5 | from django.utils import timezone 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | from .managers import CustomUserManager 9 | 10 | 11 | class User(AbstractUser, PermissionsMixin): 12 | class Types(models.TextChoices): 13 | STAFF = "STAFF", "Staff" 14 | ENDUSER = "ENDUSER", "End User" 15 | 16 | GENDER_CHOICES = ( 17 | ("male", "Male"), 18 | ("female", "Female"), 19 | ) 20 | 21 | username = None 22 | email = models.EmailField( 23 | _("email address"), 24 | unique=True, 25 | db_index=True 26 | ) 27 | first_name = models.CharField(_("first name"), max_length=30, blank=True) 28 | last_name = models.CharField(_("last name"), max_length=30, blank=True) 29 | gender = models.CharField( 30 | _("Gender"), max_length=30, choices=GENDER_CHOICES, blank=True, null=True 31 | ) 32 | type = models.CharField( 33 | _("User Type"), 34 | max_length=50, 35 | choices=Types.choices, 36 | default=Types.STAFF, 37 | ) 38 | is_verified = models.BooleanField( 39 | default=False, 40 | help_text=_("Designates whether this user has verified their email.") 41 | ) 42 | is_custom_admin = models.BooleanField( 43 | default=False, 44 | help_text=_("Designates whether this user has dashboard access to the site.") 45 | ) 46 | date_joined = models.DateTimeField(default=timezone.now) 47 | 48 | USERNAME_FIELD = "email" 49 | REQUIRED_FIELDS = [] 50 | 51 | objects = CustomUserManager() 52 | 53 | class Meta: 54 | verbose_name = "User" 55 | verbose_name_plural = "Users" 56 | 57 | def __str__(self): 58 | return self.email 59 | 60 | 61 | # Type-Based Query Managers 62 | class StaffManager(CustomUserManager): 63 | def get_queryset(self, *args, **kwargs): 64 | return super().get_queryset(*args, **kwargs).filter(type=User.Types.STAFF) 65 | 66 | 67 | class EndUserManager(CustomUserManager): 68 | def get_queryset(self, *args, **kwargs): 69 | return super().get_queryset(*args, **kwargs).filter(type=User.Types.ENDUSER) 70 | 71 | 72 | # Staff Profile Model 73 | class StaffUserProfile(models.Model): 74 | user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="staff_profile") 75 | 76 | def __str__(self): 77 | return self.user.email 78 | 79 | class Meta: 80 | verbose_name = "Staff Profile" 81 | verbose_name_plural = "Staff Profiles" 82 | 83 | 84 | # End User Profile Model 85 | class EndUserProfile(models.Model): 86 | user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="end_user_profile") 87 | 88 | def __str__(self): 89 | return self.user.email 90 | 91 | class Meta: 92 | verbose_name = "End User Profile" 93 | verbose_name_plural = "End User Profiles" 94 | 95 | 96 | 97 | # Staff Proxy Model 98 | class Staff(User): 99 | objects = StaffManager() 100 | 101 | class Meta: 102 | proxy = True 103 | 104 | def save(self, *args, **kwargs): 105 | if not self.pk: 106 | self.type = User.Types.STAFF 107 | return super().save(*args, **kwargs) 108 | 109 | @property 110 | def profile(self): 111 | try: 112 | return self.staff_profile 113 | except StaffUserProfile.DoesNotExist: 114 | return None # Or handle as needed 115 | 116 | 117 | # EndUser Proxy Model 118 | class EndUser(User): 119 | objects = EndUserManager() 120 | 121 | class Meta: 122 | proxy = True 123 | 124 | def save(self, *args, **kwargs): 125 | if not self.pk: 126 | self.type = User.Types.ENDUSER 127 | return super().save(*args, **kwargs) 128 | 129 | @property 130 | def profile(self): 131 | try: 132 | return self.end_user_profile 133 | except EndUserProfile.DoesNotExist: 134 | return None # Or handle as needed 135 | 136 | 137 | # Optimized Signal for Creating & Updating Profiles 138 | # @receiver(post_save, sender=User) 139 | # def create_or_update_user_profile(sender, instance, created, **kwargs): 140 | # """Ensure only the correct profile exists when a user is created or updated.""" 141 | # if created or instance.type != User.objects.get(pk=instance.pk).type: 142 | # if instance.type == User.Types.STAFF: 143 | # StaffUserProfile.objects.get_or_create(user=instance) 144 | # EndUserProfile.objects.filter(user=instance).delete() 145 | # elif instance.type == User.Types.ENDUSER: 146 | # EndUserProfile.objects.get_or_create(user=instance) 147 | # StaffUserProfile.objects.filter(user=instance).delete() 148 | 149 | 150 | import users.signals -------------------------------------------------------------------------------- /users/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_save 2 | from django.dispatch import receiver 3 | from .models import User, StaffUserProfile, EndUserProfile 4 | 5 | @receiver(post_save, sender=User) 6 | def create_or_update_user_profile(sender, instance, created, **kwargs): 7 | print(f"Signal triggered for {instance.email}, created={created}, type={instance.type}") 8 | if created or (not created and instance.type != User.objects.get(pk=instance.pk).type): 9 | if instance.type == User.Types.STAFF: 10 | StaffUserProfile.objects.get_or_create(user=instance) 11 | EndUserProfile.objects.filter(user=instance).delete() 12 | elif instance.type == User.Types.ENDUSER: 13 | EndUserProfile.objects.get_or_create(user=instance) 14 | StaffUserProfile.objects.filter(user=instance).delete() 15 | print(f"Post-signal: Staff profiles={StaffUserProfile.objects.filter(user=instance).count()}, EndUser profiles={EndUserProfile.objects.filter(user=instance).count()}") -------------------------------------------------------------------------------- /users/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.test import TestCase 3 | from .models import StaffUserProfile, EndUserProfile 4 | 5 | 6 | class UsersManagersTests(TestCase): 7 | def setUp(self): 8 | self.User = get_user_model() 9 | 10 | def test_create_user(self): 11 | # Test creation of a regular user 12 | user = self.User.objects.create_user(email="normal@user.com", password="foo") 13 | self.assertEqual(user.email, "normal@user.com") 14 | self.assertTrue(user.is_active) 15 | self.assertFalse(user.is_staff) 16 | self.assertFalse(user.is_superuser) 17 | self.assertEqual(user.type, self.User.Types.ENDUSER) 18 | 19 | # Verify EndUserProfile creation 20 | self.assertTrue(EndUserProfile.objects.filter(user=user).exists()) 21 | 22 | # Check username behavior 23 | with self.assertRaises(AttributeError): 24 | _ = user.username 25 | 26 | # Test missing parameters 27 | with self.assertRaises(TypeError): 28 | self.User.objects.create_user() 29 | with self.assertRaises(ValueError): 30 | self.User.objects.create_user(email="", password="foo") 31 | 32 | def test_create_staff_user(self): 33 | # Test creation of a staff user 34 | user = self.User.objects.create_user( 35 | email="staff@user.com", password="foo", type=self.User.Types.STAFF 36 | ) 37 | self.assertEqual(user.email, "staff@user.com") 38 | self.assertTrue(user.is_active) 39 | self.assertFalse(user.is_staff) 40 | self.assertFalse(user.is_superuser) 41 | self.assertEqual(user.type, self.User.Types.STAFF) 42 | 43 | # Verify StaffUserProfile creation 44 | self.assertTrue(StaffUserProfile.objects.filter(user=user).exists()) 45 | 46 | def test_create_superuser(self): 47 | # Test creation of a superuser 48 | admin_user = self.User.objects.create_superuser( 49 | email="super@user.com", password="foo" 50 | ) 51 | self.assertEqual(admin_user.email, "super@user.com") 52 | self.assertTrue(admin_user.is_active) 53 | self.assertTrue(admin_user.is_staff) 54 | self.assertTrue(admin_user.is_superuser) 55 | self.assertEqual(admin_user.type, self.User.Types.STAFF) 56 | 57 | # Verify no profiles are created for superuser 58 | self.assertFalse(StaffUserProfile.objects.filter(user=admin_user).exists()) 59 | self.assertFalse(EndUserProfile.objects.filter(user=admin_user).exists()) 60 | 61 | # Test invalid superuser creation 62 | with self.assertRaises(ValueError): 63 | self.User.objects.create_superuser( 64 | email="super@user.com", password="foo", is_superuser=False 65 | ) 66 | 67 | def test_email_normalization(self): 68 | # Test email normalization 69 | user = self.User.objects.create_user(email="NORMAL@USER.COM", password="foo") 70 | self.assertEqual(user.email, "normal@user.com") 71 | 72 | def test_type_field(self): 73 | # Test setting the type field 74 | staff_user = self.User.objects.create_user( 75 | email="staff@user.com", password="foo", type=self.User.Types.STAFF 76 | ) 77 | self.assertEqual(staff_user.type, self.User.Types.STAFF) 78 | 79 | end_user = self.User.objects.create_user( 80 | email="enduser@user.com", password="foo", type=self.User.Types.ENDUSER 81 | ) 82 | self.assertEqual(end_user.type, self.User.Types.ENDUSER) 83 | 84 | def test_profile_creation_on_save(self): 85 | # Test profile creation based on type 86 | staff_user = self.User.objects.create_user( 87 | email="staff@user.com", password="foo", type=self.User.Types.STAFF 88 | ) 89 | self.assertTrue(StaffUserProfile.objects.filter(user=staff_user).exists()) 90 | self.assertFalse(EndUserProfile.objects.filter(user=staff_user).exists()) 91 | 92 | end_user = self.User.objects.create_user( 93 | email="enduser@user.com", password="foo", type=self.User.Types.ENDUSER 94 | ) 95 | self.assertTrue(EndUserProfile.objects.filter(user=end_user).exists()) 96 | self.assertFalse(StaffUserProfile.objects.filter(user=end_user).exists()) -------------------------------------------------------------------------------- /users/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | --------------------------------------------------------------------------------