diff --git a/.gitignore b/.gitignore index a1bce921..06916665 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ scratches/ data/ .DS_Store + +node_modules/ +static/css/output.css + +*.code-workspace diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ee857556..1c918614 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,7 @@ repos: - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files + exclude: ^static/images/ - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.13.3 @@ -20,6 +21,15 @@ repos: - id: ruff-format types_or: [python, pyi] + - repo: local + hooks: + - id: prettier-jinja-templates + name: Format Jinja templates with Prettier + entry: pnpm exec prettier --plugin=prettier-plugin-jinja-template --parser=jinja-template --write + language: system + types: [file] + files: ^templates/.*\.html$ + # - repo: local # hooks: # - id: django-check diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..6e465c62 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "plugins": ["prettier-plugin-jinja-template", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "templates/**/*.html", + "options": { + "parser": "jinja-template" + } + } + ] +} diff --git a/README.md b/README.md index 41287e9a..07842b93 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,13 @@ These instructions will guide you through setting up the project for local devel ### Prerequisites - Python 3.11 or later -- [uv](https://github.com/astral-sh/uv) - A fast Python package installer and resolver. -- **Docker and Docker Compose** - Required for running the PostgreSQL database. +- [uv](https://github.com/astral-sh/uv) - Python package manager +- **Docker and Docker Compose** - Required for running PostgreSQL database - Git +- Make + +> **Note:** This application requires PostgreSQL (uses `ArrayField`). Docker is the easiest way to run PostgreSQL locally. -> **Note:** This application requires PostgreSQL, which uses `ArrayField`. Docker is the recommended way to run PostgreSQL locally. ### 1. Install Dependencies @@ -23,7 +25,12 @@ Then, sync the project dependencies. This will create a virtual environment in ` uv sync --dev ``` -> **Note on RDkit:** If you have a different version of rdkit installed globally, the dependency installation may fail. If this happens, please uninstall the global version and run `uv sync` again. +Note on RDkit installation: if you have rdkit installed on your system globally with a different version of python, the installation will try to link against that and subsequent calls fail. Only option remove global rdkit and rerun sync. + +--- + +The frontend requires `pnpm` to correctly display in development. +[Install it here](https://pnpm.io/installation). ### 2. Set Up Environment File @@ -44,6 +51,7 @@ uv run poe setup ``` This single command will: + 1. Start the PostgreSQL database using Docker Compose. 2. Run database migrations. 3. Bootstrap initial data (anonymous user, default packages, models). @@ -54,9 +62,12 @@ After setup, start the development server: uv run poe dev ``` +This will start the css-watcher as well as the django-development server, The application will be available at `http://localhost:8000`. -#### Other useful Poe commands: +**Note:** The development server automatically starts a CSS watcher (`pnpm run dev`) alongside the Django server to rebuild CSS files when changes are detected. This ensures your styles are always up-to-date during development. + +#### Other useful Poe commands You can list all available commands by running `uv run poe --help`. @@ -66,6 +77,7 @@ uv run poe db-down # Stop PostgreSQL uv run poe migrate # Run migrations only uv run poe bootstrap # Bootstrap data only uv run poe shell # Open the Django shell +uv run poe build # Build frontend assets and collect static files uv run poe clean # Remove database volumes (WARNING: destroys all data) ``` diff --git a/envipath/settings.py b/envipath/settings.py index 2618b01c..dd6491d0 100644 --- a/envipath/settings.py +++ b/envipath/settings.py @@ -92,7 +92,7 @@ TEMPLATES = [ }, ] -ALLOWED_HTML_TAGS = {'b', 'i', 'u', 'br', 'em', 'mark', 'p', 's', 'strong'} +ALLOWED_HTML_TAGS = {"b", "i", "u", "br", "em", "mark", "p", "s", "strong"} WSGI_APPLICATION = "envipath.wsgi.application" @@ -245,6 +245,7 @@ LOGGING = { ENVIFORMER_PRESENT = os.environ.get("ENVIFORMER_PRESENT", "False") == "True" ENVIFORMER_DEVICE = os.environ.get("ENVIFORMER_DEVICE", "cpu") + # If celery is not present set always eager to true which will cause delayed tasks to block until finished FLAG_CELERY_PRESENT = os.environ.get("FLAG_CELERY_PRESENT", "False") == "True" if not FLAG_CELERY_PRESENT: @@ -345,6 +346,14 @@ LOGIN_EXEMPT_URLS = [ "/password_reset/", "/reset/", "/microsoft/", + "/terms", + "/privacy", + "/cookie-policy", + "/about", + "/contact", + "/jobs", + "/cite", + "/legal", ] # MS AD/Entra diff --git a/epdb/templatetags/templatetags.py b/epdb/templatetags/envipytags.py similarity index 100% rename from epdb/templatetags/templatetags.py rename to epdb/templatetags/envipytags.py diff --git a/epdb/urls.py b/epdb/urls.py index 25e18680..b4bdb9f2 100644 --- a/epdb/urls.py +++ b/epdb/urls.py @@ -193,4 +193,13 @@ urlpatterns = [ re_path(r"^jobs", v.jobs, name="jobs"), # OAuth Stuff path("o/userinfo/", v.userinfo, name="oauth_userinfo"), + # Static Pages + re_path(r"^terms$", v.static_terms_of_use, name="terms_of_use"), + re_path(r"^privacy$", v.static_privacy_policy, name="privacy_policy"), + re_path(r"^cookie-policy$", v.static_cookie_policy, name="cookie_policy"), + re_path(r"^about$", v.static_about_us, name="about_us"), + re_path(r"^contact$", v.static_contact_support, name="contact_support"), + re_path(r"^careers$", v.static_careers, name="careers"), + re_path(r"^cite$", v.static_cite, name="cite"), + re_path(r"^legal$", v.static_legal, name="legal"), ] diff --git a/epdb/views.py b/epdb/views.py index 36bb0d6e..08134917 100644 --- a/epdb/views.py +++ b/epdb/views.py @@ -90,6 +90,7 @@ def login(request): if username != request.POST.get("username"): context["message"] = "Login failed!" return render(request, "static/login.html", context) + password = request.POST.get("password") # Get email for username and check if the account is active @@ -104,6 +105,7 @@ def login(request): except get_user_model().DoesNotExist: context["message"] = "Login failed!" return render(request, "static/login.html", context) + try: user = authenticate(username=email, password=password) except Exception: @@ -141,9 +143,14 @@ def register(request): context = get_base_context(request) if request.method == "GET": - context["title"] = "enviPath" - context["next"] = request.GET.get("next", "") - return render(request, "static/register.html", context) + # Redirect to unified login page with signup tab + next_url = request.GET.get("next", "") + redirect_url = reverse("login") + "#signup" + + if next_url: + redirect_url += f"?next={next_url}" + + return redirect(redirect_url) elif request.method == "POST": context["title"] = "enviPath" if next := request.POST.get("next"): @@ -156,18 +163,18 @@ def register(request): if not (username and email and password): context["message"] = "Invalid username/email/password" - return render(request, "static/register.html", context) + return render(request, "static/login.html", context) if password != rpassword or password == "": context["message"] = "Registration failed, provided passwords differ!" - return render(request, "static/register.html", context) + return render(request, "static/login.html", context) try: u = UserManager.create_user(username, email, password) logger.info(f"Created user {u.username} ({u.pk})") except Exception: context["message"] = "Registration failed! Couldn't create User Account." - return render(request, "static/register.html", context) + return render(request, "static/login.html", context) if s.ADMIN_APPROVAL_REQUIRED: context["success_message"] = ( @@ -674,7 +681,7 @@ def search(request): if request.method == "GET": package_urls = request.GET.getlist("packages") - searchterm = request.GET.get("search").strip() + searchterm = request.GET.get("search", "").strip() mode = request.GET.get("mode") @@ -2894,3 +2901,60 @@ def userinfo(request): "email_verified": user.is_active, } return JsonResponse(res) + + +# Static Pages +def static_terms_of_use(request): + context = get_base_context(request) + context["title"] = "enviPath - Terms of Use" + context["public_mode"] = True + return render(request, "static/terms_of_use.html", context) + + +def static_privacy_policy(request): + context = get_base_context(request) + context["title"] = "enviPath - Privacy Policy" + context["public_mode"] = True + return render(request, "static/privacy_policy.html", context) + + +def static_cookie_policy(request): + context = get_base_context(request) + context["title"] = "enviPath - Cookie Policy" + context["public_mode"] = True + return render(request, "static/cookie_policy.html", context) + + +def static_about_us(request): + context = get_base_context(request) + context["title"] = "enviPath - About Us" + context["public_mode"] = True + return render(request, "static/about_us.html", context) + + +def static_contact_support(request): + context = get_base_context(request) + context["title"] = "enviPath - Contact & Support" + context["public_mode"] = True + return render(request, "static/contact.html", context) + + +def static_careers(request): + context = get_base_context(request) + context["title"] = "enviPath - Careers" + context["public_mode"] = True + return render(request, "static/careers.html", context) + + +def static_cite(request): + context = get_base_context(request) + context["title"] = "enviPath - How to Cite" + context["public_mode"] = True + return render(request, "static/cite.html", context) + + +def static_legal(request): + context = get_base_context(request) + context["title"] = "enviPath - Legal Information" + context["public_mode"] = True + return render(request, "static/legal.html", context) diff --git a/package.json b/package.json new file mode 100644 index 00000000..ad9c079e --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "envipy", + "version": "1.0.0", + "private": true, + "description": "enviPath UI - Tailwind CSS + DaisyUI", + "scripts": { + "dev": "tailwindcss -i static/css/input.css -o static/css/output.css --watch=always", + "build": "tailwindcss -i static/css/input.css -o static/css/output.css --minify" + }, + "devDependencies": { + "@tailwindcss/cli": "^4.1.16", + "@tailwindcss/postcss": "^4.1.16", + "daisyui": "^5.4.3", + "postcss": "^8.5.6", + "prettier": "^3.6.2", + "prettier-plugin-jinja-template": "^2.1.0", + "prettier-plugin-tailwindcss": "^0.7.1", + "tailwindcss": "^4.1.16" + }, + "keywords": [ + "django", + "tailwindcss", + "daisyui" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4335a75d..883a5034 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,740 @@ -lockfileVersion: 6.0 -specifiers: {} -dependencies: {} -packages: {} +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@tailwindcss/cli': + specifier: ^4.1.16 + version: 4.1.16 + '@tailwindcss/postcss': + specifier: ^4.1.16 + version: 4.1.16 + daisyui: + specifier: ^5.4.3 + version: 5.4.3 + postcss: + specifier: ^8.5.6 + version: 8.5.6 + prettier: + specifier: ^3.6.2 + version: 3.6.2 + prettier-plugin-jinja-template: + specifier: ^2.1.0 + version: 2.1.0(prettier@3.6.2) + prettier-plugin-tailwindcss: + specifier: ^0.7.1 + version: 0.7.1(prettier@3.6.2) + tailwindcss: + specifier: ^4.1.16 + version: 4.1.16 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@tailwindcss/cli@4.1.16': + resolution: {integrity: sha512-dsnANPrh2ZooHyZ/8uJhc9ecpcYtufToc21NY09NS9vF16rxPCjJ8dP7TUAtPqlUJTHSmRkN2hCdoYQIlgh4fw==} + hasBin: true + + '@tailwindcss/node@4.1.16': + resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==} + + '@tailwindcss/oxide-android-arm64@4.1.16': + resolution: {integrity: sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.16': + resolution: {integrity: sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.16': + resolution: {integrity: sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.16': + resolution: {integrity: sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': + resolution: {integrity: sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': + resolution: {integrity: sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': + resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': + resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.16': + resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.16': + resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': + resolution: {integrity: sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': + resolution: {integrity: sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.16': + resolution: {integrity: sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.16': + resolution: {integrity: sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + daisyui@5.4.3: + resolution: {integrity: sha512-dfDCJnN4utErGoWfElgdEE252FtfHV9Mxj5Dq1+JzUq3nVkluWdF3JYykP0Xy/y/yArnPXYztO1tLNCow3kjmg==} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prettier-plugin-jinja-template@2.1.0: + resolution: {integrity: sha512-mzoCp2Oy9BDSug80fw3B3J4n4KQj1hRvoQOL1akqcDKBb5nvYxrik9zUEDs4AEJ6nK7QDTGoH0y9rx7AlnQ78Q==} + peerDependencies: + prettier: ^3.0.0 + + prettier-plugin-tailwindcss@0.7.1: + resolution: {integrity: sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ==} + engines: {node: '>=20.19'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-hermes': '*' + '@prettier/plugin-oxc': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-hermes': + optional: true + '@prettier/plugin-oxc': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-svelte: + optional: true + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tailwindcss@4.1.16: + resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + + '@tailwindcss/cli@4.1.16': + dependencies: + '@parcel/watcher': 2.5.1 + '@tailwindcss/node': 4.1.16 + '@tailwindcss/oxide': 4.1.16 + enhanced-resolve: 5.18.3 + mri: 1.2.0 + picocolors: 1.1.1 + tailwindcss: 4.1.16 + + '@tailwindcss/node@4.1.16': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.16 + + '@tailwindcss/oxide-android-arm64@4.1.16': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.16': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.16': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.16': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.16': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': + optional: true + + '@tailwindcss/oxide@4.1.16': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.16 + '@tailwindcss/oxide-darwin-arm64': 4.1.16 + '@tailwindcss/oxide-darwin-x64': 4.1.16 + '@tailwindcss/oxide-freebsd-x64': 4.1.16 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.16 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.16 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.16 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.16 + '@tailwindcss/oxide-linux-x64-musl': 4.1.16 + '@tailwindcss/oxide-wasm32-wasi': 4.1.16 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.16 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.16 + + '@tailwindcss/postcss@4.1.16': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.16 + '@tailwindcss/oxide': 4.1.16 + postcss: 8.5.6 + tailwindcss: 4.1.16 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + daisyui@5.4.3: {} + + detect-libc@1.0.3: {} + + detect-libc@2.1.2: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + graceful-fs@4.2.11: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + jiti@2.6.1: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mri@1.2.0: {} + + nanoid@3.3.11: {} + + node-addon-api@7.1.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prettier-plugin-jinja-template@2.1.0(prettier@3.6.2): + dependencies: + prettier: 3.6.2 + + prettier-plugin-tailwindcss@0.7.1(prettier@3.6.2): + dependencies: + prettier: 3.6.2 + + prettier@3.6.2: {} + + source-map-js@1.2.1: {} + + tailwindcss@4.1.16: {} + + tapable@2.3.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 diff --git a/pyproject.toml b/pyproject.toml index 347f1e04..767d3dd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,22 +67,54 @@ docstring-code-format = true [tool.poe.tasks] # Main tasks setup = { sequence = ["db-up", "migrate", "bootstrap"], help = "Complete setup: start database, run migrations, and bootstrap data" } -dev = { cmd = "python manage.py runserver", help = "Start the development server", deps = ["db-up"] } +dev = { shell = """ +# Start pnpm CSS watcher in background +pnpm run dev & +PNPM_PID=$! +echo "Started CSS watcher (PID: $PNPM_PID)" + +# Cleanup function +cleanup() { + echo "\nShutting down..." + if kill -0 $PNPM_PID 2>/dev/null; then + kill $PNPM_PID + echo "✓ CSS watcher stopped" + fi + if [ ! -z "${DJ_PID:-}" ] && kill -0 $DJ_PID 2>/dev/null; then + kill $DJ_PID + echo "✓ Django server stopped" + fi +} + +# Set trap for cleanup +trap cleanup EXIT INT TERM + +# Start Django dev server in background +uv run python manage.py runserver & +DJ_PID=$! + +# Wait for Django to finish +wait $DJ_PID +""", help = "Start the development server with CSS watcher", deps = ["db-up", "js-deps"] } +build = { sequence = ["build-frontend", "collectstatic"], help = "Build frontend assets and collect static files" } # Database tasks db-up = { cmd = "docker compose -f docker-compose.dev.yml up -d", help = "Start PostgreSQL database using Docker Compose" } db-down = { cmd = "docker compose -f docker-compose.dev.yml down", help = "Stop PostgreSQL database" } +# Frontend tasks +js-deps = { cmd = "pnpm install", help = "Install frontend dependencies" } + # Full cleanup tasks clean = { sequence = ["clean-db"], help = "Remove model files and database volumes (WARNING: destroys all data!)" } clean-db = { cmd = "docker compose -f docker-compose.dev.yml down -v", help = "Removes the database container and volume." } # Django tasks -migrate = { cmd = "python manage.py migrate", help = "Run database migrations" } +migrate = { cmd = "uv run python manage.py migrate", help = "Run database migrations" } bootstrap = { shell = """ echo "Bootstrapping initial data..." echo "This will take a bit ⏱️. Get yourself some coffee..." -python manage.py bootstrap +uv run python manage.py bootstrap echo "✓ Bootstrap complete" echo "" echo "Default admin credentials:" @@ -90,4 +122,8 @@ echo " Username: admin" echo " Email: admin@envipath.com" echo " Password: SuperSafe" """, help = "Bootstrap initial data (anonymous user, packages, models)" } -shell = { cmd = "python manage.py shell", help = "Open Django shell" } +shell = { cmd = "uv run python manage.py shell", help = "Open Django shell" } + +# Build tasks +build-frontend = { cmd = "pnpm run build", help = "Build frontend assets using pnpm", deps = ["js-deps"] } +collectstatic = { cmd = "uv run python manage.py collectstatic --noinput", help = "Collect static files for production", deps = ["build-frontend"] } diff --git a/static/css/daisyui-theme.css b/static/css/daisyui-theme.css new file mode 100644 index 00000000..3e85ddc4 --- /dev/null +++ b/static/css/daisyui-theme.css @@ -0,0 +1,84 @@ +/** + * DaisyUI Themes - Generated by Style Dictionary + * Theme mappings defined in tokens/daisyui-themes.json + */ + +/* Light theme (default) */ +@plugin "daisyui/theme" { + name: "envipath"; + default: true; + color-scheme: light; + + --color-base-100: var(--color-neutral-50); + --color-base-200: var(--color-neutral-100); + --color-base-300: var(--color-neutral-200); + --color-base-content: var(--color-neutral-900); + --color-primary: var(--color-primary-500); + --color-primary-content: var(--color-primary-50); + --color-secondary: var(--color-secondary-500); + --color-secondary-content: var(--color-secondary-50); + --color-accent: var(--color-accent-500); + --color-accent-content: var(--color-accent-50); + --color-neutral: var(--color-neutral-950); + --color-neutral-content: var(--color-neutral-100); + --color-info: var(--color-info-500); + --color-info-content: var(--color-info-950); + --color-success: var(--color-success-500); + --color-success-content: var(--color-success-950); + --color-warning: var(--color-warning-500); + --color-warning-content: var(--color-warning-950); + --color-error: var(--color-error-500); + --color-error-content: var(--color-error-950); + + /* border radius */ + --radius-selector: 1rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + + /* base sizes */ + --size-selector: 0.25rem; + --size-field: 0.25rem; + + /* border size */ + --border: 1px; + + /* effects */ + --depth: 1; + --noise: 0; +} + +/* Dark theme (prefers-color-scheme: dark) */ +@plugin "daisyui/theme" { + name: "envipath-dark"; + prefersdark: true; + color-scheme: dark; + + --color-primary: var(--color-primary-400); + --color-primary-content: var(--color-neutral-950); + --color-secondary: var(--color-secondary-400); + --color-secondary-content: var(--color-neutral-950); + --color-accent: var(--color-primary-500); + --color-accent-content: var(--color-neutral-950); + --color-neutral: var(--color-neutral-300); + --color-neutral-content: var(--color-neutral-900); + --color-base-100: var(--color-neutral-900); + --color-base-200: var(--color-neutral-800); + --color-base-300: var(--color-neutral-700); + --color-base-content: var(--color-neutral-50); + --color-info: var(--color-primary-400); + --color-info-content: var(--color-neutral-950); + --color-success: var(--color-success-400); + --color-success-content: var(--color-neutral-950); + --color-warning: var(--color-warning-400); + --color-warning-content: var(--color-neutral-950); + --color-error: var(--color-error-400); + --color-error-content: var(--color-neutral-950); + --radius-selector: 1rem; + --radius-field: 0.25rem; + --radius-box: 0.5rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; +} diff --git a/static/css/input.css b/static/css/input.css new file mode 100644 index 00000000..43ad742b --- /dev/null +++ b/static/css/input.css @@ -0,0 +1,35 @@ +@import "tailwindcss"; + +/* fira-code-latin-wght-normal */ +@font-face { + font-family: 'Fira Code Variable'; + font-style: normal; + font-display: swap; + font-weight: 300 700; + src: url(https://cdn.jsdelivr.net/fontsource/fonts/fira-code:vf@latest/latin-wght-normal.woff2) format('woff2-variations'); + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} + +/* inter-latin-wght-normal */ +@font-face { + font-family: 'Inter Variable'; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(https://cdn.jsdelivr.net/fontsource/fonts/inter:vf@latest/latin-wght-normal.woff2) format('woff2-variations'); + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} + + +/* Tell Tailwind where to find Django templates and Python files */ +@source "../../templates"; + +/* Custom theme configuration - must come before plugins */ +@import "./theme.css"; + +/* Import DaisyUI plugin */ +@plugin "daisyui" { + logs: true; +} + +@import "./daisyui-theme.css"; diff --git a/static/css/theme.css b/static/css/theme.css new file mode 100644 index 00000000..0d830c5e --- /dev/null +++ b/static/css/theme.css @@ -0,0 +1,111 @@ +/** + * Tailwind v4 Theme - Generated by Style Dictionary + * This creates Tailwind utility classes from design tokens + */ + +@theme { + /* Colors */ + --color-primary-50: oklch(0.98 0.02 201); + --color-primary-100: oklch(0.96 0.04 203); + --color-primary-200: oklch(0.92 0.08 205); + --color-primary-300: oklch(0.87 0.12 207); + --color-primary-400: oklch(0.80 0.13 212); + --color-primary-500: oklch(0.71 0.13 215); + --color-primary-600: oklch(0.61 0.11 222); + --color-primary-700: oklch(0.52 0.09 223); + --color-primary-800: oklch(0.45 0.08 224); + --color-primary-900: oklch(0.40 0.07 227); + --color-primary-950: oklch(0.30 0.05 230); + --color-secondary-50: oklch(0.98 0.02 166); + --color-secondary-100: oklch(0.95 0.05 163); + --color-secondary-200: oklch(0.90 0.09 164); + --color-secondary-300: oklch(0.85 0.13 165); + --color-secondary-400: oklch(0.77 0.15 163); + --color-secondary-500: oklch(0.70 0.15 162); + --color-secondary-600: oklch(0.60 0.13 163); + --color-secondary-700: oklch(0.51 0.10 166); + --color-secondary-800: oklch(0.43 0.09 167); + --color-secondary-900: oklch(0.38 0.07 169); + --color-secondary-950: oklch(0.26 0.05 173); + --color-success-50: oklch(0.98 0.02 156); + --color-success-100: oklch(0.96 0.04 157); + --color-success-200: oklch(0.93 0.08 156); + --color-success-300: oklch(0.87 0.14 154); + --color-success-400: oklch(0.80 0.18 152); + --color-success-500: oklch(0.72 0.19 150); + --color-success-600: oklch(0.63 0.17 149); + --color-success-700: oklch(0.53 0.14 150); + --color-success-800: oklch(0.45 0.11 151); + --color-success-900: oklch(0.39 0.09 153); + --color-success-950: oklch(0.27 0.06 153); + --color-warning-50: oklch(0.99 0.03 102); + --color-warning-100: oklch(0.97 0.07 103); + --color-warning-200: oklch(0.95 0.12 102); + --color-warning-300: oklch(0.91 0.17 98); + --color-warning-400: oklch(0.86 0.17 92); + --color-warning-500: oklch(0.80 0.16 86); + --color-warning-600: oklch(0.68 0.14 76); + --color-warning-700: oklch(0.55 0.12 66); + --color-warning-800: oklch(0.48 0.10 62); + --color-warning-900: oklch(0.42 0.09 58); + --color-warning-950: oklch(0.29 0.06 54); + --color-error-50: oklch(0.97 0.01 17); + --color-error-100: oklch(0.94 0.03 18); + --color-error-200: oklch(0.88 0.06 18); + --color-error-300: oklch(0.81 0.10 20); + --color-error-400: oklch(0.71 0.17 22); + --color-error-500: oklch(0.64 0.21 25); + --color-error-600: oklch(0.58 0.22 27); + --color-error-700: oklch(0.51 0.19 28); + --color-error-800: oklch(0.44 0.16 27); + --color-error-900: oklch(0.40 0.13 26); + --color-error-950: oklch(0.26 0.09 26); + --color-neutral-50: oklch(0.98 0.00 248); + --color-neutral-100: oklch(0.97 0.01 248); + --color-neutral-200: oklch(0.93 0.01 256); + --color-neutral-300: oklch(0.87 0.02 253); + --color-neutral-400: oklch(0.71 0.04 257); + --color-neutral-500: oklch(0.55 0.04 257); + --color-neutral-600: oklch(0.45 0.04 257); + --color-neutral-700: oklch(0.37 0.04 257); + --color-neutral-800: oklch(0.28 0.04 260); + --color-neutral-900: oklch(0.28 0.04 260); + --color-neutral-950: oklch(0.28 0.04 260); + + /* Spacing */ + --spacing-0: 0; + --spacing-1: 0.25rem; + --spacing-2: 0.5rem; + --spacing-3: 0.75rem; + --spacing-4: 1rem; + --spacing-5: 1.25rem; + --spacing-6: 1.5rem; + --spacing-7: 1.75rem; + --spacing-8: 2rem; + --spacing-10: 2.5rem; + --spacing-12: 3rem; + --spacing-16: 4rem; + --spacing-20: 5rem; + --spacing-24: 6rem; + --spacing-32: 8rem; + --spacing-40: 10rem; + --spacing-48: 12rem; + --spacing-56: 14rem; + --spacing-64: 16rem; + + /* Typography */ + --font-family-sans: 'Inter Variable', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-family-mono: 'Fira Code Variable', 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace; + --font-family-base: 'Inter Variable', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-size-xs: 0.75rem; + --font-size-sm: 0.875rem; + --font-size-base: 1rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.5rem; + --font-size-3xl: 1.875rem; + --font-size-4xl: 2.25rem; + --font-size-5xl: 3rem; + --font-size-6xl: 3.75rem; + --font-size-7xl: 4.5rem; +} diff --git a/static/images/ep-rule-artwork.png b/static/images/ep-rule-artwork.png new file mode 100644 index 00000000..5cd34ca6 Binary files /dev/null and b/static/images/ep-rule-artwork.png differ diff --git a/static/images/hero.png b/static/images/hero.png new file mode 100644 index 00000000..863d00c5 Binary files /dev/null and b/static/images/hero.png differ diff --git a/static/images/linkedin.png b/static/images/linkedin.png new file mode 100644 index 00000000..be244b05 Binary files /dev/null and b/static/images/linkedin.png differ diff --git a/static/images/logo-eawag.svg b/static/images/logo-eawag.svg new file mode 100644 index 00000000..4dc93042 --- /dev/null +++ b/static/images/logo-eawag.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/images/logo-long.svg b/static/images/logo-long.svg deleted file mode 100644 index 3a13bac9..00000000 --- a/static/images/logo-long.svg +++ /dev/null @@ -1,225 +0,0 @@ - - - -image/svg+xml diff --git a/static/images/logo-mission.svg b/static/images/logo-mission.svg new file mode 100644 index 00000000..d9a90002 --- /dev/null +++ b/static/images/logo-mission.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/static/images/logo-name.svg b/static/images/logo-name.svg new file mode 100644 index 00000000..8d6adb6d --- /dev/null +++ b/static/images/logo-name.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/images/logo-square.svg b/static/images/logo-square.svg new file mode 100644 index 00000000..5502b571 --- /dev/null +++ b/static/images/logo-square.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/images/uoa-logo-small.png b/static/images/uoa-logo-small.png new file mode 100755 index 00000000..8d9ff890 Binary files /dev/null and b/static/images/uoa-logo-small.png differ diff --git a/static/images/uoa.png b/static/images/uoa.png deleted file mode 100644 index 8fbbaf3c..00000000 Binary files a/static/images/uoa.png and /dev/null differ diff --git a/static/js/discourse-api.js b/static/js/discourse-api.js new file mode 100644 index 00000000..469b73c5 --- /dev/null +++ b/static/js/discourse-api.js @@ -0,0 +1,170 @@ +/** + * Discourse API Integration for enviPath Community + * Handles fetching topics from the Discourse forum API + */ + +class DiscourseAPI { + constructor() { + this.baseUrl = 'https://community.envipath.org'; + this.categoryId = 10; // Announcements category + this.limit = 3; // Number of topics to fetch + } + + /** + * Fetch topics from Discourse API + * @param {number} limit - Number of topics to fetch + * @returns {Promise} Array of topic objects + */ + async fetchTopics(limit = this.limit) { + try { + const url = `${this.baseUrl}/c/announcements/${this.categoryId}.json`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return this.processTopics(data.topic_list.topics, limit); + } catch (error) { + console.error('Error fetching Discourse topics:', error); + return this.getFallbackTopics(); + } + } + + /** + * Process raw Discourse topics into standardized format + * @param {Array} topics - Raw topics from Discourse API + * @param {number} limit - Number of topics to return + * @returns {Array} Processed topics + */ + processTopics(topics, limit) { + return topics + .slice(0, limit) + .map(topic => ({ + id: topic.id, + title: topic.title, + excerpt: this.extractExcerpt(topic.excerpt), + url: `${this.baseUrl}/t/${topic.slug}/${topic.id}`, + replies: topic.reply_count, + views: topic.views, + created_at: topic.created_at, + category: 'Announcements', + category_id: this.categoryId, + author: topic.last_poster_username, + author_avatar: this.getAvatarUrl(topic.last_poster_username) + })) + .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); // Latest first + } + + /** + * Extract excerpt from topic content + * @param {string} excerpt - Raw excerpt from Discourse + * @returns {string} Cleaned excerpt + */ + extractExcerpt(excerpt) { + if (!excerpt) return 'Click to read more'; + + // Remove HTML tags and clean up; collapse whitespace; do not add manual ellipsis + return excerpt + .replace(/<[^>]*>/g, '') // Remove HTML tags + .replace(/ /g, ' ') // Replace   with spaces + .replace(/&/g, '&') // Replace & with & + .replace(/</g, '<') // Replace < with < + .replace(/>/g, '>') // Replace > with > + .replace(/\s+/g, ' ') // Collapse all whitespace/newlines + .trim() + } + + /** + * Get avatar URL for user + * @param {string} username - Username + * @returns {string} Avatar URL + */ + getAvatarUrl(username) { + if (!username) return `${this.baseUrl}/letter_avatar_proxy/v4/letter/u/1.png`; + return `${this.baseUrl}/user_avatar/${this.baseUrl.replace('https://', '')}/${username}/40/1_1.png`; + } + + /** + * Get fallback topics when API fails + * @returns {Array} Fallback topics + */ + getFallbackTopics() { + return [ + { + id: 110, + title: "enviPath Beta Update: Major Improvements to Prediction, Analysis & Collaboration!", + excerpt: "We're excited to announce major updates to the enviPath beta platform! This release includes significant improvements to our prediction algorithms, enhanced analysis tools, and new collaboration features that will make environmental biotransformation research more accessible and efficient.", + url: "https://community.envipath.org/t/envipath-beta-update-major-improvements-to-prediction-analysis-collaboration/110", + replies: 0, + views: 16, + created_at: "2025-09-23T00:00:00Z", + category: "Announcements", + category_id: 10, + author: "wicker", + author_avatar: "https://community.envipath.org/user_avatar/community.envipath.org/wicker/40/1_1.png" + } + ]; + } + + /** + * Format date for display + * @param {string} dateString - ISO date string + * @returns {string} Formatted date + */ + formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleDateString(); + } + + + /** + * Load topics and call render function + * @param {string} containerId - ID of container element + * @param {Function} renderCallback - Function to render topics + */ + async loadTopics(containerId = 'community-news-container', renderCallback = null) { + const container = document.getElementById(containerId); + if (!container) { + console.error(`Container with ID '${containerId}' not found`); + return; + } + + // Hide loading spinner + const loading = document.getElementById('loading'); + if (loading) { + loading.style.display = 'none'; + } + + try { + const topics = await this.fetchTopics(); + + if (renderCallback && typeof renderCallback === 'function') { + renderCallback(topics); + } else { + // Default rendering - just log topics + console.log('Topics loaded:', topics); + } + } catch (error) { + console.error('Error loading topics:', error); + container.innerHTML = '

No updates found. Head over to the community to see the latest discussions.

'; + } + } +} + +// Export for use in other scripts +window.DiscourseAPI = DiscourseAPI; + +// Auto-initialize if container exists +document.addEventListener('DOMContentLoaded', function() { + if (document.getElementById('community-news-container')) { + const discourseAPI = new DiscourseAPI(); + discourseAPI.loadTopics('community-news-container', function(topics) { + // This will be handled by the template's render function + if (window.renderDiscourseTopics) { + window.renderDiscourseTopics(topics); + } + }); + } +}); diff --git a/templates/framework.html b/templates/framework.html index 80c7a6d5..2f6ea7da 100644 --- a/templates/framework.html +++ b/templates/framework.html @@ -1,15 +1,17 @@ - + {% load static %} {{ title }} - - {# TODO use bundles from bootstrap 3.3.7 #} + + {# Favicon #} + + + {# Tailwind CSS disabled for legacy Bootstrap framework #} + {# Pages using this framework will be migrated to framework_modern.html incrementally #} + {# #} + + {# Legacy Bootstrap 3.3.7 - scoped to .legacy-bootstrap #} @@ -20,7 +22,16 @@ - + + {# Bootstrap compatibility styles #} + + - {# Favicon #} - + @@ -68,6 +78,8 @@ + +
+ + +
{% if breadcrumbs %}
@@ -221,7 +235,8 @@ {% endif %}
- + +

@@ -258,6 +273,9 @@
+
+ + + + {# Font Awesome #} + + + {# Discourse embed for community #} + + + + + {# General EP JS #} + + {# Modal Steps for Stepwise Modal Wizards #} + + + {% if not debug %} + + + + {% endif %} + + + {% include "includes/navbar.html" %} + + {# Main Content Area #} +
+ {% block main_content %} + {# Breadcrumbs - outside main content, optional #} + {% if breadcrumbs %} +
+ +
+ {% endif %} + + {# Main content container - paper effect on medium+ screens #} +
+ {# Messages - inside paper #} + {% if message %} +
+ {{ message }} +
+ {% endif %} + + {# Page content - no enforced styles #} + {% block content %} + {% endblock content %} + + {# License - inside paper if present #} + {% if meta.url_contains_package and meta.current_package.license %} +
+ +
+ License +
+
+ + License + +
+
+ {% endif %} +
+ {% endblock main_content %} +
+ + {% include "includes/footer.html" %} + + {# Floating Help Tab #} + {% if not public_mode %} + + {% endif %} + + {# Modals - TODO: Convert these to DaisyUI modals #} + {% block modals %} + {# Note: These modals still use Bootstrap markup and will need conversion #} + {% include "modals/cite_modal.html" %} + {% include "modals/signup_modal.html" %} + {% include "modals/predict_modal.html" %} + {% include "modals/batch_predict_modal.html" %} + {% endblock %} + + + + diff --git a/templates/includes/footer.html b/templates/includes/footer.html new file mode 100644 index 00000000..7408d29d --- /dev/null +++ b/templates/includes/footer.html @@ -0,0 +1,69 @@ +{% load static %} +
+ + + +
diff --git a/templates/includes/navbar.html b/templates/includes/navbar.html new file mode 100644 index 00000000..6b79815b --- /dev/null +++ b/templates/includes/navbar.html @@ -0,0 +1,71 @@ +{% load static %} +{# Modern DaisyUI Navbar #} + + + diff --git a/templates/index/index.html b/templates/index/index.html index fe83f845..1ca5394d 100644 --- a/templates/index/index.html +++ b/templates/index/index.html @@ -1,186 +1,360 @@ -{% extends "framework.html" %} +{% extends "framework_modern.html" %} {% load static %} -{% block content %} - -