diff --git a/requirements.dev.txt b/requirements.dev.txt index 2dbd799..2d43048 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -7,6 +7,8 @@ coverage cssselect==1.3.0 dj-database-url Django==6.0 +django-compressor +django-libsass django-stubs==5.2.8 django-stubs-ext==5.2.8 djangorestframework diff --git a/src/apps/dashboard/tests/integrated/test_views.py b/src/apps/dashboard/tests/integrated/test_views.py index 9311471..94af23b 100644 --- a/src/apps/dashboard/tests/integrated/test_views.py +++ b/src/apps/dashboard/tests/integrated/test_views.py @@ -302,3 +302,9 @@ class SetThemeTest(TestCase): # they mustn't be button els for swatch in locked: self.assertNotEqual(swatch.tag, "button") + + def test_theme_picker_count_matches_context(self): + response = self.client.get(self.url) + parsed = lxml.html.fromstring(response.content) + swatches = parsed.cssselect(".swatch") + self.assertEqual(len(swatches), len(response.context["themes"])) diff --git a/src/apps/dashboard/views.py b/src/apps/dashboard/views.py index 0e1997a..be240eb 100644 --- a/src/apps/dashboard/views.py +++ b/src/apps/dashboard/views.py @@ -8,10 +8,19 @@ from apps.lyric.models import User UNLOCKED_THEMES = frozenset(["theme-default"]) +THEMES = [ + {"name": "theme-default", "label": "Earthman", "locked": False}, + {"name": "theme-nirvana", "label": "Nirvana", "locked": True}, + {"name": "theme-sheol", "label": "Sheol", "locked": True}, +] def home_page(request): - return render(request, "apps/dashboard/home.html", {"form": ItemForm()}) + return render( + request, "apps/dashboard/home.html", { + "form": ItemForm(), + "themes": THEMES, + }) def new_list(request): form = ItemForm(data=request.POST) diff --git a/src/core/settings.py b/src/core/settings.py index 1c5f3ec..0de7b59 100644 --- a/src/core/settings.py +++ b/src/core/settings.py @@ -11,8 +11,11 @@ https://docs.djangoproject.com/en/6.0/ref/settings/ """ from pathlib import Path -import os import dj_database_url +import os +import sys +if 'test' in sys.argv: + COMPRESS_ENABLED = False # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent diff --git a/src/static_src/scss/_base.scss b/src/static_src/scss/_base.scss new file mode 100644 index 0000000..fd6fecd --- /dev/null +++ b/src/static_src/scss/_base.scss @@ -0,0 +1,129 @@ +body { + background-color: rgba(var(--priUser), 1); + color: rgba(var(--secUser), 1); + font-family: Georgia, serif; + min-height: 100vh; + + a { + text-decoration: none; + font-weight: 700; + color: rgba(var(--terUser), 1); + + &:hover { + color: rgba(var(--ninUser), 1); + text-shadow: 0 0 0.5rem rgba(var(--terUser), 1); + } + } + + .container { + max-width: 960px; + margin: 0 auto; + padding: 1rem; + + .navbar { + padding: 0.75rem 0; + border-bottom: 0.1rem solid rgba(var(--secUser), 0.4); + + .container-fluid { + display: flex; + align-items: center; + gap: 1rem; + } + + .navbar-brand { + margin-right: auto; + + h1 { + font-size: 2rem; + } + } + + .navbar-text, + .navbar-link { + color: rgba(var(--quaUser), 1); + font-size: 0.875rem; + } + } + + .input-group { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + + .form-control { + width: auto; + } + } + + .form-control { + background-color: rgba(var(--priUser), 1); + color: rgba(var(--secUser), 1); + border: 0.1rem solid rgba(var(--secUser), 0.5); + --_pad-v: 0.5rem; + padding: var(--_pad-v) 0.75rem; + border-radius: calc((var(--_pad-v) * 2 + 1em) / 3); + width: 100%; + font-family: inherit; + + &.is-invalid { + border-color: rgba(var(--priRd), 1); + } + + &.form-control-lg { + --_pad-v: 0.75rem; + padding: var(--_pad-v) 1rem; + font-size: 1.125rem; + } + + &.is-invalid ~ .invalid-feedback { + display: block; + } + } + + .invalid-feedback { + display: none; + color: rgba(var(--priRd), 1); + font-size: 0.875rem; + margin-top: 0.25rem; + } + + .alert { + padding: 0.75rem 1rem; + margin: 0.75rem 0; + border-radius: 0.5rem; + border: 0.1rem solid rgba(var(--priYl), 0.5); + color: rgba(var(--priYl), 1); + + &.alert-success { + border-color: rgba(var(--priGn), 0.5); + color: rgba(var(--priGn), 1); + } + + &.alert-warning { + border-color: rgba(var(--priOr), 0.5); + color: rgba(var(--priOr), 1); + } + } + + .row { + padding: 2rem 0; + + .col-md-12 { + width: 100%; + justify-content: center; + } + + .col-lg-6 { + max-width: 600px; + margin: 0 auto; + + h2 { + font-size: 2.5rem; + color: rgba(var(--quaUser), 1); + margin-bottom: 1rem; + } + } + } + } +} \ No newline at end of file diff --git a/src/static_src/scss/_button-pad.scss b/src/static_src/scss/_button-pad.scss new file mode 100644 index 0000000..a8c954c --- /dev/null +++ b/src/static_src/scss/_button-pad.scss @@ -0,0 +1,233 @@ +.btn { + min-width: 2rem; + min-height: 2rem; + text-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.5); + border: 0.15rem solid rgba(var(--priUser), 1); + border-radius: 50%; + font-weight: 700; + font-size: 0.63rem; + text-transform: uppercase; + margin: 0.25rem; + flex-shrink: 0; + + &:hover, + &:active { + cursor: pointer; + } + + &:active { + font-size: 0.61rem; + border: 0.18rem solid rgba(var(--priUser), 1); + } + + &.btn-cancel { + color: rgba(var(--priOr), 1); + border-color: rgba(var(--priOr), 1); + background-color: rgba(var(--terOr), 1); + box-shadow: + 0.1rem 0.1rem 0.12rem rgba(var(--terOr), 0.25), + 0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25), + 0.25rem 0.25rem 0.25rem rgba(var(--terOr), 0.12) + ; + + &:hover { + text-shadow: + 0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25), + 0 0 1rem rgba(var(--priOr), 1) + ; + box-shadow: + 0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priOr), 0.12) + ; + } + + &:active { + text-shadow: + -0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25), + 0 0 0.12rem rgba(var(--priOr), 1) + ; + box-shadow: + -0.1rem -0.1rem 0.12rem rgba(var(--terOr), 0.25), + -0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priOr), 0.12) + ; + } + } + + &.btn-caution { + color: rgba(var(--priYl), 1); + border-color: rgba(var(--priYl), 1); + background-color: rgba(var(--terYl), 1); + box-shadow: + 0.1rem 0.1rem 0.12rem rgba(var(--terYl), 0.25), + 0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25), + 0.25rem 0.25rem 0.25rem rgba(var(--terYl), 0.12) + ; + + &:hover { + text-shadow: + 0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25), + 0 0 1rem rgba(var(--priYl), 1) + ; + box-shadow: + 0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priYl), 0.12) + ; + } + + &:active { + border: 0.18rem solid rgba(var(--priYl), 1); + text-shadow: + -0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25), + 0 0 0.12rem rgba(var(--priYl), 1) + ; + box-shadow: + -0.1rem -0.1rem 0.12rem rgba(var(--terYl), 0.25), + -0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priYl), 0.12) + ; + } + } + + &.btn-confirm { + color: rgba(var(--priGn), 1); + border-color: rgba(var(--priGn), 1); + background-color: rgba(var(--terGn), 1); + box-shadow: + 0.1rem 0.1rem 0.12rem rgba(var(--terGn), 0.25), + 0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25), + 0.25rem 0.25rem 0.25rem rgba(var(--terGn), 0.12) + ; + + &:hover { + text-shadow: + 0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25), + 0 0 1rem rgba(var(--priGn), 1) + ; + box-shadow: + 0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priGn), 0.12) + ; + } + + &:active { + border: 0.18rem solid rgba(var(--priGn), 1); + text-shadow: + -0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25), + 0 0 0.12rem rgba(var(--priGn), 1) + ; + box-shadow: + -0.1rem -0.1rem 0.12rem rgba(var(--terGn), 0.25), + -0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priGn), 0.12) + ; + } + } + + &.btn-danger { + color: rgba(var(--priRd), 1); + background-color: rgba(var(--terRd), 1); + border-color: rgba(var(--priRd), 1); + box-shadow: + 0.1rem 0.1rem 0.12rem rgba(var(--terRd), 0.25), + 0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25), + 0.25rem 0.25rem 0.25rem rgba(var(--terRd), 0.12) + ; + + &:hover { + text-shadow: + 0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25), + 0 0 1rem rgba(var(--priRd), 1) + ; + box-shadow: + 0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priRd), 0.12) + ; + } + + &:active { + text-shadow: + -0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25), + 0 0 0.12rem rgba(var(--priRd), 1) + ; + box-shadow: + -0.1rem -0.1rem 0.12rem rgba(var(--terRd), 0.25), + -0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priRd), 0.12) + ; + } + + &.stop-player { + width: 4rem; + height: 4rem; + font-size: 0.9rem; + border: 0.2rem solid rgba(var(--priRd), 1); + box-shadow: + 0.1rem 0.1rem 0.25rem rgba(var(--terRd), 0.5), + 0.25rem 0.25rem 1rem rgba(0, 0, 0, 0.5), + 0.5rem 0.5rem 0.5rem rgba(var(--terRd), 0.25) + ; + + &:hover { + text-shadow: + 0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.5), + 0 0 1rem rgba(var(--priRd), 1) + ; + box-shadow: + 0.25rem 0.25rem 1rem rgba(0, 0, 0, 0.5), + 0 0 1rem rgba(var(--priRd), 0.25) + ; + } + + &:active { + font-size: 0.88rem; + border: 0.25rem solid rgba(var(--priRd), 1); + text-shadow: + -0.1rem -0.1rem 0.5rem rgba(0, 0, 0, 0.5), + 0 0 0.25rem rgba(var(--priRd), 1) + ; + box-shadow: + -0.1rem -0.1rem 0.25rem rgba(var(--terRd), 0.5), + -0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.5), + 0 0 1rem rgba(var(--priRd), 0.25) + ; + } + } + } + + &.btn-disabled { + cursor: default !important; + font-size: 1.2rem; + color: rgba(var(--secUser), 0.25); + background-color: rgba(var(--priUser), 1); + border-color: rgba(var(--secUser), 0.25); + box-shadow: + 0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.5), + 0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25), + 0.25rem 0.25rem 0.25rem rgba(var(--secUser), 0.12) + ; + + &:hover { + text-shadow: + 0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25), + 0 0 1rem rgba(var(--priUser), 1) + ; + box-shadow: + 0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--priUser), 0.12) + ; + } + + &:active { + text-shadow: + -0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25), + 0 0 0.12rem rgba(var(--priUser), 1) + ; + box-shadow: + -0.1rem -0.1rem 0.12rem rgba(var(--priUser), 0.75), + -0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25), + 0 0 0.5rem rgba(var(--secUser), 0.12) + ; + } + } +} diff --git a/src/static_src/scss/_theme-picker.scss b/src/static_src/scss/_theme-picker.scss new file mode 100644 index 0000000..4624532 --- /dev/null +++ b/src/static_src/scss/_theme-picker.scss @@ -0,0 +1,37 @@ +.theme-picker { + display: flex; + gap: 1rem; +} + +.theme-picker-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.swatch { + width: 4rem; + height: 4rem; + border-radius: 0.5rem; + background: linear-gradient( + to bottom, + rgba(var(--priUser), 1) 0%, + rgba(var(--priUser), 1) 33%, + rgba(var(--secUser), 1) 33%, + rgba(var(--secUser), 1) 66%, + rgba(var(--terUser), 1) 66%, + rgba(var(--terUser), 1) 100% + ); + border: 0.15rem solid rgba(var(--secUser), 0.5); + + &.active { + border: 0.2rem solid rgba(var(--ninUser), 1); + box-shadow: 0 0 0.5rem rgba(var(--ninUser), 0.5); + } + + &.locked { + opacity: 0.5; + filter: saturate(0.4); + } +} \ No newline at end of file diff --git a/src/static_src/scss/core.scss b/src/static_src/scss/core.scss index 503d6e2..89aadb2 100644 --- a/src/static_src/scss/core.scss +++ b/src/static_src/scss/core.scss @@ -1,4 +1,8 @@ @import 'rootvars'; +@import 'base'; +@import 'button-pad'; +@import 'theme-picker'; + input, textarea, diff --git a/src/templates/apps/dashboard/_partials/_form.html b/src/templates/apps/dashboard/_partials/_form.html index 6de5867..949df5c 100644 --- a/src/templates/apps/dashboard/_partials/_form.html +++ b/src/templates/apps/dashboard/_partials/_form.html @@ -3,7 +3,7 @@ -
| {{ forloop.counter }}. {{ item.text }} |