Sidebar refactor

This commit is contained in:
Sonny Bakker 2024-10-06 20:39:05 +02:00
parent 03b5847641
commit fbb6405da9
113 changed files with 1321 additions and 637 deletions

View file

@ -12,6 +12,7 @@ dependencies = [
'django-celery-beat~=2.7.0', 'django-celery-beat~=2.7.0',
'django-registration-redux~=2.7', 'django-registration-redux~=2.7',
'django-rest-framework', 'django-rest-framework',
'djangorestframework-camel-case',
'pymemcache', 'pymemcache',
'python-dotenv~=1.0.1', 'python-dotenv~=1.0.1',
'ftfy~=6.2', 'ftfy~=6.2',

View file

@ -1,45 +1,47 @@
{% extends "base.html" %} {% extends "sidebar.html" %}
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<main id="integrations--page" class="main"> <main id="integrations--page" class="main" data-render-sidebar=true>
<section class="section"> <div class="main__container">
{% include "components/header/header.html" with title="Integrations" only %} <section class="section">
{% include "components/header/header.html" with title="Integrations" only %}
<div class="integrations"> <div class="integrations">
<h3 class="integrations__title">Reddit</h3> <h3 class="integrations__title">Reddit</h3>
<div class="integrations__controls"> <div class="integrations__controls">
{% if reddit_authorization_url %} {% if reddit_authorization_url %}
<a class="link button button--reddit" href="{{ reddit_authorization_url }}"> <a class="link button button--reddit" href="{{ reddit_authorization_url }}">
{% trans "Authorize account" %} {% trans "Authorize account" %}
</a> </a>
{% else %} {% else %}
<button class="button button--reddit button--disabled" disabled> <button class="button button--reddit button--disabled" disabled>
{% trans "Authorize account" %} {% trans "Authorize account" %}
</button> </button>
{% endif %} {% endif %}
{% if reddit_refresh_url %} {% if reddit_refresh_url %}
<a class="link button button--reddit" href="{{ reddit_refresh_url }}"> <a class="link button button--reddit" href="{{ reddit_refresh_url }}">
{% trans "Refresh token" %} {% trans "Refresh token" %}
</a> </a>
{% else %} {% else %}
<button class="button button--reddit button--disabled" disabled> <button class="button button--reddit button--disabled" disabled>
{% trans "Refresh token" %} {% trans "Refresh token" %}
</button> </button>
{% endif %} {% endif %}
{% if reddit_revoke_url %} {% if reddit_revoke_url %}
<a class="link button button--reddit" href="{{ reddit_revoke_url }}"> <a class="link button button--reddit" href="{{ reddit_revoke_url }}">
{% trans "Deauthorize account" %} {% trans "Deauthorize account" %}
</a> </a>
{% else %} {% else %}
<button class="button button--reddit button--disabled" disabled> <button class="button button--reddit button--disabled" disabled>
{% trans "Deauthorize account" %} {% trans "Deauthorize account" %}
</button> </button>
{% endif %} {% endif %}
</div>
</div> </div>
</div> </section>
</section> </div>
</main> </main>
{% endblock %} {% endblock %}

View file

@ -1,7 +1,9 @@
{% extends "base.html" %} {% extends "sidebar.html" %}
{% block content %} {% block content %}
<main id="login--page" class="main"> <main id="login--page" class="main" data-render-sidebar=true>
{% include "accounts/components/login-form.html" with form=form title="Login" confirm_text="Login" %} <div class="main__container">
{% include "accounts/components/login-form.html" with form=form title="Login" confirm_text="Login" %}
</div>
</main> </main>
{% endblock %} {% endblock %}

View file

@ -1,8 +1,12 @@
{% extends "base.html" %} {% extends "sidebar.html" %}
{% block content %} {% block content %}
<main id="password-change--page" class="main"> {% url 'accounts:settings:home' as 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 %} <main id="password-change--page" class="main" data-render-sidebar=true>
<div class="main__container">
{% include "components/form/form.html" with form=form title="Change password" confirm_text="Change password" cancel_url=cancel_url %}
</div>
</main> </main>
{% endblock %} {% endblock %}

View file

@ -1,20 +1,22 @@
{% extends "base.html" %} {% extends "sidebar.html" %}
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<main id="reddit--page" class="main"> <main id="reddit--page" class="main" data-render-sidebar=true>
<section class="section text-section"> <div class="main__container">
{% if error %} <section class="section text-section">
<h1 class="h1">{% trans "Reddit authorization failed" %}</h1> {% if error %}
<p>{{ error }}</p> <h1 class="h1">{% trans "Reddit authorization failed" %}</h1>
{% elif access_token and refresh_token %} <p>{{ error }}</p>
<h1 class="h1">{% trans "Reddit account is linked" %}</h1> {% elif access_token and refresh_token %}
<p>{% trans "Your reddit account was successfully linked." %}</p> <h1 class="h1">{% trans "Reddit account is linked" %}</h1>
{% endif %} <p>{% trans "Your reddit account was successfully linked." %}</p>
{% endif %}
<p> <p>
<a class="link" href="{% url 'accounts:settings:integrations' %}">{% trans "Return to integrations page" %}</a> <a class="link" href="{% url 'accounts:settings:integrations' %}">{% trans "Return to integrations page" %}</a>
</p> </p>
</section> </section>
</div>
</main> </main>
{% endblock %} {% endblock %}

View file

@ -1,7 +1,9 @@
{% extends "base.html" %} {% extends "sidebar.html" %}
{% block content %} {% block content %}
<main id="settings--page" class="main"> <main id="settings--page" class="main" data-render-sidebar=true>
{% include "accounts/components/settings-form.html" with form=form title="User profile" confirm_text="Save" %} <div class="main__container">
{% include "accounts/components/settings-form.html" with form=form title="User profile" confirm_text="Save" %}
</div>
</main> </main>
{% endblock %} {% endblock %}

View file

@ -1,8 +1,10 @@
from django.contrib.auth import views as django_views from django.contrib.auth import views as django_views
from django.urls import reverse_lazy 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" template_name = "accounts/views/login.html"
success_url = reverse_lazy("index") success_url = reverse_lazy("index")

View file

@ -13,12 +13,13 @@ from newsreader.news.collection.reddit import (
revoke_reddit_token, revoke_reddit_token,
) )
from newsreader.news.collection.tasks import RedditTokenTask from newsreader.news.collection.tasks import RedditTokenTask
from newsreader.utils.views import NavListMixin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class IntegrationsView(TemplateView): class IntegrationsView(NavListMixin, TemplateView):
template_name = "accounts/views/integrations.html" template_name = "accounts/views/integrations.html"
def get_context_data(self, **kwargs): 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" template_name = "accounts/views/reddit.html"
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):

View file

@ -1,32 +1,34 @@
from django.contrib.auth import views as django_views from django.contrib.auth import views as django_views
from django.urls import reverse_lazy from django.urls import reverse_lazy
from newsreader.utils.views import NavListMixin
# PasswordResetView sends the mail # PasswordResetView sends the mail
# PasswordResetDoneView shows a success message for the above # PasswordResetDoneView shows a success message for the above
# PasswordResetConfirmView checks the link the user clicked and # PasswordResetConfirmView checks the link the user clicked and
# prompts for a new password # prompts for a new password
# PasswordResetCompleteView shows a success message for the above # 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" template_name = "password-reset/password-reset.html"
subject_template_name = "password-reset/password-reset-subject.txt" subject_template_name = "password-reset/password-reset-subject.txt"
email_template_name = "password-reset/password-reset-email.html" email_template_name = "password-reset/password-reset-email.html"
success_url = reverse_lazy("accounts:password-reset-done") 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" 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" template_name = "password-reset/password-reset-confirm.html"
success_url = reverse_lazy("accounts:password-reset-complete") 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" 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" template_name = "accounts/views/password-change.html"
success_url = reverse_lazy("accounts:settings") success_url = reverse_lazy("accounts:settings")

View file

@ -4,6 +4,8 @@ from django.views.generic import TemplateView
from registration.backends.default import views as registration_views from registration.backends.default import views as registration_views
from newsreader.utils.views import NavListMixin
# RegistrationView shows a registration form and sends the email # RegistrationView shows a registration form and sends the email
# RegistrationCompleteView shows after filling in the registration form # 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 # ActivationCompleteView shows the success screen when activation was succesful
# ActivationResendView can be used when activation links are expired # ActivationResendView can be used when activation links are expired
# RegistrationClosedView shows when registration is disabled # RegistrationClosedView shows when registration is disabled
class RegistrationView(registration_views.RegistrationView): class RegistrationView(NavListMixin, registration_views.RegistrationView):
disallowed_url = reverse_lazy("accounts:register-closed") disallowed_url = reverse_lazy("accounts:register-closed")
template_name = "registration/registration_form.html" template_name = "registration/registration_form.html"
success_url = reverse_lazy("accounts:register-complete") success_url = reverse_lazy("accounts:register-complete")
class RegistrationCompleteView(TemplateView): class RegistrationCompleteView(NavListMixin, TemplateView):
template_name = "registration/registration_complete.html" template_name = "registration/registration_complete.html"
class RegistrationClosedView(TemplateView): class RegistrationClosedView(NavListMixin, TemplateView):
template_name = "registration/registration_closed.html" template_name = "registration/registration_closed.html"
# Redirects or renders failed activation template # Redirects or renders failed activation template
class ActivationView(registration_views.ActivationView): class ActivationView(NavListMixin, registration_views.ActivationView):
template_name = "registration/activation_failure.html" template_name = "registration/activation_failure.html"
def get_success_url(self, user): def get_success_url(self, user):
return ("accounts:activate-complete", (), {}) return ("accounts:activate-complete", (), {})
class ActivationCompleteView(TemplateView): class ActivationCompleteView(NavListMixin, TemplateView):
template_name = "registration/activation_complete.html" template_name = "registration/activation_complete.html"
# Renders activation form resend or resend_activation_complete # 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" template_name = "registration/activation_resend_form.html"
def render_form_submitted_template(self, form): def render_form_submitted_template(self, form):

View file

@ -4,9 +4,10 @@ from django.views.generic.edit import FormView, ModelFormMixin
from newsreader.accounts.forms import UserSettingsForm from newsreader.accounts.forms import UserSettingsForm
from newsreader.accounts.models import User 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" template_name = "accounts/views/settings.html"
success_url = reverse_lazy("accounts:settings:home") success_url = reverse_lazy("accounts:settings:home")
form_class = UserSettingsForm form_class = UserSettingsForm

View file

@ -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"

View file

@ -227,6 +227,7 @@ AXES_FAILURE_LIMIT = 5
AXES_COOLOFF_TIME = 3 # in hours AXES_COOLOFF_TIME = 3 # in hours
AXES_RESET_ON_SUCCESS = True AXES_RESET_ON_SUCCESS = True
# TODO: verify parser works correctly
REST_FRAMEWORK = { REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": ( "DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.SessionAuthentication",
@ -235,7 +236,13 @@ REST_FRAMEWORK = {
"rest_framework.permissions.IsAuthenticated", "rest_framework.permissions.IsAuthenticated",
"newsreader.accounts.permissions.IsOwner", "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 = { SWAGGER_SETTINGS = {

View file

@ -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 (
<li key={index} className="nav-list__item">
<a href={link}>{name}</a>
</li>
);
});
const className = this.props.includeBorder ? "nav-list nav-list--bordered": "nav-list";
return <ol className={className}>{links}</ol>;
}
}
export default NavList;

View file

@ -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 (
<div className="sidebar">
<div className="sidebar__nav">
<NavList navLinks={this.props.navLinks} includeBorder={this.props.includeBorder} />
{this.props.children}
</div>
<label htmlFor="menu-input" className="sidebar__close" />
</div>
);
}
}
export default Sidebar;

View file

@ -2,3 +2,4 @@ import './lib/index.js';
import './pages/homepage/index.js'; import './pages/homepage/index.js';
import './pages/categories/index.js'; import './pages/categories/index.js';
import './pages/rules/index.js'; import './pages/rules/index.js';
import './pages/default/index.js';

View file

@ -6,6 +6,7 @@ import Card from '../../components/Card.js';
import CategoryCard from './components/CategoryCard.js'; import CategoryCard from './components/CategoryCard.js';
import CategoryModal from './components/CategoryModal.js'; import CategoryModal from './components/CategoryModal.js';
import Messages from '../../components/Messages.js'; import Messages from '../../components/Messages.js';
import Sidebar from '../../components/Sidebar.js';
class App extends React.Component { class App extends React.Component {
selectCategory = ::this.selectCategory; selectCategory = ::this.selectCategory;
@ -90,15 +91,19 @@ class App extends React.Component {
return ( return (
<> <>
{this.state.message && <Messages messages={[this.state.message]} />} {this.state.message && <Messages messages={[this.state.message]} />}
<Card header={pageHeader} /> <Sidebar navLinks={this.props.navLinks} />
{cards}
{selectedCategory && ( <div className="main__container">
<CategoryModal <Card header={pageHeader} />
category={selectedCategory} {cards}
handleCancel={this.deselectCategory} {selectedCategory && (
handleDelete={this.deleteCategory} <CategoryModal
/> category={selectedCategory}
)} handleCancel={this.deselectCategory}
handleDelete={this.deleteCategory}
/>
)}
</div>
</> </>
); );
} }

View file

@ -12,11 +12,15 @@ if (page) {
let createUrl = document.getElementById('createUrl').textContent; let createUrl = document.getElementById('createUrl').textContent;
let updateUrl = document.getElementById('updateUrl').textContent; let updateUrl = document.getElementById('updateUrl').textContent;
let linkScript = document.getElementById('Links');
let navLinks = JSON.parse(linkScript.textContent);
ReactDOM.render( ReactDOM.render(
<App <App
categories={categories} categories={categories}
createUrl={createUrl.substring(1, createUrl.length - 2)} createUrl={createUrl.substring(1, createUrl.length - 2)}
updateUrl={updateUrl.substring(1, updateUrl.length - 4)} updateUrl={updateUrl.substring(1, updateUrl.length - 4)}
navLinks={navLinks}
/>, />,
page page
); );

View file

@ -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(
<Sidebar navLinks={navLinks} />,
mainElement
),
document.createElement('div')
);
}

View file

@ -4,14 +4,25 @@ import { connect } from 'react-redux';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { fetchCategories } from './actions/categories'; import { fetchCategories } from './actions/categories';
import { filterPosts } from './components/postlist/filters.js';
import ScrollTop from './components/ScrollTop.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 PostList from './components/postlist/PostList.js';
import PostModal from './components/PostModal.js'; import PostModal from './components/PostModal.js';
import Messages from '../../components/Messages.js'; import Messages from '../../components/Messages.js';
class App extends React.Component { class App extends React.Component {
state = { postListNode: null }
constructor(props) {
super(props);
this.postListRef = node => {
this.setState({ postListNode: node });
};
}
componentDidMount() { componentDidMount() {
this.props.fetchCategories(); this.props.fetchCategories();
} }
@ -19,11 +30,13 @@ class App extends React.Component {
render() { render() {
return ( return (
<> <>
<Sidebar /> <HomepageSidebar navLinks={this.props.navLinks} />
<PostList <PostList
feedUrl={this.props.feedUrl} feedUrl={this.props.feedUrl}
subredditUrl={this.props.subredditUrl} subredditUrl={this.props.subredditUrl}
timezone={this.props.timezone} timezone={this.props.timezone}
forwardedRef={this.postListRef}
postsByType={this.props.postsByType}
/> />
{!isEqual(this.props.post, {}) && ( {!isEqual(this.props.post, {}) && (
@ -40,7 +53,7 @@ class App extends React.Component {
/> />
)} )}
<ScrollTop /> <ScrollTop postListNode={this.state.postListNode} />
{this.props.error && ( {this.props.error && (
<Messages messages={[{ type: 'error', text: this.props.error.message }]} /> <Messages messages={[{ type: 'error', text: this.props.error.message }]} />
@ -52,6 +65,7 @@ class App extends React.Component {
const mapStateToProps = state => { const mapStateToProps = state => {
const { error } = state.error; const { error } = state.error;
const postsByType = filterPosts(state)
if (!isEqual(state.selected.post, {})) { if (!isEqual(state.selected.post, {})) {
const ruleId = state.selected.post.rule.id; const ruleId = state.selected.post.rule.id;
@ -65,10 +79,11 @@ const mapStateToProps = state => {
rule, rule,
post: state.selected.post, post: state.selected.post,
selectedType: state.selected.item.type, selectedType: state.selected.item.type,
postsByType: postsByType,
}; };
} }
return { error, post: state.selected.post }; return { error, post: state.selected.post, postsByType: postsByType, };
}; };
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({

View file

@ -47,7 +47,7 @@ class PostModal extends React.Component {
const titleClassName = post.read ? 'post__title post__title--read' : 'post__title'; const titleClassName = post.read ? 'post__title post__title--read' : 'post__title';
const readButtonDisabled = const readButtonDisabled =
post.read || this.props.isUpdating || this.props.selectedType === SAVED_TYPE; 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 = ''; let ruleUrl = '';
@ -63,59 +63,64 @@ class PostModal extends React.Component {
return ( return (
<div className="modal post-modal"> <div className="modal post-modal">
<div className="post"> <div className="post">
<div className="post__header"> <div className="post__container">
<div className="post__actions"> <div className="post__header">
<button <div className="post__actions">
className={`button read-button ${readButtonDisabled && <button
'button--disabled'}`} className={`button read-button ${readButtonDisabled &&
onClick={() => 'button--disabled'}`}
!readButtonDisabled && this.props.markPostRead(post, token) onClick={() =>
} !readButtonDisabled && this.props.markPostRead(post, token)
> }
<i className="fas fa-check" /> Mark as read >
</button> <i className="fas fa-check" /> Mark as read
<button </button>
className="button post__close-button" <button
onClick={() => this.props.unSelectPost()} className="button post__close-button"
> onClick={() => this.props.unSelectPost()}
<i className="fas fa-times"></i> Close >
</button> <i className="fas fa-times"></i> Close
</div> </button>
<div className="post__heading"> </div>
<h2 className={titleClassName}>{`${post.title} `}</h2> <div className="post__heading">
<div className="post__meta-info"> <h2 className={titleClassName}>{`${post.title} `}</h2>
<span className="post__date"> <div className="post__meta">
{publicationDate} {this.props.timezone} <div className="post__text">
</span> <span className="post__date">{publicationDate}</span>
{post.author && <span className="post__author">{post.author}</span>} {post.author && <span className="post__author">{post.author}</span>}
{this.props.category && ( </div>
<span className="badge post__category" title={this.props.category.name}>
<div className="post__buttons">
{this.props.category && (
<span className="badge post__category" title={this.props.category.name}>
<a
href={`${this.props.categoriesUrl}/${this.props.category.id}/`}
target="_blank"
rel="noopener noreferrer"
>
{this.props.category.name}
</a>
</span>
)}
<span className="badge post__rule" title={this.props.rule.name}>
<a href={ruleUrl} target="_blank" rel="noopener noreferrer">
{this.props.rule.name}
</a>
</span>
<a <a
href={`${this.props.categoriesUrl}/${this.props.category.id}/`} className="post__link"
href={post.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{this.props.category.name} <i className="fas fa-external-link-alt" />
</a> </a>
</span> <span
)} className={savedIconClass}
<span className="badge post__rule" title={this.props.rule.name}> onClick={() => this.props.toggleSaved(post, token)}
<a href={ruleUrl} target="_blank" rel="noopener noreferrer"> />
{this.props.rule.name} </div>
</a> </div>
</span>
<a
className="post__link"
href={post.url}
target="_blank"
rel="noopener noreferrer"
>
<i className="fas fa-external-link-alt" />
</a>
<span
className={savedIconClass}
onClick={() => this.props.toggleSaved(post, token)}
/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -3,35 +3,48 @@ import React from 'react';
export default class ScrollTop extends React.Component { export default class ScrollTop extends React.Component {
scrollListener = ::this.scrollListener; scrollListener = ::this.scrollListener;
state = { showTop: false, showBottom: false }; state = {
listenerAttached: false,
showTop: false,
showBottom: false
};
componentDidMount() { componentDidUpdate() {
window.addEventListener('scroll', this.scrollListener); if (this.props.postListNode && !this.state.listenerAttached) {
this.props.postListNode.addEventListener('scroll', this.scrollListener);
this.setState({ listenerAttached: true });
}
} }
scrollListener() { 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({ this.setState({
showTop: window.pageYOffset > 0 ? true : false, showTop: postList.scrollTop > window.innerHeight,
showBottom: showBottom, showBottom: !elementEnd,
}); });
} }
render() { render() {
return ( const postList = this.props.postListNode;
return postList && (
<div className="scroll-to-top"> <div className="scroll-to-top">
{this.state.showTop && ( {this.state.showTop && (
<i <i
className="scroll-to-top__icon scroll-to-top__icon--top" className="scroll-to-top__icon scroll-to-top__icon--top"
onClick={() => window.scrollTo(0, 0)} onClick={() => postList.scroll({ top: 0 })}
/> />
)} )}
{this.state.showBottom && ( {this.state.showBottom && (
<i <i
className="scroll-to-top__icon scroll-to-top__icon--bottom" className="scroll-to-top__icon scroll-to-top__icon--bottom"
onClick={() => window.scrollTo(0, document.body.scrollHeight)} onClick={() => postList.scroll({ top: postList.scrollHeight })}
/> />
)} )}
</div> </div>

View file

@ -37,7 +37,7 @@ class PostItem extends React.Component {
<div className="posts-info"> <div className="posts-info">
<span className="posts-info__date" title={publicationDate}> <span className="posts-info__date" title={publicationDate}>
{publicationDate} {this.props.timezone} {post.author && `By ${post.author}`} {publicationDate} {post.author && `By ${post.author}`}
</span> </span>
{[CATEGORY_TYPE, SAVED_TYPE].includes(this.props.selected.type) && ( {[CATEGORY_TYPE, SAVED_TYPE].includes(this.props.selected.type) && (
<span className="badge"> <span className="badge">

View file

@ -65,7 +65,6 @@ class PostList extends React.Component {
selected: this.props.selected, selected: this.props.selected,
feedUrl: this.props.feedUrl, feedUrl: this.props.feedUrl,
subredditUrl: this.props.subredditUrl, subredditUrl: this.props.subredditUrl,
timezone: this.props.timezone,
}; };
if (isLastItem?.id === item.id) { if (isLastItem?.id === item.id) {
@ -96,7 +95,7 @@ class PostList extends React.Component {
); );
} else { } else {
return ( return (
<div className="posts"> <div className="posts" ref={this.props.forwardedRef}>
<ul className="posts__list">{postItems}</ul> <ul className="posts__list">{postItems}</ul>
{this.props.isFetching && <LoadingIndicator />} {this.props.isFetching && <LoadingIndicator />}
</div> </div>
@ -107,7 +106,6 @@ class PostList extends React.Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
isFetching: state.posts.isFetching, isFetching: state.posts.isFetching,
postsByType: filterPosts(state),
next: state.selected.next, next: state.selected.next,
lastReached: state.selected.lastReached, lastReached: state.selected.lastReached,
selected: state.selected.item, selected: state.selected.item,
@ -118,4 +116,4 @@ const mapDispatchToProps = dispatch => ({
fetchSavedPosts: (next = false) => dispatch(fetchSavedPosts(next)), fetchSavedPosts: (next = false) => dispatch(fetchSavedPosts(next)),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(PostList); export default connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(PostList);

View file

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { isEqual } from 'lodash';
import { CATEGORY_TYPE } from '../../constants.js'; import { CATEGORY_TYPE } from '../../constants.js';
import { selectCategory, fetchCategory } from '../../actions/categories.js'; import { selectCategory, fetchCategory } from '../../actions/categories.js';
import { fetchPostsBySection } from '../../actions/posts.js'; import { fetchPostsBySection } from '../../actions/posts.js';
import { isSelected } from './functions.js'; import { isSelected } from './functions.js';
import RuleItem from './RuleItem.js'; import RuleItem from './RuleItem.js';

View file

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { isEqual } from 'lodash';
import { RULE_TYPE } from '../../constants.js'; import { RULE_TYPE } from '../../constants.js';
import { selectRule, fetchRule } from '../../actions/rules.js'; import { selectRule, fetchRule } from '../../actions/rules.js';
import { fetchPostsBySection } from '../../actions/posts.js'; import { fetchPostsBySection } from '../../actions/posts.js';
import { isSelected } from './functions.js'; import { isSelected } from './functions.js';
class RuleItem extends React.Component { class RuleItem extends React.Component {

View file

@ -1,20 +1,20 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { isEqual } from 'lodash';
import { filterCategories, filterRules } from './filters.js'; import Sidebar from "../../../../components/Sidebar.js";
import { CATEGORY_TYPE, RULE_TYPE } from '../../constants.js';
import LoadingIndicator from '../../../../components/LoadingIndicator.js'; import LoadingIndicator from '../../../../components/LoadingIndicator.js';
import { CATEGORY_TYPE, RULE_TYPE } from '../../constants.js';
import CategoryItem from './CategoryItem.js'; import CategoryItem from './CategoryItem.js';
import SavedItem from './SavedItem.js'; import SavedItem from './SavedItem.js';
import ReadButton from './ReadButton.js'; import ReadButton from './ReadButton.js';
// TODO: show empty category message import { filterCategories, filterRules } from './filters.js';
class Sidebar extends React.Component {
class HomepageSidebar extends React.Component {
render() { render() {
const categoryItems = this.props.categories.items.map(category => { const categoryItems = this.props.categories.items.map(category => { const rules = this.props.rules.items.filter(rule => {
const rules = this.props.rules.items.filter(rule => {
return rule.category === category.id; return rule.category === category.id;
}); });
@ -32,22 +32,24 @@ class Sidebar extends React.Component {
this.props.selected.item && this.props.selected.item &&
[CATEGORY_TYPE, RULE_TYPE].includes(this.props.selected.item.type); [CATEGORY_TYPE, RULE_TYPE].includes(this.props.selected.item.type);
return ( return (
<div className="sidebar"> <Sidebar navLinks={this.props.navLinks} includeBorder={true} >
{(this.props.categories.isFetching || this.props.rules.isFetching) && ( {(this.props.categories.isFetching || this.props.rules.isFetching) && (
<LoadingIndicator /> <LoadingIndicator />
)} )}
<ul className="sidebar__nav"> <ul className="sidebar__list">
<SavedItem selected={this.props.selected.item} /> <SavedItem selected={this.props.selected.item} />
{categoryItems} {categoryItems}
</ul> </ul>
{showReadButton && <ReadButton />} {showReadButton && <ReadButton />}
</div> </Sidebar>
); );
} }
} };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
categories: { ...state.categories, items: filterCategories(state.categories.items) }, categories: { ...state.categories, items: filterCategories(state.categories.items) },
@ -55,4 +57,4 @@ const mapStateToProps = state => ({
selected: state.selected, selected: state.selected,
}); });
export default connect(mapStateToProps)(Sidebar); export default connect(mapStateToProps)(HomepageSidebar);

View file

@ -14,6 +14,8 @@ if (page) {
const settings = JSON.parse(document.getElementById('homepageSettings').textContent); const settings = JSON.parse(document.getElementById('homepageSettings').textContent);
const { feedUrl, subredditUrl, categoriesUrl } = settings; const { feedUrl, subredditUrl, categoriesUrl } = settings;
const navLinks = JSON.parse(document.getElementById('Links').textContent);
const app = ( const app = (
<Provider store={store}> <Provider store={store}>
<App <App
@ -22,6 +24,7 @@ if (page) {
categoriesUrl={categoriesUrl.substring(1, categoriesUrl.length - 3)} categoriesUrl={categoriesUrl.substring(1, categoriesUrl.length - 3)}
timezone={settings.timezone} timezone={settings.timezone}
autoMarking={settings.autoMarking} autoMarking={settings.autoMarking}
navLinks={navLinks}
/> />
</Provider> </Provider>
); );

View file

@ -1,9 +1,13 @@
{% extends "base.html" %} {% extends "sidebar.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<main id="rule--page" class="main"> {% url "news:collection:rules" as cancel_url %}
{% 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" %} <main id="rule--page" class="main" data-render-sidebar=true>
<div class="main__container">
{% include "components/form/form.html" with form=form title="Add a feed" cancel_url=cancel_url confirm_text="Add feed" %}
</div>
</main> </main>
{% endblock %} {% endblock %}

View file

@ -1,14 +1,17 @@
{% extends "base.html" %} {% extends "sidebar.html" %}
{% load static i18n %} {% load static i18n %}
{% block content %} {% block content %}
<main id="rule--page" class="main"> {% url "news:collection:rules" as cancel_url %}
{% if feed.error %} {% trans "Failed to retrieve posts" as title %}
{% 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 %} <main id="rule--page" class="main" data-render-sidebar=true>
{% include "components/form/form.html" with form=form title="Update feed" cancel_url=cancel_url confirm_text="Save feed" %} <div class="main__container">
{% 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" %}
<div>
</main> </main>
{% endblock %} {% endblock %}

View file

@ -1,9 +1,13 @@
{% extends "base.html" %} {% extends "sidebar.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<main id="import--page" class="main"> {% url "news:collection:rules" as cancel_url %}
{% 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" %} <main id="import--page" class="main" data-render-sidebar=true>
<div class="main__container">
{% include "components/form/form.html" with form=form title="Import an OPML file" cancel_url=cancel_url confirm_text="Import feeds" %}
</div>
</main> </main>
{% endblock %} {% endblock %}

View file

@ -1,102 +1,139 @@
{% extends "base.html" %} {% extends "sidebar.html" %}
{% load i18n static filters %} {% load i18n static filters %}
{% block content %} {% block content %}
<main id="rules--page" class="main"> <main id="rules--page" class="main" data-render-sidebar=true>
<form class="form rules-form"> <div class="main__container">
{% csrf_token %} <form class="form rules-form">
{% csrf_token %}
<section class="section form__section form__section--actions"> <section class="section form__section form__section--actions">
<div class="form__actions"> <div class="form__actions">
<a class="link button button--confirm" href="{% url "news:collection:feed-create" %}">{% trans "Add a feed" %}</a> <a class="link button button--confirm" href="{% url "news:collection:feed-create" %}">{% trans "Add a feed" %}</a>
<a class="link button button--confirm" href="{% url "news:collection:import" %}">{% trans "Import feeds" %}</a> <a class="link button button--confirm" href="{% url "news:collection:import" %}">{% trans "Import feeds" %}</a>
<a class="link button button--reddit" href="{% url "news:collection:subreddit-create" %}">{% trans "Add a subreddit" %}</a> <a class="link button button--reddit" href="{% url "news:collection:subreddit-create" %}">{% trans "Add a subreddit" %}</a>
</div> </div>
</section> </section>
<section class="section form__section form__section--actions"> <section class="section form__section form__section--actions">
<fieldset class="fieldset form__fieldset"> <fieldset class="fieldset form__fieldset">
<input type="submit" class="button button--primary" formaction="{% url "news:collection:rules-enable" %}" formmethod="post" value="{% trans "Enable" %}" /> <input type="submit" class="button button--primary" formaction="{% url "news:collection:rules-enable" %}" formmethod="post" value="{% trans "Enable" %}" />
<input type="submit" class="button button--primary" formaction="{% url "news:collection:rules-disable" %}" formmethod="post" value="{% trans "Disable" %}" /> <input type="submit" class="button button--primary" formaction="{% url "news:collection:rules-disable" %}" formmethod="post" value="{% trans "Disable" %}" />
<input type="submit" class="button button--error" formaction="{% url "news:collection:rules-delete" %}" formmethod="post" value="{% trans "Delete" %}"/> <input type="submit" class="button button--error" formaction="{% url "news:collection:rules-delete" %}" formmethod="post" value="{% trans "Delete" %}"/>
</fieldset> </fieldset>
</section> </section>
<section class="section form__section"> <section class="section form__section">
<table class="table rules-table" border="0" cellspacing="0"> <table class="table rules-table" border="0" cellspacing="0">
<thead class="table__header rules-table__header"> <thead class="table__header rules-table__header">
<tr class="table__row rules-table__row"> <tr class="table__row rules-table__row">
<th class="table__heading rules-table__heading--select"> <th class="table__heading rules-table__heading--select">
{% include "components/form/checkbox.html" with id="select-all" data_input="rules" id_for_label="select-all" %} {% include "components/form/checkbox.html" with id="select-all" data_input="rules" id_for_label="select-all" %}
</th> </th>
<th class="table__heading rules-table__heading--name">{% trans "Name" %}</th> <th class="table__heading rules-table__heading rules-table__heading--name">{% trans "Name" %}</th>
<th class="table__heading rules-table__heading--category">{% trans "Category" %}</th> <th class="table__heading rules-table__heading rules-table__heading--category">{% trans "Category" %}</th>
<th class="table__heading rules-table__heading--url">{% trans "URL" %}</th> <th class="table__heading rules-table__heading rules-table__heading--url">{% trans "URL" %}</th>
<th class="table__heading rules-table__heading--succeeded">{% trans "Successfuly ran" %}</th> <th class="table__heading rules-table__heading rules-table__heading--succeeded">{% trans "Successfuly ran" %}</th>
<th class="table__heading rules-table__heading--enabled">{% trans "Enabled" %}</th> <th class="table__heading rules-table__heading rules-table__heading--enabled">{% trans "Enabled" %}</th>
</tr>
</thead>
<tbody class="table__body">
{% for rule in rules %}
<tr class="table__row {% if rule.failed %}table__row--error {% endif %}rules-table__row">
<td class="table__item rules-table__item">
{% 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 %}
</td>
<td class="table__item rules-table__item" title="{{ rule.name }}">
<a class="link" href="{{ rule.update_url }}">{{ rule.name }}</a>
</td>
<td class="table__item rules-table__item" title="{{ rule.category.name }}">
{% if rule.category %}
<a class="link" href="{% url 'news:core:category-update' pk=rule.category.pk %}">{{ rule.category.name }}</a>
{% endif %}
</td>
<td class="table__item rules-table__item" title="{{ rule.source_url }}">
<a class="link" href="{{ rule.source_url }}" target="_blank" rel="noopener noreferrer">{{ rule.source_url }}</a>
</td>
<td class="table__item rules-table__item">
{% if rule.failed %}
<i class="fas fa-exclamation-triangle"></i>
{% else %}
<i class="fas fa-check"></i>
{% endif %}
</td>
<td class="table__item rules-table__item">
{% if rule.enabled %}
<i class="fas fa-check"></i>
{% else %}
<i class="fas fa-pause"></i>
{% endif %}
</td>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody class="table__body rules-table__body">
</table> {% for rule in rules %}
</section> <tr class="table__row {% if rule.failed %}table__row--error{% endif %} rules-table__row">
</form> <td class="table__item rules-table__item--select">
{% 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 %}
</td>
<div class="table__footer"> <td
<div class="pagination"> class="table__item rules-table__item rules-table__item--name"
<span class="pagination__previous"> title="{{ rule.name }}"
{% if page_obj.has_previous %} >
<a class="link button" href="?page=1">{% trans "first" %}</a> <a class="link" href="{{ rule.update_url }}">
<a class="link button" href="?page={{ page_obj.previous_page_number }}">{% trans "previous" %}</a> {{ rule.name }}
{% endif %} </a>
</span> </td>
<span class="pagination__current"> <td
{% blocktrans with current_number=page_obj.number total_count=page_obj.paginator.num_pages %} class="table__item rules-table__item rules-table__item--category"
Page {{ current_number }} of {{ total_count }} title="{{ rule.category.name }}"
{% endblocktrans %} >
</span> {% if rule.category %}
<a
class="link"
href="{% url 'news:core:category-update' pk=rule.category.pk %}"
>
{{ rule.category.name }}
</a>
{% endif %}
</td>
<span class="pagination__next"> <td
{% if page_obj.has_next %} class="table__item rules-table__item rules-table__item--url"
<a class="link button" href="?page={{ page_obj.next_page_number }}">{% trans "next" %}</a> title="{{ rule.source_url }}"
<a class="link button" href="?page={{ page_obj.paginator.num_pages }}">{% trans "last" %}</a> >
{% endif %} <a
</span> class="link"
href="{{ rule.source_url }}"
target="_blank"
rel="noopener noreferrer"
>
{{ rule.source_url }}
</a>
</td>
<td class="table__item rules-table__item rules-table__item--failed">
{% if rule.failed %}
<i class="fas fa-exclamation-triangle"></i>
{% else %}
<i class="fas fa-check"></i>
{% endif %}
</td>
<td class="table__item rules-table__item rules-table__item--enabled">
{% if rule.enabled %}
<i class="fas fa-check"></i>
{% else %}
<i class="fas fa-pause"></i>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
</form>
<div class="table__footer">
<div class="pagination">
<span class="pagination__previous">
{% if page_obj.has_previous %}
<a class="link button" href="?page=1">{% trans "first" %}</a>
<a class="link button" href="?page={{ page_obj.previous_page_number }}">
{% trans "previous" %}
</a>
{% endif %}
</span>
<span class="pagination__current">
{% blocktrans with current_number=page_obj.number total_count=page_obj.paginator.num_pages %}
Page {{ current_number }} of {{ total_count }}
{% endblocktrans %}
</span>
<span class="pagination__next">
{% if page_obj.has_next %}
<a class="link button" href="?page={{ page_obj.next_page_number }}">
{% trans "next" %}
</a>
<a class="link button" href="?page={{ page_obj.paginator.num_pages }}">
{% trans "last" %}
</a>
{% endif %}
</span>
</div>
</div> </div>
</div> </div>
</main> </main>

View file

@ -1,9 +1,13 @@
{% extends "base.html" %} {% extends "sidebar.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<main id="subreddit--page" class="main"> {% url "news:collection:rules" as cancel_url %}
{% 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" %} <main id="subreddit--page" class="main" data-render-sidebar=true>
<div class="main__container">
{% include "components/form/form.html" with form=form title="Add a subreddit" cancel_url=cancel_url confirm_text="Add subrredit" %}
</div>
</main> </main>
{% endblock %} {% endblock %}

View file

@ -8,9 +8,10 @@ from django_celery_beat.models import IntervalSchedule, PeriodicTask
from newsreader.news.collection.models import CollectionRule from newsreader.news.collection.models import CollectionRule
from newsreader.news.core.models import Category from newsreader.news.core.models import Category
from newsreader.utils.views import NavListMixin
class CollectionRuleViewMixin: class CollectionRuleViewMixin(NavListMixin):
queryset = CollectionRule.objects.order_by("name") queryset = CollectionRule.objects.order_by("name")
def get_queryset(self): def get_queryset(self):

View file

@ -14,6 +14,7 @@ from newsreader.news.collection.views.base import (
TaskCreationMixin, TaskCreationMixin,
) )
from newsreader.utils.opml import parse_opml from newsreader.utils.opml import parse_opml
from newsreader.utils.views import NavListMixin
class FeedUpdateView(CollectionRuleViewMixin, CollectionRuleDetailMixin, UpdateView): class FeedUpdateView(CollectionRuleViewMixin, CollectionRuleDetailMixin, UpdateView):
@ -36,7 +37,7 @@ class FeedCreateView(
form_class = FeedForm form_class = FeedForm
class OPMLImportView(FormView): class OPMLImportView(NavListMixin, FormView):
form_class = OPMLImportForm form_class = OPMLImportForm
template_name = "news/collection/views/import.html" template_name = "news/collection/views/import.html"

View file

@ -7,15 +7,20 @@ from django.views.generic.list import ListView
from newsreader.news.collection.forms import CollectionRuleBulkForm from newsreader.news.collection.forms import CollectionRuleBulkForm
from newsreader.news.collection.views.base import CollectionRuleViewMixin 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 paginate_by = 50
template_name = "news/collection/views/rules.html" template_name = "news/collection/views/rules.html"
context_object_name = "rules" context_object_name = "rules"
class CollectionRuleBulkView(FormView): class CollectionRuleBulkView(NavListMixin, FormView):
form_class = CollectionRuleBulkForm form_class = CollectionRuleBulkForm
def get_redirect_url(self): def get_redirect_url(self):

View file

@ -32,6 +32,7 @@
{{ categories_update_url|json_script:"updateUrl" }} {{ categories_update_url|json_script:"updateUrl" }}
{{ categories_create_url|json_script:"createUrl" }} {{ categories_create_url|json_script:"createUrl" }}
{{ sidebar_links|json_script:"Links" }}
{{ block.super }} {{ block.super }}
{% endblock %} {% endblock %}

View file

@ -1,9 +1,13 @@
{% extends "base.html" %} {% extends "sidebar.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<main id="category--page" class="main"> {% url "news:core:categories" as cancel_url %}
{% 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" %} <main id="category--page" class="main" data-render-sidebar=true>
<div class="main__container">
{% include "components/form/form.html" with form=form title="Create category" cancel_url=cancel_url confirm_text="Create category" %}
</div>
</main> </main>
{% endblock %} {% endblock %}

View file

@ -1,9 +1,13 @@
{% extends "base.html" %} {% extends "sidebar.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<main id="category--page" class="main"> {% url "news:core:categories" as cancel_url %}
{% 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" %} <main id="category--page" class="main" data-render-sidebar=true>
<div class="main__container">
{% include "components/form/form.html" with form=form title="Update category" cancel_url=cancel_url confirm_text="Save category" %}
</div>
</main> </main>
{% endblock %} {% endblock %}

View file

@ -7,6 +7,7 @@
{% block scripts %} {% block scripts %}
{{ homepageSettings|json_script:"homepageSettings" }} {{ homepageSettings|json_script:"homepageSettings" }}
{{ sidebar_links|json_script:"Links" }}
{{ block.super }} {{ block.super }}
{% endblock scripts %} {% endblock scripts %}

View file

@ -7,9 +7,10 @@ from django.views.generic.list import ListView
from newsreader.news.collection.models import CollectionRule from newsreader.news.collection.models import CollectionRule
from newsreader.news.core.forms import CategoryForm from newsreader.news.core.forms import CategoryForm
from newsreader.news.core.models import Category 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" template_name = "news/core/views/homepage.html"
def get_context_data(self, **kwargs): 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") queryset = Category.objects.prefetch_related("rules").order_by("name")
def get_queryset(self): def get_queryset(self):

View file

@ -2,7 +2,7 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: Rubik, sans-serif; font-family: Inter;
font-size: $font-size; font-size: $font-size;
} }

View file

@ -1,3 +1,6 @@
@import '../../partials/variables';
@import '../../lib/mixins';
.card { .card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -9,6 +12,10 @@
background-color: var(--background-color); background-color: var(--background-color);
@media (max-width: $mobile-breakpoint) {
width: initial;
}
&__header { &__header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -16,17 +23,17 @@
padding: 15px 0; padding: 15px 0;
border-bottom: 2px var(--lightest-accent-color) solid; border-bottom: 2px var(--border-color) solid;
} }
&__content { &__content {
display: flex; display: flex;
padding: 10px; padding: 10px 0;
} }
&__footer { &__footer {
display: flex; display: flex;
padding: 10px; padding: 10px 0;
} }
& .favicon { & .favicon {

View file

@ -0,0 +1,11 @@
.checkbox-list {
padding: 0;
&__item {
gap: 10px;
& > * {
margin: initial;
}
}
}

View file

@ -0,0 +1 @@
@import './checkbox-list';

View file

@ -1,6 +1,8 @@
.fieldset { .fieldset {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-wrap: wrap;
gap: 10px 0;
padding: 15px; padding: 15px;
border: none; border: none;

View file

@ -1,3 +1,5 @@
@import '../../partials/variables';
.form { .form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -6,6 +8,10 @@
background-color: var(--background-color); background-color: var(--background-color);
@media (max-width: $mobile-breakpoint) {
width: 100%;
}
&__section { &__section {
&--last { &--last {
& .form__fieldset { & .form__fieldset {
@ -44,6 +50,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 15px; gap: 15px;
flex-wrap: wrap;
@include block-padding; @include block-padding;
} }

View file

@ -1,5 +1,21 @@
@import '../../partials/variables';
.rules-form { .rules-form {
@extend .form; @extend .form;
width: 90%; width: 90%;
@media (max-width: $wqhd-breakpoint) {
width: initial;
}
& .form__fieldset {
gap: 15px;
& > * {
margin: initial;
}
}
} }

View file

@ -27,3 +27,6 @@
@import './posts/index'; @import './posts/index';
@import './posts-info/index'; @import './posts-info/index';
@import './scroll-to-top/index'; @import './scroll-to-top/index';
@import './menu/index';
@import './nav-list/index';
@import './checkbox-list/index';

View file

@ -7,6 +7,7 @@
&__controls { &__controls {
display: flex; display: flex;
flex-wrap: wrap;
gap: 10px; gap: 10px;
} }
} }

View file

@ -9,9 +9,7 @@
align-items: center; align-items: center;
padding: 10px 0; padding: 10px 0;
& > * { gap: 15px;
margin: 0 15px;
}
} }
} }

View file

@ -9,7 +9,7 @@
position: absolute; position: absolute;
left: 6px; left: 6px;
width: 13px; 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; animation: loading-indicator 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite;
&:nth-child(1){ &:nth-child(1){

View file

@ -1,7 +1,22 @@
.main { @import '../../partials/variables';
display: flex;
flex-direction: column;
align-items: center;
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;
}
}
} }

View file

@ -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";
}
}

View file

@ -0,0 +1 @@
@import './menu';

View file

@ -1,3 +1,6 @@
@import '../../partials/variables';
@import '../../partials/colors';
.messages { .messages {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -6,7 +9,7 @@
width: 100%; width: 100%;
margin: 5px 0 20px 0; margin: 5px 0 20px 0;
color: $font-color; color: var(--font-color);
&__item { &__item {
width: 80%; width: 80%;
@ -17,6 +20,10 @@
background-color: $transparant-blue; background-color: $transparant-blue;
@media (max-width: $mobile-breakpoint) {
width: 90%;
}
&--error { &--error {
background-color: $transparant-red; background-color: $transparant-red;
} }
@ -42,12 +49,10 @@
} }
&--fixed &__item { &--fixed &__item {
color: $white;
background-color: $blue; background-color: $blue;
} }
&--fixed &__item--error { &--fixed &__item--error {
color: $white;
background-color: $red; background-color: $red;
} }
@ -56,7 +61,6 @@
} }
&--fixed &__item--success { &--fixed &__item--success {
color: $white;
background-color: $green; background-color: $green;
} }
} }

View file

@ -1,3 +1,5 @@
@import '../../partials/variables';
.modal { .modal {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -8,7 +10,7 @@
height: 100%; height: 100%;
top: 0; top: 0;
background-color: $dark; background-color: var(--background-color);
&__item { &__item {
display: flex; display: flex;
@ -20,15 +22,17 @@
width: 60%; width: 60%;
background-color: var(--accent-color); @media (max-width: $mobile-breakpoint) {
width: initial;
}
} }
&__header { &__header {
padding: 5px 20px; padding: 5px 0;
} }
&__content { &__content {
padding: 10px 30px; padding: 10px 0;
} }
&__footer { &__footer {
@ -36,6 +40,6 @@
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
padding: 10px; padding: 10px 0;
} }
} }

View file

@ -1,3 +1,5 @@
@import '../../partials/variables';
.post-modal { .post-modal {
@extend .modal; @extend .modal;
@ -6,5 +8,9 @@
cursor: pointer; cursor: pointer;
@media (min-width: $tablet-breakpoint) {
background-color: var(--background-color-secondary);
}
z-index: 1000; z-index: 1000;
} }

View file

@ -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);
}
}
}

View file

@ -0,0 +1 @@
@import './nav-list';

View file

@ -1,3 +1,6 @@
@import '../../partials/variables';
@import '../../lib/functions';
.nav { .nav {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -5,34 +8,43 @@
padding: 10px 0; padding: 10px 0;
width: 100%; width: 100%;
height: map-deep-get($nav, height);
position: sticky; background-color: var(--background-color);
top: 0;
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; display: flex;
justify-content: flex-start; justify-content: flex-start;
width: 80%; width: 80%;
list-style-type: none; list-style-type: none;
}
&__item { @media (max-width: $mobile-breakpoint) {
margin: 0px 10px; display: none;
& a {
@extend .button;
font-size: 0.9em !important;
font-weight: 600;
} }
} }
&__item:last-child { & .nav-list {
margin: 0 10px 0 auto; 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);
}
} }
} }

View file

@ -4,8 +4,8 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 60%; height: max-content;
height: 80vh; margin: 20px 0 0 0;
&__message { &__message {
font-size: 16px; font-size: 16px;

View file

@ -1,20 +1,44 @@
@import '../../partials/variables';
@import '../../partials/colors';
@import '../../lib/functions';
@import '../../elements/button/';
.post { .post {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
position: relative; position: relative;
width: 80%; width: 35%;
height: 90%;
@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; margin: 2% auto 5% auto;
padding: 0 0 20px 0;
overflow-y: auto; overflow-y: auto;
background-color: var(--background-color); background-color: var(--background-color);
border-radius: 0.25em;
cursor: initial; cursor: initial;
&__container {
width: 90%;
}
&__header { &__header {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -23,8 +47,6 @@
position: sticky; position: sticky;
top: 0; top: 0;
width: 100%;
background-color: var(--background-color); background-color: var(--background-color);
} }
@ -33,8 +55,15 @@
justify-content: flex-end; justify-content: flex-end;
width: 100%; width: 100%;
padding: 20px 50px 0; padding: 20px 0;
gap: 20px; gap: 20px;
@media (max-width: $mobile-breakpoint) {
justify-content: space-between;
flex-direction: row-reverse;
gap: 0;
}
} }
&__heading { &__heading {
@ -42,11 +71,18 @@
flex-direction: column; flex-direction: column;
padding: 20px 0 10px 0; padding: 20px 0 10px 0;
width: 75%; @media (min-width: $hd-breakpoint) {
width: 80%;
}
@media (max-width: $hd-breakpoint) {
width: 100%;
padding: 0;
}
} }
&__title { &__title {
font-size: $header-size; font-size: map-deep-get($post, "header-font-size");
&--read { &--read {
color: var(--read-color); color: var(--read-color);
@ -62,8 +98,6 @@
} }
&__rule, &__category { &__rule, &__category {
background-color: var(--lightest-accent-color) !important;
& a { & a {
color: var(--font-color); color: var(--font-color);
} }
@ -74,7 +108,15 @@
flex-direction: column; flex-direction: column;
padding: 10px 0 30px 0; padding: 10px 0 30px 0;
width: 75%;
@media (min-width: $hd-breakpoint) {
width: 72%;
}
@media (max-width: $hd-breakpoint) {
width: 90%;
padding: 0;
}
& p { & p {
padding: 10px 0; padding: 10px 0;
@ -98,19 +140,66 @@
&__close-button { &__close-button {
background-color: var(--info-color); background-color: var(--info-color);
color: var(--font-color); color: $white;
& i { & i {
padding: 0 $fa-padding 0 0; padding: 0 $fa-padding 0 0;
} }
} }
&__meta-info { &__meta {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin: 15px 0; margin: 15px 0;
gap: 10px; 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);
}
}
} }
} }

View file

@ -7,11 +7,27 @@
&__date { &__date {
align-self: center; 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 { & .badge {
& .link { & .link {
color: var(--font-color); color: inherit;
}
@media (max-width: $mobile-breakpoint){
display: none;
} }
} }
} }

View file

@ -1,7 +1,15 @@
.posts { @import '../../partials/variables';
margin: 0 0 2% 20px; @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 { &__list {
display: flex; display: flex;
@ -11,6 +19,10 @@
padding: 0; padding: 0;
list-style: none; list-style: none;
@media (max-width: $mobile-breakpoint) {
width: initial;
}
} }
&__item { &__item {
@ -21,12 +33,22 @@
max-width: max-content; max-width: max-content;
@media (max-width: $mobile-breakpoint) {
max-width: 100vw;
}
&:first-child { &:first-child {
padding: 0 10px 10px 10px; padding: 0 10px 10px 10px;
} }
& .badge { & .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 { &:last-child {
@ -38,7 +60,7 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
font-size: $header-size; font-size: map-deep-get($post, header-font-size);
&--read { &--read {
color: var(--read-color); color: var(--read-color);

View file

@ -1,3 +1,5 @@
@import '../../partials/variables';
.rules { .rules {
padding: 0; padding: 0;
@ -6,19 +8,27 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
border-bottom-right-radius: .25em;
border-top-right-radius: .25em;
padding: 5px 5px 5px 20px; padding: 5px 5px 5px 20px;
@media (max-width: $mobile-breakpoint) {
margin: 10px 0;
padding: 25px 5px 20px 5px;
}
& * { & * {
padding: 0 2px 0 2px; padding: 0 2px 0 2px;
} }
&:hover { &:hover {
cursor: pointer; cursor: pointer;
background-color: var(--lighter-accent-color); background-color: var(--selected-color);
} }
&--selected { &--selected {
background-color: var(--lighter-accent-color); background-color: var(--selected-color);
} }
} }

View file

@ -1,3 +1,5 @@
@import '../../partials/variables';
.scroll-to-top { .scroll-to-top {
display: flex; display: flex;
gap: 10px; gap: 10px;
@ -8,6 +10,10 @@
margin: 0 0 20px 0; margin: 0 0 20px 0;
@media (max-width: $mobile-breakpoint) {
display: none;
}
&:hover { &:hover {
cursor: pointer; cursor: pointer;
} }
@ -16,7 +22,7 @@
font-style: initial; font-style: initial;
padding: 10px; padding: 10px;
background-color: var(--lightest-accent-color); background-color: var(--background-color-secondary);
&--top:before { &--top:before {
@include font-awesome; @include font-awesome;

View file

@ -1,31 +1,77 @@
@import '../../partials/variables';
@import '../../lib/functions';
.sidebar { .sidebar {
display: flex; display: none; // hide the sidebar by default, homepage enables it by default
flex-direction: column;
align-items: center;
align-self: start;
position: sticky; --easeOutExpo: cubic-bezier(0.16, 1, 0.3, 1);
top: 50px; --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 { &__nav {
width: 100%; display: flex;
max-height: 80vh; flex-direction: column;
overflow: auto; overflow-y: scroll;
background-color: var(--background-color);
padding: 10px;
}
&__close {
display: none;
@media (max-width: $mobile-breakpoint) {
display: initial;
}
}
&__list {
list-style: none; list-style: none;
}
&__item {
@media (max-width: $mobile-breakpoint) {
margin: 10px 0;
}
} }
&__container { &__container {
display: flex; display: flex;
align-items: center; align-items: center;
border-bottom-right-radius: .25em;
border-top-right-radius: .25em;
padding: 5px; padding: 5px;
@media (max-width: $mobile-breakpoint) {
padding: 25px 5px;
}
&--selected, &:hover { &--selected, &:hover {
background-color: var(--lighter-accent-color); background-color: var(--selected-color);
} }
} }
@ -51,6 +97,60 @@
} }
.read-button { .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);
} }
} }

View file

@ -1,7 +1,17 @@
@import '../../partials/variables';
.rules-table { .rules-table {
&__heading { padding: 15px;
&__heading, &__item {
padding: 10px;
&--select { &--select {
width: 5%; width: 5%;
& .checkbox {
margin: 0;
}
} }
&--name { &--name {
@ -10,10 +20,18 @@
&--category { &--category {
width: 15%; width: 15%;
@media (max-width: $mobile-breakpoint) {
display: none;
}
} }
&--url { &--url {
width: 40%; width: 40%;
@media (max-width: $mobile-breakpoint) {
display: none;
}
} }
&--succeeded { &--succeeded {

View file

@ -2,7 +2,7 @@
table-layout: fixed; table-layout: fixed;
background-color: var(--background-color); background-color: var(--background-color);
width: 90%; width: 100%;
padding: 20px; padding: 20px;
text-align: left; text-align: left;
@ -10,6 +10,10 @@
&__heading { &__heading {
@extend .h1; @extend .h1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
&__row { &__row {
@ -27,7 +31,7 @@
} }
&__footer { &__footer {
width: 80%; width: 100%;
padding: 10px 0; padding: 10px 0;
} }
} }

View file

@ -1,3 +1,6 @@
@import '../../partials/variables';
@import '../../partials/colors';
.badge { .badge {
display: inline-block; display: inline-block;
@ -6,7 +9,12 @@
text-align: center; text-align: center;
background-color: var(--lighter-accent-color); background-color: var(--background-color-secondary);
font-size: small; font-size: small;
border-radius: 0.25em;
@media (max-width: $mobile-breakpoint) {
font-size: inherit;
}
} }

View file

@ -1,4 +1,4 @@
.button { @mixin button {
display: flex; display: flex;
align-items: center; align-items: center;
@ -7,16 +7,15 @@
@include button-padding; @include button-padding;
border: none; border: none;
border-radius: 0.25em;
font-size: 16px;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
} }
&--success, &--confirm { &--success, &--confirm {
color: var(--confirm-button-font-color) !important;
background-color: var(--confirm-color); background-color: var(--confirm-color);
color: $white !important;
} }
&--error, &--cancel { &--error, &--cancel {
@ -40,6 +39,15 @@
&--disabled { &--disabled {
color: var(--font-color) !important; color: var(--font-color) !important;
background-color: $gray !important; background-color: var(--background-color-secondary) !important;
&:hover {
cursor: default;
}
} }
}
.button {
@include button;
} }

View file

@ -1,10 +1,18 @@
@import '../../partials/variables';
@import '../../partials/colors';
.read-button { .read-button {
@extend .button; @extend .button;
color: var(--confirm-button-font-color); color: var(--confirm-font-color);
background-color: var(--confirm-color); background-color: var(--confirm-color);
& i { & i {
padding: 0 $fa-padding 0 0; padding: 0 $fa-padding 0 0;
} }
@media (max-width: $mobile-breakpoint) {
width: max-content;
}
} }

View file

@ -2,8 +2,6 @@
display: block; display: block;
height: 20px; height: 20px;
width: 20px; width: 20px;
margin: 0 0 0 20px;
& input[type=checkbox] { & input[type=checkbox] {
position: absolute; position: absolute;
@ -14,7 +12,7 @@
&:checked + .checkbox__label { &:checked + .checkbox__label {
.checkbox__box { .checkbox__box {
background-color: var(--lightest-accent-color); background-color: var(--info-color);
} }
} }
} }
@ -29,7 +27,7 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
border: 2px solid var(--lighter-accent-color); border: 1.5px solid var(--border-color);
cursor: pointer; cursor: pointer;
} }
} }

View file

@ -1,7 +1,8 @@
.help-text { .help-text {
@extend .small; @extend .small;
padding: 5px 15px; padding: 10px 0;
} }
.helptext { .helptext {

View file

@ -2,12 +2,8 @@
@include text-padding; @include text-padding;
color: var(--font-color); color: var(--font-color);
background-color: var(--accent-color); background-color: var(--background-color-secondary);
border: 1px var(--lighter-accent-color) solid; border: 1px var(--border-color) solid;
&:focus {
border: 1px var(--lightest-accent-color) solid;
}
&[type="file"] { &[type="file"] {
width: 40%; width: 40%;
@ -15,7 +11,6 @@
&[type="checkbox"] { &[type="checkbox"] {
align-self: flex-start; align-self: flex-start;
margin: 0 0 0 10px;
} }
} }

View file

@ -1,5 +1,5 @@
.label { .label {
@include text-padding; padding: 10px 0;
} }
label { label {

View file

@ -0,0 +1,7 @@
@function map-deep-get($map, $keys...) {
@each $key in $keys {
$map: map-get($map, $key);
}
@return $map;
}

View file

@ -1,9 +1,26 @@
#homepage--page { #homepage--page {
display: flex; background-color: initial;
flex-direction: row;
align-items: initial;
width: 100%;
margin: 20px 0 0 0; display: grid;
background-color: initial;
@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;
}
} }

View file

@ -1,11 +1,24 @@
#login--page { #login--page {
margin: 5% auto;
width: 50%;
& .form { & .form {
@extend .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 { h4 {
margin: 0; margin: 0;
@ -21,9 +34,6 @@
&__fieldset { &__fieldset {
@extend .form__fieldset; @extend .form__fieldset;
&--last {
}
} }
} }
} }

View file

@ -1,5 +1,3 @@
#rules--page { #rules--page {
& .table { // TODO: remove scss
width: 100%;
}
} }

View file

@ -3,6 +3,7 @@
&__section { &__section {
&--last { &--last {
& .fieldset { & .fieldset {
flex-wrap: wrap;
gap: 15px; gap: 15px;
justify-content: flex-start; justify-content: flex-start;
} }

View file

@ -1,58 +1,64 @@
$orange: rgba(255, 212, 153, 1); $orange: #ff2a51;
$green: rgba(89, 181, 128, 1); $green: #007936;
$red: lighten(rgba(231, 76, 60, 1), 10%); $red: #d30038;
$gray: rgba(227, 227, 227, 1); $blue: #0085f2;
$blue: rgba(111, 164, 196, 1);
$white: rgba(255, 255, 255, 1); $white: #fff;
$black: rgba(0, 0, 0, 1); $black: #000;
$dark: rgba(0, 0, 0, 0.4);
$reddit-orange: rgba(255, 69, 0, 1);
$transparant-red: transparentize($red, 0.8); $transparant-red: transparentize($red, 0.8);
$transparant-blue: transparentize($blue, 0.8); $transparant-blue: transparentize($blue, 0.8);
$transparant-orange: transparentize($orange, 0.4); $transparant-orange: transparentize($orange, 0.4);
$transparant-green: transparentize($green, 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 // White theme
$background-color: rgba(255, 249, 176, 1); $background-color: $white;
$background-color-secondary: #f9f9fb;
$font-color: rgba(83, 87, 91, 1); $font-color: #1b1b1b;
$link-color: rgba(45, 142, 202, 1);
$read-color: darken($gainsboro, 10%);
$confirm-button-font-color: rgba(255, 255, 255, 1);
$accent-color: rgba(255, 171, 115, 1); $link-color: #0069c2;
$lighter-accent-color: rgba(255, 211, 132, 1); $selected-color: #0085f230;
$lightest-accent-color: rgba(255, 174, 192, 1); $read-color: darken($font-color, 10%);
$confirm-color: rgba(117, 207, 184, 1); $confirm-color: $green;
$danger-color: rgba(237, 118, 105, 1); $confirm-font-color: $white;
$warning-color: rgba(255, 218, 119, 1);
$info-color: rgba(162, 213, 242, 1); $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 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-link-color: $link-color;
$dark-read-color: darken($dark-font-color, 20%); $dark-read-color: darken($dark-font-color, 5%);
$dark-confirm-button-font-color: $dark-font-color;
$dark-accent-color: rgba(19, 59, 92, 1); $dark-confirm-color: $green;
$dark-lighter-accent-color: rgba(30, 95, 116, 1); $dark-confirm-font-color: $white;
$dark-lightest-accent-color: rgba(88, 61, 114, 1);
$dark-confirm-color: rgba(0, 121, 101, 1); $dark-danger-color: $red;
$dark-danger-color: rgba(175, 45, 45, 1); $dark-danger-font-color: $white;
$dark-warning-color: rgba(238, 187, 77, 1);
$dark-info-color: rgba(31, 111, 139, 1); $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);

View file

@ -1,12 +1,15 @@
@font-face { @font-face {
font-family: Rubik; font-family: Inter;
src: url('../assets/fonts/Rubik-Regular.ttf'); font-style: normal;
font-display: swap;
src: url('../assets/fonts/Inter-VariableFont_opsz,wght.ttf');
} }
@font-face { @font-face {
font-family: Rubik; font-family: Inter;
src: url('../assets/fonts/Rubik-Bold.ttf'); font-style: italic;
font-weight: bold; font-display: swap;
src: url('../assets/fonts/Inter-Italic-VariableFont_opsz,wght.ttf');
} }
@font-face { @font-face {

View file

@ -1,33 +1,37 @@
:root { :root {
--background-color: #{$background-color}; --background-color: #{$background-color};
--background-color-secondary: #{$background-color-secondary};
--font-color: #{$font-color}; --font-color: #{$font-color};
--link-color: #{$link-color}; --link-color: #{$link-color};
--selected-color: #{$selected-color};
--read-color: #{$read-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-color: #{$confirm-color};
--confirm-font-color: #{$confirm-font-color};
--danger-color: #{$danger-color}; --danger-color: #{$danger-color};
--danger-font-color: #{$danger-color};
--warning-color: #{$warning-color}; --warning-color: #{$warning-color};
--warning-font-color: #{$warning-color};
--info-color: #{$info-color}; --info-color: #{$info-color};
--info-font-color: #{$info-color};
--border-color: #{$border-color};
&.dark-theme { &.dark-theme {
--background-color: #{$dark-background-color}; --background-color: #{$dark-background-color};
--background-color-secondary: #{$dark-background-color-secondary};
--font-color: #{$dark-font-color}; --font-color: #{$dark-font-color};
--link-color: #{$dark-link-color}; --link-color: #{$dark-link-color};
--read-color: #{$dark-read-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-color: #{$dark-confirm-color};
--confirm-font-color: #{$dark-confirm-font-color};
--danger-color: #{$dark-danger-color}; --danger-color: #{$dark-danger-color};
--warning-color: #{$dark-warning-color}; --warning-color: #{$dark-warning-color};
--info-color: #{$dark-info-color}; --info-color: #{$dark-info-color};

View file

@ -1,4 +1,36 @@
$fa-padding: 7px; $fa-padding: 7px;
$header-size: 1.2em; // Fonts
$font-size: 1.1em; $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,
)
);

View file

@ -4,6 +4,7 @@
<html> <html>
<head> <head>
<title>Newreader</title> <title>Newreader</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link type="image/png" href="{% static 'favicon.png' %}" rel="shortcut icon" /> <link type="image/png" href="{% static 'favicon.png' %}" rel="shortcut icon" />
{% block head %} {% block head %}
<link href="{% static 'css/main.css' %}" rel="stylesheet" /> <link href="{% static 'css/main.css' %}" rel="stylesheet" />
@ -11,22 +12,14 @@
</head> </head>
<body class="body"> <body class="body">
<input id="menu-input" type="checkbox" />
<nav class="nav"> <nav class="nav">
<ol> <div class="menu">
{% if request.user.is_authenticated %} <label class="menu__icon fas fa-bars" for="menu-input" />
<li class="nav__item"><a href="{% url 'index' %}">Home</a></li> </div>
<li class="nav__item"><a href="{% url 'news:core:categories' %}">Categories</a></li>
<li class="nav__item"><a href="{% url 'news:collection:rules' %}">Sources</a></li> {% include "components/nav-list/nav-list.html" with request=request only %}
<li class="nav__item"><a href="{% url 'accounts:settings:home' %}">Settings</a></li>
{% if request.user.is_superuser %}
<li class="nav__item"><a href="{% url 'admin:index' %}">Admin</a></li>
{% endif %}
<li class="nav__item"><a href="{% url 'accounts:logout' %}">Logout</a></li>
{% else %}
<li class="nav__item"><a href="{% url 'accounts:login' %}">Login</a></li>
<li class="nav__item"><a href="{% url 'accounts:register' %}">Register</a></li>
{% endif %}
</ol>
<i class="theme-switcher fas fa-adjust"></i> <i class="theme-switcher fas fa-adjust"></i>
</nav> </nav>

View file

@ -0,0 +1,17 @@
<ol class="nav-list">
{% if request.user.is_authenticated %}
<li class="nav-list__item"><a href="{% url 'index' %}">Home</a></li>
<li class="nav-list__item"><a href="{% url 'news:core:categories' %}">Categories</a></li>
<li class="nav-list__item"><a href="{% url 'news:collection:rules' %}">Sources</a></li>
<li class="nav-list__item"><a href="{% url 'accounts:settings:home' %}">Settings</a></li>
{% if request.user.is_superuser %}
<li class="nav-list__item"><a href="{% url 'admin:index' %}">Admin</a></li>
{% endif %}
<li class="nav-list__item"><a href="{% url 'accounts:logout' %}">Logout</a></li>
{% else %}
<li class="nav-list__item"><a href="{% url 'accounts:login' %}">Login</a></li>
<li class="nav-list__item"><a href="{% url 'accounts:register' %}">Register</a></li>
{% endif %}
</ol>

Some files were not shown because too many files have changed in this diff Show more