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,8 +1,9 @@
{% 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>
<div class="main__container">
<section class="section"> <section class="section">
{% include "components/header/header.html" with title="Integrations" only %} {% include "components/header/header.html" with title="Integrations" only %}
@ -41,5 +42,6 @@
</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>
<div class="main__container">
{% 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" %}
</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 %}
<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 %} {% 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,8 +1,9 @@
{% 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>
<div class="main__container">
<section class="section text-section"> <section class="section text-section">
{% if error %} {% if error %}
<h1 class="h1">{% trans "Reddit authorization failed" %}</h1> <h1 class="h1">{% trans "Reddit authorization failed" %}</h1>
@ -16,5 +17,6 @@
<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>
<div class="main__container">
{% 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" %}
</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,6 +91,9 @@ class App extends React.Component {
return ( return (
<> <>
{this.state.message && <Messages messages={[this.state.message]} />} {this.state.message && <Messages messages={[this.state.message]} />}
<Sidebar navLinks={this.props.navLinks} />
<div className="main__container">
<Card header={pageHeader} /> <Card header={pageHeader} />
{cards} {cards}
{selectedCategory && ( {selectedCategory && (
@ -99,6 +103,7 @@ class App extends React.Component {
handleDelete={this.deleteCategory} 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,6 +63,7 @@ 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__container">
<div className="post__header"> <div className="post__header">
<div className="post__actions"> <div className="post__actions">
<button <button
@ -83,11 +84,13 @@ class PostModal extends React.Component {
</div> </div>
<div className="post__heading"> <div className="post__heading">
<h2 className={titleClassName}>{`${post.title} `}</h2> <h2 className={titleClassName}>{`${post.title} `}</h2>
<div className="post__meta-info"> <div className="post__meta">
<span className="post__date"> <div className="post__text">
{publicationDate} {this.props.timezone} <span className="post__date">{publicationDate}</span>
</span>
{post.author && <span className="post__author">{post.author}</span>} {post.author && <span className="post__author">{post.author}</span>}
</div>
<div className="post__buttons">
{this.props.category && ( {this.props.category && (
<span className="badge post__category" title={this.props.category.name}> <span className="badge post__category" title={this.props.category.name}>
<a <a
@ -119,6 +122,8 @@ class PostModal extends React.Component {
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
{/* HTML is sanitized by the collectors */} {/* HTML is sanitized by the collectors */}
<div className="post__body" dangerouslySetInnerHTML={{ __html: post.body }} /> <div className="post__body" dangerouslySetInnerHTML={{ __html: post.body }} />

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 %}
<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" %} {% 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 %}
<main id="rule--page" class="main" data-render-sidebar=true>
<div class="main__container">
{% if feed.error %}
{% include "components/textbox/textbox.html" with title=title body=feed.error class="text-section--error" only %} {% include "components/textbox/textbox.html" with title=title body=feed.error class="text-section--error" only %}
{% endif %} {% endif %}
{% 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" %} {% 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 %}
<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" %} {% 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,8 +1,9 @@
{% 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>
<div class="main__container">
<form class="form rules-form"> <form class="form rules-form">
{% csrf_token %} {% csrf_token %}
@ -29,40 +30,68 @@
<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> </tr>
</thead> </thead>
<tbody class="table__body"> <tbody class="table__body rules-table__body">
{% for rule in rules %} {% for rule in rules %}
<tr class="table__row {% if rule.failed %}table__row--error{% endif %} rules-table__row"> <tr class="table__row {% if rule.failed %}table__row--error{% endif %} rules-table__row">
<td class="table__item rules-table__item"> <td class="table__item rules-table__item--select">
{% with rule|id_for_label:"rules" as id_for_label %} {% 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 %} {% include "components/form/checkbox.html" with name="rules" value=rule.pk id=id_for_label id_for_label=id_for_label %}
{% endwith %} {% endwith %}
</td> </td>
<td class="table__item rules-table__item" title="{{ rule.name }}">
<a class="link" href="{{ rule.update_url }}">{{ rule.name }}</a> <td
class="table__item rules-table__item rules-table__item--name"
title="{{ rule.name }}"
>
<a class="link" href="{{ rule.update_url }}">
{{ rule.name }}
</a>
</td> </td>
<td class="table__item rules-table__item" title="{{ rule.category.name }}">
<td
class="table__item rules-table__item rules-table__item--category"
title="{{ rule.category.name }}"
>
{% if rule.category %} {% if rule.category %}
<a class="link" href="{% url 'news:core:category-update' pk=rule.category.pk %}">{{ rule.category.name }}</a> <a
class="link"
href="{% url 'news:core:category-update' pk=rule.category.pk %}"
>
{{ rule.category.name }}
</a>
{% endif %} {% endif %}
</td> </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
class="table__item rules-table__item rules-table__item--url"
title="{{ rule.source_url }}"
>
<a
class="link"
href="{{ rule.source_url }}"
target="_blank"
rel="noopener noreferrer"
>
{{ rule.source_url }}
</a>
</td> </td>
<td class="table__item rules-table__item">
<td class="table__item rules-table__item rules-table__item--failed">
{% if rule.failed %} {% if rule.failed %}
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
{% else %} {% else %}
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
{% endif %} {% endif %}
</td> </td>
<td class="table__item rules-table__item">
<td class="table__item rules-table__item rules-table__item--enabled">
{% if rule.enabled %} {% if rule.enabled %}
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
{% else %} {% else %}
@ -81,7 +110,9 @@
<span class="pagination__previous"> <span class="pagination__previous">
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
<a class="link button" href="?page=1">{% trans "first" %}</a> <a class="link button" href="?page=1">{% trans "first" %}</a>
<a class="link button" href="?page={{ page_obj.previous_page_number }}">{% trans "previous" %}</a> <a class="link button" href="?page={{ page_obj.previous_page_number }}">
{% trans "previous" %}
</a>
{% endif %} {% endif %}
</span> </span>
@ -93,11 +124,17 @@
<span class="pagination__next"> <span class="pagination__next">
{% if page_obj.has_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.next_page_number }}">
<a class="link button" href="?page={{ page_obj.paginator.num_pages }}">{% trans "last" %}</a> {% trans "next" %}
</a>
<a class="link button" href="?page={{ page_obj.paginator.num_pages }}">
{% trans "last" %}
</a>
{% endif %} {% endif %}
</span> </span>
</div> </div>
</div> </div>
</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="subreddit--page" class="main">
{% url "news:collection:rules" as cancel_url %} {% url "news:collection:rules" as cancel_url %}
<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" %} {% 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 %}
<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" %} {% 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 %}
<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" %} {% 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 @@
@import '../../partials/variables';
.main { .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; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
margin: 20px 0; @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;
@media (max-width: $mobile-breakpoint) {
display: none;
}
} }
&__item { & .nav-list {
margin: 0px 10px; width: 80%;
& a { @media (max-width: $mobile-breakpoint) {
@extend .button; display: none;
font-size: 0.9em !important;
font-weight: 600;
}
} }
&__item:last-child { &__item:last-child {
margin: 0 10px 0 auto; margin: 0 10px 0 auto;
border-right: 2px solid var(--lighter-accent-color); 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;
flex-direction: row;
align-items: initial;
width: 100%;
margin: 20px 0 0 0;
background-color: initial; 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;
}
} }

View file

@ -1,11 +1,24 @@
#login--page { #login--page {
margin: 5% auto;
width: 50%;
& .form { & .form {
@extend .form; @extend .form;
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%; 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