├── .gitignore ├── README.md ├── course-platform.code-workspace ├── local-cdn └── .gitkeep ├── requirements.txt └── src ├── cfehome ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py ├── views.py └── wsgi.py ├── courses ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_alter_course_access.py │ ├── 0003_alter_course_image.py │ ├── 0004_lesson.py │ ├── 0005_lesson_can_preview_lesson_status.py │ ├── 0006_lesson_thumbnail_lesson_video.py │ ├── 0007_alter_lesson_options_lesson_order.py │ ├── 0008_lesson_updated.py │ ├── 0009_alter_lesson_options_course_timestamp_course_updated_and_more.py │ ├── 0010_course_public_id.py │ ├── 0011_lesson_public_id.py │ ├── 0012_alter_course_public_id_alter_lesson_public_id.py │ └── __init__.py ├── models.py ├── services.py ├── tests.py ├── urls.py └── views.py ├── emails ├── __init__.py ├── admin.py ├── apps.py ├── css.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_email_active.py │ ├── 0003_emailverificationevent_token.py │ └── __init__.py ├── models.py ├── services.py ├── tests.py └── views.py ├── helpers ├── __init__.py └── _cloudinary │ ├── __init__.py │ ├── config.py │ └── services.py ├── manage.py ├── templates ├── admin │ └── base_site.html ├── auth │ └── login-logout.html ├── base.html ├── base │ ├── hero.html │ ├── js.html │ └── navbar.html ├── courses │ ├── detail.html │ ├── email-required.html │ ├── lesson-coming-soon.html │ ├── lesson.html │ ├── list.html │ └── snippets │ │ └── list-display.html ├── emails │ └── hx │ │ ├── form.html │ │ └── logout-btn.html ├── home.html └── videos │ └── snippets │ └── embed.html └── theme ├── __init__.py ├── apps.py ├── static_src ├── .gitignore ├── package-lock.json ├── package.json ├── postcss.config.js ├── src │ └── styles.css └── tailwind.config.js └── templates └── base.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | local-cdn/media/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 113 | .pdm.toml 114 | .pdm-python 115 | .pdm-build/ 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | #.idea/ 166 | 167 | 168 | 169 | # Logs 170 | logs 171 | *.log 172 | npm-debug.log* 173 | yarn-debug.log* 174 | yarn-error.log* 175 | lerna-debug.log* 176 | .pnpm-debug.log* 177 | 178 | # Diagnostic reports (https://nodejs.org/api/report.html) 179 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 180 | 181 | # Runtime data 182 | pids 183 | *.pid 184 | *.seed 185 | *.pid.lock 186 | 187 | # Directory for instrumented libs generated by jscoverage/JSCover 188 | lib-cov 189 | 190 | # Coverage directory used by tools like istanbul 191 | coverage 192 | *.lcov 193 | 194 | # nyc test coverage 195 | .nyc_output 196 | 197 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 198 | .grunt 199 | 200 | # Bower dependency directory (https://bower.io/) 201 | bower_components 202 | 203 | # node-waf configuration 204 | .lock-wscript 205 | 206 | # Compiled binary addons (https://nodejs.org/api/addons.html) 207 | build/Release 208 | 209 | # Dependency directories 210 | node_modules/ 211 | jspm_packages/ 212 | 213 | # Snowpack dependency directory (https://snowpack.dev/) 214 | web_modules/ 215 | 216 | # TypeScript cache 217 | *.tsbuildinfo 218 | 219 | # Optional npm cache directory 220 | .npm 221 | 222 | # Optional eslint cache 223 | .eslintcache 224 | 225 | # Optional stylelint cache 226 | .stylelintcache 227 | 228 | # Microbundle cache 229 | .rpt2_cache/ 230 | .rts2_cache_cjs/ 231 | .rts2_cache_es/ 232 | .rts2_cache_umd/ 233 | 234 | # Optional REPL history 235 | .node_repl_history 236 | 237 | # Output of 'npm pack' 238 | *.tgz 239 | 240 | # Yarn Integrity file 241 | .yarn-integrity 242 | 243 | # dotenv environment variable files 244 | .env 245 | .env.development.local 246 | .env.test.local 247 | .env.production.local 248 | .env.local 249 | 250 | # parcel-bundler cache (https://parceljs.org/) 251 | .cache 252 | .parcel-cache 253 | 254 | # Next.js build output 255 | .next 256 | out 257 | 258 | # Nuxt.js build / generate output 259 | .nuxt 260 | dist 261 | 262 | # Gatsby files 263 | .cache/ 264 | # Comment in the public line in if your project uses Gatsby and not Next.js 265 | # https://nextjs.org/blog/next-9-1#public-directory-support 266 | # public 267 | 268 | # vuepress build output 269 | .vuepress/dist 270 | 271 | # vuepress v2.x temp and cache directory 272 | .temp 273 | .cache 274 | 275 | # Docusaurus cache and generated files 276 | .docusaurus 277 | 278 | # Serverless directories 279 | .serverless/ 280 | 281 | # FuseBox cache 282 | .fusebox/ 283 | 284 | # DynamoDB Local files 285 | .dynamodb/ 286 | 287 | # TernJS port file 288 | .tern-port 289 | 290 | # Stores VSCode versions used for testing VSCode extensions 291 | .vscode-test 292 | 293 | # yarn v2 294 | .yarn/cache 295 | .yarn/unplugged 296 | .yarn/build-state.yml 297 | .yarn/install-state.gz 298 | .pnp.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building a Course Platform 2 | 3 | Watch the tutorial [here](https://youtu.be/I_IchaIdmnA) 4 | 5 | ## Tech Stack 6 | 7 | Tech Stack: 8 | 9 | - [Django](https://djangoproject.com) v5.1 10 | - [Python](https://python.org) v3.12 11 | - [HTMX](https://htmx.org) 12 | - [django-htmx](https://github.com/adamchainz/django-htmx) 13 | - [tailwind](https://tailwindcss.com) 14 | - [django-tailwind](https://django-tailwind.readthedocs.io/en/latest/installation.html) 15 | - [Flowbite](https://flowbite.com) 16 | - [Cloudinary](https://cld.media/cfe) 17 | 18 | ## Overview 19 | What we are building 20 | 21 | - Courses: 22 | - Title 23 | - Description 24 | - Thumbnail/Image 25 | - Access: 26 | - Anyone 27 | - Email required 28 | - Purchase required 29 | - User required (n/a) 30 | - Status: 31 | - Published 32 | - Coming Soon 33 | - Draft 34 | - Lessons 35 | - Title 36 | - Description 37 | - Video 38 | - Status: Published, Coming Soon, Draft 39 | 40 | 41 | - Email verification for short-lived access 42 | - Views: 43 | - Collect user email 44 | - Verify user email 45 | - Activate session 46 | - Models: 47 | - Email 48 | - EmailVerificationToken 49 | -------------------------------------------------------------------------------- /course-platform.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /local-cdn/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Course-Platform/31ce5bca89ee2d0b2f07286ca32a089cc8c7967e/local-cdn/.gitkeep -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=5.1,<5.2 2 | pillow 3 | cloudinary 4 | python-decouple 5 | django-htmx 6 | django-tailwind[reload] -------------------------------------------------------------------------------- /src/cfehome/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Course-Platform/31ce5bca89ee2d0b2f07286ca32a089cc8c7967e/src/cfehome/__init__.py -------------------------------------------------------------------------------- /src/cfehome/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for cfehome 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", "cfehome.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /src/cfehome/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for cfehome project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.1.1. 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 | 13 | from pathlib import Path 14 | 15 | from decouple import config # os.environ.get() 16 | 17 | 18 | from decouple import config 19 | 20 | BASE_URL = config("BASE_URL", default='http://127.0.0.1:8000') 21 | # default backend 22 | # EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 23 | EMAIL_HOST = config("EMAIL_HOST", cast=str, default=None) 24 | EMAIL_PORT = config("EMAIL_PORT", cast=str, default='587') # Recommended 25 | EMAIL_ADDRESS = "hungrypy@gmail.com" 26 | EMAIL_HOST_USER = config("EMAIL_HOST_USER", cast=str, default=None) 27 | EMAIL_HOST_PASSWORD = config("EMAIL_HOST_PASSWORD", cast=str, default=None) 28 | EMAIL_USE_TLS = config("EMAIL_USE_TLS", cast=bool, default=True) # Use EMAIL_PORT 587 for TLS 29 | 30 | ADMIN_USER_NAME=config("ADMIN_USER_NAME", default="Justin") 31 | ADMIN_USER_EMAIL=config("ADMIN_USER_EMAIL", default=None) 32 | 33 | MANAGERS=[] 34 | ADMINS=[] 35 | if all([ADMIN_USER_NAME, ADMIN_USER_EMAIL]): 36 | ADMINS +=[ 37 | (f'{ADMIN_USER_NAME}', f'{ADMIN_USER_EMAIL}') 38 | ] 39 | MANAGERS=ADMINS 40 | 41 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 42 | BASE_DIR = Path(__file__).resolve().parent.parent 43 | LOCAL_CDN = BASE_DIR.parent / "local-cdn" 44 | TEMPLATE_DIR = BASE_DIR / "templates" 45 | 46 | # Quick-start development settings - unsuitable for production 47 | # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ 48 | 49 | # SECURITY WARNING: keep the secret key used in production secret! 50 | SECRET_KEY = "django-insecure-)m9pn5k+cz^l1pos#-_dr@nqcmqtynn1xo9f$b=6uney85a+cl" 51 | 52 | # SECURITY WARNING: don't run with debug turned on in production! 53 | DEBUG = True 54 | 55 | ALLOWED_HOSTS = [] 56 | 57 | 58 | # Application definition 59 | 60 | INSTALLED_APPS = [ 61 | "django.contrib.admin", 62 | "django.contrib.auth", 63 | "django.contrib.contenttypes", 64 | "django.contrib.sessions", 65 | "django.contrib.messages", 66 | "django.contrib.staticfiles", 67 | # third party 68 | "django_htmx", 69 | "tailwind", 70 | "theme", # django-tailwind theme app 71 | # internal 72 | "courses", 73 | "emails" 74 | ] 75 | 76 | TAILWIND_APP_NAME="theme" # django-tailwind theme app 77 | INTERNAL_IPS = [ 78 | "0.0.0.0", 79 | "127.0.0.1", 80 | ] 81 | 82 | MIDDLEWARE = [ 83 | "django.middleware.security.SecurityMiddleware", 84 | "django.contrib.sessions.middleware.SessionMiddleware", 85 | "django.middleware.common.CommonMiddleware", 86 | "django.middleware.csrf.CsrfViewMiddleware", 87 | "django.contrib.auth.middleware.AuthenticationMiddleware", 88 | "django.contrib.messages.middleware.MessageMiddleware", 89 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 90 | "django_htmx.middleware.HtmxMiddleware", 91 | ] 92 | 93 | if DEBUG: 94 | # django-tailwind theme app 95 | INSTALLED_APPS.append('django_browser_reload') 96 | MIDDLEWARE.append("django_browser_reload.middleware.BrowserReloadMiddleware") 97 | 98 | ROOT_URLCONF = "cfehome.urls" 99 | 100 | TEMPLATES = [ 101 | { 102 | "BACKEND": "django.template.backends.django.DjangoTemplates", 103 | "DIRS": [ 104 | TEMPLATE_DIR, 105 | ], 106 | "APP_DIRS": True, 107 | "OPTIONS": { 108 | "context_processors": [ 109 | "django.template.context_processors.debug", 110 | "django.template.context_processors.request", 111 | "django.contrib.auth.context_processors.auth", 112 | "django.contrib.messages.context_processors.messages", 113 | ], 114 | }, 115 | }, 116 | ] 117 | 118 | WSGI_APPLICATION = "cfehome.wsgi.application" 119 | 120 | 121 | # Database 122 | # https://docs.djangoproject.com/en/5.1/ref/settings/#databases 123 | 124 | DATABASES = { 125 | "default": { 126 | "ENGINE": "django.db.backends.sqlite3", 127 | "NAME": BASE_DIR / "db.sqlite3", 128 | } 129 | } 130 | 131 | 132 | # Password validation 133 | # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators 134 | 135 | AUTH_PASSWORD_VALIDATORS = [ 136 | { 137 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 138 | }, 139 | { 140 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 141 | }, 142 | { 143 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 144 | }, 145 | { 146 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 147 | }, 148 | ] 149 | 150 | 151 | # Internationalization 152 | # https://docs.djangoproject.com/en/5.1/topics/i18n/ 153 | 154 | LANGUAGE_CODE = "en-us" 155 | 156 | TIME_ZONE = "UTC" 157 | 158 | USE_I18N = True 159 | 160 | USE_TZ = True 161 | 162 | 163 | # Static files (CSS, JavaScript, Images) 164 | # https://docs.djangoproject.com/en/5.1/howto/static-files/ 165 | 166 | # whitenoise/nginx 167 | STATIC_URL = "static/" 168 | 169 | # nginx 170 | MEDIA_URL = "media/" 171 | MEDIA_ROOT = LOCAL_CDN / "media" 172 | 173 | # Default primary key field type 174 | # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field 175 | 176 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 177 | 178 | 179 | # cloudinary video config 180 | CLOUDINARY_CLOUD_NAME = config("CLOUDINARY_CLOUD_NAME", default="") 181 | CLOUDINARY_PUBLIC_API_KEY = config("CLOUDINARY_PUBLIC_API_KEY", default="") 182 | CLOUDINARY_SECRET_API_KEY= config("CLOUDINARY_SECRET_API_KEY") 183 | -------------------------------------------------------------------------------- /src/cfehome/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for cfehome project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/5.1/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.conf import settings 18 | from django.contrib import admin 19 | from django.urls import path, include 20 | from django.conf.urls.static import static 21 | 22 | from emails.views import verify_email_token_view, email_token_login_view, logout_btn_hx_view 23 | from . import views 24 | 25 | 26 | urlpatterns = [ 27 | path("", views.home_view), 28 | path("login/", views.login_logout_template_view), 29 | path("logout/", views.login_logout_template_view), 30 | path('hx/login/', email_token_login_view), 31 | path('hx/logout/', logout_btn_hx_view), 32 | path('verify//', verify_email_token_view), 33 | path("courses/", include("courses.urls")), 34 | path("admin/", admin.site.urls), 35 | ] 36 | 37 | if settings.DEBUG: 38 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 39 | urlpatterns += [ 40 | path("__reload__/", include("django_browser_reload.urls")), 41 | ] 42 | -------------------------------------------------------------------------------- /src/cfehome/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.shortcuts import render 3 | 4 | from emails import services as emails_services 5 | from emails.models import Email, EmailVerificationEvent 6 | from emails.forms import EmailForm 7 | 8 | 9 | def login_logout_template_view(request): 10 | return render(request, "auth/login-logout.html", {}) 11 | 12 | EMAIL_ADDRESS = settings.EMAIL_ADDRESS 13 | def home_view(request, *args, **kwargs): 14 | template_name = "home.html" 15 | # request POST data 16 | print(request.POST) 17 | form = EmailForm(request.POST or None) 18 | context = { 19 | "form": form, 20 | "message": "" 21 | } 22 | if form.is_valid(): 23 | email_val = form.cleaned_data.get('email') 24 | obj = emails_services.start_verification_event(email_val) 25 | print(obj) 26 | context['form'] = EmailForm() 27 | context['message'] = f"Succcess! Check your email for verification from {EMAIL_ADDRESS}" 28 | else: 29 | print(form.errors) 30 | print('email_id', request.session.get('email_id')) 31 | return render(request, template_name, context) -------------------------------------------------------------------------------- /src/cfehome/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for cfehome 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", "cfehome.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /src/courses/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Course-Platform/31ce5bca89ee2d0b2f07286ca32a089cc8c7967e/src/courses/__init__.py -------------------------------------------------------------------------------- /src/courses/admin.py: -------------------------------------------------------------------------------- 1 | import helpers 2 | from cloudinary import CloudinaryImage 3 | from django.contrib import admin 4 | from django.utils.html import format_html 5 | # Register your models here. 6 | from .models import Course, Lesson 7 | 8 | 9 | class LessonInline(admin.StackedInline): 10 | model = Lesson 11 | readonly_fields = [ 12 | 'public_id', 13 | 'updated', 14 | 'display_image', 15 | 'display_video', 16 | ] 17 | extra = 0 18 | 19 | def display_image(self, obj, *args, **kwargs): 20 | url = helpers.get_cloudinary_image_object( 21 | obj, 22 | field_name='thumbnail', 23 | width=200 24 | ) 25 | return format_html(f"") 26 | 27 | display_image.short_description = "Current Image" 28 | 29 | def display_video(self, obj, *args, **kwargs): 30 | video_embed_html = helpers.get_cloudinary_video_object( 31 | obj, 32 | field_name='video', 33 | as_html=True, 34 | width=550 35 | ) 36 | return video_embed_html 37 | 38 | display_video.short_description = "Current Video" 39 | 40 | 41 | @admin.register(Course) 42 | class CourseAdmin(admin.ModelAdmin): 43 | inlines = [LessonInline] 44 | list_display = ['title', 'status', 'access'] 45 | list_filter = ['status', 'access'] 46 | fields = ['public_id', 'title', 'description', 'status', 'image', 'access', 'display_image'] 47 | readonly_fields = ['public_id', 'display_image'] 48 | 49 | def display_image(self, obj, *args, **kwargs): 50 | url = helpers.get_cloudinary_image_object( 51 | obj, 52 | field_name='image', 53 | width=200 54 | ) 55 | return format_html(f"") 56 | 57 | display_image.short_description = "Current Image" 58 | 59 | 60 | # admin.site.register(Course, CourseAdmin) -------------------------------------------------------------------------------- /src/courses/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoursesConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "courses" 7 | -------------------------------------------------------------------------------- /src/courses/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-10 17:27 2 | 3 | import courses.models 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Course", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("title", models.CharField(max_length=120)), 26 | ("description", models.TextField(blank=True, null=True)), 27 | ( 28 | "image", 29 | models.ImageField( 30 | blank=True, null=True, upload_to=courses.models.handle_upload 31 | ), 32 | ), 33 | ( 34 | "access", 35 | models.CharField( 36 | choices=[ 37 | ("any", "Anyone"), 38 | ("email_required", "Email required"), 39 | ], 40 | default="email_required", 41 | max_length=14, 42 | ), 43 | ), 44 | ( 45 | "status", 46 | models.CharField( 47 | choices=[ 48 | ("publish", "Published"), 49 | ("soon", "Coming Soon"), 50 | ("draft", "Draft"), 51 | ], 52 | default="draft", 53 | max_length=10, 54 | ), 55 | ), 56 | ], 57 | ), 58 | ] 59 | -------------------------------------------------------------------------------- /src/courses/migrations/0002_alter_course_access.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-10 17:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("courses", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="course", 14 | name="access", 15 | field=models.CharField( 16 | choices=[("any", "Anyone"), ("email", "Email required")], 17 | default="email", 18 | max_length=5, 19 | ), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /src/courses/migrations/0003_alter_course_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-10 19:16 2 | 3 | import cloudinary.models 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("courses", "0002_alter_course_access"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="course", 15 | name="image", 16 | field=cloudinary.models.CloudinaryField( 17 | max_length=255, null=True, verbose_name="image" 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/courses/migrations/0004_lesson.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-11 19:46 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("courses", "0003_alter_course_image"), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Lesson", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("title", models.CharField(max_length=120)), 26 | ("description", models.TextField(blank=True, null=True)), 27 | ( 28 | "course", 29 | models.ForeignKey( 30 | on_delete=django.db.models.deletion.CASCADE, to="courses.course" 31 | ), 32 | ), 33 | ], 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /src/courses/migrations/0005_lesson_can_preview_lesson_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-11 19:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("courses", "0004_lesson"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="lesson", 14 | name="can_preview", 15 | field=models.BooleanField( 16 | default=False, 17 | help_text="If user does not have access to course, can they see this?", 18 | ), 19 | ), 20 | migrations.AddField( 21 | model_name="lesson", 22 | name="status", 23 | field=models.CharField( 24 | choices=[ 25 | ("publish", "Published"), 26 | ("soon", "Coming Soon"), 27 | ("draft", "Draft"), 28 | ], 29 | default="publish", 30 | max_length=10, 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /src/courses/migrations/0006_lesson_thumbnail_lesson_video.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-11 20:02 2 | 3 | import cloudinary.models 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("courses", "0005_lesson_can_preview_lesson_status"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="lesson", 15 | name="thumbnail", 16 | field=cloudinary.models.CloudinaryField( 17 | blank=True, max_length=255, null=True, verbose_name="image" 18 | ), 19 | ), 20 | migrations.AddField( 21 | model_name="lesson", 22 | name="video", 23 | field=cloudinary.models.CloudinaryField( 24 | blank=True, max_length=255, null=True, verbose_name="video" 25 | ), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /src/courses/migrations/0007_alter_lesson_options_lesson_order.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-11 20:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("courses", "0006_lesson_thumbnail_lesson_video"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="lesson", 14 | options={"ordering": ["order"]}, 15 | ), 16 | migrations.AddField( 17 | model_name="lesson", 18 | name="order", 19 | field=models.IntegerField(default=0), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /src/courses/migrations/0008_lesson_updated.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-11 20:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("courses", "0007_alter_lesson_options_lesson_order"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="lesson", 14 | name="updated", 15 | field=models.DateTimeField(auto_now=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/courses/migrations/0009_alter_lesson_options_course_timestamp_course_updated_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-11 20:23 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("courses", "0008_lesson_updated"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="lesson", 15 | options={"ordering": ["order", "-updated"]}, 16 | ), 17 | migrations.AddField( 18 | model_name="course", 19 | name="timestamp", 20 | field=models.DateTimeField( 21 | auto_now_add=True, default=django.utils.timezone.now 22 | ), 23 | preserve_default=False, 24 | ), 25 | migrations.AddField( 26 | model_name="course", 27 | name="updated", 28 | field=models.DateTimeField(auto_now=True), 29 | ), 30 | migrations.AddField( 31 | model_name="lesson", 32 | name="timestamp", 33 | field=models.DateTimeField( 34 | auto_now_add=True, default=django.utils.timezone.now 35 | ), 36 | preserve_default=False, 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /src/courses/migrations/0010_course_public_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-11 21:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ( 9 | "courses", 10 | "0009_alter_lesson_options_course_timestamp_course_updated_and_more", 11 | ), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name="course", 17 | name="public_id", 18 | field=models.CharField(blank=True, max_length=130, null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/courses/migrations/0011_lesson_public_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-11 21:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("courses", "0010_course_public_id"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="lesson", 14 | name="public_id", 15 | field=models.CharField(blank=True, max_length=130, null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/courses/migrations/0012_alter_course_public_id_alter_lesson_public_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-13 21:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("courses", "0011_lesson_public_id"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="course", 14 | name="public_id", 15 | field=models.CharField( 16 | blank=True, db_index=True, max_length=130, null=True 17 | ), 18 | ), 19 | migrations.AlterField( 20 | model_name="lesson", 21 | name="public_id", 22 | field=models.CharField( 23 | blank=True, db_index=True, max_length=130, null=True 24 | ), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /src/courses/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Course-Platform/31ce5bca89ee2d0b2f07286ca32a089cc8c7967e/src/courses/migrations/__init__.py -------------------------------------------------------------------------------- /src/courses/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import helpers 3 | from django.db import models 4 | from django.utils.text import slugify 5 | from cloudinary.models import CloudinaryField 6 | 7 | helpers.cloudinary_init() 8 | 9 | class AccessRequirement(models.TextChoices): 10 | ANYONE = "any", "Anyone" 11 | EMAIL_REQUIRED = "email", "Email required" 12 | 13 | class PublishStatus(models.TextChoices): 14 | PUBLISHED = "publish", "Published" 15 | COMING_SOON = "soon", "Coming Soon" 16 | DRAFT = "draft", "Draft" 17 | 18 | 19 | def handle_upload(instance, filename): 20 | return f"{filename}" 21 | 22 | # from courses.models import Course 23 | # Course.objects.all() -> list out all courses 24 | # Course.objects.first() -> first row of all courses 25 | 26 | def generate_public_id(instance, *args, **kwargs): 27 | title = instance.title 28 | unique_id = str(uuid.uuid4()).replace("-", "") 29 | if not title: 30 | return unique_id 31 | slug = slugify(title) 32 | unique_id_short = unique_id[:5] 33 | return f"{slug}-{unique_id_short}" 34 | 35 | 36 | def get_public_id_prefix(instance, *args, **kwargs): 37 | if hasattr(instance, 'path'): 38 | path = instance.path 39 | if path.startswith("/"): 40 | path = path[1:] 41 | if path.endswith('/'): 42 | path = path[:-1] 43 | return path 44 | public_id = instance.public_id 45 | model_class = instance.__class__ 46 | model_name = model_class.__name__ 47 | model_name_slug = slugify(model_name) 48 | if not public_id: 49 | return f"{model_name_slug}" 50 | return f"{model_name_slug}/{public_id}" 51 | 52 | def get_display_name(instance, *args, **kwargs): 53 | if hasattr(instance, 'get_display_name'): 54 | return instance.get_display_name() 55 | elif hasattr(instance, 'title'): 56 | return instance.title 57 | model_class = instance.__class__ 58 | model_name = model_class.__name__ 59 | return f"{model_name} Upload" 60 | 61 | # get_thumbnail_display_name = lambda instance: get_display_name(instance, is_thumbnail=True) 62 | 63 | class Course(models.Model): 64 | title = models.CharField(max_length=120) 65 | description = models.TextField(blank=True, null=True) 66 | # uuid = models.UUIDField(default=uuid.uuid1, unique=True) 67 | public_id = models.CharField(max_length=130, blank=True, null=True, db_index=True) 68 | # image = models.ImageField(upload_to=handle_upload, blank=True, null=True) 69 | image = CloudinaryField( 70 | "image", 71 | null=True, 72 | public_id_prefix=get_public_id_prefix, 73 | display_name=get_display_name, 74 | tags=["course", "thumbnail"] 75 | ) 76 | access = models.CharField( 77 | max_length=5, 78 | choices=AccessRequirement.choices, 79 | default=AccessRequirement.EMAIL_REQUIRED 80 | ) 81 | status = models.CharField( 82 | max_length=10, 83 | choices=PublishStatus.choices, 84 | default=PublishStatus.DRAFT 85 | ) 86 | timestamp = models.DateTimeField(auto_now_add=True) 87 | updated = models.DateTimeField(auto_now=True) 88 | 89 | def save(self, *args, **kwargs): 90 | # before save 91 | if self.public_id == "" or self.public_id is None: 92 | self.public_id = generate_public_id(self) 93 | super().save(*args, **kwargs) 94 | # after save 95 | 96 | def get_absolute_url(self): 97 | return self.path 98 | 99 | @property 100 | def path(self): 101 | return f"/courses/{self.public_id}" 102 | 103 | def get_display_name(self): 104 | return f"{self.title} - Course" 105 | 106 | def get_thumbnail(self): 107 | if not self.image: 108 | return None 109 | return helpers.get_cloudinary_image_object( 110 | self, 111 | field_name='image', 112 | as_html=False, 113 | width=382 114 | ) 115 | 116 | def get_display_image(self): 117 | if not self.image: 118 | return None 119 | return helpers.get_cloudinary_image_object( 120 | self, 121 | field_name='image', 122 | as_html=False, 123 | width=750 124 | ) 125 | 126 | @property 127 | def is_published(self): 128 | return self.status == PublishStatus.PUBLISHED 129 | 130 | 131 | """ 132 | - Lessons 133 | - Title 134 | - Description 135 | - Video 136 | - Status: Published, Coming Soon, Draft 137 | """ 138 | 139 | # Lesson.objects.all() # lesson queryset -> all rows 140 | # Lesson.objects.first() 141 | # course_obj = Course.objects.first() 142 | # course_qs = Course.objects.filter(id=course_obj.id) 143 | # Lesson.objects.filter(course__id=course_obj.id) 144 | # course_obj.lesson_set.all() 145 | # lesson_obj = Lesson.objects.first() 146 | # ne_course_obj = lesson_obj.course 147 | # ne_course_lessons = ne_course_obj.lesson_set.all() 148 | # lesson_obj.course_id 149 | # course_obj.lesson_set.all().order_by("-title") 150 | 151 | class Lesson(models.Model): 152 | course = models.ForeignKey(Course, on_delete=models.CASCADE) 153 | # course_id 154 | public_id = models.CharField(max_length=130, blank=True, null=True, db_index=True) 155 | title = models.CharField(max_length=120) 156 | description = models.TextField(blank=True, null=True) 157 | thumbnail = CloudinaryField("image", 158 | public_id_prefix=get_public_id_prefix, 159 | display_name=get_display_name, 160 | tags = ['thumbnail', 'lesson'], 161 | blank=True, null=True) 162 | video = CloudinaryField("video", 163 | public_id_prefix=get_public_id_prefix, 164 | display_name=get_display_name, 165 | blank=True, 166 | null=True, 167 | type='private', 168 | tags = ['video', 'lesson'], 169 | resource_type='video') 170 | order = models.IntegerField(default=0) 171 | can_preview = models.BooleanField(default=False, help_text="If user does not have access to course, can they see this?") 172 | status = models.CharField( 173 | max_length=10, 174 | choices=PublishStatus.choices, 175 | default=PublishStatus.PUBLISHED 176 | ) 177 | timestamp = models.DateTimeField(auto_now_add=True) 178 | updated = models.DateTimeField(auto_now=True) 179 | 180 | class Meta: 181 | ordering = ['order', '-updated'] 182 | 183 | def save(self, *args, **kwargs): 184 | # before save 185 | if self.public_id == "" or self.public_id is None: 186 | self.public_id = generate_public_id(self) 187 | super().save(*args, **kwargs) 188 | 189 | def get_absolute_url(self): 190 | return self.path 191 | 192 | @property 193 | def path(self): 194 | course_path = self.course.path 195 | if course_path.endswith("/"): 196 | course_path = course_path[:-1] 197 | return f"{course_path}/lessons/{self.public_id}" 198 | 199 | @property 200 | def requires_email(self): 201 | return self.course.access == AccessRequirement.EMAIL_REQUIRED 202 | 203 | def get_display_name(self): 204 | return f"{self.title} - {self.course.get_display_name()}" 205 | 206 | @property 207 | def is_coming_soon(self): 208 | return self.status == PublishStatus.COMING_SOON 209 | 210 | @property 211 | def has_video(self): 212 | return self.video is not None 213 | 214 | def get_thumbnail(self): 215 | width = 382 216 | if self.thumbnail: 217 | return helpers.get_cloudinary_image_object( 218 | self, 219 | field_name='thumbnail', 220 | format='jpg', 221 | as_html=False, 222 | width=width 223 | ) 224 | elif self.video: 225 | return helpers.get_cloudinary_image_object( 226 | self, 227 | field_name='video', 228 | format='jpg', 229 | as_html=False, 230 | width=width 231 | ) 232 | return -------------------------------------------------------------------------------- /src/courses/services.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q 2 | from .models import Course, Lesson, PublishStatus 3 | 4 | 5 | def get_publish_courses(): 6 | return Course.objects.filter(status=PublishStatus.PUBLISHED) 7 | 8 | def get_course_detail(course_id=None): 9 | if course_id is None: 10 | return None 11 | obj = None 12 | try: 13 | obj = Course.objects.get( 14 | status=PublishStatus.PUBLISHED, 15 | public_id=course_id 16 | ) 17 | except: 18 | pass 19 | return obj 20 | 21 | def get_course_lessons(course_obj=None): 22 | lessons = Lesson.objects.none() 23 | if not isinstance(course_obj, Course): 24 | return lessons 25 | lessons = course_obj.lesson_set.filter( 26 | course__status=PublishStatus.PUBLISHED, 27 | status__in=[PublishStatus.PUBLISHED, PublishStatus.COMING_SOON] 28 | ) 29 | return lessons 30 | 31 | 32 | def get_lesson_detail(course_id=None, lesson_id=None): 33 | if lesson_id is None and course_id is None: 34 | return None 35 | obj = None 36 | try: 37 | obj = Lesson.objects.get( 38 | course__public_id=course_id, 39 | course__status=PublishStatus.PUBLISHED, 40 | status__in=[PublishStatus.PUBLISHED, PublishStatus.COMING_SOON], 41 | public_id=lesson_id 42 | ) 43 | except Exception as e: 44 | print("lesson detail", e) 45 | pass 46 | return obj -------------------------------------------------------------------------------- /src/courses/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/courses/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path("/lessons//", views.lesson_detail_view), 7 | path("/", views.course_detail_view), 8 | path("", views.course_list_view), 9 | ] 10 | -------------------------------------------------------------------------------- /src/courses/views.py: -------------------------------------------------------------------------------- 1 | import helpers 2 | from django.http import Http404, JsonResponse 3 | from django.shortcuts import render, redirect 4 | 5 | from . import services 6 | 7 | def course_list_view(request): 8 | queryset = services.get_publish_courses() 9 | context = { 10 | "object_list": queryset 11 | } 12 | template_name = "courses/list.html" 13 | if request.htmx: 14 | template_name = "courses/snippets/list-display.html" 15 | context['queryset'] = queryset[:3] 16 | return render(request, template_name, context) 17 | 18 | 19 | def course_detail_view(request, course_id=None, *args, **kwarg): 20 | course_obj = services.get_course_detail(course_id=course_id) 21 | if course_obj is None: 22 | raise Http404 23 | lessons_queryset = services.get_course_lessons(course_obj) 24 | context = { 25 | "object": course_obj, 26 | "lessons_queryset": lessons_queryset, 27 | } 28 | # return JsonResponse({"data": course_obj.id, 'lesson_ids': [x.path for x in lessons_queryset] }) 29 | return render(request, "courses/detail.html", context) 30 | 31 | 32 | def lesson_detail_view(request, course_id=None, lesson_id=None, *args, **kwargs): 33 | print(course_id, lesson_id) 34 | lesson_obj = services.get_lesson_detail( 35 | course_id=course_id, 36 | lesson_id=lesson_id 37 | ) 38 | if lesson_obj is None: 39 | raise Http404 40 | email_id_exists = request.session.get('email_id') 41 | if lesson_obj.requires_email and not email_id_exists: 42 | print(request.path) 43 | request.session['next_url'] = request.path 44 | return render(request, "courses/email-required.html", {}) 45 | # template_name = "courses/purchase-required.html" 46 | template_name = "courses/lesson-coming-soon.html" 47 | context = { 48 | "object": lesson_obj 49 | } 50 | if not lesson_obj.is_coming_soon and lesson_obj.has_video: 51 | """ 52 | Lesson is published 53 | Video is available 54 | go forward 55 | """ 56 | template_name = "courses/lesson.html" 57 | video_embed_html = helpers.get_cloudinary_video_object( 58 | lesson_obj, 59 | field_name='video', 60 | as_html=True, 61 | width=1250 62 | ) 63 | context['video_embed'] = video_embed_html 64 | return render(request, template_name, context) -------------------------------------------------------------------------------- /src/emails/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Course-Platform/31ce5bca89ee2d0b2f07286ca32a089cc8c7967e/src/emails/__init__.py -------------------------------------------------------------------------------- /src/emails/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import Email, EmailVerificationEvent 5 | 6 | admin.site.register(Email) 7 | admin.site.register(EmailVerificationEvent) -------------------------------------------------------------------------------- /src/emails/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class EmailsConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "emails" 7 | -------------------------------------------------------------------------------- /src/emails/css.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | EMAIL_FIELD_CSS = "bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" -------------------------------------------------------------------------------- /src/emails/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from . import css, services 4 | 5 | 6 | class EmailForm(forms.Form): 7 | email = forms.EmailField( 8 | widget=forms.EmailInput( 9 | attrs = { 10 | "id": "email-login-input", 11 | "class": css.EMAIL_FIELD_CSS, 12 | "placeholder": "your email login" 13 | } 14 | ) 15 | ) 16 | # class Meta: 17 | # model = EmailVerificationEvent 18 | # fields = ['email'] 19 | def clean_email(self): 20 | email = self.cleaned_data.get('email') 21 | verified = services.verify_email(email) 22 | if verified: 23 | raise forms.ValidationError("Invalid email. Please try again") 24 | return email -------------------------------------------------------------------------------- /src/emails/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-17 17:25 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Email", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("email", models.EmailField(max_length=254, unique=True)), 26 | ("timestamp", models.DateTimeField(auto_now_add=True)), 27 | ], 28 | ), 29 | migrations.CreateModel( 30 | name="EmailVerificationEvent", 31 | fields=[ 32 | ( 33 | "id", 34 | models.BigAutoField( 35 | auto_created=True, 36 | primary_key=True, 37 | serialize=False, 38 | verbose_name="ID", 39 | ), 40 | ), 41 | ("email", models.EmailField(max_length=254)), 42 | ("attempts", models.IntegerField(default=0)), 43 | ("last_attempt_at", models.DateTimeField(blank=True, null=True)), 44 | ("expired", models.BooleanField(default=False)), 45 | ("expired_at", models.DateTimeField(blank=True, null=True)), 46 | ("timestamp", models.DateTimeField(auto_now_add=True)), 47 | ( 48 | "parent", 49 | models.ForeignKey( 50 | null=True, 51 | on_delete=django.db.models.deletion.SET_NULL, 52 | to="emails.email", 53 | ), 54 | ), 55 | ], 56 | ), 57 | ] 58 | -------------------------------------------------------------------------------- /src/emails/migrations/0002_email_active.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-17 18:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("emails", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="email", 14 | name="active", 15 | field=models.BooleanField(default=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/emails/migrations/0003_emailverificationevent_token.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.1 on 2024-09-17 19:41 2 | 3 | import uuid 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("emails", "0002_email_active"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="emailverificationevent", 15 | name="token", 16 | field=models.UUIDField(default=uuid.uuid1), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/emails/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Course-Platform/31ce5bca89ee2d0b2f07286ca32a089cc8c7967e/src/emails/migrations/__init__.py -------------------------------------------------------------------------------- /src/emails/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.conf import settings 3 | from django.db import models 4 | 5 | # Create your models here. 6 | class Email(models.Model): 7 | email = models.EmailField(unique=True) 8 | active = models.BooleanField(default=True) 9 | timestamp = models.DateTimeField(auto_now_add=True) 10 | 11 | # class Purchase(models.Model): 12 | # email = models.ForeignKey(Email, on_delete=models.SET_NULL, null=True) 13 | # course = models.ForeignKey(Course, on_delete=models.SET_NULL, null=True) 14 | 15 | 16 | class EmailVerificationEvent(models.Model): 17 | parent = models.ForeignKey(Email, on_delete=models.SET_NULL, null=True) 18 | email = models.EmailField() 19 | # token 20 | token = models.UUIDField(default=uuid.uuid1) 21 | attempts = models.IntegerField(default=0) 22 | last_attempt_at = models.DateTimeField( 23 | auto_now=False, 24 | auto_now_add=False, 25 | blank=True, 26 | null=True 27 | ) 28 | expired = models.BooleanField(default=False) 29 | expired_at = models.DateTimeField( 30 | auto_now=False, 31 | auto_now_add=False, 32 | blank=True, 33 | null=True 34 | ) 35 | timestamp = models.DateTimeField(auto_now_add=True) 36 | 37 | 38 | def get_link(self): 39 | return f"{settings.BASE_URL}/verify/{self.token}/" -------------------------------------------------------------------------------- /src/emails/services.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.mail import send_mail 3 | from django.utils import timezone 4 | from .models import Email, EmailVerificationEvent 5 | 6 | EMAIL_HOST_USER = settings.EMAIL_HOST_USER 7 | 8 | def verify_email(email): 9 | qs = Email.objects.filter(email=email, active=False) 10 | return qs.exists() 11 | 12 | def get_verification_email_msg(verification_instance, as_html=False): 13 | if not isinstance(verification_instance, EmailVerificationEvent): 14 | return None 15 | verify_link = verification_instance.get_link() 16 | if as_html: 17 | return f"

Verify your email with the following

{verify_link}

" 18 | return f"Verify your email with the following:\n{verify_link}" 19 | 20 | 21 | def start_verification_event(email): 22 | email_obj, created = Email.objects.get_or_create(email=email) 23 | obj = EmailVerificationEvent.objects.create( 24 | parent=email_obj, 25 | email=email 26 | ) 27 | sent = send_verification_email(obj.id) 28 | return obj, sent 29 | 30 | # celery task -> background task 31 | def send_verification_email(verify_obj_id,): 32 | verify_obj = EmailVerificationEvent.objects.get(id=verify_obj_id) 33 | email = verify_obj.email 34 | subject = "Verify your email" 35 | text_msg = get_verification_email_msg(verify_obj, as_html=False) 36 | text_html = get_verification_email_msg(verify_obj, as_html=True) 37 | from_user_email_addr = EMAIL_HOST_USER 38 | to_user_email = email 39 | # send an verification email 40 | return send_mail( 41 | subject, 42 | text_msg, 43 | from_user_email_addr, 44 | [to_user_email], 45 | fail_silently=False, 46 | html_message=text_html 47 | ) 48 | 49 | 50 | def verify_token(token, max_attempts=5): 51 | qs = EmailVerificationEvent.objects.filter(token=token) 52 | if not qs.exists() and not qs.count() == 1: 53 | return False, "Invalid token", None 54 | """ 55 | Has token 56 | """ 57 | has_email_expired = qs.filter(expired=True) 58 | if has_email_expired.exists(): 59 | """ token exipred""" 60 | return False, "Token expired, try again.", None 61 | """ 62 | Has token, not expired 63 | """ 64 | max_attempts_reached = qs.filter(attempts__gte=max_attempts) 65 | if max_attempts_reached.exists(): 66 | """ update max attempts + 1""" 67 | # max_tempts_reached.update() 68 | return False, "Token expired, used too many times", None 69 | """Token valid""" 70 | """ update attempts, expire token if attempts > max""" 71 | obj = qs.first() 72 | obj.attempts += 1 73 | obj.last_attempt_at = timezone.now() 74 | if obj.attempts > max_attempts: 75 | """invalidation process""" 76 | obj.expired = True 77 | obj.expired_at = timezone.now() 78 | obj.save() 79 | email_obj = obj.parent # Email.objects.get() 80 | return True, "Welcome", email_obj -------------------------------------------------------------------------------- /src/emails/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/emails/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import messages 3 | from django.http import HttpResponse 4 | from django.shortcuts import render, redirect 5 | from django_htmx.http import HttpResponseClientRedirect 6 | 7 | from . import services 8 | 9 | from .forms import EmailForm 10 | 11 | EMAIL_ADDRESS = settings.EMAIL_ADDRESS 12 | 13 | 14 | def logout_btn_hx_view(request): 15 | if not request.htmx: 16 | return redirect('/') 17 | if request.method == "POST": 18 | try: 19 | del request.session['email_id'] 20 | except: 21 | pass 22 | email_id_in_session = request.session.get('email_id') 23 | if not email_id_in_session: 24 | return HttpResponseClientRedirect('/') 25 | return render(request, "emails/hx/logout-btn.html", {}) 26 | 27 | def email_token_login_view(request): 28 | if not request.htmx: 29 | return redirect('/') 30 | email_id_in_session = request.session.get('email_id') 31 | template_name = "emails/hx/form.html" 32 | form = EmailForm(request.POST or None) 33 | context = { 34 | "form": form, 35 | "message": "", 36 | "show_form": not email_id_in_session, 37 | } 38 | if form.is_valid(): 39 | email_val = form.cleaned_data.get('email') 40 | obj = services.start_verification_event(email_val) 41 | context['form'] = EmailForm() 42 | context['message'] = f"Succcess! Check your email for verification from {EMAIL_ADDRESS}" 43 | # return HttpResponseClientRedirect('/check-your-email') 44 | return render(request, template_name, context) 45 | else: 46 | print(form.errors) 47 | return render(request, template_name, context) 48 | 49 | # Create your views here. 50 | def verify_email_token_view(request, token, *args, **kwargs): 51 | did_verify, msg, email_obj = services.verify_token(token) 52 | if not did_verify: 53 | try: 54 | del request.session['email_id'] 55 | except: 56 | pass 57 | messages.error(request, msg) 58 | return redirect("/login/") 59 | messages.success(request, msg) 60 | # django -> request.session.get('email_id) 61 | request.session['email_id'] = f"{email_obj.id}" 62 | next_url = request.session.get('next_url') or "/" 63 | if not next_url.startswith("/"): 64 | next_url = "/" 65 | return redirect(next_url) -------------------------------------------------------------------------------- /src/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from ._cloudinary import ( 2 | cloudinary_init, 3 | get_cloudinary_image_object, 4 | get_cloudinary_video_object 5 | ) 6 | 7 | __all__ = ["cloudinary_init", 'get_cloudinary_image_object', 'get_cloudinary_video_object'] -------------------------------------------------------------------------------- /src/helpers/_cloudinary/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import cloudinary_init 2 | from .services import get_cloudinary_image_object, get_cloudinary_video_object 3 | 4 | __all__ = [ 5 | 'cloudinary_init', 6 | 'get_cloudinary_image_object' 7 | 'get_cloudinary_video_object', 8 | ] -------------------------------------------------------------------------------- /src/helpers/_cloudinary/config.py: -------------------------------------------------------------------------------- 1 | import cloudinary 2 | from django.conf import settings 3 | 4 | CLOUDINARY_CLOUD_NAME = settings.CLOUDINARY_CLOUD_NAME 5 | CLOUDINARY_PUBLIC_API_KEY = settings.CLOUDINARY_PUBLIC_API_KEY 6 | CLOUDINARY_SECRET_API_KEY= settings.CLOUDINARY_SECRET_API_KEY 7 | 8 | def cloudinary_init(): 9 | cloudinary.config( 10 | cloud_name = CLOUDINARY_CLOUD_NAME, 11 | api_key = CLOUDINARY_PUBLIC_API_KEY, 12 | api_secret = CLOUDINARY_SECRET_API_KEY, 13 | secure=True 14 | ) 15 | -------------------------------------------------------------------------------- /src/helpers/_cloudinary/services.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.template.loader import get_template 3 | 4 | 5 | def get_cloudinary_image_object(instance, 6 | field_name="image", 7 | as_html=False, 8 | format=None, 9 | width=1200 10 | ): 11 | if not hasattr(instance, field_name): 12 | return "" 13 | image_object = getattr(instance, field_name) 14 | if not image_object: 15 | return "" 16 | image_options = { 17 | "width": width 18 | } 19 | if format is not None: 20 | image_options['format'] = format 21 | if as_html: 22 | return image_object.image(**image_options) 23 | url = image_object.build_url(**image_options) 24 | return url 25 | 26 | video_html = """ 27 | 28 | """ 29 | 30 | def get_cloudinary_video_object(instance, 31 | field_name="video", 32 | as_html=False, 33 | width=None, 34 | height=None, 35 | sign_url=True, # for private videos 36 | fetch_format = "auto", 37 | quality = "auto", 38 | controls=True, 39 | autoplay=True, 40 | ): 41 | if not hasattr(instance, field_name): 42 | return "" 43 | video_object = getattr(instance, field_name) 44 | if not video_object: 45 | return "" 46 | video_options = { 47 | "sign_url": sign_url, 48 | "fetch_format": fetch_format, 49 | "quality": quality, 50 | "controls": controls, 51 | "autoplay": autoplay, 52 | } 53 | if width is not None: 54 | video_options['width'] =width 55 | if height is not None: 56 | video_options['height'] =height 57 | if height and width: 58 | video_options['crop'] = "limit" 59 | url = video_object.build_url(**video_options) 60 | if as_html: 61 | template_name = "videos/snippets/embed.html" 62 | tmpl = get_template(template_name) 63 | cloud_name = settings.CLOUDINARY_CLOUD_NAME 64 | _html = tmpl.render({'video_url': url, 'cloud_name': cloud_name, 'base_color': "#007cae"}) 65 | return _html 66 | return url -------------------------------------------------------------------------------- /src/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", "cfehome.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 | -------------------------------------------------------------------------------- /src/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/base_site.html' %} 2 | 3 | 4 | {% block extrahead %} 5 | {{ block.super }} 6 | {% include 'base/js.html' %} 7 | {% endblock %} -------------------------------------------------------------------------------- /src/templates/auth/login-logout.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head_title %} 4 | 5 | {% if request.session.email_id %} 6 | Logout 7 | {% else %} 8 | Login 9 | {% endif %} 10 | 11 | - {{ block.super }} 12 | {% endblock head_title %} 13 | 14 | {% block content %} 15 | 16 |
17 |
18 |
19 |
20 |

21 | {% if request.session.email_id %} 22 | Are sure you want to logout? 23 | {% else %} 24 | Sign in to your account 25 | {% endif %} 26 |

27 |
28 |
29 |
30 |
31 |
32 | 33 | 34 | 35 | {% endblock content %} -------------------------------------------------------------------------------- /src/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static tailwind_tags %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% tailwind_css %} 9 | 10 | {% block head_title %}Hello World from Course Platform 11 | {% endblock head_title %} 12 | 13 | 14 | {% include 'base/navbar.html' %} 15 | 16 | 17 |
18 | {% block content %} 19 |

20 | Hello world from base! 21 |

22 | {% endblock content %} 23 |
24 | {% include 'base/js.html' %} 25 | 26 | -------------------------------------------------------------------------------- /src/templates/base/hero.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | New Flowbite is out! See what's new 5 | 6 | 7 |

We invest in the world’s potential

8 |

Here at Flowbite we focus on markets where technology, innovation, and capital can unlock long-term value and drive economic growth.

9 | 19 | 52 |
53 | -------------------------------------------------------------------------------- /src/templates/base/js.html: -------------------------------------------------------------------------------- 1 | {% load django_htmx %} 2 | 3 | 4 | 5 | 6 | {% django_htmx_script %} 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/templates/base/navbar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32 | -------------------------------------------------------------------------------- /src/templates/courses/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 | 6 | 7 |
8 |
9 |
10 |

{{ object.title }}

11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | {{ object.description|linebreaks }} 19 |
20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 |

Lessons

31 |
32 | {% include 'courses/snippets/list-display.html' with queryset=lessons_queryset %} 33 |
34 | 35 | {% endblock content %} -------------------------------------------------------------------------------- /src/templates/courses/email-required.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head_title %} 4 | 5 | Unlock content 6 | - {{ block.super }} 7 | {% endblock head_title %} 8 | 9 | {% block content %} 10 | 11 |
12 |
13 |
14 |
15 |

16 | Verify your email to unlock. 17 |

18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | 26 | {% endblock content %} -------------------------------------------------------------------------------- /src/templates/courses/lesson-coming-soon.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 | 6 | 7 |

{{ object.title }}

8 | 9 | 10 |

Lesson coming soon

11 | 12 | 13 | {% endblock content %} -------------------------------------------------------------------------------- /src/templates/courses/lesson.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 | 6 | 7 |

{{ object.title }}

8 | 9 | 10 | {{ video_embed }} 11 | 12 | {% endblock content %} -------------------------------------------------------------------------------- /src/templates/courses/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 | 6 | 7 |
8 |
9 |
10 |

Courses

11 |

We have awesome courses.

12 |
13 | {% include 'courses/snippets/list-display.html' with queryset=object_list %} 14 |
15 |
16 | 17 | 18 | 19 | {% endblock content %} -------------------------------------------------------------------------------- /src/templates/courses/snippets/list-display.html: -------------------------------------------------------------------------------- 1 |
2 | {% for object in queryset %} 3 | 27 | {% endfor %} 28 |
-------------------------------------------------------------------------------- /src/templates/emails/hx/form.html: -------------------------------------------------------------------------------- 1 | {% if show_form %} 2 | {% if message != "" %} 3 | {{ message }} 4 | {% else %} 5 |
6 | {% csrf_token %} 7 | {{ form }} 8 | 9 |
10 | {% endif %} 11 | {% else %} 12 |
13 | {% endif %} -------------------------------------------------------------------------------- /src/templates/emails/hx/logout-btn.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block head_title %} 4 | Welcome to the Course Platform 5 | {% endblock head_title %} 6 | 7 | 8 | 9 | {% block content %} 10 | {% include 'base/hero.html' %} 11 | 12 | 13 |
14 | 15 |
16 |
17 |
18 |

Courses

19 |

We have awesome courses.

20 |
21 | 22 |
23 |
24 |
25 | 26 | 27 | 28 | {% endblock content %} 29 | -------------------------------------------------------------------------------- /src/templates/videos/snippets/embed.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /src/theme/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Course-Platform/31ce5bca89ee2d0b2f07286ca32a089cc8c7967e/src/theme/__init__.py -------------------------------------------------------------------------------- /src/theme/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ThemeConfig(AppConfig): 5 | name = 'theme' 6 | -------------------------------------------------------------------------------- /src/theme/static_src/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /src/theme/static_src/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "theme", 3 | "version": "3.8.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "theme", 9 | "version": "3.8.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@tailwindcss/aspect-ratio": "^0.4.2", 13 | "@tailwindcss/forms": "^0.5.7", 14 | "@tailwindcss/typography": "^0.5.10", 15 | "cross-env": "^7.0.3", 16 | "flowbite": "^2.5.1", 17 | "postcss": "^8.4.32", 18 | "postcss-import": "^15.1.0", 19 | "postcss-nested": "^6.0.1", 20 | "postcss-simple-vars": "^7.0.1", 21 | "rimraf": "^5.0.5", 22 | "tailwindcss": "^3.4.0" 23 | } 24 | }, 25 | "node_modules/@alloc/quick-lru": { 26 | "version": "5.2.0", 27 | "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", 28 | "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", 29 | "dev": true, 30 | "engines": { 31 | "node": ">=10" 32 | }, 33 | "funding": { 34 | "url": "https://github.com/sponsors/sindresorhus" 35 | } 36 | }, 37 | "node_modules/@isaacs/cliui": { 38 | "version": "8.0.2", 39 | "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 40 | "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 41 | "dev": true, 42 | "dependencies": { 43 | "string-width": "^5.1.2", 44 | "string-width-cjs": "npm:string-width@^4.2.0", 45 | "strip-ansi": "^7.0.1", 46 | "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 47 | "wrap-ansi": "^8.1.0", 48 | "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 49 | }, 50 | "engines": { 51 | "node": ">=12" 52 | } 53 | }, 54 | "node_modules/@jridgewell/gen-mapping": { 55 | "version": "0.3.5", 56 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", 57 | "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", 58 | "dev": true, 59 | "dependencies": { 60 | "@jridgewell/set-array": "^1.2.1", 61 | "@jridgewell/sourcemap-codec": "^1.4.10", 62 | "@jridgewell/trace-mapping": "^0.3.24" 63 | }, 64 | "engines": { 65 | "node": ">=6.0.0" 66 | } 67 | }, 68 | "node_modules/@jridgewell/resolve-uri": { 69 | "version": "3.1.2", 70 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 71 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 72 | "dev": true, 73 | "engines": { 74 | "node": ">=6.0.0" 75 | } 76 | }, 77 | "node_modules/@jridgewell/set-array": { 78 | "version": "1.2.1", 79 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", 80 | "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", 81 | "dev": true, 82 | "engines": { 83 | "node": ">=6.0.0" 84 | } 85 | }, 86 | "node_modules/@jridgewell/sourcemap-codec": { 87 | "version": "1.5.0", 88 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 89 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 90 | "dev": true 91 | }, 92 | "node_modules/@jridgewell/trace-mapping": { 93 | "version": "0.3.25", 94 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", 95 | "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", 96 | "dev": true, 97 | "dependencies": { 98 | "@jridgewell/resolve-uri": "^3.1.0", 99 | "@jridgewell/sourcemap-codec": "^1.4.14" 100 | } 101 | }, 102 | "node_modules/@nodelib/fs.scandir": { 103 | "version": "2.1.5", 104 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 105 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 106 | "dev": true, 107 | "dependencies": { 108 | "@nodelib/fs.stat": "2.0.5", 109 | "run-parallel": "^1.1.9" 110 | }, 111 | "engines": { 112 | "node": ">= 8" 113 | } 114 | }, 115 | "node_modules/@nodelib/fs.stat": { 116 | "version": "2.0.5", 117 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 118 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 119 | "dev": true, 120 | "engines": { 121 | "node": ">= 8" 122 | } 123 | }, 124 | "node_modules/@nodelib/fs.walk": { 125 | "version": "1.2.8", 126 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 127 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 128 | "dev": true, 129 | "dependencies": { 130 | "@nodelib/fs.scandir": "2.1.5", 131 | "fastq": "^1.6.0" 132 | }, 133 | "engines": { 134 | "node": ">= 8" 135 | } 136 | }, 137 | "node_modules/@pkgjs/parseargs": { 138 | "version": "0.11.0", 139 | "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", 140 | "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", 141 | "dev": true, 142 | "optional": true, 143 | "engines": { 144 | "node": ">=14" 145 | } 146 | }, 147 | "node_modules/@popperjs/core": { 148 | "version": "2.11.8", 149 | "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", 150 | "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", 151 | "dev": true, 152 | "funding": { 153 | "type": "opencollective", 154 | "url": "https://opencollective.com/popperjs" 155 | } 156 | }, 157 | "node_modules/@rollup/plugin-node-resolve": { 158 | "version": "15.2.3", 159 | "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", 160 | "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", 161 | "dev": true, 162 | "dependencies": { 163 | "@rollup/pluginutils": "^5.0.1", 164 | "@types/resolve": "1.20.2", 165 | "deepmerge": "^4.2.2", 166 | "is-builtin-module": "^3.2.1", 167 | "is-module": "^1.0.0", 168 | "resolve": "^1.22.1" 169 | }, 170 | "engines": { 171 | "node": ">=14.0.0" 172 | }, 173 | "peerDependencies": { 174 | "rollup": "^2.78.0||^3.0.0||^4.0.0" 175 | }, 176 | "peerDependenciesMeta": { 177 | "rollup": { 178 | "optional": true 179 | } 180 | } 181 | }, 182 | "node_modules/@rollup/pluginutils": { 183 | "version": "5.1.0", 184 | "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", 185 | "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", 186 | "dev": true, 187 | "dependencies": { 188 | "@types/estree": "^1.0.0", 189 | "estree-walker": "^2.0.2", 190 | "picomatch": "^2.3.1" 191 | }, 192 | "engines": { 193 | "node": ">=14.0.0" 194 | }, 195 | "peerDependencies": { 196 | "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" 197 | }, 198 | "peerDependenciesMeta": { 199 | "rollup": { 200 | "optional": true 201 | } 202 | } 203 | }, 204 | "node_modules/@tailwindcss/aspect-ratio": { 205 | "version": "0.4.2", 206 | "resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz", 207 | "integrity": "sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==", 208 | "dev": true, 209 | "peerDependencies": { 210 | "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" 211 | } 212 | }, 213 | "node_modules/@tailwindcss/forms": { 214 | "version": "0.5.9", 215 | "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz", 216 | "integrity": "sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==", 217 | "dev": true, 218 | "dependencies": { 219 | "mini-svg-data-uri": "^1.2.3" 220 | }, 221 | "peerDependencies": { 222 | "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20" 223 | } 224 | }, 225 | "node_modules/@tailwindcss/typography": { 226 | "version": "0.5.15", 227 | "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz", 228 | "integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==", 229 | "dev": true, 230 | "dependencies": { 231 | "lodash.castarray": "^4.4.0", 232 | "lodash.isplainobject": "^4.0.6", 233 | "lodash.merge": "^4.6.2", 234 | "postcss-selector-parser": "6.0.10" 235 | }, 236 | "peerDependencies": { 237 | "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20" 238 | } 239 | }, 240 | "node_modules/@types/estree": { 241 | "version": "1.0.6", 242 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", 243 | "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", 244 | "dev": true 245 | }, 246 | "node_modules/@types/resolve": { 247 | "version": "1.20.2", 248 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", 249 | "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", 250 | "dev": true 251 | }, 252 | "node_modules/ansi-regex": { 253 | "version": "6.1.0", 254 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", 255 | "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", 256 | "dev": true, 257 | "engines": { 258 | "node": ">=12" 259 | }, 260 | "funding": { 261 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 262 | } 263 | }, 264 | "node_modules/ansi-styles": { 265 | "version": "6.2.1", 266 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", 267 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", 268 | "dev": true, 269 | "engines": { 270 | "node": ">=12" 271 | }, 272 | "funding": { 273 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 274 | } 275 | }, 276 | "node_modules/any-promise": { 277 | "version": "1.3.0", 278 | "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 279 | "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", 280 | "dev": true 281 | }, 282 | "node_modules/anymatch": { 283 | "version": "3.1.3", 284 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 285 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 286 | "dev": true, 287 | "dependencies": { 288 | "normalize-path": "^3.0.0", 289 | "picomatch": "^2.0.4" 290 | }, 291 | "engines": { 292 | "node": ">= 8" 293 | } 294 | }, 295 | "node_modules/arg": { 296 | "version": "5.0.2", 297 | "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", 298 | "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", 299 | "dev": true 300 | }, 301 | "node_modules/balanced-match": { 302 | "version": "1.0.2", 303 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 304 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 305 | "dev": true 306 | }, 307 | "node_modules/binary-extensions": { 308 | "version": "2.3.0", 309 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", 310 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", 311 | "dev": true, 312 | "engines": { 313 | "node": ">=8" 314 | }, 315 | "funding": { 316 | "url": "https://github.com/sponsors/sindresorhus" 317 | } 318 | }, 319 | "node_modules/brace-expansion": { 320 | "version": "2.0.1", 321 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 322 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 323 | "dev": true, 324 | "dependencies": { 325 | "balanced-match": "^1.0.0" 326 | } 327 | }, 328 | "node_modules/braces": { 329 | "version": "3.0.3", 330 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 331 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 332 | "dev": true, 333 | "dependencies": { 334 | "fill-range": "^7.1.1" 335 | }, 336 | "engines": { 337 | "node": ">=8" 338 | } 339 | }, 340 | "node_modules/builtin-modules": { 341 | "version": "3.3.0", 342 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", 343 | "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", 344 | "dev": true, 345 | "engines": { 346 | "node": ">=6" 347 | }, 348 | "funding": { 349 | "url": "https://github.com/sponsors/sindresorhus" 350 | } 351 | }, 352 | "node_modules/camelcase-css": { 353 | "version": "2.0.1", 354 | "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", 355 | "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", 356 | "dev": true, 357 | "engines": { 358 | "node": ">= 6" 359 | } 360 | }, 361 | "node_modules/chokidar": { 362 | "version": "3.6.0", 363 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 364 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 365 | "dev": true, 366 | "dependencies": { 367 | "anymatch": "~3.1.2", 368 | "braces": "~3.0.2", 369 | "glob-parent": "~5.1.2", 370 | "is-binary-path": "~2.1.0", 371 | "is-glob": "~4.0.1", 372 | "normalize-path": "~3.0.0", 373 | "readdirp": "~3.6.0" 374 | }, 375 | "engines": { 376 | "node": ">= 8.10.0" 377 | }, 378 | "funding": { 379 | "url": "https://paulmillr.com/funding/" 380 | }, 381 | "optionalDependencies": { 382 | "fsevents": "~2.3.2" 383 | } 384 | }, 385 | "node_modules/chokidar/node_modules/glob-parent": { 386 | "version": "5.1.2", 387 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 388 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 389 | "dev": true, 390 | "dependencies": { 391 | "is-glob": "^4.0.1" 392 | }, 393 | "engines": { 394 | "node": ">= 6" 395 | } 396 | }, 397 | "node_modules/color-convert": { 398 | "version": "2.0.1", 399 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 400 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 401 | "dev": true, 402 | "dependencies": { 403 | "color-name": "~1.1.4" 404 | }, 405 | "engines": { 406 | "node": ">=7.0.0" 407 | } 408 | }, 409 | "node_modules/color-name": { 410 | "version": "1.1.4", 411 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 412 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 413 | "dev": true 414 | }, 415 | "node_modules/commander": { 416 | "version": "4.1.1", 417 | "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", 418 | "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", 419 | "dev": true, 420 | "engines": { 421 | "node": ">= 6" 422 | } 423 | }, 424 | "node_modules/cross-env": { 425 | "version": "7.0.3", 426 | "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", 427 | "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", 428 | "dev": true, 429 | "dependencies": { 430 | "cross-spawn": "^7.0.1" 431 | }, 432 | "bin": { 433 | "cross-env": "src/bin/cross-env.js", 434 | "cross-env-shell": "src/bin/cross-env-shell.js" 435 | }, 436 | "engines": { 437 | "node": ">=10.14", 438 | "npm": ">=6", 439 | "yarn": ">=1" 440 | } 441 | }, 442 | "node_modules/cross-spawn": { 443 | "version": "7.0.3", 444 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 445 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 446 | "dev": true, 447 | "dependencies": { 448 | "path-key": "^3.1.0", 449 | "shebang-command": "^2.0.0", 450 | "which": "^2.0.1" 451 | }, 452 | "engines": { 453 | "node": ">= 8" 454 | } 455 | }, 456 | "node_modules/cssesc": { 457 | "version": "3.0.0", 458 | "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", 459 | "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", 460 | "dev": true, 461 | "bin": { 462 | "cssesc": "bin/cssesc" 463 | }, 464 | "engines": { 465 | "node": ">=4" 466 | } 467 | }, 468 | "node_modules/deepmerge": { 469 | "version": "4.3.1", 470 | "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", 471 | "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", 472 | "dev": true, 473 | "engines": { 474 | "node": ">=0.10.0" 475 | } 476 | }, 477 | "node_modules/didyoumean": { 478 | "version": "1.2.2", 479 | "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", 480 | "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", 481 | "dev": true 482 | }, 483 | "node_modules/dlv": { 484 | "version": "1.1.3", 485 | "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", 486 | "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", 487 | "dev": true 488 | }, 489 | "node_modules/eastasianwidth": { 490 | "version": "0.2.0", 491 | "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 492 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 493 | "dev": true 494 | }, 495 | "node_modules/emoji-regex": { 496 | "version": "9.2.2", 497 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", 498 | "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", 499 | "dev": true 500 | }, 501 | "node_modules/estree-walker": { 502 | "version": "2.0.2", 503 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 504 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", 505 | "dev": true 506 | }, 507 | "node_modules/fast-glob": { 508 | "version": "3.3.2", 509 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", 510 | "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", 511 | "dev": true, 512 | "dependencies": { 513 | "@nodelib/fs.stat": "^2.0.2", 514 | "@nodelib/fs.walk": "^1.2.3", 515 | "glob-parent": "^5.1.2", 516 | "merge2": "^1.3.0", 517 | "micromatch": "^4.0.4" 518 | }, 519 | "engines": { 520 | "node": ">=8.6.0" 521 | } 522 | }, 523 | "node_modules/fast-glob/node_modules/glob-parent": { 524 | "version": "5.1.2", 525 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 526 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 527 | "dev": true, 528 | "dependencies": { 529 | "is-glob": "^4.0.1" 530 | }, 531 | "engines": { 532 | "node": ">= 6" 533 | } 534 | }, 535 | "node_modules/fastq": { 536 | "version": "1.17.1", 537 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", 538 | "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", 539 | "dev": true, 540 | "dependencies": { 541 | "reusify": "^1.0.4" 542 | } 543 | }, 544 | "node_modules/fill-range": { 545 | "version": "7.1.1", 546 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 547 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 548 | "dev": true, 549 | "dependencies": { 550 | "to-regex-range": "^5.0.1" 551 | }, 552 | "engines": { 553 | "node": ">=8" 554 | } 555 | }, 556 | "node_modules/flowbite": { 557 | "version": "2.5.1", 558 | "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-2.5.1.tgz", 559 | "integrity": "sha512-7jP1jy9c3QP7y+KU9lc8ueMkTyUdMDvRP+lteSWgY5TigSZjf9K1kqZxmqjhbx2gBnFQxMl1GAjVThCa8cEpKA==", 560 | "dev": true, 561 | "dependencies": { 562 | "@popperjs/core": "^2.9.3", 563 | "flowbite-datepicker": "^1.3.0", 564 | "mini-svg-data-uri": "^1.4.3" 565 | } 566 | }, 567 | "node_modules/flowbite-datepicker": { 568 | "version": "1.3.0", 569 | "resolved": "https://registry.npmjs.org/flowbite-datepicker/-/flowbite-datepicker-1.3.0.tgz", 570 | "integrity": "sha512-CLVqzuoE2vkUvWYK/lJ6GzT0be5dlTbH3uuhVwyB67+PjqJWABm2wv68xhBf5BqjpBxvTSQ3mrmLHpPJ2tvrSQ==", 571 | "dev": true, 572 | "dependencies": { 573 | "@rollup/plugin-node-resolve": "^15.2.3", 574 | "flowbite": "^2.0.0" 575 | } 576 | }, 577 | "node_modules/foreground-child": { 578 | "version": "3.3.0", 579 | "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", 580 | "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", 581 | "dev": true, 582 | "dependencies": { 583 | "cross-spawn": "^7.0.0", 584 | "signal-exit": "^4.0.1" 585 | }, 586 | "engines": { 587 | "node": ">=14" 588 | }, 589 | "funding": { 590 | "url": "https://github.com/sponsors/isaacs" 591 | } 592 | }, 593 | "node_modules/fsevents": { 594 | "version": "2.3.3", 595 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 596 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 597 | "dev": true, 598 | "hasInstallScript": true, 599 | "optional": true, 600 | "os": [ 601 | "darwin" 602 | ], 603 | "engines": { 604 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 605 | } 606 | }, 607 | "node_modules/function-bind": { 608 | "version": "1.1.2", 609 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 610 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 611 | "dev": true, 612 | "funding": { 613 | "url": "https://github.com/sponsors/ljharb" 614 | } 615 | }, 616 | "node_modules/glob": { 617 | "version": "10.4.5", 618 | "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", 619 | "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", 620 | "dev": true, 621 | "dependencies": { 622 | "foreground-child": "^3.1.0", 623 | "jackspeak": "^3.1.2", 624 | "minimatch": "^9.0.4", 625 | "minipass": "^7.1.2", 626 | "package-json-from-dist": "^1.0.0", 627 | "path-scurry": "^1.11.1" 628 | }, 629 | "bin": { 630 | "glob": "dist/esm/bin.mjs" 631 | }, 632 | "funding": { 633 | "url": "https://github.com/sponsors/isaacs" 634 | } 635 | }, 636 | "node_modules/glob-parent": { 637 | "version": "6.0.2", 638 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 639 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 640 | "dev": true, 641 | "dependencies": { 642 | "is-glob": "^4.0.3" 643 | }, 644 | "engines": { 645 | "node": ">=10.13.0" 646 | } 647 | }, 648 | "node_modules/hasown": { 649 | "version": "2.0.2", 650 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 651 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 652 | "dev": true, 653 | "dependencies": { 654 | "function-bind": "^1.1.2" 655 | }, 656 | "engines": { 657 | "node": ">= 0.4" 658 | } 659 | }, 660 | "node_modules/is-binary-path": { 661 | "version": "2.1.0", 662 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 663 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 664 | "dev": true, 665 | "dependencies": { 666 | "binary-extensions": "^2.0.0" 667 | }, 668 | "engines": { 669 | "node": ">=8" 670 | } 671 | }, 672 | "node_modules/is-builtin-module": { 673 | "version": "3.2.1", 674 | "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", 675 | "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", 676 | "dev": true, 677 | "dependencies": { 678 | "builtin-modules": "^3.3.0" 679 | }, 680 | "engines": { 681 | "node": ">=6" 682 | }, 683 | "funding": { 684 | "url": "https://github.com/sponsors/sindresorhus" 685 | } 686 | }, 687 | "node_modules/is-core-module": { 688 | "version": "2.15.1", 689 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", 690 | "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", 691 | "dev": true, 692 | "dependencies": { 693 | "hasown": "^2.0.2" 694 | }, 695 | "engines": { 696 | "node": ">= 0.4" 697 | }, 698 | "funding": { 699 | "url": "https://github.com/sponsors/ljharb" 700 | } 701 | }, 702 | "node_modules/is-extglob": { 703 | "version": "2.1.1", 704 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 705 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 706 | "dev": true, 707 | "engines": { 708 | "node": ">=0.10.0" 709 | } 710 | }, 711 | "node_modules/is-fullwidth-code-point": { 712 | "version": "3.0.0", 713 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 714 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 715 | "dev": true, 716 | "engines": { 717 | "node": ">=8" 718 | } 719 | }, 720 | "node_modules/is-glob": { 721 | "version": "4.0.3", 722 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 723 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 724 | "dev": true, 725 | "dependencies": { 726 | "is-extglob": "^2.1.1" 727 | }, 728 | "engines": { 729 | "node": ">=0.10.0" 730 | } 731 | }, 732 | "node_modules/is-module": { 733 | "version": "1.0.0", 734 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 735 | "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", 736 | "dev": true 737 | }, 738 | "node_modules/is-number": { 739 | "version": "7.0.0", 740 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 741 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 742 | "dev": true, 743 | "engines": { 744 | "node": ">=0.12.0" 745 | } 746 | }, 747 | "node_modules/isexe": { 748 | "version": "2.0.0", 749 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 750 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 751 | "dev": true 752 | }, 753 | "node_modules/jackspeak": { 754 | "version": "3.4.3", 755 | "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", 756 | "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", 757 | "dev": true, 758 | "dependencies": { 759 | "@isaacs/cliui": "^8.0.2" 760 | }, 761 | "funding": { 762 | "url": "https://github.com/sponsors/isaacs" 763 | }, 764 | "optionalDependencies": { 765 | "@pkgjs/parseargs": "^0.11.0" 766 | } 767 | }, 768 | "node_modules/jiti": { 769 | "version": "1.21.6", 770 | "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", 771 | "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", 772 | "dev": true, 773 | "bin": { 774 | "jiti": "bin/jiti.js" 775 | } 776 | }, 777 | "node_modules/lilconfig": { 778 | "version": "2.1.0", 779 | "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", 780 | "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", 781 | "dev": true, 782 | "engines": { 783 | "node": ">=10" 784 | } 785 | }, 786 | "node_modules/lines-and-columns": { 787 | "version": "1.2.4", 788 | "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", 789 | "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", 790 | "dev": true 791 | }, 792 | "node_modules/lodash.castarray": { 793 | "version": "4.4.0", 794 | "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", 795 | "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", 796 | "dev": true 797 | }, 798 | "node_modules/lodash.isplainobject": { 799 | "version": "4.0.6", 800 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 801 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", 802 | "dev": true 803 | }, 804 | "node_modules/lodash.merge": { 805 | "version": "4.6.2", 806 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 807 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 808 | "dev": true 809 | }, 810 | "node_modules/lru-cache": { 811 | "version": "10.4.3", 812 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 813 | "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 814 | "dev": true 815 | }, 816 | "node_modules/merge2": { 817 | "version": "1.4.1", 818 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 819 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 820 | "dev": true, 821 | "engines": { 822 | "node": ">= 8" 823 | } 824 | }, 825 | "node_modules/micromatch": { 826 | "version": "4.0.8", 827 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", 828 | "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", 829 | "dev": true, 830 | "dependencies": { 831 | "braces": "^3.0.3", 832 | "picomatch": "^2.3.1" 833 | }, 834 | "engines": { 835 | "node": ">=8.6" 836 | } 837 | }, 838 | "node_modules/mini-svg-data-uri": { 839 | "version": "1.4.4", 840 | "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", 841 | "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", 842 | "dev": true, 843 | "bin": { 844 | "mini-svg-data-uri": "cli.js" 845 | } 846 | }, 847 | "node_modules/minimatch": { 848 | "version": "9.0.5", 849 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", 850 | "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 851 | "dev": true, 852 | "dependencies": { 853 | "brace-expansion": "^2.0.1" 854 | }, 855 | "engines": { 856 | "node": ">=16 || 14 >=14.17" 857 | }, 858 | "funding": { 859 | "url": "https://github.com/sponsors/isaacs" 860 | } 861 | }, 862 | "node_modules/minipass": { 863 | "version": "7.1.2", 864 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", 865 | "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", 866 | "dev": true, 867 | "engines": { 868 | "node": ">=16 || 14 >=14.17" 869 | } 870 | }, 871 | "node_modules/mz": { 872 | "version": "2.7.0", 873 | "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", 874 | "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", 875 | "dev": true, 876 | "dependencies": { 877 | "any-promise": "^1.0.0", 878 | "object-assign": "^4.0.1", 879 | "thenify-all": "^1.0.0" 880 | } 881 | }, 882 | "node_modules/nanoid": { 883 | "version": "3.3.7", 884 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 885 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 886 | "dev": true, 887 | "funding": [ 888 | { 889 | "type": "github", 890 | "url": "https://github.com/sponsors/ai" 891 | } 892 | ], 893 | "bin": { 894 | "nanoid": "bin/nanoid.cjs" 895 | }, 896 | "engines": { 897 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 898 | } 899 | }, 900 | "node_modules/normalize-path": { 901 | "version": "3.0.0", 902 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 903 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 904 | "dev": true, 905 | "engines": { 906 | "node": ">=0.10.0" 907 | } 908 | }, 909 | "node_modules/object-assign": { 910 | "version": "4.1.1", 911 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 912 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 913 | "dev": true, 914 | "engines": { 915 | "node": ">=0.10.0" 916 | } 917 | }, 918 | "node_modules/object-hash": { 919 | "version": "3.0.0", 920 | "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", 921 | "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", 922 | "dev": true, 923 | "engines": { 924 | "node": ">= 6" 925 | } 926 | }, 927 | "node_modules/package-json-from-dist": { 928 | "version": "1.0.0", 929 | "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", 930 | "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", 931 | "dev": true 932 | }, 933 | "node_modules/path-key": { 934 | "version": "3.1.1", 935 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 936 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 937 | "dev": true, 938 | "engines": { 939 | "node": ">=8" 940 | } 941 | }, 942 | "node_modules/path-parse": { 943 | "version": "1.0.7", 944 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 945 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 946 | "dev": true 947 | }, 948 | "node_modules/path-scurry": { 949 | "version": "1.11.1", 950 | "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", 951 | "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", 952 | "dev": true, 953 | "dependencies": { 954 | "lru-cache": "^10.2.0", 955 | "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 956 | }, 957 | "engines": { 958 | "node": ">=16 || 14 >=14.18" 959 | }, 960 | "funding": { 961 | "url": "https://github.com/sponsors/isaacs" 962 | } 963 | }, 964 | "node_modules/picocolors": { 965 | "version": "1.1.0", 966 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", 967 | "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", 968 | "dev": true 969 | }, 970 | "node_modules/picomatch": { 971 | "version": "2.3.1", 972 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 973 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 974 | "dev": true, 975 | "engines": { 976 | "node": ">=8.6" 977 | }, 978 | "funding": { 979 | "url": "https://github.com/sponsors/jonschlinkert" 980 | } 981 | }, 982 | "node_modules/pify": { 983 | "version": "2.3.0", 984 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 985 | "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", 986 | "dev": true, 987 | "engines": { 988 | "node": ">=0.10.0" 989 | } 990 | }, 991 | "node_modules/pirates": { 992 | "version": "4.0.6", 993 | "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", 994 | "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", 995 | "dev": true, 996 | "engines": { 997 | "node": ">= 6" 998 | } 999 | }, 1000 | "node_modules/postcss": { 1001 | "version": "8.4.47", 1002 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", 1003 | "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", 1004 | "dev": true, 1005 | "funding": [ 1006 | { 1007 | "type": "opencollective", 1008 | "url": "https://opencollective.com/postcss/" 1009 | }, 1010 | { 1011 | "type": "tidelift", 1012 | "url": "https://tidelift.com/funding/github/npm/postcss" 1013 | }, 1014 | { 1015 | "type": "github", 1016 | "url": "https://github.com/sponsors/ai" 1017 | } 1018 | ], 1019 | "dependencies": { 1020 | "nanoid": "^3.3.7", 1021 | "picocolors": "^1.1.0", 1022 | "source-map-js": "^1.2.1" 1023 | }, 1024 | "engines": { 1025 | "node": "^10 || ^12 || >=14" 1026 | } 1027 | }, 1028 | "node_modules/postcss-import": { 1029 | "version": "15.1.0", 1030 | "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", 1031 | "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", 1032 | "dev": true, 1033 | "dependencies": { 1034 | "postcss-value-parser": "^4.0.0", 1035 | "read-cache": "^1.0.0", 1036 | "resolve": "^1.1.7" 1037 | }, 1038 | "engines": { 1039 | "node": ">=14.0.0" 1040 | }, 1041 | "peerDependencies": { 1042 | "postcss": "^8.0.0" 1043 | } 1044 | }, 1045 | "node_modules/postcss-js": { 1046 | "version": "4.0.1", 1047 | "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", 1048 | "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", 1049 | "dev": true, 1050 | "dependencies": { 1051 | "camelcase-css": "^2.0.1" 1052 | }, 1053 | "engines": { 1054 | "node": "^12 || ^14 || >= 16" 1055 | }, 1056 | "funding": { 1057 | "type": "opencollective", 1058 | "url": "https://opencollective.com/postcss/" 1059 | }, 1060 | "peerDependencies": { 1061 | "postcss": "^8.4.21" 1062 | } 1063 | }, 1064 | "node_modules/postcss-load-config": { 1065 | "version": "4.0.2", 1066 | "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", 1067 | "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", 1068 | "dev": true, 1069 | "funding": [ 1070 | { 1071 | "type": "opencollective", 1072 | "url": "https://opencollective.com/postcss/" 1073 | }, 1074 | { 1075 | "type": "github", 1076 | "url": "https://github.com/sponsors/ai" 1077 | } 1078 | ], 1079 | "dependencies": { 1080 | "lilconfig": "^3.0.0", 1081 | "yaml": "^2.3.4" 1082 | }, 1083 | "engines": { 1084 | "node": ">= 14" 1085 | }, 1086 | "peerDependencies": { 1087 | "postcss": ">=8.0.9", 1088 | "ts-node": ">=9.0.0" 1089 | }, 1090 | "peerDependenciesMeta": { 1091 | "postcss": { 1092 | "optional": true 1093 | }, 1094 | "ts-node": { 1095 | "optional": true 1096 | } 1097 | } 1098 | }, 1099 | "node_modules/postcss-load-config/node_modules/lilconfig": { 1100 | "version": "3.1.2", 1101 | "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", 1102 | "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", 1103 | "dev": true, 1104 | "engines": { 1105 | "node": ">=14" 1106 | }, 1107 | "funding": { 1108 | "url": "https://github.com/sponsors/antonk52" 1109 | } 1110 | }, 1111 | "node_modules/postcss-nested": { 1112 | "version": "6.2.0", 1113 | "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", 1114 | "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", 1115 | "dev": true, 1116 | "funding": [ 1117 | { 1118 | "type": "opencollective", 1119 | "url": "https://opencollective.com/postcss/" 1120 | }, 1121 | { 1122 | "type": "github", 1123 | "url": "https://github.com/sponsors/ai" 1124 | } 1125 | ], 1126 | "dependencies": { 1127 | "postcss-selector-parser": "^6.1.1" 1128 | }, 1129 | "engines": { 1130 | "node": ">=12.0" 1131 | }, 1132 | "peerDependencies": { 1133 | "postcss": "^8.2.14" 1134 | } 1135 | }, 1136 | "node_modules/postcss-nested/node_modules/postcss-selector-parser": { 1137 | "version": "6.1.2", 1138 | "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", 1139 | "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", 1140 | "dev": true, 1141 | "dependencies": { 1142 | "cssesc": "^3.0.0", 1143 | "util-deprecate": "^1.0.2" 1144 | }, 1145 | "engines": { 1146 | "node": ">=4" 1147 | } 1148 | }, 1149 | "node_modules/postcss-selector-parser": { 1150 | "version": "6.0.10", 1151 | "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", 1152 | "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", 1153 | "dev": true, 1154 | "dependencies": { 1155 | "cssesc": "^3.0.0", 1156 | "util-deprecate": "^1.0.2" 1157 | }, 1158 | "engines": { 1159 | "node": ">=4" 1160 | } 1161 | }, 1162 | "node_modules/postcss-simple-vars": { 1163 | "version": "7.0.1", 1164 | "resolved": "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-7.0.1.tgz", 1165 | "integrity": "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A==", 1166 | "dev": true, 1167 | "engines": { 1168 | "node": ">=14.0" 1169 | }, 1170 | "funding": { 1171 | "type": "opencollective", 1172 | "url": "https://opencollective.com/postcss/" 1173 | }, 1174 | "peerDependencies": { 1175 | "postcss": "^8.2.1" 1176 | } 1177 | }, 1178 | "node_modules/postcss-value-parser": { 1179 | "version": "4.2.0", 1180 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", 1181 | "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", 1182 | "dev": true 1183 | }, 1184 | "node_modules/queue-microtask": { 1185 | "version": "1.2.3", 1186 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 1187 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 1188 | "dev": true, 1189 | "funding": [ 1190 | { 1191 | "type": "github", 1192 | "url": "https://github.com/sponsors/feross" 1193 | }, 1194 | { 1195 | "type": "patreon", 1196 | "url": "https://www.patreon.com/feross" 1197 | }, 1198 | { 1199 | "type": "consulting", 1200 | "url": "https://feross.org/support" 1201 | } 1202 | ] 1203 | }, 1204 | "node_modules/read-cache": { 1205 | "version": "1.0.0", 1206 | "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", 1207 | "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", 1208 | "dev": true, 1209 | "dependencies": { 1210 | "pify": "^2.3.0" 1211 | } 1212 | }, 1213 | "node_modules/readdirp": { 1214 | "version": "3.6.0", 1215 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1216 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1217 | "dev": true, 1218 | "dependencies": { 1219 | "picomatch": "^2.2.1" 1220 | }, 1221 | "engines": { 1222 | "node": ">=8.10.0" 1223 | } 1224 | }, 1225 | "node_modules/resolve": { 1226 | "version": "1.22.8", 1227 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 1228 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 1229 | "dev": true, 1230 | "dependencies": { 1231 | "is-core-module": "^2.13.0", 1232 | "path-parse": "^1.0.7", 1233 | "supports-preserve-symlinks-flag": "^1.0.0" 1234 | }, 1235 | "bin": { 1236 | "resolve": "bin/resolve" 1237 | }, 1238 | "funding": { 1239 | "url": "https://github.com/sponsors/ljharb" 1240 | } 1241 | }, 1242 | "node_modules/reusify": { 1243 | "version": "1.0.4", 1244 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 1245 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 1246 | "dev": true, 1247 | "engines": { 1248 | "iojs": ">=1.0.0", 1249 | "node": ">=0.10.0" 1250 | } 1251 | }, 1252 | "node_modules/rimraf": { 1253 | "version": "5.0.10", 1254 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", 1255 | "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", 1256 | "dev": true, 1257 | "dependencies": { 1258 | "glob": "^10.3.7" 1259 | }, 1260 | "bin": { 1261 | "rimraf": "dist/esm/bin.mjs" 1262 | }, 1263 | "funding": { 1264 | "url": "https://github.com/sponsors/isaacs" 1265 | } 1266 | }, 1267 | "node_modules/run-parallel": { 1268 | "version": "1.2.0", 1269 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 1270 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 1271 | "dev": true, 1272 | "funding": [ 1273 | { 1274 | "type": "github", 1275 | "url": "https://github.com/sponsors/feross" 1276 | }, 1277 | { 1278 | "type": "patreon", 1279 | "url": "https://www.patreon.com/feross" 1280 | }, 1281 | { 1282 | "type": "consulting", 1283 | "url": "https://feross.org/support" 1284 | } 1285 | ], 1286 | "dependencies": { 1287 | "queue-microtask": "^1.2.2" 1288 | } 1289 | }, 1290 | "node_modules/shebang-command": { 1291 | "version": "2.0.0", 1292 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1293 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1294 | "dev": true, 1295 | "dependencies": { 1296 | "shebang-regex": "^3.0.0" 1297 | }, 1298 | "engines": { 1299 | "node": ">=8" 1300 | } 1301 | }, 1302 | "node_modules/shebang-regex": { 1303 | "version": "3.0.0", 1304 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1305 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1306 | "dev": true, 1307 | "engines": { 1308 | "node": ">=8" 1309 | } 1310 | }, 1311 | "node_modules/signal-exit": { 1312 | "version": "4.1.0", 1313 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", 1314 | "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", 1315 | "dev": true, 1316 | "engines": { 1317 | "node": ">=14" 1318 | }, 1319 | "funding": { 1320 | "url": "https://github.com/sponsors/isaacs" 1321 | } 1322 | }, 1323 | "node_modules/source-map-js": { 1324 | "version": "1.2.1", 1325 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 1326 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 1327 | "dev": true, 1328 | "engines": { 1329 | "node": ">=0.10.0" 1330 | } 1331 | }, 1332 | "node_modules/string-width": { 1333 | "version": "5.1.2", 1334 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", 1335 | "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 1336 | "dev": true, 1337 | "dependencies": { 1338 | "eastasianwidth": "^0.2.0", 1339 | "emoji-regex": "^9.2.2", 1340 | "strip-ansi": "^7.0.1" 1341 | }, 1342 | "engines": { 1343 | "node": ">=12" 1344 | }, 1345 | "funding": { 1346 | "url": "https://github.com/sponsors/sindresorhus" 1347 | } 1348 | }, 1349 | "node_modules/string-width-cjs": { 1350 | "name": "string-width", 1351 | "version": "4.2.3", 1352 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1353 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1354 | "dev": true, 1355 | "dependencies": { 1356 | "emoji-regex": "^8.0.0", 1357 | "is-fullwidth-code-point": "^3.0.0", 1358 | "strip-ansi": "^6.0.1" 1359 | }, 1360 | "engines": { 1361 | "node": ">=8" 1362 | } 1363 | }, 1364 | "node_modules/string-width-cjs/node_modules/ansi-regex": { 1365 | "version": "5.0.1", 1366 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1367 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1368 | "dev": true, 1369 | "engines": { 1370 | "node": ">=8" 1371 | } 1372 | }, 1373 | "node_modules/string-width-cjs/node_modules/emoji-regex": { 1374 | "version": "8.0.0", 1375 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1376 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1377 | "dev": true 1378 | }, 1379 | "node_modules/string-width-cjs/node_modules/strip-ansi": { 1380 | "version": "6.0.1", 1381 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1382 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1383 | "dev": true, 1384 | "dependencies": { 1385 | "ansi-regex": "^5.0.1" 1386 | }, 1387 | "engines": { 1388 | "node": ">=8" 1389 | } 1390 | }, 1391 | "node_modules/strip-ansi": { 1392 | "version": "7.1.0", 1393 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 1394 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 1395 | "dev": true, 1396 | "dependencies": { 1397 | "ansi-regex": "^6.0.1" 1398 | }, 1399 | "engines": { 1400 | "node": ">=12" 1401 | }, 1402 | "funding": { 1403 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 1404 | } 1405 | }, 1406 | "node_modules/strip-ansi-cjs": { 1407 | "name": "strip-ansi", 1408 | "version": "6.0.1", 1409 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1410 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1411 | "dev": true, 1412 | "dependencies": { 1413 | "ansi-regex": "^5.0.1" 1414 | }, 1415 | "engines": { 1416 | "node": ">=8" 1417 | } 1418 | }, 1419 | "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { 1420 | "version": "5.0.1", 1421 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1422 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1423 | "dev": true, 1424 | "engines": { 1425 | "node": ">=8" 1426 | } 1427 | }, 1428 | "node_modules/sucrase": { 1429 | "version": "3.35.0", 1430 | "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", 1431 | "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", 1432 | "dev": true, 1433 | "dependencies": { 1434 | "@jridgewell/gen-mapping": "^0.3.2", 1435 | "commander": "^4.0.0", 1436 | "glob": "^10.3.10", 1437 | "lines-and-columns": "^1.1.6", 1438 | "mz": "^2.7.0", 1439 | "pirates": "^4.0.1", 1440 | "ts-interface-checker": "^0.1.9" 1441 | }, 1442 | "bin": { 1443 | "sucrase": "bin/sucrase", 1444 | "sucrase-node": "bin/sucrase-node" 1445 | }, 1446 | "engines": { 1447 | "node": ">=16 || 14 >=14.17" 1448 | } 1449 | }, 1450 | "node_modules/supports-preserve-symlinks-flag": { 1451 | "version": "1.0.0", 1452 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 1453 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 1454 | "dev": true, 1455 | "engines": { 1456 | "node": ">= 0.4" 1457 | }, 1458 | "funding": { 1459 | "url": "https://github.com/sponsors/ljharb" 1460 | } 1461 | }, 1462 | "node_modules/tailwindcss": { 1463 | "version": "3.4.12", 1464 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz", 1465 | "integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==", 1466 | "dev": true, 1467 | "dependencies": { 1468 | "@alloc/quick-lru": "^5.2.0", 1469 | "arg": "^5.0.2", 1470 | "chokidar": "^3.5.3", 1471 | "didyoumean": "^1.2.2", 1472 | "dlv": "^1.1.3", 1473 | "fast-glob": "^3.3.0", 1474 | "glob-parent": "^6.0.2", 1475 | "is-glob": "^4.0.3", 1476 | "jiti": "^1.21.0", 1477 | "lilconfig": "^2.1.0", 1478 | "micromatch": "^4.0.5", 1479 | "normalize-path": "^3.0.0", 1480 | "object-hash": "^3.0.0", 1481 | "picocolors": "^1.0.0", 1482 | "postcss": "^8.4.23", 1483 | "postcss-import": "^15.1.0", 1484 | "postcss-js": "^4.0.1", 1485 | "postcss-load-config": "^4.0.1", 1486 | "postcss-nested": "^6.0.1", 1487 | "postcss-selector-parser": "^6.0.11", 1488 | "resolve": "^1.22.2", 1489 | "sucrase": "^3.32.0" 1490 | }, 1491 | "bin": { 1492 | "tailwind": "lib/cli.js", 1493 | "tailwindcss": "lib/cli.js" 1494 | }, 1495 | "engines": { 1496 | "node": ">=14.0.0" 1497 | } 1498 | }, 1499 | "node_modules/tailwindcss/node_modules/postcss-selector-parser": { 1500 | "version": "6.1.2", 1501 | "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", 1502 | "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", 1503 | "dev": true, 1504 | "dependencies": { 1505 | "cssesc": "^3.0.0", 1506 | "util-deprecate": "^1.0.2" 1507 | }, 1508 | "engines": { 1509 | "node": ">=4" 1510 | } 1511 | }, 1512 | "node_modules/thenify": { 1513 | "version": "3.3.1", 1514 | "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", 1515 | "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", 1516 | "dev": true, 1517 | "dependencies": { 1518 | "any-promise": "^1.0.0" 1519 | } 1520 | }, 1521 | "node_modules/thenify-all": { 1522 | "version": "1.6.0", 1523 | "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", 1524 | "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", 1525 | "dev": true, 1526 | "dependencies": { 1527 | "thenify": ">= 3.1.0 < 4" 1528 | }, 1529 | "engines": { 1530 | "node": ">=0.8" 1531 | } 1532 | }, 1533 | "node_modules/to-regex-range": { 1534 | "version": "5.0.1", 1535 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1536 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1537 | "dev": true, 1538 | "dependencies": { 1539 | "is-number": "^7.0.0" 1540 | }, 1541 | "engines": { 1542 | "node": ">=8.0" 1543 | } 1544 | }, 1545 | "node_modules/ts-interface-checker": { 1546 | "version": "0.1.13", 1547 | "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", 1548 | "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", 1549 | "dev": true 1550 | }, 1551 | "node_modules/util-deprecate": { 1552 | "version": "1.0.2", 1553 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1554 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 1555 | "dev": true 1556 | }, 1557 | "node_modules/which": { 1558 | "version": "2.0.2", 1559 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1560 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1561 | "dev": true, 1562 | "dependencies": { 1563 | "isexe": "^2.0.0" 1564 | }, 1565 | "bin": { 1566 | "node-which": "bin/node-which" 1567 | }, 1568 | "engines": { 1569 | "node": ">= 8" 1570 | } 1571 | }, 1572 | "node_modules/wrap-ansi": { 1573 | "version": "8.1.0", 1574 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", 1575 | "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 1576 | "dev": true, 1577 | "dependencies": { 1578 | "ansi-styles": "^6.1.0", 1579 | "string-width": "^5.0.1", 1580 | "strip-ansi": "^7.0.1" 1581 | }, 1582 | "engines": { 1583 | "node": ">=12" 1584 | }, 1585 | "funding": { 1586 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1587 | } 1588 | }, 1589 | "node_modules/wrap-ansi-cjs": { 1590 | "name": "wrap-ansi", 1591 | "version": "7.0.0", 1592 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1593 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1594 | "dev": true, 1595 | "dependencies": { 1596 | "ansi-styles": "^4.0.0", 1597 | "string-width": "^4.1.0", 1598 | "strip-ansi": "^6.0.0" 1599 | }, 1600 | "engines": { 1601 | "node": ">=10" 1602 | }, 1603 | "funding": { 1604 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1605 | } 1606 | }, 1607 | "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { 1608 | "version": "5.0.1", 1609 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1610 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1611 | "dev": true, 1612 | "engines": { 1613 | "node": ">=8" 1614 | } 1615 | }, 1616 | "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { 1617 | "version": "4.3.0", 1618 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1619 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1620 | "dev": true, 1621 | "dependencies": { 1622 | "color-convert": "^2.0.1" 1623 | }, 1624 | "engines": { 1625 | "node": ">=8" 1626 | }, 1627 | "funding": { 1628 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1629 | } 1630 | }, 1631 | "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { 1632 | "version": "8.0.0", 1633 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1634 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1635 | "dev": true 1636 | }, 1637 | "node_modules/wrap-ansi-cjs/node_modules/string-width": { 1638 | "version": "4.2.3", 1639 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1640 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1641 | "dev": true, 1642 | "dependencies": { 1643 | "emoji-regex": "^8.0.0", 1644 | "is-fullwidth-code-point": "^3.0.0", 1645 | "strip-ansi": "^6.0.1" 1646 | }, 1647 | "engines": { 1648 | "node": ">=8" 1649 | } 1650 | }, 1651 | "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { 1652 | "version": "6.0.1", 1653 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1654 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1655 | "dev": true, 1656 | "dependencies": { 1657 | "ansi-regex": "^5.0.1" 1658 | }, 1659 | "engines": { 1660 | "node": ">=8" 1661 | } 1662 | }, 1663 | "node_modules/yaml": { 1664 | "version": "2.5.1", 1665 | "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", 1666 | "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", 1667 | "dev": true, 1668 | "bin": { 1669 | "yaml": "bin.mjs" 1670 | }, 1671 | "engines": { 1672 | "node": ">= 14" 1673 | } 1674 | } 1675 | } 1676 | } 1677 | -------------------------------------------------------------------------------- /src/theme/static_src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "theme", 3 | "version": "3.8.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "npm run dev", 7 | "build": "npm run build:clean && npm run build:tailwind", 8 | "build:clean": "rimraf ../static/css/dist", 9 | "build:tailwind": "cross-env NODE_ENV=production tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css --minify", 10 | "dev": "cross-env NODE_ENV=development tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css -w", 11 | "tailwindcss": "node ./node_modules/tailwindcss/lib/cli.js" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "flowbite": "^2.5.1", 18 | "@tailwindcss/aspect-ratio": "^0.4.2", 19 | "@tailwindcss/forms": "^0.5.7", 20 | "@tailwindcss/typography": "^0.5.10", 21 | "cross-env": "^7.0.3", 22 | "postcss": "^8.4.32", 23 | "postcss-import": "^15.1.0", 24 | "postcss-nested": "^6.0.1", 25 | "postcss-simple-vars": "^7.0.1", 26 | "rimraf": "^5.0.5", 27 | "tailwindcss": "^3.4.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/theme/static_src/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "postcss-simple-vars": {}, 5 | "postcss-nested": {} 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /src/theme/static_src/src/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/theme/static_src/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | darkMode: 'class', 3 | content: [ 4 | // Templates within theme app (e.g. base.html) 5 | '../templates/**/*.html', 6 | // Templates in other apps 7 | '../../templates/**/*.html', 8 | // Ignore files in node_modules 9 | '!../../**/node_modules', 10 | // Include JavaScript files that might contain Tailwind CSS classes 11 | '../../**/*.js', 12 | // Include Python files that might contain Tailwind CSS classes 13 | '../../**/*.py', 14 | // Include flowbite files 15 | "./node_modules/flowbite/**/*.js" 16 | ], 17 | theme: { 18 | extend: { 19 | colors: { 20 | primary: {"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a","950":"#172554"} 21 | } 22 | }, 23 | fontFamily: { 24 | 'body': [ 25 | 'Inter', 26 | 'ui-sans-serif', 27 | 'system-ui', 28 | '-apple-system', 29 | 'system-ui', 30 | 'Segoe UI', 31 | 'Roboto', 32 | 'Helvetica Neue', 33 | 'Arial', 34 | 'Noto Sans', 35 | 'sans-serif', 36 | 'Apple Color Emoji', 37 | 'Segoe UI Emoji', 38 | 'Segoe UI Symbol', 39 | 'Noto Color Emoji' 40 | ], 41 | 'sans': [ 42 | 'Inter', 43 | 'ui-sans-serif', 44 | 'system-ui', 45 | '-apple-system', 46 | 'system-ui', 47 | 'Segoe UI', 48 | 'Roboto', 49 | 'Helvetica Neue', 50 | 'Arial', 51 | 'Noto Sans', 52 | 'sans-serif', 53 | 'Apple Color Emoji', 54 | 'Segoe UI Emoji', 55 | 'Segoe UI Symbol', 56 | 'Noto Color Emoji' 57 | ] 58 | } 59 | }, 60 | plugins: [ 61 | /** 62 | * '@tailwindcss/forms' is the forms plugin that provides a minimal styling 63 | * for forms. If you don't like it or have own styling for forms, 64 | * comment the line below to disable '@tailwindcss/forms'. 65 | */ 66 | require('@tailwindcss/forms'), 67 | require('@tailwindcss/typography'), 68 | require('@tailwindcss/aspect-ratio'), 69 | require('flowbite/plugin') 70 | ], 71 | } 72 | -------------------------------------------------------------------------------- /src/theme/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static tailwind_tags %} 2 | 3 | 4 | 5 | Django Tailwind 6 | 7 | 8 | 9 | {% tailwind_css %} 10 | 11 | 12 | 13 |
14 |
15 |

Django + Tailwind = ❤️

16 |
17 |
18 | 19 | 20 | --------------------------------------------------------------------------------