diff --git a/pyproject.toml b/pyproject.toml index b09cc28..d937a6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ 'django-celery-beat~=2.7.0', 'django-registration-redux~=2.7', 'django-rest-framework', + 'djangorestframework-camel-case', 'pymemcache', 'python-dotenv~=1.0.1', 'ftfy~=6.2', diff --git a/src/newsreader/accounts/templates/accounts/views/integrations.html b/src/newsreader/accounts/templates/accounts/views/integrations.html index e1ea99d..559d3d2 100644 --- a/src/newsreader/accounts/templates/accounts/views/integrations.html +++ b/src/newsreader/accounts/templates/accounts/views/integrations.html @@ -1,45 +1,47 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load i18n %} {% block content %} -
-
- {% include "components/header/header.html" with title="Integrations" only %} +
+
+
+ {% include "components/header/header.html" with title="Integrations" only %} -
-

Reddit

-
- {% if reddit_authorization_url %} - - {% trans "Authorize account" %} - - {% else %} - - {% endif %} +
+

Reddit

+
+ {% if reddit_authorization_url %} + + {% trans "Authorize account" %} + + {% else %} + + {% endif %} - {% if reddit_refresh_url %} - - {% trans "Refresh token" %} - - {% else %} - - {% endif %} + {% if reddit_refresh_url %} + + {% trans "Refresh token" %} + + {% else %} + + {% endif %} - {% if reddit_revoke_url %} - - {% trans "Deauthorize account" %} - - {% else %} - - {% endif %} + {% if reddit_revoke_url %} + + {% trans "Deauthorize account" %} + + {% else %} + + {% endif %} +
-
-
+
+
{% endblock %} diff --git a/src/newsreader/accounts/templates/accounts/views/login.html b/src/newsreader/accounts/templates/accounts/views/login.html index b4c391d..b83a4dd 100644 --- a/src/newsreader/accounts/templates/accounts/views/login.html +++ b/src/newsreader/accounts/templates/accounts/views/login.html @@ -1,7 +1,9 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% block content %} -
- {% include "accounts/components/login-form.html" with form=form title="Login" confirm_text="Login" %} +
+
+ {% include "accounts/components/login-form.html" with form=form title="Login" confirm_text="Login" %} +
{% endblock %} diff --git a/src/newsreader/accounts/templates/accounts/views/password-change.html b/src/newsreader/accounts/templates/accounts/views/password-change.html index d6eb918..1995b97 100644 --- a/src/newsreader/accounts/templates/accounts/views/password-change.html +++ b/src/newsreader/accounts/templates/accounts/views/password-change.html @@ -1,8 +1,12 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} + {% block content %} -
- {% url 'accounts:settings:home' as cancel_url %} - {% include "components/form/form.html" with form=form title="Change password" confirm_text="Change password" cancel_url=cancel_url %} + {% url 'accounts:settings:home' as cancel_url %} + +
+
+ {% include "components/form/form.html" with form=form title="Change password" confirm_text="Change password" cancel_url=cancel_url %} +
{% endblock %} diff --git a/src/newsreader/accounts/templates/accounts/views/reddit.html b/src/newsreader/accounts/templates/accounts/views/reddit.html index 353ca72..9fa8378 100644 --- a/src/newsreader/accounts/templates/accounts/views/reddit.html +++ b/src/newsreader/accounts/templates/accounts/views/reddit.html @@ -1,20 +1,22 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load i18n %} {% block content %} -
-
- {% if error %} -

{% trans "Reddit authorization failed" %}

-

{{ error }}

- {% elif access_token and refresh_token %} -

{% trans "Reddit account is linked" %}

-

{% trans "Your reddit account was successfully linked." %}

- {% endif %} +
+
+
+ {% if error %} +

{% trans "Reddit authorization failed" %}

+

{{ error }}

+ {% elif access_token and refresh_token %} +

{% trans "Reddit account is linked" %}

+

{% trans "Your reddit account was successfully linked." %}

+ {% endif %} -

- {% trans "Return to integrations page" %} -

-
+

+ {% trans "Return to integrations page" %} +

+
+
{% endblock %} diff --git a/src/newsreader/accounts/templates/accounts/views/settings.html b/src/newsreader/accounts/templates/accounts/views/settings.html index bf01f8e..590fa0f 100644 --- a/src/newsreader/accounts/templates/accounts/views/settings.html +++ b/src/newsreader/accounts/templates/accounts/views/settings.html @@ -1,7 +1,9 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% block content %} -
- {% include "accounts/components/settings-form.html" with form=form title="User profile" confirm_text="Save" %} +
+
+ {% include "accounts/components/settings-form.html" with form=form title="User profile" confirm_text="Save" %} +
{% endblock %} diff --git a/src/newsreader/accounts/views/auth.py b/src/newsreader/accounts/views/auth.py index 0663768..cef1f61 100644 --- a/src/newsreader/accounts/views/auth.py +++ b/src/newsreader/accounts/views/auth.py @@ -1,8 +1,10 @@ from django.contrib.auth import views as django_views from django.urls import reverse_lazy +from newsreader.utils.views import NavListMixin -class LoginView(django_views.LoginView): + +class LoginView(NavListMixin, django_views.LoginView): template_name = "accounts/views/login.html" success_url = reverse_lazy("index") diff --git a/src/newsreader/accounts/views/integrations.py b/src/newsreader/accounts/views/integrations.py index 7bb28e5..1235195 100644 --- a/src/newsreader/accounts/views/integrations.py +++ b/src/newsreader/accounts/views/integrations.py @@ -13,12 +13,13 @@ from newsreader.news.collection.reddit import ( revoke_reddit_token, ) from newsreader.news.collection.tasks import RedditTokenTask +from newsreader.utils.views import NavListMixin logger = logging.getLogger(__name__) -class IntegrationsView(TemplateView): +class IntegrationsView(NavListMixin, TemplateView): template_name = "accounts/views/integrations.html" def get_context_data(self, **kwargs): @@ -55,7 +56,7 @@ class IntegrationsView(TemplateView): } -class RedditTemplateView(TemplateView): +class RedditTemplateView(NavListMixin, TemplateView): template_name = "accounts/views/reddit.html" def get(self, request, *args, **kwargs): diff --git a/src/newsreader/accounts/views/password.py b/src/newsreader/accounts/views/password.py index b89b5c2..9f792ec 100644 --- a/src/newsreader/accounts/views/password.py +++ b/src/newsreader/accounts/views/password.py @@ -1,32 +1,34 @@ from django.contrib.auth import views as django_views from django.urls import reverse_lazy +from newsreader.utils.views import NavListMixin + # PasswordResetView sends the mail # PasswordResetDoneView shows a success message for the above # PasswordResetConfirmView checks the link the user clicked and # prompts for a new password # PasswordResetCompleteView shows a success message for the above -class PasswordResetView(django_views.PasswordResetView): +class PasswordResetView(NavListMixin, django_views.PasswordResetView): template_name = "password-reset/password-reset.html" subject_template_name = "password-reset/password-reset-subject.txt" email_template_name = "password-reset/password-reset-email.html" success_url = reverse_lazy("accounts:password-reset-done") -class PasswordResetDoneView(django_views.PasswordResetDoneView): +class PasswordResetDoneView(NavListMixin, django_views.PasswordResetDoneView): template_name = "password-reset/password-reset-done.html" -class PasswordResetConfirmView(django_views.PasswordResetConfirmView): +class PasswordResetConfirmView(NavListMixin, django_views.PasswordResetConfirmView): template_name = "password-reset/password-reset-confirm.html" success_url = reverse_lazy("accounts:password-reset-complete") -class PasswordResetCompleteView(django_views.PasswordResetCompleteView): +class PasswordResetCompleteView(NavListMixin, django_views.PasswordResetCompleteView): template_name = "password-reset/password-reset-complete.html" -class PasswordChangeView(django_views.PasswordChangeView): +class PasswordChangeView(NavListMixin, django_views.PasswordChangeView): template_name = "accounts/views/password-change.html" success_url = reverse_lazy("accounts:settings") diff --git a/src/newsreader/accounts/views/registration.py b/src/newsreader/accounts/views/registration.py index 814b91e..755c960 100644 --- a/src/newsreader/accounts/views/registration.py +++ b/src/newsreader/accounts/views/registration.py @@ -4,6 +4,8 @@ from django.views.generic import TemplateView from registration.backends.default import views as registration_views +from newsreader.utils.views import NavListMixin + # RegistrationView shows a registration form and sends the email # RegistrationCompleteView shows after filling in the registration form @@ -11,34 +13,34 @@ from registration.backends.default import views as registration_views # ActivationCompleteView shows the success screen when activation was succesful # ActivationResendView can be used when activation links are expired # RegistrationClosedView shows when registration is disabled -class RegistrationView(registration_views.RegistrationView): +class RegistrationView(NavListMixin, registration_views.RegistrationView): disallowed_url = reverse_lazy("accounts:register-closed") template_name = "registration/registration_form.html" success_url = reverse_lazy("accounts:register-complete") -class RegistrationCompleteView(TemplateView): +class RegistrationCompleteView(NavListMixin, TemplateView): template_name = "registration/registration_complete.html" -class RegistrationClosedView(TemplateView): +class RegistrationClosedView(NavListMixin, TemplateView): template_name = "registration/registration_closed.html" # Redirects or renders failed activation template -class ActivationView(registration_views.ActivationView): +class ActivationView(NavListMixin, registration_views.ActivationView): template_name = "registration/activation_failure.html" def get_success_url(self, user): return ("accounts:activate-complete", (), {}) -class ActivationCompleteView(TemplateView): +class ActivationCompleteView(NavListMixin, TemplateView): template_name = "registration/activation_complete.html" # Renders activation form resend or resend_activation_complete -class ActivationResendView(registration_views.ResendActivationView): +class ActivationResendView(NavListMixin, registration_views.ResendActivationView): template_name = "registration/activation_resend_form.html" def render_form_submitted_template(self, form): diff --git a/src/newsreader/accounts/views/settings.py b/src/newsreader/accounts/views/settings.py index 4bd047b..7210449 100644 --- a/src/newsreader/accounts/views/settings.py +++ b/src/newsreader/accounts/views/settings.py @@ -4,9 +4,10 @@ from django.views.generic.edit import FormView, ModelFormMixin from newsreader.accounts.forms import UserSettingsForm from newsreader.accounts.models import User +from newsreader.utils.views import NavListMixin -class SettingsView(ModelFormMixin, FormView): +class SettingsView(NavListMixin, ModelFormMixin, FormView): template_name = "accounts/views/settings.html" success_url = reverse_lazy("accounts:settings:home") form_class = UserSettingsForm diff --git a/src/newsreader/assets/fonts/Inter-Italic-VariableFont_opsz,wght.ttf b/src/newsreader/assets/fonts/Inter-Italic-VariableFont_opsz,wght.ttf new file mode 100644 index 0000000..43ed4f5 Binary files /dev/null and b/src/newsreader/assets/fonts/Inter-Italic-VariableFont_opsz,wght.ttf differ diff --git a/src/newsreader/assets/fonts/Inter-VariableFont_opsz,wght.ttf b/src/newsreader/assets/fonts/Inter-VariableFont_opsz,wght.ttf new file mode 100644 index 0000000..e31b51e Binary files /dev/null and b/src/newsreader/assets/fonts/Inter-VariableFont_opsz,wght.ttf differ diff --git a/src/newsreader/assets/fonts/METADATA.pb b/src/newsreader/assets/fonts/METADATA.pb deleted file mode 100755 index 18857e1..0000000 --- a/src/newsreader/assets/fonts/METADATA.pb +++ /dev/null @@ -1,101 +0,0 @@ -name: "Rubik" -designer: "Hubert and Fischer, Meir Sadan, Cyreal" -license: "OFL" -category: "SANS_SERIF" -date_added: "2015-07-22" -fonts { - name: "Rubik" - style: "normal" - weight: 300 - filename: "Rubik-Light.ttf" - post_script_name: "Rubik-Light" - full_name: "Rubik Light" - copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)" -} -fonts { - name: "Rubik" - style: "italic" - weight: 300 - filename: "Rubik-LightItalic.ttf" - post_script_name: "Rubik-LightItalic" - full_name: "Rubik Light Italic" - copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)" -} -fonts { - name: "Rubik" - style: "normal" - weight: 400 - filename: "Rubik-Regular.ttf" - post_script_name: "Rubik-Regular" - full_name: "Rubik Regular" - copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)" -} -fonts { - name: "Rubik" - style: "italic" - weight: 400 - filename: "Rubik-Italic.ttf" - post_script_name: "Rubik-Italic" - full_name: "Rubik Italic" - copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)" -} -fonts { - name: "Rubik" - style: "normal" - weight: 500 - filename: "Rubik-Medium.ttf" - post_script_name: "Rubik-Medium" - full_name: "Rubik Medium" - copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)" -} -fonts { - name: "Rubik" - style: "italic" - weight: 500 - filename: "Rubik-MediumItalic.ttf" - post_script_name: "Rubik-MediumItalic" - full_name: "Rubik Medium Italic" - copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)" -} -fonts { - name: "Rubik" - style: "normal" - weight: 700 - filename: "Rubik-Bold.ttf" - post_script_name: "Rubik-Bold" - full_name: "Rubik Bold" - copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)" -} -fonts { - name: "Rubik" - style: "italic" - weight: 700 - filename: "Rubik-BoldItalic.ttf" - post_script_name: "Rubik-BoldItalic" - full_name: "Rubik Bold Italic" - copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)" -} -fonts { - name: "Rubik" - style: "normal" - weight: 900 - filename: "Rubik-Black.ttf" - post_script_name: "Rubik-Black" - full_name: "Rubik Black" - copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)" -} -fonts { - name: "Rubik" - style: "italic" - weight: 900 - filename: "Rubik-BlackItalic.ttf" - post_script_name: "Rubik-BlackItalic" - full_name: "Rubik Black Italic" - copyright: "Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)" -} -subsets: "cyrillic" -subsets: "cyrillic-ext" -subsets: "hebrew" -subsets: "latin" -subsets: "latin-ext" -subsets: "menu" diff --git a/src/newsreader/assets/fonts/Rubik-Black.ttf b/src/newsreader/assets/fonts/Rubik-Black.ttf deleted file mode 100755 index 0ffcec9..0000000 Binary files a/src/newsreader/assets/fonts/Rubik-Black.ttf and /dev/null differ diff --git a/src/newsreader/assets/fonts/Rubik-BlackItalic.ttf b/src/newsreader/assets/fonts/Rubik-BlackItalic.ttf deleted file mode 100755 index 5bb1d4b..0000000 Binary files a/src/newsreader/assets/fonts/Rubik-BlackItalic.ttf and /dev/null differ diff --git a/src/newsreader/assets/fonts/Rubik-Bold.ttf b/src/newsreader/assets/fonts/Rubik-Bold.ttf deleted file mode 100755 index 5493b22..0000000 Binary files a/src/newsreader/assets/fonts/Rubik-Bold.ttf and /dev/null differ diff --git a/src/newsreader/assets/fonts/Rubik-BoldItalic.ttf b/src/newsreader/assets/fonts/Rubik-BoldItalic.ttf deleted file mode 100755 index d380dac..0000000 Binary files a/src/newsreader/assets/fonts/Rubik-BoldItalic.ttf and /dev/null differ diff --git a/src/newsreader/assets/fonts/Rubik-Italic.ttf b/src/newsreader/assets/fonts/Rubik-Italic.ttf deleted file mode 100755 index cf43a4b..0000000 Binary files a/src/newsreader/assets/fonts/Rubik-Italic.ttf and /dev/null differ diff --git a/src/newsreader/assets/fonts/Rubik-Light.ttf b/src/newsreader/assets/fonts/Rubik-Light.ttf deleted file mode 100755 index f6e44cc..0000000 Binary files a/src/newsreader/assets/fonts/Rubik-Light.ttf and /dev/null differ diff --git a/src/newsreader/assets/fonts/Rubik-LightItalic.ttf b/src/newsreader/assets/fonts/Rubik-LightItalic.ttf deleted file mode 100755 index b9c5631..0000000 Binary files a/src/newsreader/assets/fonts/Rubik-LightItalic.ttf and /dev/null differ diff --git a/src/newsreader/assets/fonts/Rubik-Medium.ttf b/src/newsreader/assets/fonts/Rubik-Medium.ttf deleted file mode 100755 index 5a3f898..0000000 Binary files a/src/newsreader/assets/fonts/Rubik-Medium.ttf and /dev/null differ diff --git a/src/newsreader/assets/fonts/Rubik-MediumItalic.ttf b/src/newsreader/assets/fonts/Rubik-MediumItalic.ttf deleted file mode 100755 index 5b5bf1f..0000000 Binary files a/src/newsreader/assets/fonts/Rubik-MediumItalic.ttf and /dev/null differ diff --git a/src/newsreader/assets/fonts/Rubik-Regular.ttf b/src/newsreader/assets/fonts/Rubik-Regular.ttf deleted file mode 100755 index abdc5bc..0000000 Binary files a/src/newsreader/assets/fonts/Rubik-Regular.ttf and /dev/null differ diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py index 7728b5f..815e2b2 100644 --- a/src/newsreader/conf/base.py +++ b/src/newsreader/conf/base.py @@ -227,6 +227,7 @@ AXES_FAILURE_LIMIT = 5 AXES_COOLOFF_TIME = 3 # in hours AXES_RESET_ON_SUCCESS = True +# TODO: verify parser works correctly REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework.authentication.SessionAuthentication", @@ -235,7 +236,13 @@ REST_FRAMEWORK = { "rest_framework.permissions.IsAuthenticated", "newsreader.accounts.permissions.IsOwner", ), - "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), + "DEFAULT_RENDERER_CLASSES": ( + "djangorestframework_camel_case.render.CamelCaseJSONRenderer", + ), + + "DEFAULT_PARSER_CLASSES": ( + "djangorestframework_camel_case.parser.CamelCaseJSONParser", + ), } SWAGGER_SETTINGS = { diff --git a/src/newsreader/js/components/NavList.js b/src/newsreader/js/components/NavList.js new file mode 100644 index 0000000..db6efbc --- /dev/null +++ b/src/newsreader/js/components/NavList.js @@ -0,0 +1,20 @@ +import React from 'react'; + +class NavList extends React.Component { + render() { + const entries = Object.entries(this.props.navLinks); + const links = entries.map(([name, link], index) => { + return ( +
  • + {name} +
  • + ); + }); + + const className = this.props.includeBorder ? "nav-list nav-list--bordered": "nav-list"; + + return
      {links}
    ; + } +} + +export default NavList; diff --git a/src/newsreader/js/components/Sidebar.js b/src/newsreader/js/components/Sidebar.js new file mode 100644 index 0000000..49fcfd6 --- /dev/null +++ b/src/newsreader/js/components/Sidebar.js @@ -0,0 +1,23 @@ +import React from 'react'; + +import NavList from './NavList.js'; + +// TODO: show empty category message +class Sidebar extends React.Component { + render() { + return ( +
    +
    + + + + {this.props.children} +
    + +
    + ); + } +} + +export default Sidebar; diff --git a/src/newsreader/js/index.js b/src/newsreader/js/index.js index ccb9553..0cb4335 100644 --- a/src/newsreader/js/index.js +++ b/src/newsreader/js/index.js @@ -2,3 +2,4 @@ import './lib/index.js'; import './pages/homepage/index.js'; import './pages/categories/index.js'; import './pages/rules/index.js'; +import './pages/default/index.js'; diff --git a/src/newsreader/js/pages/categories/App.js b/src/newsreader/js/pages/categories/App.js index a035b46..b20ff1d 100644 --- a/src/newsreader/js/pages/categories/App.js +++ b/src/newsreader/js/pages/categories/App.js @@ -6,6 +6,7 @@ import Card from '../../components/Card.js'; import CategoryCard from './components/CategoryCard.js'; import CategoryModal from './components/CategoryModal.js'; import Messages from '../../components/Messages.js'; +import Sidebar from '../../components/Sidebar.js'; class App extends React.Component { selectCategory = ::this.selectCategory; @@ -90,15 +91,19 @@ class App extends React.Component { return ( <> {this.state.message && } - - {cards} - {selectedCategory && ( - - )} + + +
    + + {cards} + {selectedCategory && ( + + )} +
    ); } diff --git a/src/newsreader/js/pages/categories/index.js b/src/newsreader/js/pages/categories/index.js index 791fdbd..77d6940 100644 --- a/src/newsreader/js/pages/categories/index.js +++ b/src/newsreader/js/pages/categories/index.js @@ -12,11 +12,15 @@ if (page) { let createUrl = document.getElementById('createUrl').textContent; let updateUrl = document.getElementById('updateUrl').textContent; + let linkScript = document.getElementById('Links'); + let navLinks = JSON.parse(linkScript.textContent); + ReactDOM.render( , page ); diff --git a/src/newsreader/js/pages/default/index.js b/src/newsreader/js/pages/default/index.js new file mode 100644 index 0000000..f00ee88 --- /dev/null +++ b/src/newsreader/js/pages/default/index.js @@ -0,0 +1,20 @@ +import React from 'react'; +import ReactDOM from "react-dom"; + +import Sidebar from "../../components/Sidebar"; + +const mainElements = [...document.getElementsByClassName('main')]; +const mainElement = mainElements.find(element => element.dataset.renderSidebar); + +if (mainElement) { + let linkScript = document.getElementById('Links'); + let navLinks = JSON.parse(linkScript.textContent); + + ReactDOM.render( + ReactDOM.createPortal( + , + mainElement + ), + document.createElement('div') + ); +} diff --git a/src/newsreader/js/pages/homepage/App.js b/src/newsreader/js/pages/homepage/App.js index 30b76e4..51d3f1c 100644 --- a/src/newsreader/js/pages/homepage/App.js +++ b/src/newsreader/js/pages/homepage/App.js @@ -4,14 +4,25 @@ import { connect } from 'react-redux'; import { isEqual } from 'lodash'; import { fetchCategories } from './actions/categories'; +import { filterPosts } from './components/postlist/filters.js'; import ScrollTop from './components/ScrollTop.js'; -import Sidebar from './components/sidebar/Sidebar.js'; +import HomepageSidebar from './components/sidebar/Sidebar.js'; import PostList from './components/postlist/PostList.js'; import PostModal from './components/PostModal.js'; import Messages from '../../components/Messages.js'; class App extends React.Component { + state = { postListNode: null } + + constructor(props) { + super(props); + + this.postListRef = node => { + this.setState({ postListNode: node }); + }; + } + componentDidMount() { this.props.fetchCategories(); } @@ -19,11 +30,13 @@ class App extends React.Component { render() { return ( <> - + {!isEqual(this.props.post, {}) && ( @@ -40,7 +53,7 @@ class App extends React.Component { /> )} - + {this.props.error && ( @@ -52,6 +65,7 @@ class App extends React.Component { const mapStateToProps = state => { const { error } = state.error; + const postsByType = filterPosts(state) if (!isEqual(state.selected.post, {})) { const ruleId = state.selected.post.rule.id; @@ -65,10 +79,11 @@ const mapStateToProps = state => { rule, post: state.selected.post, selectedType: state.selected.item.type, + postsByType: postsByType, }; } - return { error, post: state.selected.post }; + return { error, post: state.selected.post, postsByType: postsByType, }; }; const mapDispatchToProps = dispatch => ({ diff --git a/src/newsreader/js/pages/homepage/components/PostModal.js b/src/newsreader/js/pages/homepage/components/PostModal.js index 35f6231..67e8c6c 100644 --- a/src/newsreader/js/pages/homepage/components/PostModal.js +++ b/src/newsreader/js/pages/homepage/components/PostModal.js @@ -47,7 +47,7 @@ class PostModal extends React.Component { const titleClassName = post.read ? 'post__title post__title--read' : 'post__title'; const readButtonDisabled = post.read || this.props.isUpdating || this.props.selectedType === SAVED_TYPE; - const savedIconClass = post.saved ? 'saved-icon saved-icon--saved' : 'saved-icon'; + const savedIconClass = post.saved ? 'post__save post__save--saved saved-icon saved-icon--saved' : 'post__save saved-icon'; let ruleUrl = ''; @@ -63,59 +63,64 @@ class PostModal extends React.Component { return (
    -
    -
    - - -
    -
    -

    {`${post.title} `}

    -
    - - {publicationDate} {this.props.timezone} - - {post.author && {post.author}} - {this.props.category && ( - +
    +
    +
    + + +
    +
    +

    {`${post.title} `}

    +
    +
    + {publicationDate} + {post.author && {post.author}} +
    + +
    + {this.props.category && ( + + + {this.props.category.name} + + + )} + + + {this.props.rule.name} + + - {this.props.category.name} + - - )} - - - {this.props.rule.name} - - - - - - this.props.toggleSaved(post, token)} - /> + this.props.toggleSaved(post, token)} + /> +
    +
    diff --git a/src/newsreader/js/pages/homepage/components/ScrollTop.js b/src/newsreader/js/pages/homepage/components/ScrollTop.js index 24228b1..1255b8f 100644 --- a/src/newsreader/js/pages/homepage/components/ScrollTop.js +++ b/src/newsreader/js/pages/homepage/components/ScrollTop.js @@ -3,35 +3,48 @@ import React from 'react'; export default class ScrollTop extends React.Component { scrollListener = ::this.scrollListener; - state = { showTop: false, showBottom: false }; + state = { + listenerAttached: false, + showTop: false, + showBottom: false + }; - componentDidMount() { - window.addEventListener('scroll', this.scrollListener); + componentDidUpdate() { + if (this.props.postListNode && !this.state.listenerAttached) { + this.props.postListNode.addEventListener('scroll', this.scrollListener); + + this.setState({ listenerAttached: true }); + } } scrollListener() { - const showBottom = window.innerHeight + window.scrollY < document.body.offsetHeight; + const postList = this.props.postListNode; + const elementEnd = ( + postList.scrollTop + postList.offsetHeight>= postList.scrollHeight + ); this.setState({ - showTop: window.pageYOffset > 0 ? true : false, - showBottom: showBottom, + showTop: postList.scrollTop > window.innerHeight, + showBottom: !elementEnd, }); } render() { - return ( + const postList = this.props.postListNode; + + return postList && (
    {this.state.showTop && ( window.scrollTo(0, 0)} + onClick={() => postList.scroll({ top: 0 })} /> )} {this.state.showBottom && ( window.scrollTo(0, document.body.scrollHeight)} + onClick={() => postList.scroll({ top: postList.scrollHeight })} /> )}
    diff --git a/src/newsreader/js/pages/homepage/components/postlist/PostItem.js b/src/newsreader/js/pages/homepage/components/postlist/PostItem.js index 87329a5..9322730 100644 --- a/src/newsreader/js/pages/homepage/components/postlist/PostItem.js +++ b/src/newsreader/js/pages/homepage/components/postlist/PostItem.js @@ -37,7 +37,7 @@ class PostItem extends React.Component {
    - {publicationDate} {this.props.timezone} {post.author && `By ${post.author}`} + {publicationDate} {post.author && `By ${post.author}`} {[CATEGORY_TYPE, SAVED_TYPE].includes(this.props.selected.type) && ( diff --git a/src/newsreader/js/pages/homepage/components/postlist/PostList.js b/src/newsreader/js/pages/homepage/components/postlist/PostList.js index 3de8899..ba85bbd 100644 --- a/src/newsreader/js/pages/homepage/components/postlist/PostList.js +++ b/src/newsreader/js/pages/homepage/components/postlist/PostList.js @@ -65,7 +65,6 @@ class PostList extends React.Component { selected: this.props.selected, feedUrl: this.props.feedUrl, subredditUrl: this.props.subredditUrl, - timezone: this.props.timezone, }; if (isLastItem?.id === item.id) { @@ -96,7 +95,7 @@ class PostList extends React.Component { ); } else { return ( -
    +
      {postItems}
    {this.props.isFetching && }
    @@ -107,7 +106,6 @@ class PostList extends React.Component { const mapStateToProps = state => ({ isFetching: state.posts.isFetching, - postsByType: filterPosts(state), next: state.selected.next, lastReached: state.selected.lastReached, selected: state.selected.item, @@ -118,4 +116,4 @@ const mapDispatchToProps = dispatch => ({ fetchSavedPosts: (next = false) => dispatch(fetchSavedPosts(next)), }); -export default connect(mapStateToProps, mapDispatchToProps)(PostList); +export default connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(PostList); diff --git a/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js b/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js index 5d384db..9fcbeb7 100644 --- a/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js +++ b/src/newsreader/js/pages/homepage/components/sidebar/CategoryItem.js @@ -1,10 +1,10 @@ import React from 'react'; import { connect } from 'react-redux'; -import { isEqual } from 'lodash'; import { CATEGORY_TYPE } from '../../constants.js'; import { selectCategory, fetchCategory } from '../../actions/categories.js'; import { fetchPostsBySection } from '../../actions/posts.js'; + import { isSelected } from './functions.js'; import RuleItem from './RuleItem.js'; diff --git a/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js b/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js index 11289c5..a9a9110 100644 --- a/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js +++ b/src/newsreader/js/pages/homepage/components/sidebar/RuleItem.js @@ -1,10 +1,10 @@ import React from 'react'; import { connect } from 'react-redux'; -import { isEqual } from 'lodash'; import { RULE_TYPE } from '../../constants.js'; import { selectRule, fetchRule } from '../../actions/rules.js'; import { fetchPostsBySection } from '../../actions/posts.js'; + import { isSelected } from './functions.js'; class RuleItem extends React.Component { diff --git a/src/newsreader/js/pages/homepage/components/sidebar/Sidebar.js b/src/newsreader/js/pages/homepage/components/sidebar/Sidebar.js index 88a69f2..adea6bd 100644 --- a/src/newsreader/js/pages/homepage/components/sidebar/Sidebar.js +++ b/src/newsreader/js/pages/homepage/components/sidebar/Sidebar.js @@ -1,20 +1,20 @@ import React from 'react'; import { connect } from 'react-redux'; -import { isEqual } from 'lodash'; -import { filterCategories, filterRules } from './filters.js'; - -import { CATEGORY_TYPE, RULE_TYPE } from '../../constants.js'; +import Sidebar from "../../../../components/Sidebar.js"; import LoadingIndicator from '../../../../components/LoadingIndicator.js'; +import { CATEGORY_TYPE, RULE_TYPE } from '../../constants.js'; + import CategoryItem from './CategoryItem.js'; import SavedItem from './SavedItem.js'; import ReadButton from './ReadButton.js'; -// TODO: show empty category message -class Sidebar extends React.Component { +import { filterCategories, filterRules } from './filters.js'; + + +class HomepageSidebar extends React.Component { render() { - const categoryItems = this.props.categories.items.map(category => { - const rules = this.props.rules.items.filter(rule => { + const categoryItems = this.props.categories.items.map(category => { const rules = this.props.rules.items.filter(rule => { return rule.category === category.id; }); @@ -32,22 +32,24 @@ class Sidebar extends React.Component { this.props.selected.item && [CATEGORY_TYPE, RULE_TYPE].includes(this.props.selected.item.type); + return ( -
    + {(this.props.categories.isFetching || this.props.rules.isFetching) && ( )} -
      +
        {categoryItems}
      {showReadButton && } -
    + ); } -} +}; + const mapStateToProps = state => ({ categories: { ...state.categories, items: filterCategories(state.categories.items) }, @@ -55,4 +57,4 @@ const mapStateToProps = state => ({ selected: state.selected, }); -export default connect(mapStateToProps)(Sidebar); +export default connect(mapStateToProps)(HomepageSidebar); diff --git a/src/newsreader/js/pages/homepage/index.js b/src/newsreader/js/pages/homepage/index.js index acc2d89..2ac09fe 100644 --- a/src/newsreader/js/pages/homepage/index.js +++ b/src/newsreader/js/pages/homepage/index.js @@ -14,6 +14,8 @@ if (page) { const settings = JSON.parse(document.getElementById('homepageSettings').textContent); const { feedUrl, subredditUrl, categoriesUrl } = settings; + const navLinks = JSON.parse(document.getElementById('Links').textContent); + const app = ( ); diff --git a/src/newsreader/news/collection/templates/news/collection/views/feed-create.html b/src/newsreader/news/collection/templates/news/collection/views/feed-create.html index c24791a..ed5aef8 100644 --- a/src/newsreader/news/collection/templates/news/collection/views/feed-create.html +++ b/src/newsreader/news/collection/templates/news/collection/views/feed-create.html @@ -1,9 +1,13 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load static %} + {% block content %} -
    - {% url "news:collection:rules" as cancel_url %} - {% include "components/form/form.html" with form=form title="Add a feed" cancel_url=cancel_url confirm_text="Add feed" %} + {% url "news:collection:rules" as cancel_url %} + +
    +
    + {% include "components/form/form.html" with form=form title="Add a feed" cancel_url=cancel_url confirm_text="Add feed" %} +
    {% endblock %} diff --git a/src/newsreader/news/collection/templates/news/collection/views/feed-update.html b/src/newsreader/news/collection/templates/news/collection/views/feed-update.html index 46df17d..ea614ef 100644 --- a/src/newsreader/news/collection/templates/news/collection/views/feed-update.html +++ b/src/newsreader/news/collection/templates/news/collection/views/feed-update.html @@ -1,14 +1,17 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load static i18n %} {% block content %} -
    - {% if feed.error %} - {% trans "Failed to retrieve posts" as title %} - {% include "components/textbox/textbox.html" with title=title body=feed.error class="text-section--error" only %} - {% endif %} + {% url "news:collection:rules" as cancel_url %} + {% trans "Failed to retrieve posts" as title %} - {% url "news:collection:rules" as cancel_url %} - {% include "components/form/form.html" with form=form title="Update feed" cancel_url=cancel_url confirm_text="Save feed" %} +
    +
    + {% if feed.error %} + {% include "components/textbox/textbox.html" with title=title body=feed.error class="text-section--error" only %} + {% endif %} + + {% include "components/form/form.html" with form=form title="Update feed" cancel_url=cancel_url confirm_text="Save feed" %} +
    {% endblock %} diff --git a/src/newsreader/news/collection/templates/news/collection/views/import.html b/src/newsreader/news/collection/templates/news/collection/views/import.html index 9719847..b93894c 100644 --- a/src/newsreader/news/collection/templates/news/collection/views/import.html +++ b/src/newsreader/news/collection/templates/news/collection/views/import.html @@ -1,9 +1,13 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load static %} + {% block content %} -
    - {% url "news:collection:rules" as cancel_url %} - {% include "components/form/form.html" with form=form title="Import an OPML file" cancel_url=cancel_url confirm_text="Import feeds" %} + {% url "news:collection:rules" as cancel_url %} + +
    +
    + {% include "components/form/form.html" with form=form title="Import an OPML file" cancel_url=cancel_url confirm_text="Import feeds" %} +
    {% endblock %} diff --git a/src/newsreader/news/collection/templates/news/collection/views/rules.html b/src/newsreader/news/collection/templates/news/collection/views/rules.html index 9356761..2430e31 100644 --- a/src/newsreader/news/collection/templates/news/collection/views/rules.html +++ b/src/newsreader/news/collection/templates/news/collection/views/rules.html @@ -1,102 +1,139 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load i18n static filters %} {% block content %} -
    -
    - {% csrf_token %} +
    +
    + + {% csrf_token %} -
    - -
    +
    + +
    -
    -
    - - - -
    -
    +
    +
    + + + +
    +
    -
    - - - - - - - - - - - - - {% for rule in rules %} - - - - - - - +
    +
    - {% include "components/form/checkbox.html" with id="select-all" data_input="rules" id_for_label="select-all" %} - {% trans "Name" %}{% trans "Category" %}{% trans "URL" %}{% trans "Successfuly ran" %}{% trans "Enabled" %}
    - {% with rule|id_for_label:"rules" as id_for_label %} - {% include "components/form/checkbox.html" with name="rules" value=rule.pk id=id_for_label id_for_label=id_for_label %} - {% endwith %} - - {{ rule.name }} - - {% if rule.category %} - {{ rule.category.name }} - {% endif %} - - {{ rule.source_url }} - - {% if rule.failed %} - - {% else %} - - {% endif %} - - {% if rule.enabled %} - - {% else %} - - {% endif %} -
    + + + + + + + + - {% endfor %} - -
    + {% include "components/form/checkbox.html" with id="select-all" data_input="rules" id_for_label="select-all" %} + {% trans "Name" %}{% trans "Category" %}{% trans "URL" %}{% trans "Successfuly ran" %}{% trans "Enabled" %}
    -
    - + + + {% for rule in rules %} + + + {% with rule|id_for_label:"rules" as id_for_label %} + {% include "components/form/checkbox.html" with name="rules" value=rule.pk id=id_for_label id_for_label=id_for_label %} + {% endwith %} + -
    diff --git a/src/newsreader/news/collection/templates/news/collection/views/subreddit-create.html b/src/newsreader/news/collection/templates/news/collection/views/subreddit-create.html index 6250e4e..f28d578 100644 --- a/src/newsreader/news/collection/templates/news/collection/views/subreddit-create.html +++ b/src/newsreader/news/collection/templates/news/collection/views/subreddit-create.html @@ -1,9 +1,13 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load static %} + {% block content %} -
    - {% url "news:collection:rules" as cancel_url %} - {% include "components/form/form.html" with form=form title="Add a subreddit" cancel_url=cancel_url confirm_text="Add subrredit" %} + {% url "news:collection:rules" as cancel_url %} + +
    +
    + {% include "components/form/form.html" with form=form title="Add a subreddit" cancel_url=cancel_url confirm_text="Add subrredit" %} +
    {% endblock %} diff --git a/src/newsreader/news/collection/views/base.py b/src/newsreader/news/collection/views/base.py index 7096218..afce363 100644 --- a/src/newsreader/news/collection/views/base.py +++ b/src/newsreader/news/collection/views/base.py @@ -8,9 +8,10 @@ from django_celery_beat.models import IntervalSchedule, PeriodicTask from newsreader.news.collection.models import CollectionRule from newsreader.news.core.models import Category +from newsreader.utils.views import NavListMixin -class CollectionRuleViewMixin: +class CollectionRuleViewMixin(NavListMixin): queryset = CollectionRule.objects.order_by("name") def get_queryset(self): diff --git a/src/newsreader/news/collection/views/feed.py b/src/newsreader/news/collection/views/feed.py index ade8423..88c80e7 100644 --- a/src/newsreader/news/collection/views/feed.py +++ b/src/newsreader/news/collection/views/feed.py @@ -14,6 +14,7 @@ from newsreader.news.collection.views.base import ( TaskCreationMixin, ) from newsreader.utils.opml import parse_opml +from newsreader.utils.views import NavListMixin class FeedUpdateView(CollectionRuleViewMixin, CollectionRuleDetailMixin, UpdateView): @@ -36,7 +37,7 @@ class FeedCreateView( form_class = FeedForm -class OPMLImportView(FormView): +class OPMLImportView(NavListMixin, FormView): form_class = OPMLImportForm template_name = "news/collection/views/import.html" diff --git a/src/newsreader/news/collection/views/rules.py b/src/newsreader/news/collection/views/rules.py index 202092b..5d142c4 100644 --- a/src/newsreader/news/collection/views/rules.py +++ b/src/newsreader/news/collection/views/rules.py @@ -7,15 +7,20 @@ from django.views.generic.list import ListView from newsreader.news.collection.forms import CollectionRuleBulkForm from newsreader.news.collection.views.base import CollectionRuleViewMixin +from newsreader.utils.views import NavListMixin -class CollectionRuleListView(CollectionRuleViewMixin, ListView): +class CollectionRuleListView( + CollectionRuleViewMixin, + NavListMixin, + ListView +): paginate_by = 50 template_name = "news/collection/views/rules.html" context_object_name = "rules" -class CollectionRuleBulkView(FormView): +class CollectionRuleBulkView(NavListMixin, FormView): form_class = CollectionRuleBulkForm def get_redirect_url(self): diff --git a/src/newsreader/news/core/templates/news/core/views/categories.html b/src/newsreader/news/core/templates/news/core/views/categories.html index 6a6cdae..78fc663 100644 --- a/src/newsreader/news/core/templates/news/core/views/categories.html +++ b/src/newsreader/news/core/templates/news/core/views/categories.html @@ -32,6 +32,7 @@ {{ categories_update_url|json_script:"updateUrl" }} {{ categories_create_url|json_script:"createUrl" }} + {{ sidebar_links|json_script:"Links" }} {{ block.super }} {% endblock %} diff --git a/src/newsreader/news/core/templates/news/core/views/category-create.html b/src/newsreader/news/core/templates/news/core/views/category-create.html index 6da166f..17a42d2 100644 --- a/src/newsreader/news/core/templates/news/core/views/category-create.html +++ b/src/newsreader/news/core/templates/news/core/views/category-create.html @@ -1,9 +1,13 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load static %} + {% block content %} -
    - {% url "news:core:categories" as cancel_url %} - {% include "components/form/form.html" with form=form title="Create category" cancel_url=cancel_url confirm_text="Create category" %} + {% url "news:core:categories" as cancel_url %} + +
    +
    + {% include "components/form/form.html" with form=form title="Create category" cancel_url=cancel_url confirm_text="Create category" %} +
    {% endblock %} diff --git a/src/newsreader/news/core/templates/news/core/views/category-update.html b/src/newsreader/news/core/templates/news/core/views/category-update.html index 1ec1487..31cd742 100644 --- a/src/newsreader/news/core/templates/news/core/views/category-update.html +++ b/src/newsreader/news/core/templates/news/core/views/category-update.html @@ -1,9 +1,13 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load static %} + {% block content %} -
    - {% url "news:core:categories" as cancel_url %} - {% include "components/form/form.html" with form=form title="Update category" cancel_url=cancel_url confirm_text="Save category" %} + {% url "news:core:categories" as cancel_url %} + +
    +
    + {% include "components/form/form.html" with form=form title="Update category" cancel_url=cancel_url confirm_text="Save category" %} +
    {% endblock %} diff --git a/src/newsreader/news/core/templates/news/core/views/homepage.html b/src/newsreader/news/core/templates/news/core/views/homepage.html index a135314..7e59f65 100644 --- a/src/newsreader/news/core/templates/news/core/views/homepage.html +++ b/src/newsreader/news/core/templates/news/core/views/homepage.html @@ -7,6 +7,7 @@ {% block scripts %} {{ homepageSettings|json_script:"homepageSettings" }} + {{ sidebar_links|json_script:"Links" }} {{ block.super }} {% endblock scripts %} diff --git a/src/newsreader/news/core/views.py b/src/newsreader/news/core/views.py index 9664c68..d05603a 100644 --- a/src/newsreader/news/core/views.py +++ b/src/newsreader/news/core/views.py @@ -7,9 +7,10 @@ from django.views.generic.list import ListView from newsreader.news.collection.models import CollectionRule from newsreader.news.core.forms import CategoryForm from newsreader.news.core.models import Category +from newsreader.utils.views import NavListMixin -class NewsView(TemplateView): +class NewsView(NavListMixin, TemplateView): template_name = "news/core/views/homepage.html" def get_context_data(self, **kwargs): @@ -29,7 +30,7 @@ class NewsView(TemplateView): } -class CategoryViewMixin: +class CategoryViewMixin(NavListMixin): queryset = Category.objects.prefetch_related("rules").order_by("name") def get_queryset(self): diff --git a/src/newsreader/scss/components/body/_body.scss b/src/newsreader/scss/components/body/_body.scss index 34bba4c..964970a 100644 --- a/src/newsreader/scss/components/body/_body.scss +++ b/src/newsreader/scss/components/body/_body.scss @@ -2,7 +2,7 @@ margin: 0; padding: 0; - font-family: Rubik, sans-serif; + font-family: Inter; font-size: $font-size; } diff --git a/src/newsreader/scss/components/card/_card.scss b/src/newsreader/scss/components/card/_card.scss index fbde877..96737a4 100644 --- a/src/newsreader/scss/components/card/_card.scss +++ b/src/newsreader/scss/components/card/_card.scss @@ -1,3 +1,6 @@ +@import '../../partials/variables'; +@import '../../lib/mixins'; + .card { display: flex; flex-direction: column; @@ -9,6 +12,10 @@ background-color: var(--background-color); + @media (max-width: $mobile-breakpoint) { + width: initial; + } + &__header { display: flex; justify-content: space-between; @@ -16,17 +23,17 @@ padding: 15px 0; - border-bottom: 2px var(--lightest-accent-color) solid; + border-bottom: 2px var(--border-color) solid; } &__content { display: flex; - padding: 10px; + padding: 10px 0; } &__footer { display: flex; - padding: 10px; + padding: 10px 0; } & .favicon { diff --git a/src/newsreader/scss/components/checkbox-list/_checkbox-list.scss b/src/newsreader/scss/components/checkbox-list/_checkbox-list.scss new file mode 100644 index 0000000..76b9b97 --- /dev/null +++ b/src/newsreader/scss/components/checkbox-list/_checkbox-list.scss @@ -0,0 +1,11 @@ +.checkbox-list { + padding: 0; + + &__item { + gap: 10px; + + & > * { + margin: initial; + } + } +} diff --git a/src/newsreader/scss/components/checkbox-list/index.scss b/src/newsreader/scss/components/checkbox-list/index.scss new file mode 100644 index 0000000..3f7f471 --- /dev/null +++ b/src/newsreader/scss/components/checkbox-list/index.scss @@ -0,0 +1 @@ +@import './checkbox-list'; diff --git a/src/newsreader/scss/components/fieldset/_fieldset.scss b/src/newsreader/scss/components/fieldset/_fieldset.scss index c2588b5..8fd35b0 100644 --- a/src/newsreader/scss/components/fieldset/_fieldset.scss +++ b/src/newsreader/scss/components/fieldset/_fieldset.scss @@ -1,6 +1,8 @@ .fieldset { display: flex; flex-direction: column; + flex-wrap: wrap; + gap: 10px 0; padding: 15px; border: none; diff --git a/src/newsreader/scss/components/form/_form.scss b/src/newsreader/scss/components/form/_form.scss index e43110c..513fec9 100644 --- a/src/newsreader/scss/components/form/_form.scss +++ b/src/newsreader/scss/components/form/_form.scss @@ -1,3 +1,5 @@ +@import '../../partials/variables'; + .form { display: flex; flex-direction: column; @@ -6,6 +8,10 @@ background-color: var(--background-color); + @media (max-width: $mobile-breakpoint) { + width: 100%; + } + &__section { &--last { & .form__fieldset { @@ -44,6 +50,7 @@ display: flex; flex-direction: row; gap: 15px; + flex-wrap: wrap; @include block-padding; } diff --git a/src/newsreader/scss/components/form/_rules-form.scss b/src/newsreader/scss/components/form/_rules-form.scss index 44d4765..f917746 100644 --- a/src/newsreader/scss/components/form/_rules-form.scss +++ b/src/newsreader/scss/components/form/_rules-form.scss @@ -1,5 +1,21 @@ +@import '../../partials/variables'; + + .rules-form { @extend .form; width: 90%; + + @media (max-width: $wqhd-breakpoint) { + width: initial; + } + + + & .form__fieldset { + gap: 15px; + + & > * { + margin: initial; + } + } } diff --git a/src/newsreader/scss/components/index.scss b/src/newsreader/scss/components/index.scss index d64031b..dba0131 100644 --- a/src/newsreader/scss/components/index.scss +++ b/src/newsreader/scss/components/index.scss @@ -27,3 +27,6 @@ @import './posts/index'; @import './posts-info/index'; @import './scroll-to-top/index'; +@import './menu/index'; +@import './nav-list/index'; +@import './checkbox-list/index'; diff --git a/src/newsreader/scss/components/integrations/_integrations.scss b/src/newsreader/scss/components/integrations/_integrations.scss index 815184e..3fbb593 100644 --- a/src/newsreader/scss/components/integrations/_integrations.scss +++ b/src/newsreader/scss/components/integrations/_integrations.scss @@ -7,6 +7,7 @@ &__controls { display: flex; + flex-wrap: wrap; gap: 10px; } } diff --git a/src/newsreader/scss/components/list/_list.scss b/src/newsreader/scss/components/list/_list.scss index 75e5e94..d90e26e 100644 --- a/src/newsreader/scss/components/list/_list.scss +++ b/src/newsreader/scss/components/list/_list.scss @@ -9,9 +9,7 @@ align-items: center; padding: 10px 0; - & > * { - margin: 0 15px; - } + gap: 15px; } } diff --git a/src/newsreader/scss/components/loading-indicator/_loading-indicator.scss b/src/newsreader/scss/components/loading-indicator/_loading-indicator.scss index 0651d1d..d28e87d 100644 --- a/src/newsreader/scss/components/loading-indicator/_loading-indicator.scss +++ b/src/newsreader/scss/components/loading-indicator/_loading-indicator.scss @@ -9,7 +9,7 @@ position: absolute; left: 6px; width: 13px; - background-color: $lavendal-pink; + background-color: var(--font-color); animation: loading-indicator 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite; &:nth-child(1){ diff --git a/src/newsreader/scss/components/main/_main.scss b/src/newsreader/scss/components/main/_main.scss index 5d0143f..2990649 100644 --- a/src/newsreader/scss/components/main/_main.scss +++ b/src/newsreader/scss/components/main/_main.scss @@ -1,7 +1,22 @@ -.main { - display: flex; - flex-direction: column; - align-items: center; +@import '../../partials/variables'; - margin: 20px 0; +.main { + @media (max-width: $mobile-breakpoint) { + display: grid; + grid: [stack] 1fr / min-content [stack] 1fr; + + & .sidebar, .post-message, .posts, #{&}__container { + grid-area: stack; + } + } + + &__container { + display: flex; + flex-direction: column; + align-items: center; + + @media (max-width: $mobile-breakpoint) { + display: initial; + } + } } diff --git a/src/newsreader/scss/components/menu/_menu.scss b/src/newsreader/scss/components/menu/_menu.scss new file mode 100644 index 0000000..fe0025e --- /dev/null +++ b/src/newsreader/scss/components/menu/_menu.scss @@ -0,0 +1,26 @@ +@import '../../partials/variables'; + +.menu { + user-select: none; + touch-action: manipulation; + display: none; + + @media (max-width: $mobile-breakpoint) { + display: initial; + } + + &__icon { + color: var(--font-color); + + padding: 0; + } +} + +#menu-input { + display: none; + + &:checked ~ * .menu__icon::before { + @include font-awesome; + content: "\f410"; + } +} diff --git a/src/newsreader/scss/components/menu/index.scss b/src/newsreader/scss/components/menu/index.scss new file mode 100644 index 0000000..9cac78c --- /dev/null +++ b/src/newsreader/scss/components/menu/index.scss @@ -0,0 +1 @@ +@import './menu'; diff --git a/src/newsreader/scss/components/messages/_messages.scss b/src/newsreader/scss/components/messages/_messages.scss index 6e626c9..0e7a0a0 100644 --- a/src/newsreader/scss/components/messages/_messages.scss +++ b/src/newsreader/scss/components/messages/_messages.scss @@ -1,3 +1,6 @@ +@import '../../partials/variables'; +@import '../../partials/colors'; + .messages { display: flex; flex-direction: column; @@ -6,7 +9,7 @@ width: 100%; margin: 5px 0 20px 0; - color: $font-color; + color: var(--font-color); &__item { width: 80%; @@ -17,6 +20,10 @@ background-color: $transparant-blue; + @media (max-width: $mobile-breakpoint) { + width: 90%; + } + &--error { background-color: $transparant-red; } @@ -42,12 +49,10 @@ } &--fixed &__item { - color: $white; background-color: $blue; } &--fixed &__item--error { - color: $white; background-color: $red; } @@ -56,7 +61,6 @@ } &--fixed &__item--success { - color: $white; background-color: $green; } } diff --git a/src/newsreader/scss/components/modal/_modal.scss b/src/newsreader/scss/components/modal/_modal.scss index 4ed5b41..a0b764e 100644 --- a/src/newsreader/scss/components/modal/_modal.scss +++ b/src/newsreader/scss/components/modal/_modal.scss @@ -1,3 +1,5 @@ +@import '../../partials/variables'; + .modal { display: flex; flex-direction: column; @@ -8,7 +10,7 @@ height: 100%; top: 0; - background-color: $dark; + background-color: var(--background-color); &__item { display: flex; @@ -20,15 +22,17 @@ width: 60%; - background-color: var(--accent-color); + @media (max-width: $mobile-breakpoint) { + width: initial; + } } &__header { - padding: 5px 20px; + padding: 5px 0; } &__content { - padding: 10px 30px; + padding: 10px 0; } &__footer { @@ -36,6 +40,6 @@ flex-direction: row; justify-content: space-between; - padding: 10px; + padding: 10px 0; } } diff --git a/src/newsreader/scss/components/modal/_post-modal.scss b/src/newsreader/scss/components/modal/_post-modal.scss index a286abd..1a8d18f 100644 --- a/src/newsreader/scss/components/modal/_post-modal.scss +++ b/src/newsreader/scss/components/modal/_post-modal.scss @@ -1,3 +1,5 @@ +@import '../../partials/variables'; + .post-modal { @extend .modal; @@ -6,5 +8,9 @@ cursor: pointer; + @media (min-width: $tablet-breakpoint) { + background-color: var(--background-color-secondary); + } + z-index: 1000; } diff --git a/src/newsreader/scss/components/nav-list/_nav-list.scss b/src/newsreader/scss/components/nav-list/_nav-list.scss new file mode 100644 index 0000000..f81ce94 --- /dev/null +++ b/src/newsreader/scss/components/nav-list/_nav-list.scss @@ -0,0 +1,16 @@ +.nav-list { + display: flex; + justify-content: flex-start; + + list-style-type: none; + + &__item { + margin: 0px 10px; + + & a { + @extend .button; + + color: var(--font-color); + } + } +} diff --git a/src/newsreader/scss/components/nav-list/index.scss b/src/newsreader/scss/components/nav-list/index.scss new file mode 100644 index 0000000..1201d0b --- /dev/null +++ b/src/newsreader/scss/components/nav-list/index.scss @@ -0,0 +1 @@ +@import './nav-list'; diff --git a/src/newsreader/scss/components/navbar/_navbar.scss b/src/newsreader/scss/components/navbar/_navbar.scss index afdacce..0fcf3a2 100644 --- a/src/newsreader/scss/components/navbar/_navbar.scss +++ b/src/newsreader/scss/components/navbar/_navbar.scss @@ -1,3 +1,6 @@ +@import '../../partials/variables'; +@import '../../lib/functions'; + .nav { display: flex; justify-content: center; @@ -5,34 +8,43 @@ padding: 10px 0; width: 100%; + height: map-deep-get($nav, height); - position: sticky; - top: 0; + background-color: var(--background-color); - background-color: var(--lightest-accent-color); + border-bottom: 2px var(--border-color) solid; - ol { + @media (max-width: $mobile-breakpoint) { + justify-content: space-between; + height: map-deep-get($nav, mobile, height); + padding: 10px; + + font-size: map-deep-get($nav, mobile, font-size); + } + + &__list { display: flex; justify-content: flex-start; width: 80%; list-style-type: none; - } - &__item { - margin: 0px 10px; - - & a { - @extend .button; - - font-size: 0.9em !important; - font-weight: 600; + @media (max-width: $mobile-breakpoint) { + display: none; } } - &__item:last-child { - margin: 0 10px 0 auto; + & .nav-list { + width: 80%; - border-right: 2px solid var(--lighter-accent-color); + @media (max-width: $mobile-breakpoint) { + display: none; + } + + &__item:last-child { + margin: 0 10px 0 auto; + + border-right: 2px solid var(--border-color); + } } } diff --git a/src/newsreader/scss/components/post-message/_post-message.scss b/src/newsreader/scss/components/post-message/_post-message.scss index e876e7f..712ccd9 100644 --- a/src/newsreader/scss/components/post-message/_post-message.scss +++ b/src/newsreader/scss/components/post-message/_post-message.scss @@ -4,8 +4,8 @@ justify-content: center; align-items: center; - width: 60%; - height: 80vh; + height: max-content; + margin: 20px 0 0 0; &__message { font-size: 16px; diff --git a/src/newsreader/scss/components/post/_post.scss b/src/newsreader/scss/components/post/_post.scss index 7ebb7b3..b7d5ef4 100644 --- a/src/newsreader/scss/components/post/_post.scss +++ b/src/newsreader/scss/components/post/_post.scss @@ -1,20 +1,44 @@ +@import '../../partials/variables'; +@import '../../partials/colors'; +@import '../../lib/functions'; +@import '../../elements/button/'; + .post { display: flex; flex-direction: column; align-items: center; position: relative; - width: 80%; - height: 90%; + width: 35%; + + @media (max-width: $wqhd-breakpoint) { + width: 50%; + } + + @media (max-width: $mobile-breakpoint) { + width: 100%; + height: 80%; + + margin: 0; + } + + height: max-content; margin: 2% auto 5% auto; + padding: 0 0 20px 0; overflow-y: auto; background-color: var(--background-color); + border-radius: 0.25em; cursor: initial; + + &__container { + width: 90%; + } + &__header { display: flex; flex-direction: column; @@ -23,8 +47,6 @@ position: sticky; top: 0; - width: 100%; - background-color: var(--background-color); } @@ -33,8 +55,15 @@ justify-content: flex-end; width: 100%; - padding: 20px 50px 0; + padding: 20px 0; gap: 20px; + + @media (max-width: $mobile-breakpoint) { + justify-content: space-between; + flex-direction: row-reverse; + + gap: 0; + } } &__heading { @@ -42,11 +71,18 @@ flex-direction: column; padding: 20px 0 10px 0; - width: 75%; + @media (min-width: $hd-breakpoint) { + width: 80%; + } + + @media (max-width: $hd-breakpoint) { + width: 100%; + padding: 0; + } } &__title { - font-size: $header-size; + font-size: map-deep-get($post, "header-font-size"); &--read { color: var(--read-color); @@ -62,8 +98,6 @@ } &__rule, &__category { - background-color: var(--lightest-accent-color) !important; - & a { color: var(--font-color); } @@ -74,7 +108,15 @@ flex-direction: column; padding: 10px 0 30px 0; - width: 75%; + + @media (min-width: $hd-breakpoint) { + width: 72%; + } + + @media (max-width: $hd-breakpoint) { + width: 90%; + padding: 0; + } & p { padding: 10px 0; @@ -98,19 +140,66 @@ &__close-button { background-color: var(--info-color); - color: var(--font-color); + color: $white; & i { padding: 0 $fa-padding 0 0; } } - &__meta-info { + &__meta { display: flex; flex-direction: row; align-items: center; margin: 15px 0; gap: 10px; + + @media (max-width: $hd-breakpoint) { + flex-wrap: wrap; + } + } + + &__text, &__buttons { + display: flex; + flex-direction: inherit; + align-items: inherit; + gap: inherit; + } + + &__text { + @media (max-width: $hd-breakpoint) { + flex: 100%; + } + } + + &__buttons { + @media (max-width: $hd-breakpoint) { + flex-wrap: inherit; + } + } + + &__link { + @media (max-width: $tablet-breakpoint) { + @include button; + + background-color: var(--info-color); + color: $white; + width: 100px; + } + } + + &__save { + @media (max-width: $tablet-breakpoint) { + @include button; + + background-color: var(--confirm-color); + color: $white; + width: 100px; + + &--saved { + color: var(--read-color); + } + } } } diff --git a/src/newsreader/scss/components/posts-info/_posts-info.scss b/src/newsreader/scss/components/posts-info/_posts-info.scss index 47eae4f..ca76155 100644 --- a/src/newsreader/scss/components/posts-info/_posts-info.scss +++ b/src/newsreader/scss/components/posts-info/_posts-info.scss @@ -7,11 +7,27 @@ &__date { align-self: center; + + @media (max-width: $mobile-breakpoint){ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + &__link, .saved-icon { + @media (max-width: $mobile-breakpoint){ + display: none; + } } & .badge { & .link { - color: var(--font-color); + color: inherit; + } + + @media (max-width: $mobile-breakpoint){ + display: none; } } } diff --git a/src/newsreader/scss/components/posts/_posts.scss b/src/newsreader/scss/components/posts/_posts.scss index d40cddb..9978e73 100644 --- a/src/newsreader/scss/components/posts/_posts.scss +++ b/src/newsreader/scss/components/posts/_posts.scss @@ -1,7 +1,15 @@ -.posts { - margin: 0 0 2% 20px; +@import '../../partials/variables'; +@import '../../lib/functions'; - width: 70%; +.posts { + height: calc(100vh - map-deep-get($nav, height)); + overflow-y: scroll; + padding: 0 0 0 10px; + + @media (max-width: $mobile-breakpoint) { + height: calc(100vh - map-deep-get($nav, mobile, height)); + padding: 0; + } &__list { display: flex; @@ -11,6 +19,10 @@ padding: 0; list-style: none; + + @media (max-width: $mobile-breakpoint) { + width: initial; + } } &__item { @@ -21,12 +33,22 @@ max-width: max-content; + @media (max-width: $mobile-breakpoint) { + max-width: 100vw; + } + &:first-child { padding: 0 10px 10px 10px; } & .badge { - background-color: var(--lightest-accent-color); + background-color: var(--background-color-secondary); + + @media (max-width: $mobile-breakpoint){ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } &:last-child { @@ -38,7 +60,7 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-size: $header-size; + font-size: map-deep-get($post, header-font-size); &--read { color: var(--read-color); diff --git a/src/newsreader/scss/components/rules/_rules.scss b/src/newsreader/scss/components/rules/_rules.scss index 404d0e5..99ec039 100644 --- a/src/newsreader/scss/components/rules/_rules.scss +++ b/src/newsreader/scss/components/rules/_rules.scss @@ -1,3 +1,5 @@ +@import '../../partials/variables'; + .rules { padding: 0; @@ -6,19 +8,27 @@ justify-content: space-between; align-items: center; + border-bottom-right-radius: .25em; + border-top-right-radius: .25em; + padding: 5px 5px 5px 20px; + @media (max-width: $mobile-breakpoint) { + margin: 10px 0; + padding: 25px 5px 20px 5px; + } + & * { padding: 0 2px 0 2px; } &:hover { cursor: pointer; - background-color: var(--lighter-accent-color); + background-color: var(--selected-color); } &--selected { - background-color: var(--lighter-accent-color); + background-color: var(--selected-color); } } diff --git a/src/newsreader/scss/components/scroll-to-top/_scroll-to-top.scss b/src/newsreader/scss/components/scroll-to-top/_scroll-to-top.scss index 84b40e6..c745c62 100644 --- a/src/newsreader/scss/components/scroll-to-top/_scroll-to-top.scss +++ b/src/newsreader/scss/components/scroll-to-top/_scroll-to-top.scss @@ -1,3 +1,5 @@ +@import '../../partials/variables'; + .scroll-to-top { display: flex; gap: 10px; @@ -8,6 +10,10 @@ margin: 0 0 20px 0; + @media (max-width: $mobile-breakpoint) { + display: none; + } + &:hover { cursor: pointer; } @@ -16,7 +22,7 @@ font-style: initial; padding: 10px; - background-color: var(--lightest-accent-color); + background-color: var(--background-color-secondary); &--top:before { @include font-awesome; diff --git a/src/newsreader/scss/components/sidebar/_sidebar.scss b/src/newsreader/scss/components/sidebar/_sidebar.scss index 0521af4..186a382 100644 --- a/src/newsreader/scss/components/sidebar/_sidebar.scss +++ b/src/newsreader/scss/components/sidebar/_sidebar.scss @@ -1,31 +1,77 @@ +@import '../../partials/variables'; +@import '../../lib/functions'; + .sidebar { - display: flex; - flex-direction: column; - align-items: center; - align-self: start; + display: none; // hide the sidebar by default, homepage enables it by default - position: sticky; - top: 50px; + --easeOutExpo: cubic-bezier(0.16, 1, 0.3, 1); + --duration: .6s; - width: 20%; + height: calc(100vh - map-deep-get($nav, height)); + + font-size: map-deep-get($nav, font-size); + + @media (max-width: $mobile-breakpoint) { + display: grid; + grid-template-columns: [nav] 5fr [escape] 1fr; + height: calc(100vh - map-deep-get($nav, mobile, height)); + + font-size: map-deep-get($sidebar, mobile, font-size); + + overflow: hidden auto; + overscroll-behavior: contain; + + visibility: hidden; + transform: translateX(-110vw); + will-change: transform; + transition: + transform var(--duration) var(--easeOutExpo), + visibility 0s linear var(--duration); + } &__nav { - width: 100%; - max-height: 80vh; - overflow: auto; + display: flex; + flex-direction: column; + overflow-y: scroll; + background-color: var(--background-color); + + padding: 10px; + } + + &__close { + display: none; + + @media (max-width: $mobile-breakpoint) { + display: initial; + } + } + + &__list { list-style: none; + } + &__item { + @media (max-width: $mobile-breakpoint) { + margin: 10px 0; + } } &__container { display: flex; align-items: center; + border-bottom-right-radius: .25em; + border-top-right-radius: .25em; + padding: 5px; + @media (max-width: $mobile-breakpoint) { + padding: 25px 5px; + } + &--selected, &:hover { - background-color: var(--lighter-accent-color); + background-color: var(--selected-color); } } @@ -51,6 +97,60 @@ } .read-button { - margin: 20px 0 0 0; + margin: 20px 0 0 10px; + width: max-content; + + @media (max-width: $mobile-breakpoint) { + margin: auto 0 20px 10px; + font-size: inherit; + } + } + + & .nav-list { + display: none; + + &--bordered { + border-bottom: 2px var(--border-color) solid; + } + + @media (max-width: $mobile-breakpoint) { + display: flex; + flex-direction: column; + width: 100%; + + &__item { + margin: 0; + + padding: 25px 15px; + border-bottom-right-radius: 0.25em; + border-bottom-left-radius: 0.25em; + + & a { + + color: inherit; + font-size: inherit; + + align-items: initial; + justify-content: initial; + + padding: 0; + + &:before { + @include font-awesome; + + content: "\f35d"; + padding: 0 20px 0 0; + } + } + } + } + } +} + +@media (max-width: $mobile-breakpoint) { + #menu-input:checked ~ * .sidebar { + visibility: visible; + transform: translateX(0); + transition: transform var(--duration) var(--easeOutExpo); } } diff --git a/src/newsreader/scss/components/table/_rules-table.scss b/src/newsreader/scss/components/table/_rules-table.scss index 3be0430..b558045 100644 --- a/src/newsreader/scss/components/table/_rules-table.scss +++ b/src/newsreader/scss/components/table/_rules-table.scss @@ -1,7 +1,17 @@ +@import '../../partials/variables'; + .rules-table { - &__heading { + padding: 15px; + + &__heading, &__item { + padding: 10px; + &--select { width: 5%; + + & .checkbox { + margin: 0; + } } &--name { @@ -10,10 +20,18 @@ &--category { width: 15%; + + @media (max-width: $mobile-breakpoint) { + display: none; + } } &--url { width: 40%; + + @media (max-width: $mobile-breakpoint) { + display: none; + } } &--succeeded { diff --git a/src/newsreader/scss/components/table/_table.scss b/src/newsreader/scss/components/table/_table.scss index e39f4c0..b8bc660 100644 --- a/src/newsreader/scss/components/table/_table.scss +++ b/src/newsreader/scss/components/table/_table.scss @@ -2,7 +2,7 @@ table-layout: fixed; background-color: var(--background-color); - width: 90%; + width: 100%; padding: 20px; text-align: left; @@ -10,6 +10,10 @@ &__heading { @extend .h1; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } &__row { @@ -27,7 +31,7 @@ } &__footer { - width: 80%; + width: 100%; padding: 10px 0; } } diff --git a/src/newsreader/scss/elements/badge/_badge.scss b/src/newsreader/scss/elements/badge/_badge.scss index 08b4ee8..3842d45 100644 --- a/src/newsreader/scss/elements/badge/_badge.scss +++ b/src/newsreader/scss/elements/badge/_badge.scss @@ -1,3 +1,6 @@ +@import '../../partials/variables'; +@import '../../partials/colors'; + .badge { display: inline-block; @@ -6,7 +9,12 @@ text-align: center; - background-color: var(--lighter-accent-color); + background-color: var(--background-color-secondary); font-size: small; + border-radius: 0.25em; + + @media (max-width: $mobile-breakpoint) { + font-size: inherit; + } } diff --git a/src/newsreader/scss/elements/button/_button.scss b/src/newsreader/scss/elements/button/_button.scss index c0b5291..96ee2c8 100644 --- a/src/newsreader/scss/elements/button/_button.scss +++ b/src/newsreader/scss/elements/button/_button.scss @@ -1,4 +1,4 @@ -.button { +@mixin button { display: flex; align-items: center; @@ -7,16 +7,15 @@ @include button-padding; border: none; - - font-size: 16px; + border-radius: 0.25em; &:hover { cursor: pointer; } &--success, &--confirm { - color: var(--confirm-button-font-color) !important; background-color: var(--confirm-color); + color: $white !important; } &--error, &--cancel { @@ -40,6 +39,15 @@ &--disabled { color: var(--font-color) !important; - background-color: $gray !important; + background-color: var(--background-color-secondary) !important; + + &:hover { + cursor: default; + } } + +} + +.button { + @include button; } diff --git a/src/newsreader/scss/elements/button/_read-button.scss b/src/newsreader/scss/elements/button/_read-button.scss index 41bd3cc..bd09cbf 100644 --- a/src/newsreader/scss/elements/button/_read-button.scss +++ b/src/newsreader/scss/elements/button/_read-button.scss @@ -1,10 +1,18 @@ +@import '../../partials/variables'; +@import '../../partials/colors'; + .read-button { @extend .button; - color: var(--confirm-button-font-color); + color: var(--confirm-font-color); + background-color: var(--confirm-color); & i { padding: 0 $fa-padding 0 0; } + + @media (max-width: $mobile-breakpoint) { + width: max-content; + } } diff --git a/src/newsreader/scss/elements/checkbox/_checkbox.scss b/src/newsreader/scss/elements/checkbox/_checkbox.scss index 174f348..b818723 100644 --- a/src/newsreader/scss/elements/checkbox/_checkbox.scss +++ b/src/newsreader/scss/elements/checkbox/_checkbox.scss @@ -2,8 +2,6 @@ display: block; height: 20px; width: 20px; - margin: 0 0 0 20px; - & input[type=checkbox] { position: absolute; @@ -14,7 +12,7 @@ &:checked + .checkbox__label { .checkbox__box { - background-color: var(--lightest-accent-color); + background-color: var(--info-color); } } } @@ -29,7 +27,7 @@ height: 100%; width: 100%; - border: 2px solid var(--lighter-accent-color); + border: 1.5px solid var(--border-color); cursor: pointer; } } diff --git a/src/newsreader/scss/elements/help-text/_help-text.scss b/src/newsreader/scss/elements/help-text/_help-text.scss index a90552d..d91adc0 100644 --- a/src/newsreader/scss/elements/help-text/_help-text.scss +++ b/src/newsreader/scss/elements/help-text/_help-text.scss @@ -1,7 +1,8 @@ .help-text { @extend .small; - padding: 5px 15px; + padding: 10px 0; + } .helptext { diff --git a/src/newsreader/scss/elements/input/_input.scss b/src/newsreader/scss/elements/input/_input.scss index 84c2470..fd7a231 100644 --- a/src/newsreader/scss/elements/input/_input.scss +++ b/src/newsreader/scss/elements/input/_input.scss @@ -2,12 +2,8 @@ @include text-padding; color: var(--font-color); - background-color: var(--accent-color); - border: 1px var(--lighter-accent-color) solid; - - &:focus { - border: 1px var(--lightest-accent-color) solid; - } + background-color: var(--background-color-secondary); + border: 1px var(--border-color) solid; &[type="file"] { width: 40%; @@ -15,7 +11,6 @@ &[type="checkbox"] { align-self: flex-start; - margin: 0 0 0 10px; } } diff --git a/src/newsreader/scss/elements/label/_label.scss b/src/newsreader/scss/elements/label/_label.scss index 6481b02..abf59a2 100644 --- a/src/newsreader/scss/elements/label/_label.scss +++ b/src/newsreader/scss/elements/label/_label.scss @@ -1,5 +1,5 @@ .label { - @include text-padding; + padding: 10px 0; } label { diff --git a/src/newsreader/scss/lib/_functions.scss b/src/newsreader/scss/lib/_functions.scss new file mode 100644 index 0000000..cb8189f --- /dev/null +++ b/src/newsreader/scss/lib/_functions.scss @@ -0,0 +1,7 @@ +@function map-deep-get($map, $keys...) { + @each $key in $keys { + $map: map-get($map, $key); + } + + @return $map; +} diff --git a/src/newsreader/scss/pages/homepage/index.scss b/src/newsreader/scss/pages/homepage/index.scss index 30f5a50..99260a7 100644 --- a/src/newsreader/scss/pages/homepage/index.scss +++ b/src/newsreader/scss/pages/homepage/index.scss @@ -1,9 +1,26 @@ #homepage--page { - display: flex; - flex-direction: row; - align-items: initial; - width: 100%; + background-color: initial; - margin: 20px 0 0 0; - background-color: initial; + display: grid; + + @media (min-width: $mobile-breakpoint) { + grid: [stack] 1fr/20% [stack] auto; // TODO: remove this line? + } + + @media (min-width: $hd-breakpoint) { + grid: [stack] 1fr/15% [stack] auto; + } + + @media (min-width: $wqhd-breakpoint) { + grid: [stack] 1fr/12% [stack] auto; + } + + @media (min-width: $uhd-breakpoint) { + grid: [stack] 1fr/10% [stack] auto; + } + + & .sidebar { + display: grid; + grid-template-columns: [nav] 100% [escape] 0; + } } diff --git a/src/newsreader/scss/pages/login/index.scss b/src/newsreader/scss/pages/login/index.scss index f1805ed..68ac32d 100644 --- a/src/newsreader/scss/pages/login/index.scss +++ b/src/newsreader/scss/pages/login/index.scss @@ -1,11 +1,24 @@ #login--page { - margin: 5% auto; - width: 50%; - & .form { @extend .form; - width: 100%; + width: 20%; + + @media (max-width: $wqhd-breakpoint) { + width: 30%; + } + + @media (max-width: $hd-breakpoint) { + width: 40%; + } + + @media (max-width: $tablet-breakpoint) { + width: 50%; + } + + @media (max-width: $mobile-breakpoint) { + width: 100%; + } h4 { margin: 0; @@ -21,9 +34,6 @@ &__fieldset { @extend .form__fieldset; - - &--last { - } } } } diff --git a/src/newsreader/scss/pages/rules/index.scss b/src/newsreader/scss/pages/rules/index.scss index 64f46b4..de69b2d 100644 --- a/src/newsreader/scss/pages/rules/index.scss +++ b/src/newsreader/scss/pages/rules/index.scss @@ -1,5 +1,3 @@ #rules--page { - & .table { - width: 100%; - } + // TODO: remove scss } diff --git a/src/newsreader/scss/pages/settings/index.scss b/src/newsreader/scss/pages/settings/index.scss index c52f46b..8f1e57a 100644 --- a/src/newsreader/scss/pages/settings/index.scss +++ b/src/newsreader/scss/pages/settings/index.scss @@ -3,6 +3,7 @@ &__section { &--last { & .fieldset { + flex-wrap: wrap; gap: 15px; justify-content: flex-start; } diff --git a/src/newsreader/scss/partials/_colors.scss b/src/newsreader/scss/partials/_colors.scss index 66c8ad3..1807a85 100644 --- a/src/newsreader/scss/partials/_colors.scss +++ b/src/newsreader/scss/partials/_colors.scss @@ -1,58 +1,64 @@ -$orange: rgba(255, 212, 153, 1); -$green: rgba(89, 181, 128, 1); -$red: lighten(rgba(231, 76, 60, 1), 10%); -$gray: rgba(227, 227, 227, 1); -$blue: rgba(111, 164, 196, 1); +$orange: #ff2a51; +$green: #007936; +$red: #d30038; +$blue: #0085f2; -$white: rgba(255, 255, 255, 1); -$black: rgba(0, 0, 0, 1); -$dark: rgba(0, 0, 0, 0.4); - -$reddit-orange: rgba(255, 69, 0, 1); +$white: #fff; +$black: #000; $transparant-red: transparentize($red, 0.8); $transparant-blue: transparentize($blue, 0.8); $transparant-orange: transparentize($orange, 0.4); $transparant-green: transparentize($green, 0.4); -$azureish-white: rgba(205, 230, 245, 1); -$gainsboro: rgba(238, 238, 238, 1); -$nickel: rgba(112, 112, 120, 1); -$lavendal-pink: rgba(162, 155, 254, 1); - -$focus-blue: darken($azureish-white, +10%); -$checkbox-blue: rgba(34, 170, 253, 1); - // White theme -$background-color: rgba(255, 249, 176, 1); +$background-color: $white; +$background-color-secondary: #f9f9fb; -$font-color: rgba(83, 87, 91, 1); -$link-color: rgba(45, 142, 202, 1); -$read-color: darken($gainsboro, 10%); -$confirm-button-font-color: rgba(255, 255, 255, 1); +$font-color: #1b1b1b; -$accent-color: rgba(255, 171, 115, 1); -$lighter-accent-color: rgba(255, 211, 132, 1); -$lightest-accent-color: rgba(255, 174, 192, 1); +$link-color: #0069c2; +$selected-color: #0085f230; +$read-color: darken($font-color, 10%); -$confirm-color: rgba(117, 207, 184, 1); -$danger-color: rgba(237, 118, 105, 1); -$warning-color: rgba(255, 218, 119, 1); -$info-color: rgba(162, 213, 242, 1); +$confirm-color: $green; +$confirm-font-color: $white; + +$danger-color: $red; +$danger-font-color: $white; + +$warning-color: $orange; +$warning-font-color: $white; + +$info-color: $blue; +$info-font-color: $white; + +$sidebar-background-color: $background-color-secondary; + +$border-color: #cdcdcd; // Dark theme -$dark-background-color: rgba(29, 45, 80, 1); +$dark-background-color: #1b1b1b; +$dark-background-color-secondary: #313131; + +$dark-font-color: #cccccc; -$dark-font-color: darken($gray, 10%); $dark-link-color: $link-color; -$dark-read-color: darken($dark-font-color, 20%); -$dark-confirm-button-font-color: $dark-font-color; +$dark-read-color: darken($dark-font-color, 5%); -$dark-accent-color: rgba(19, 59, 92, 1); -$dark-lighter-accent-color: rgba(30, 95, 116, 1); -$dark-lightest-accent-color: rgba(88, 61, 114, 1); +$dark-confirm-color: $green; +$dark-confirm-font-color: $white; -$dark-confirm-color: rgba(0, 121, 101, 1); -$dark-danger-color: rgba(175, 45, 45, 1); -$dark-warning-color: rgba(238, 187, 77, 1); -$dark-info-color: rgba(31, 111, 139, 1); +$dark-danger-color: $red; +$dark-danger-font-color: $white; + +$dark-warning-color: $orange; +$dark-warning-font-color: $white; + +$dark-info-color: $blue; +$dark-info-font-color: $white; + +$dark-sidebar-background-color: $dark-background-color-secondary; + +// Third party +$reddit-orange: rgba(255, 69, 0, 1); diff --git a/src/newsreader/scss/partials/_fonts.scss b/src/newsreader/scss/partials/_fonts.scss index 934db2e..5b07f26 100644 --- a/src/newsreader/scss/partials/_fonts.scss +++ b/src/newsreader/scss/partials/_fonts.scss @@ -1,12 +1,15 @@ @font-face { - font-family: Rubik; - src: url('../assets/fonts/Rubik-Regular.ttf'); + font-family: Inter; + font-style: normal; + font-display: swap; + src: url('../assets/fonts/Inter-VariableFont_opsz,wght.ttf'); } @font-face { - font-family: Rubik; - src: url('../assets/fonts/Rubik-Bold.ttf'); - font-weight: bold; + font-family: Inter; + font-style: italic; + font-display: swap; + src: url('../assets/fonts/Inter-Italic-VariableFont_opsz,wght.ttf'); } @font-face { diff --git a/src/newsreader/scss/partials/_root.scss b/src/newsreader/scss/partials/_root.scss index cc8cebf..4a85e8e 100644 --- a/src/newsreader/scss/partials/_root.scss +++ b/src/newsreader/scss/partials/_root.scss @@ -1,33 +1,37 @@ :root { --background-color: #{$background-color}; + --background-color-secondary: #{$background-color-secondary}; --font-color: #{$font-color}; --link-color: #{$link-color}; + --selected-color: #{$selected-color}; --read-color: #{$read-color}; - --confirm-button-font-color: #{$confirm-button-font-color}; - - --accent-color: #{$accent-color}; - --lighter-accent-color: #{$lighter-accent-color}; - --lightest-accent-color: #{$lightest-accent-color}; --confirm-color: #{$confirm-color}; + --confirm-font-color: #{$confirm-font-color}; + --danger-color: #{$danger-color}; + --danger-font-color: #{$danger-color}; + --warning-color: #{$warning-color}; + --warning-font-color: #{$warning-color}; + --info-color: #{$info-color}; + --info-font-color: #{$info-color}; + + --border-color: #{$border-color}; &.dark-theme { --background-color: #{$dark-background-color}; + --background-color-secondary: #{$dark-background-color-secondary}; --font-color: #{$dark-font-color}; --link-color: #{$dark-link-color}; --read-color: #{$dark-read-color}; - --confirm-button-font-color: #{$dark-confirm-button-font-color}; - - --accent-color: #{$dark-accent-color}; - --lighter-accent-color: #{$dark-lighter-accent-color}; - --lightest-accent-color: #{$dark-lightest-accent-color}; --confirm-color: #{$dark-confirm-color}; + --confirm-font-color: #{$dark-confirm-font-color}; + --danger-color: #{$dark-danger-color}; --warning-color: #{$dark-warning-color}; --info-color: #{$dark-info-color}; diff --git a/src/newsreader/scss/partials/_variables.scss b/src/newsreader/scss/partials/_variables.scss index d8e8261..b4165d3 100644 --- a/src/newsreader/scss/partials/_variables.scss +++ b/src/newsreader/scss/partials/_variables.scss @@ -1,4 +1,36 @@ $fa-padding: 7px; -$header-size: 1.2em; +// Fonts $font-size: 1.1em; +$font-size-small: 0.833em; + +// Dimensions +$mobile-breakpoint: 540px; +$tablet-breakpoint: 1280px; +$hd-breakpoint: 1920px; +$wqhd-breakpoint: 2560px; +$uhd-breakpoint: 3840px; + +$nav: ( + height: 50px, + font-size: $font-size-small, + + mobile: ( + height: 75px, + font-size: 1.5em + ) +); + +// Post +$post: ( + header-font-size: 1.2em +); + +// Sidebar +$sidebar: ( + font-size: $font-size, + + mobile: ( + font-size: 1.2em, + ) +); diff --git a/src/newsreader/templates/base.html b/src/newsreader/templates/base.html index 3c65329..9900b37 100644 --- a/src/newsreader/templates/base.html +++ b/src/newsreader/templates/base.html @@ -4,6 +4,7 @@ Newreader + {% block head %} @@ -11,22 +12,14 @@ + + diff --git a/src/newsreader/templates/components/nav-list/nav-list.html b/src/newsreader/templates/components/nav-list/nav-list.html new file mode 100644 index 0000000..50ecfd8 --- /dev/null +++ b/src/newsreader/templates/components/nav-list/nav-list.html @@ -0,0 +1,17 @@ + diff --git a/src/newsreader/templates/password-reset/password-reset-complete.html b/src/newsreader/templates/password-reset/password-reset-complete.html index 0b7796f..ddd41e7 100755 --- a/src/newsreader/templates/password-reset/password-reset-complete.html +++ b/src/newsreader/templates/password-reset/password-reset-complete.html @@ -1,13 +1,15 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load i18n %} {% block content %} -
    {% trans "Password reset complete" as header_text %} {% blocktrans asvar content %} You may now log in {% endblocktrans %} - {% include "components/card/card.html" with header_text=header_text content=content %} -
    +
    +
    + {% include "components/card/card.html" with header_text=header_text content=content %} +
    +
    {% endblock %} diff --git a/src/newsreader/templates/password-reset/password-reset-confirm.html b/src/newsreader/templates/password-reset/password-reset-confirm.html index d0d5037..852283c 100755 --- a/src/newsreader/templates/password-reset/password-reset-confirm.html +++ b/src/newsreader/templates/password-reset/password-reset-confirm.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load i18n %} {% block meta %} @@ -8,23 +8,25 @@ {% endblock %} {% block content %} -
    - {% if validlink %} - {% url 'accounts:login' as cancel_url %} - {% trans "Enter your new password below to reset your password:" as title %} - {% trans "Change password" as confirm_text %} - {% include "components/form/form.html" with form=form title=title confirm_text=confirm_text cancel_url=cancel_url %} - {% else %} - {% trans "Password reset unsuccessful" as header_text %} - {% url 'accounts:password-reset' as reset_url %} - {% blocktrans asvar content %} - Password reset unsuccessful. Please - try again. - {% endblocktrans %} +
    +
    + {% if validlink %} + {% url 'accounts:login' as cancel_url %} + {% trans "Enter your new password below to reset your password:" as title %} + {% trans "Change password" as confirm_text %} + {% include "components/form/form.html" with form=form title=title confirm_text=confirm_text cancel_url=cancel_url %} + {% else %} + {% trans "Password reset unsuccessful" as header_text %} + {% url 'accounts:password-reset' as reset_url %} + {% blocktrans asvar content %} + Password reset unsuccessful. Please + try again. + {% endblocktrans %} - {% include "components/card/card.html" with header_text=header_text content=content %} - {% endif %} -
    + {% include "components/card/card.html" with header_text=header_text content=content %} + {% endif %} +
    +
    {% endblock %} {# This is used by django.contrib.auth #} diff --git a/src/newsreader/templates/password-reset/password-reset-done.html b/src/newsreader/templates/password-reset/password-reset-done.html index 7012439..b160339 100755 --- a/src/newsreader/templates/password-reset/password-reset-done.html +++ b/src/newsreader/templates/password-reset/password-reset-done.html @@ -1,16 +1,18 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load static i18n %} {% block title %}{% trans "Password reset" %}{% endblock %} {% block content %} -
    {% trans "Password reset" as header_text %} {% blocktrans asvar content %} We have sent you an email with a link to reset your password. Please check your email and click the link to continue. {% endblocktrans %} - {% include "components/card/card.html" with header_text=header_text content=content %} -
    +
    +
    + {% include "components/card/card.html" with header_text=header_text content=content %} +
    +
    {% endblock %} diff --git a/src/newsreader/templates/password-reset/password-reset.html b/src/newsreader/templates/password-reset/password-reset.html index 97e5678..0454f4e 100644 --- a/src/newsreader/templates/password-reset/password-reset.html +++ b/src/newsreader/templates/password-reset/password-reset.html @@ -1,7 +1,9 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% block content %} -
    - {% include "password-reset/password-reset-form.html" with form=form title="Reset password" confirm_text="Reset password" %} -
    +
    +
    + {% include "password-reset/password-reset-form.html" with form=form title="Reset password" confirm_text="Reset password" %} +
    +
    {% endblock %} diff --git a/src/newsreader/templates/registration/activation_complete.html b/src/newsreader/templates/registration/activation_complete.html index f8dd91b..4990231 100755 --- a/src/newsreader/templates/registration/activation_complete.html +++ b/src/newsreader/templates/registration/activation_complete.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load i18n %} {% comment %} @@ -10,15 +10,17 @@ account is now active. {% endcomment %} {% block content %} -
    - {% trans "Account activated" as header_text %} + {% trans "Account activated" as header_text %} - {% if user.is_authenticated %} - {% trans "Your account is activated. You can now log in." as content %} - {% else %} - {% trans "Your account is activated." as content %} - {% endif %} + {% if user.is_authenticated %} + {% trans "Your account is activated. You can now log in." as content %} + {% else %} + {% trans "Your account is activated." as content %} + {% endif %} - {% include "components/card/card.html" with header_text=header_text content=content %} +
    +
    + {% include "components/card/card.html" with header_text=header_text content=content %} +
    {% endblock %} diff --git a/src/newsreader/templates/registration/activation_failure.html b/src/newsreader/templates/registration/activation_failure.html index c99cc34..d20629a 100644 --- a/src/newsreader/templates/registration/activation_failure.html +++ b/src/newsreader/templates/registration/activation_failure.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load i18n %} {% comment %} @@ -11,9 +11,12 @@ Used if account activation fails. With the default setup, has the following cont {% endcomment %} {% block content %} -
    - {% trans "Activation Failure" as header_text %} - {% trans "Account activation failed." as content %} - {% include "components/card/card.html" with header_text=header_text content=content %} + {% trans "Activation Failure" as header_text %} + {% trans "Account activation failed." as content %} + +
    +
    + {% include "components/card/card.html" with header_text=header_text content=content %} +
    {% endblock %} diff --git a/src/newsreader/templates/registration/activation_resend_form.html b/src/newsreader/templates/registration/activation_resend_form.html index 5f0dd82..3910d39 100644 --- a/src/newsreader/templates/registration/activation_resend_form.html +++ b/src/newsreader/templates/registration/activation_resend_form.html @@ -1,9 +1,12 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load static %} {% block content %} -
    - {% url "accounts:login" as cancel_url %} - {% include "components/form/form.html" with form=form title="Resend activation code" cancel_url=cancel_url confirm_text="Resend code" %} + {% url "accounts:login" as cancel_url %} + +
    +
    + {% include "components/form/form.html" with form=form title="Resend activation code" cancel_url=cancel_url confirm_text="Resend code" %} +
    {% endblock %} diff --git a/src/newsreader/templates/registration/registration_closed.html b/src/newsreader/templates/registration/registration_closed.html index c7cfd9a..75091b7 100755 --- a/src/newsreader/templates/registration/registration_closed.html +++ b/src/newsreader/templates/registration/registration_closed.html @@ -1,10 +1,13 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load static i18n %} {% block content %} -
    - {% trans "Registration is closed" as header_text %} - {% trans "Sorry, but registration is closed at this moment. Come back later." as content %} - {% include "components/card/card.html" with header_text=header_text content=content %} + {% trans "Registration is closed" as header_text %} + {% trans "Sorry, but registration is closed at this moment. Come back later." as content %} + +
    +
    + {% include "components/card/card.html" with header_text=header_text content=content %} +
    {% endblock %} diff --git a/src/newsreader/templates/registration/registration_complete.html b/src/newsreader/templates/registration/registration_complete.html index ccf70b2..b2281bb 100755 --- a/src/newsreader/templates/registration/registration_complete.html +++ b/src/newsreader/templates/registration/registration_complete.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load i18n %} {% comment %} @@ -11,9 +11,12 @@ been sent. {% endcomment %} {% block content %} -
    - {% trans "Activation email sent" as header_text %} - {% trans "Please check your email to complete the registration process." as content %} - {% include "components/card/card.html" with header_text=header_text content=content %} + {% trans "Activation email sent" as header_text %} + {% trans "Please check your email to complete the registration process." as content %} + +
    +
    + {% include "components/card/card.html" with header_text=header_text content=content %} +
    {% endblock %} diff --git a/src/newsreader/templates/registration/registration_form.html b/src/newsreader/templates/registration/registration_form.html index ccc07c9..dfaa6d3 100644 --- a/src/newsreader/templates/registration/registration_form.html +++ b/src/newsreader/templates/registration/registration_form.html @@ -1,9 +1,13 @@ -{% extends "base.html" %} +{% extends "sidebar.html" %} {% load static %} + {% block content %} -
    - {% url "accounts:login" as cancel_url %} - {% include "components/form/form.html" with form=form title="Register" cancel_url=cancel_url confirm_text="Register" %} + {% url "accounts:login" as cancel_url %} + +
    +
    + {% include "components/form/form.html" with form=form title="Register" cancel_url=cancel_url confirm_text="Register" %} +
    {% endblock %} diff --git a/src/newsreader/templates/sidebar.html b/src/newsreader/templates/sidebar.html new file mode 100644 index 0000000..bf99634 --- /dev/null +++ b/src/newsreader/templates/sidebar.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block scripts %} + {{ sidebar_links|json_script:"Links" }} + + {{ block.super }} +{% endblock %} diff --git a/src/newsreader/utils/views.py b/src/newsreader/utils/views.py index 60f00ef..0a14477 100644 --- a/src/newsreader/utils/views.py +++ b/src/newsreader/utils/views.py @@ -1 +1,36 @@ -# Create your views here. +from django.urls import reverse +from django.views.generic.base import ContextMixin + + +# TODO: render menu in non-homepage pages +class NavListMixin(ContextMixin): + def get_context_data(self, **kwargs: dict): + context_data = super().get_context_data(**kwargs) + + authenticated_links = { + "Home": reverse("index"), + "Categories": reverse("news:core:categories"), + "Sources": reverse("news:collection:rules"), + "Settings": reverse("accounts:settings:home"), + } + + if self.request.user.is_authenticated: + authenticated_links["Admin"] = reverse("admin:index") + + authenticated_links["Logout"] = reverse("accounts:logout") + + unauthenticated_links = { + "Login": reverse("accounts:login"), + "Register": reverse("accounts:register"), + } + + if self.request.user.is_authenticated: + return { + **context_data, + "sidebar_links": authenticated_links + } + + return { + **context_data, + "sidebar_links": unauthenticated_links + } diff --git a/uv.lock b/uv.lock index 03a3489..fec9fbe 100644 --- a/uv.lock +++ b/uv.lock @@ -358,6 +358,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/7e/8c45ea7f85dd5d52ceddbacc6f56ecaca21ecbfc0e8c34c95618a14d5082/djangorestframework-3.15.1-py3-none-any.whl", hash = "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6", size = 1067096 }, ] +[[package]] +name = "djangorestframework-camel-case" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/87/647ce93053cb5e35e07bded676340774fe43190388b885c54aff47d8557b/djangorestframework-camel-case-1.4.2.tar.gz", hash = "sha256:cdae75846648abb6585c7470639a1d2fb064dc45f8e8b62aaa50be7f1a7a61f4", size = 8839 } + [[package]] name = "factory-boy" version = "3.3.1" @@ -512,6 +518,7 @@ dependencies = [ { name = "django-celery-beat", marker = "sys_platform == 'linux'" }, { name = "django-registration-redux", marker = "sys_platform == 'linux'" }, { name = "django-rest-framework", marker = "sys_platform == 'linux'" }, + { name = "djangorestframework-camel-case", marker = "sys_platform == 'linux'" }, { name = "feedparser", marker = "sys_platform == 'linux'" }, { name = "ftfy", marker = "sys_platform == 'linux'" }, { name = "lxml", marker = "sys_platform == 'linux'" }, @@ -556,6 +563,7 @@ requires-dist = [ { name = "django-registration-redux", specifier = "~=2.7" }, { name = "django-rest-framework" }, { name = "django-stubs", marker = "extra == 'development'" }, + { name = "djangorestframework-camel-case" }, { name = "factory-boy", marker = "extra == 'testing'" }, { name = "feedparser" }, { name = "freezegun", marker = "extra == 'testing'" },