--- /dev/null
+**/__pycache__
+**/*~
+**/#*#
+**/.#*
+src/wolnelektury/localsettings.py
+src/wolnelektury/dev.db
+
localsettings.py
+.env
dev.sqlite
*.db
*.db-journal
-FROM python:3.9-trixie AS base
+# Wolne Lektury images.
+#
+# Stages marked with ##### are build targets:
+# deps dev image (docker-compose.yml, code mounted)
+# test docker-compose.test.yml
+# prod production app image (default, last stage)
+# nginx production nginx with static files
+# postgres Postgres with Polish dictionary
+# Stages marked with ===== are intermediate.
-ARG UID=1000
-ARG GID=1000
+
+# Global build arguments. Stages using UID/GID must redeclare them
+# (bare `ARG UID`) to bring them into scope; the defaults are set here.
+
+ARG PYTHON_IMAGE=python:3.9-trixie
+ARG UID=8100
+ARG GID=8100
+
+
+##########
+# deps #############################################################
+#
+# Base system and requirements.
+# Used directly as the dev image, with code mounted.
+#
+FROM ${PYTHON_IMAGE} AS deps
+
+ARG UID
+ARG GID
RUN apt-get update && apt-get install -y \
git \
RUN mkdir /app/.ipython
+#=======#
+# app #=============================================================
+#
+# Code and production requirements. Not production yet: needs
+# staticfiles.json manifest from static-builder.
+#
+FROM deps AS app
+USER root
-FROM base AS dev
+RUN pip install --no-cache-dir gunicorn
-#RUN pip install --no-cache-dir coverage
+USER app
+COPY src /app/src
+#==================#
+# static-builder #==================================================
+#
+# Static files are collected here. Statics are then copied into
+# the nginx image, and staticfiles.json manifest is copied back
+# into the test and prod images.
+#
+FROM app AS static-builder
-FROM base AS prod
+RUN python manage.py collectstatic --noinput
-USER root
-RUN pip install --no-cache-dir gunicorn
+###########
+# nginx ############################################################
+#
+# Static files for production nginx. Config is mounted.
+#
+FROM nginx:alpine AS nginx
-USER app
+ARG UID
+ARG GID
-COPY src /app/src
+# COPY nginx.conf conf.d/...
+RUN addgroup -g $GID app && addgroup nginx app
+COPY --from=static-builder /app/var/static /app/var/static
+#===============#
+# polish-dict #=====================================================
+#
+# Polish hunspell dictionary without diacritics, to allow
+# full text search without diacritics.
+#
+FROM ${PYTHON_IMAGE} AS polish-dict
-FROM prod AS static-builder
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends hunspell-pl \
+ && rm -rf /var/lib/apt/lists/*
-RUN python manage.py collectstatic --noinput
+COPY scripts/fold_hunspell.py /usr/local/bin/fold_hunspell.py
+RUN set -eu; \
+ mkdir /out; cd /tmp; \
+ enc="$(sed -n 's/^SET[[:space:]]*//p' /usr/share/hunspell/pl_PL.aff | tr -d '\r')"; \
+ iconv -f "${enc:-UTF-8}" -t UTF-8 /usr/share/hunspell/pl_PL.dic > pl.dict; \
+ iconv -f "${enc:-UTF-8}" -t UTF-8 /usr/share/hunspell/pl_PL.aff > pl.affix; \
+ python3 /usr/local/bin/fold_hunspell.py pl.dict pl.affix \
+ /out/polish.dict /out/polish.affix; \
+ dpkg-query -W -f='hunspell-pl ${Version}\n' hunspell-pl > /out/polish.version
-FROM nginx:alpine AS nginx
+##############
+# postgres #########################################################
+#
+# Postgres with the dictionary from polish-dict.
+#
+FROM postgres:18 AS postgres
-# COPY nginx.conf conf.d/...
+COPY --from=polish-dict /out/ /tmp/polish-dict/
+RUN mv /tmp/polish-dict/* "/usr/share/postgresql/$PG_MAJOR/tsearch_data/" \
+ && rmdir /tmp/polish-dict
-COPY --from=static-builder /app/var/static /app/var/static
+COPY scripts/postgres-polish.sql /docker-entrypoint-initdb.d/10-postgres-polish.sql
+
+
+##########
+# test #############################################################
+#
+# Production image plus test requirements. Defined before `prod`,
+# so that `prod` stays the default (last) stage.
+#
+FROM app AS test
+
+USER root
+COPY requirements/requirements-test.txt /tmp/
+RUN pip install --no-cache-dir -r /tmp/requirements-test.txt
+USER app
+RUN mkdir -p /app/var/counters
+COPY --from=static-builder /app/var/static/staticfiles.json /app/var/static/staticfiles.json
+CMD ["./manage.py", "test"]
+
+
+##########
+# prod #############################################################
+#
+# Production image: app + staticfiles.json manifest.
+#
+FROM app AS prod
+
+COPY --from=static-builder /app/var/static/staticfiles.json /app/var/static/staticfiles.json
src/manage.py collectstatic --noinput
-.ONESHELL:
+TEST_COMPOSE := docker compose -f docker-compose.test.yml
+
test:
- cd src
- python -Wall -m coverage run --branch --source='.' ./manage.py test; true
- coverage html -d ../htmlcov.new
- rm -rf ../htmlcov
- mv ../htmlcov.new ../htmlcov
- coverage report
- rm .coverage
+ mkdir -p htmlcov
+ UID=$(UID) GID=$(GID) $(TEST_COMPOSE) run --rm --build test sh -c '\
+ python -Wall -m coverage run --branch --source=. ./manage.py test $(ARGS); s=$$?; \
+ coverage report --skip-covered; coverage html -d /app/htmlcov; exit $$s'; \
+ status=$$?; $(TEST_COMPOSE) down; exit $$status
up:
--- /dev/null
+# make test
+# make test ARGS=catalogue
+
+name: wolnelektury-test
+
+x-env: &env
+ POSTGRES_USER: wl
+ POSTGRES_PASSWORD: wl
+
+services:
+ test:
+ build:
+ context: .
+ target: test
+ args:
+ UID: ${UID:-1000}
+ GID: ${GID:-1000}
+ environment:
+ <<: *env
+ SECRET_KEY: test
+ ALLOWED_HOSTS: "*"
+ COVERAGE_FILE: /tmp/.coverage
+ volumes:
+ - ./htmlcov:/app/htmlcov
+ depends_on: [db]
+
+ db:
+ build: {context: ., target: postgres}
+ environment: *env
+ tmpfs: [/var/lib/postgresql]
+
x-app-base: &app-base
build:
context: .
- target: dev
+ target: deps
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
services:
web:
<<: *app-base
- ports:
- - "8000:8000"
command: ./manage.py runserver 0.0.0.0:8000
+ ports:
+ # Dev: use http://localhost:8000/ directly
+ - "127.0.0.1:8000:8000"
celery:
<<: *app-base
command: celery -A wolnelektury worker -l info
db:
- image: postgres:18
+ build:
+ context: .
+ target: postgres
env_file:
- .env
volumes:
memcached:
image: memcached:1.6-alpine
+ nginx:
+ # Just for testing production-like environment.
+ # docker compose --profile nginx up
+ profiles: ["nginx"]
+ build:
+ context: .
+ target: nginx
+ args:
+ UID: ${UID:-1000}
+ GID: ${GID:-1000}
+ ports:
+ - "127.0.0.1:8101:80"
+ volumes:
+ - ./var/media:/app/var/media:ro
+ - ./scripts/nginx.conf:/etc/nginx/conf.d/default.conf:ro
+ depends_on:
+ - web
+
volumes:
- ipython:
\ No newline at end of file
+ ipython:
django-admin-ordering==0.18
django-countries==7.6.1
--e git+https://github.com/rczajka/django-forms-builder@270cc22f80ec4681735b4e01689562fbab79ceda#egg=django-forms-builder
+-e git+https://github.com/rczajka/django-forms-builder@6533f9442f004d44ed362202b6bdab82b73dd5e9#egg=django-forms-builder
oauthlib>=3.0.1,<3.1
--- /dev/null
+#!/usr/bin/env python3
+"""
+Tworzy wersję słownika hunspell bez polskich znaków dla PostgreSQL (szablon ispell).
+
+Użycie:
+ fold_hunspell.py polish.dict polish.affix polish_ascii.dict polish_ascii.affix
+
+Pliki wejściowe muszą być w UTF-8 (po iconv z ISO-8859-2).
+
+Samo usunięcie znaków psuje warunki reguł z klasami negowanymi: np.
+`SFX H ć ło [^ąeśź]ć` (wszystko poza ą, e, ś, ź przed ć) po zamianie
+stałoby się `[^aesz]c` i przestało pasować do czasowników na -ać.
+Dlatego litera zostaje w klasie negowanej tylko wtedy, gdy oryginał
+wykluczał wszystkie litery, które się na nią składają; w przeciwnym
+razie jest usuwana, a pusta klasa zamienia się w `.`. Reguły stają się
+przez to nieco zbyt liberalne (nadmiarowe formy), ale żadna forma nie ginie.
+"""
+import re
+import sys
+import unicodedata
+
+
+def fold(text):
+ text = text.replace('ł', 'l').replace('Ł', 'L')
+ return ''.join(
+ c for c in unicodedata.normalize('NFKD', text)
+ if not unicodedata.combining(c)
+ )
+
+
+LETTERS = 'aąbcćdeęfghijklłmnńoóprsśtuvwxyzźż'
+GROUPS = {}
+for letter in LETTERS:
+ GROUPS.setdefault(fold(letter), set()).add(letter)
+
+
+def fold_negated_class(match):
+ excluded = set(match.group(1))
+ keep = []
+ for c in match.group(1):
+ folded = fold(c)
+ if GROUPS.get(folded, {c}) <= excluded and folded not in keep:
+ keep.append(folded)
+ return '[^' + ''.join(keep) + ']' if keep else '.'
+
+
+def fold_affix_line(line):
+ parts = line.split()
+ if len(parts) >= 5 and parts[0] in ('SFX', 'PFX') and '[^' in parts[4]:
+ condition = re.sub(r'\[\^([^\]]*)\]', fold_negated_class, parts[4])
+ line = line.replace(parts[4], condition, 1)
+ if line.startswith('SET '):
+ return 'SET UTF-8\n'
+ return fold(line)
+
+
+def main(dict_in, affix_in, dict_out, affix_out):
+ with open(dict_in, encoding='utf-8') as src, open(dict_out, 'w', encoding='utf-8') as dst:
+ dst.write(fold(src.read()))
+ with open(affix_in, encoding='utf-8') as src, open(affix_out, 'w', encoding='utf-8') as dst:
+ for line in src:
+ dst.write(fold_affix_line(line))
+
+
+if __name__ == '__main__':
+ main(*sys.argv[1:5])
--- /dev/null
+server {
+ listen 80;
+
+ access_log off;
+ error_log /dev/stderr warn;
+
+ client_max_body_size 4G;
+ send_timeout 300s;
+ proxy_buffers 16 16k;
+ proxy_buffer_size 32k;
+
+ location /static/ {
+ root /app/var;
+ expires 30d;
+ }
+
+ location /media/ {
+ root /app/var;
+ expires 30d;
+ }
+ location ~ ^/media/.*\.(mp3|ogg|daisy\.zip)$ {
+ add_header Content-Disposition "attachment";
+ }
+ location ~ ^/media/.*\.txt$ {
+ add_header Content-Type "text/plain; charset=utf-8";
+ }
+ location ~ ^/media/book/(pdf|epub|mobi|fb2|cover|xml)/ {
+ # kill cache
+ add_header Last-Modified $date_gmt;
+ add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
+ if_modified_since off;
+ expires off;
+ etag off;
+ }
+
+ location /api/ {
+ add_header 'Access-Control-Allow-Origin' '*' always;
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE' always;
+ add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
+ add_header 'Access-Control-Expose-Headers' 'Link' always;
+
+ if ($request_method = OPTIONS) {
+ return 204;
+ }
+
+ proxy_set_header Accept-Language "";
+ proxy_set_header Host $host;
+ proxy_pass http://web:8000;
+ }
+ location / {
+ root /app/var/static/root;
+ try_files $uri @backend;
+ }
+
+ location @backend {
+ proxy_set_header Accept-Language "";
+ proxy_set_header Host $host;
+ proxy_pass http://web:8000;
+ }
+}
--- /dev/null
+CREATE EXTENSION IF NOT EXISTS unaccent;
+
+DO $$
+ BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_ts_dict
+ WHERE dictname = 'polish_hunspell'
+ AND dictnamespace = 'public'::regnamespace) THEN
+ CREATE TEXT SEARCH DICTIONARY public.polish_hunspell (
+ TEMPLATE = ispell, DictFile = polish, AffFile = polish
+ );
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_ts_config
+ WHERE cfgname = 'polish'
+ AND cfgnamespace = 'public'::regnamespace) THEN
+ CREATE TEXT SEARCH CONFIGURATION public.polish (COPY = pg_catalog.simple);
+ END IF;
+END $$;
+
+ALTER TEXT SEARCH DICTIONARY public.polish_hunspell (
+ DictFile = polish, AffFile = polish, StopWords
+);
+
+ALTER TEXT SEARCH CONFIGURATION public.polish
+ ALTER MAPPING FOR asciiword, asciihword, hword_asciipart, word, hword, hword_part
+ WITH unaccent, polish_hunspell, simple;
+
+
@classmethod
def update_all(cls):
- cls.objects.all().update(search_vector = build_search_vector('text'))
+ cls.objects.all().update(search_vector=UnaccentSearchVector('text'))
{% load annoy %}
+{% block settings %}
+{% load title %}
+{% title book.pretty_title %}
+{% endblock %}
+
+
{% block global-content %}
<div class="l-container">
{% annoy_banner 'book-page' %}
<div class="c-media__actions lay-col lay-l-row">
<div class="c-media__btn">
{% if book.has_audio %}
- <button class="l-button l-button--media" id="audiobook"><i class="icon icon-audio"></i> {% trans "pobierz audiobook" %}</button>
+ <button class="l-button l-button--media l-button--dialog" id="audiobook"><i class="icon icon-audio"></i> {% trans "pobierz audiobook" %}</button>
{% endif %}
</div>
<div class="c-media__btn">
- <button class="l-button l-button--media" id="ebook"><i class="icon icon-book"></i> {% trans "pobierz książkę" %}</button>
+ <button class="l-button l-button--media l-button--dialog" id="ebook"><i class="icon icon-book"></i> {% trans "pobierz książkę" %}</button>
</div>
<div class="c-media__btn">
{% if first_text %}
{% endif %}
- <div class="c-media__popup" data-popup="ebook">
- <div class="c-media__popup__box">
+ <dialog class="c-media__popup__box" data-popup="ebook" closedby="any" aria-labelledby="popup-dl-ebook-title">
<div class="c-media__popup__box__lead">
- <h2>{% trans "Pobieranie e-booka" %}</h2>
+ <h2 id="popup-dl-ebook-title">{% trans "Pobieranie e-booka" %}</h2>
<p>{% trans "Wybierz wersję dla siebie" %}:</p>
</div>
<div class="c-media__popup__box__items">
<button class="c-media__popup__close">
<img src="{% static '2022/images/close.svg' %}" alt="{% trans 'Zamknij' %}">
</button>
- </div>
- </div>
+ </dialog>
{% if book.has_audio %}
- <div class="c-media__popup" data-popup="audiobook">
- <div class="c-media__popup__box">
+ <dialog class="c-media__popup__box" data-popup="audiobook" closedby="any" aria-labelledby="popup-dl-audio-title">
<div class="c-media__popup__box__lead">
- <h2>{% trans "Pobieranie audiobooka" %}</h2>
+ <h2 id="popup-dl-audio-title">{% trans "Pobieranie audiobooka" %}</h2>
<p>{% trans "Wybierz wersję dla siebie" %}:</p>
</div>
<div class="c-media__popup__box__items">
<button class="c-media__popup__close">
<img src="{% static '2022/images/close.svg' %}" alt="{% trans 'Zamknij' %}">
</button>
- </div>
- </div>
+ </dialog>
{% endif %}
</div>
{% else %}
{% load annoy %}
-{% block title %}{{ book.pretty_title }}{% endblock %}
+{% block settings %}
+{% load title %}
+{% title book.pretty_title %}
+{% endblock %}
{% block extrahead %}
%(children)s
""" % {
"depth": depth,
- "title": book.title,
+ "title": book.title.replace('&', '&').replace('<', '<'),
"audiences": ", ".join(book.audiences_pl()),
"audiobook": "audiobook" if book.has_media('mp3') else "",
"children": book_tree_texml(books_by_parent.get(book.id, ()), books_by_parent, depth + 1)
class BookInfoStub:
- _empty_fields = ['cover_url', 'variant_of']
+ _empty_fields = ['cover_url', 'variant_of', 'license']
# allow single definition for multiple-value fields
_salias = {
'authors': 'author',
from django.conf import settings
-from django.db.models import Func
-from django.contrib.postgres.search import SearchQuery, SearchVectorField
+from django.contrib.postgres.search import SearchQuery, SearchVector
-class UnaccentSearchQuery(SearchQuery):
- '''
- The idea is to run unaccent *after* the query is already passed through the language dictionary.
- '''
- def as_sql(self, *args, **kwargs):
- sql, params = super().as_sql(*args, **kwargs)
- if settings.SEARCH_USE_UNACCENT:
- sql = f'unaccent({sql}::text)::tsquery'
- return sql, params
+# Deprecated
+UnaccentSearchQuery = SearchQuery
-class UnaccentSearchVector(Func):
- '''
- We do the indexing twice, to account for non-diacritic versions.
- For example: user enters 'róże' -> stem to 'róża' -> unaccent to 'roza'.
- But user enters 'roze' -> stem leaves it as is, so we need original form in the vector.
- '''
- function='to_tsvector'
- if settings.SEARCH_USE_UNACCENT:
- template = f'''unaccent(
- %(function)s('{settings.SEARCH_CONFIG}', %(expressions)s)::text)::tsvector ||
- to_tsvector(
- '{settings.SEARCH_CONFIG_SIMPLE}',
- unaccent(%(expressions)s)
- )'''
- output_field = SearchVectorField()
+def UnaccentSearchVector(expression):
+ return SearchVector(expression, config=settings.SEARCH_CONFIG)
data = []
if len(data) < limit:
authors = catalogue.models.Tag.objects.filter(
- category='author', name_pl__iregex='\m' + prefix).only('name', 'id', 'slug', 'category')
+ category='author', name_pl__iregex=r'\m' + prefix).only('name', 'id', 'slug', 'category')
data.extend([
{
'type': 'author',
if user is not None and user.is_authenticated and len(data) < limit:
tags = social.models.UserList.objects.filter(
- user=user, name__iregex='\m' + prefix).only('name', 'id', 'slug')
+ user=user, name__iregex=r'\m' + prefix).only('name', 'id', 'slug')
data.extend([
{
'type': 'userlist',
])
if len(data) < limit:
tags = catalogue.models.Tag.objects.filter(
- category__in=('theme', 'genre', 'epoch', 'kind'), name_pl__iregex='\m' + prefix).only('name', 'id', 'slug', 'category')
+ category__in=('theme', 'genre', 'epoch', 'kind'), name_pl__iregex=r'\m' + prefix).only('name', 'id', 'slug', 'category')
data.extend([
{
'type': tag.category,
])
if len(data) < limit:
collections = catalogue.models.Collection.objects.filter(
- title_pl__iregex='\m' + prefix).only('title', 'slug')
+ title_pl__iregex=r'\m' + prefix).only('title', 'slug')
data.extend([
{
'type': 'collection',
for collection in collections[:limit - len(data)]
])
if len(data) < limit:
- for b in catalogue.models.Book.objects.filter(findable=True, title__iregex='\m' + prefix)[:limit-len(data)]:
+ for b in catalogue.models.Book.objects.filter(findable=True, title__iregex=r'\m' + prefix)[:limit-len(data)]:
author_str = b.author_unicode()
translator = b.translator()
if translator:
infos = infopages.models.InfoPage.objects.filter(
published=True,
findable=True,
- title_pl__iregex='\m' + prefix).only('title', 'id', 'slug')
+ title_pl__iregex=r'\m' + prefix).only('title', 'id', 'slug')
data.extend([
{
'type': 'info',
class UserListItemReadSerializer(UserListItemSerializer):
book = catalogue.api.serializers.BookSerializer2()
bookmark = bookmarks.api.views.BookmarkSerializer()
+ quote = bookmarks.api.views.QuoteSerializer()
+
class Meta:
model = models.UserListItem
fields = [
data = urlopen(url).read().decode('utf-16')
lines = data.split('\n')[1:]
for line in lines:
- m = re.match('^/katalog/lektura/([^,]+)\.html,', line)
+ m = re.match(r'^/katalog/lektura/([^,]+)\.html,', line)
if m is not None:
slug = m.group(1)
_url, uviews, views, _rest = line.split(',', 3)
import os
DEBUG = os.environ.get('DEBUG', '')
+SECRET_KEY = os.environ.get('SECRET_KEY', None)
ADMINS = [
# ('Your Name', 'your_email@domain.com'),
SEARCH_CONFIG = 'english'
SEARCH_CONFIG_SIMPLE = 'simple'
SEARCH_USE_UNACCENT = False
+
+# Infrastructure: tests don't need Redis or Memcached.
+CELERY_BROKER_URL = 'memory://'
+CELERY_TASK_ALWAYS_EAGER = True
+CACHES = {
+ 'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
+ 'template_fragments': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
+}
}
}
-.c-media__popup {
- position: fixed;
- z-index: 10;
- top: 0;
- left: 0;
- height: 100%;
- align-items: flex-start;
- justify-content: center;
- background-color: rgba($color-black, 0.35);
- display: none;
- overflow-y: scroll;
- padding: 10vh 16px 10px 10px;
- width: 100%;
-
- @include rwd($break-flow) {
- padding: 10vh 40px 40px 40px;
- }
-
- &.is-open {
- display: flex;
- }
-}
-
.c-media__popup__box {
- background-color: #fff;
- position: relative;
- border-radius: 10px;
- overflow: hidden;
+ padding: 0;
+ border: 0;
+ background: none;
+ border-radius: 10px;
- @include rwd($break-flow) {
- min-width: 640px;
- }
+ @include rwd($break-flow) {
+ min-width: 640px;
+ }
+
+ &::backdrop {
+ background-color: rgba($color-black, 0.35);
+ }
}
.c-media__popup__box__lead {
}
.c-media__popup__box__items {
+ background: white;
padding: 16px;
@include rwd($break-flow) {
padding: 40px;
}
.c-media__popup__close {
+ --focus-ring: #fff;
position: absolute;
padding: 0;
border: 0;
padding: 21px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0);
transition: all $ease-dynamic 350ms;
- outline: 0;
margin: 5px;
&.l-books__item--placeholder {
visibility: hidden;
}
- & * {
- outline: 0;
- }
-
&:hover {
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
}
line-height: 140%;
letter-spacing: -0.01em;
color: #FFFFFF;
+ --focus-ring: #fff;
display: flex;
flex-direction: column;
font-size: 0;
margin: 0 0 11px;
- img {
- width: 100%;
+ a {
+ display: block;
+ img {
+ width: 100%;
+ }
}
}
border-radius: 52px;
border: 1px solid #D9D9D9;
width: 100%;
- outline: 0;
transition: border $ease-out 350ms;
@input rwd($break-flow) {
padding: 0;
margin: 0 0 0 30px;
background-color: transparent;
- outline: 0;
font-weight: $regular;
@include font-size(18px);
line-height: 150%;
.l-footer {
+ --focus-ring: #fff;
color: #FFFFFF;
background: #083F4D;
margin: 0 auto;
align-items: center;
justify-content: space-between;
}
-
- &.is-open {
- position: fixed;
- top: 0; left: 0;
- width: 100%;
- z-index: $master-layer;
-
- .l-navigation__logo {
- width: unset;
- overflow: unset;
- }
-
- .l-navigation__menu {
- opacity: 1;
- visibility: visible;
- pointer-events: auto;
- }
- }
}
.l-navigation__logo {
gap: 3%;
}
- ul {
- margin: 0;
- padding: 0;
- list-style: none;
- //max-width: 212px;
- width: 45%;
+ .l-navigation__menu__group {
+ width: 45%;
@include rwd($break-menu) {
width: 100%;
margin-bottom: 20px;
}
+ h2 {
+ margin: 16px 0 0 0;
+ color: #92BD39;
+ font-weight: $semibold;
+ @include font-size(21px);
+ line-height: 140%;
+ letter-spacing: -0.01em;
+ }
+
+ ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
li {
font-weight: $regular;
font-size: 16px;
line-height: 115%;
}
- strong {
- color: #92BD39;
- font-weight: $semibold;
- @include font-size(21px);
- line-height: 140%;
- letter-spacing: -0.01em;
- }
-
a {
color: $color-white;
&:hover {
border: 0;
}
}
+ }
}
}
}
.l-quotes__item {
- outline: 0;
background: #E1F1F2;
border-radius: 10px;
width: 558px;
}
button {
- outline: 0;
width: 40px;
height: 40px;
background: transparent;
// Ebook/Audiobook Btns
(function() {
- let button = $('.c-media__btn button:not(.l-button--media--full)');
- let popupLayer = $('.c-media__popup');
- let closeButton = $('.c-media__popup__close');
+ let dialogs = $(".l-button--dialog");
+ dialogs.on('click', function () {
+ let target = $(this).attr('id');
+ $('[data-popup=' + target)[0].showModal();
+ });
+
+ $('.c-media__popup__close').on('click', function() {
+ $(this).closest('dialog')[0].close();
+ });
+})();
+
+
+// Player
+(function() {
let playButton = $('.c-player__btn--md');
let chaptersButton = $('.c-player__chapters span');
let select = $('.c-select');
let selectItem = $('.c-select li');
let volumeButton = $('.icon-volume');
- button.on('click', function () {
- let target = $(this).attr('id');
- $('[data-popup=' + target).addClass('is-open');
- $('body').addClass('popup-open');
- });
-
- closeButton.on('click', function() {
- $(this).closest('.c-media__popup').removeClass('is-open');
- $('body').removeClass('popup-open');
- });
-
- popupLayer.on('click', function(e) {
- let _this = $(this);
- if($(e.target).is(popupLayer)) {
- _this.removeClass('is-open');
- $('body').removeClass('popup-open');
- }
- });
-
chaptersButton.on('click', function() {
$(this).parent().toggleClass('is-active');
});
selectItem.removeClass('is-active');
$(this).addClass('is-active');
});
-
- $(document).keyup(function(e) {
- if (e.keyCode === 27) {
- $('.c-media__popup').removeClass('is-open');
- }
- });
})();
// Homepage books sliders
updateLiked($btn);
},
error: function() {
- window.location.href = $('#login-link').attr('href');
+ window.location.href = $('.login-link').attr('href');
}
})
} else {
updateLiked($btn);
},
error: function() {
- window.location.href = $('#login-link').attr('href')
+ window.location.href = $('.login-link').attr('href')
}
});
}
<img src="{% static "2022/images/logo.svg" %}" alt="WolneLektury.pl">
</a>
<div class="l-naviagion__search">
- <form action="/szukaj/">
- <input id="search" name="q" type="text" placeholder="szukaj tytułu, autora, motywów…" autocomplete="off" data-source="/szukaj/hint/?max=10">
+ <form action="/szukaj/" role="search">
+ <input id="search" name="q" type="text" placeholder="szukaj tytułu, autora, motywów…" autocomplete="off" data-source="/szukaj/hint/?max=10" aria-label="szukaj">
</form>
</div>
<div class="l-navigation__actions">
<div class="l-navigation__menu__blocks">
<div class="l-navigation__menu__links">
{% cache 30 menu LANGUAGE_CODE %}
- <ul>
- <li><strong>{% trans "Katalog" %}</strong></li>
+ <div class="l-navigation__menu__group">
+ <h2 id="menu-group-1">{% trans "Katalog" %}</h2>
+ <ul aria-labelledby="menu-group-1">
{% menu 'Katalog' %}
</ul>
- <ul>
- <li><strong>{% trans "Włącz się" %}</strong></li>
+ </div>
+ <div class="l-navigation__menu__group">
+ <h2 id="menu-group-2">{% trans "Włącz się" %}</h2>
+ <ul aria-labelledby="menu-group-2">
{% menu 'Włącz się' %}
</ul>
- <ul>
- <li><strong>{% trans "Aktualności" %}</strong></li>
+ </div>
+ <div class="l-navigation__menu__group">
+ <h2 id="menu-group-3">{% trans "Aktualności" %}</h2>
+ <ul aria-labelledby="menu-group-3">
{% latest_blog_posts %}
{% menu 'Aktualności' %}
</ul>
- <ul>
- <li><strong>{% trans "Narzędzia" %}</strong></li>
+ </div>
+ <div class="l-navigation__menu__group">
+ <h2 id="menu-group-4">{% trans "Narzędzia" %}</h2>
+ <ul aria-labelledby="menu-group-4">
{% menu 'Narzędzia' %}
</ul>
- <ul>
- <li><strong>{% trans "O nas" %}</strong></li>
+ </div>
+ <div class="l-navigation__menu__group">
+ <h2 id="menu-group-5">{% trans "O nas" %}</h2>
+ <ul aria-labelledby="menu-group-5">
{% menu 'O nas' %}
</ul>
+ </div>
{% endcache %}
</div>
</div>
<div class="l-navigation__menu__social">
- <ul>
+ <ul aria-label="{% trans "Media społecznościowe" %}">
<li>
<a href="https://www.facebook.com/wolnelektury/"
title="Facebook" target="_blank">
aria-label="{% trans "Menu konta" %}">
{% endif %}
{% if request.user.is_staff %}
- <img src="{% static '2022/images/icons/user-staff.svg' %}">
+ <img src="{% static '2022/images/icons/user-staff.svg' %}" alt="Użytkownik">
{% elif request.user.membership %}
- <img src="{% static '2022/images/icons/user-vip.svg' %}">
+ <img src="{% static '2022/images/icons/user-vip.svg' %}" alt="Użytkownik">
{% else %}
- <img src="{% static '2022/images/icons/user.svg' %}">
+ <img src="{% static '2022/images/icons/user.svg' %}" alt="Użytkownik">
{% endif %}
{% if hide_menu %}
</a>
{% endif %}
{% else %}
<div class="l-navigation__login">
- <a id="login-link" href='{% url 'login' %}?next={{ request.path }}'>{% trans "Zaloguj się" %}</a>
+ <a class="login-link" href='{% url 'login' %}?next={{ request.path }}'>{% trans "Zaloguj się" %}</a>
/
<a href='{% url 'register' %}?next={{ request.path }}'>{% trans "Załóż konto" %}</a>
</div>