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-registration-redux~=2.7',
'django-rest-framework',
'djangorestframework-camel-case',
'pymemcache',
'python-dotenv~=1.0.1',
'ftfy~=6.2',

View file

@ -1,8 +1,9 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% load i18n %}
{% 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">
{% include "components/header/header.html" with title="Integrations" only %}
@ -41,5 +42,6 @@
</div>
</div>
</section>
</div>
</main>
{% endblock %}

View file

@ -1,7 +1,9 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% 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" %}
</div>
</main>
{% endblock %}

View file

@ -1,8 +1,12 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% block content %}
<main id="password-change--page" class="main">
{% 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 %}
</div>
</main>
{% endblock %}

View file

@ -1,8 +1,9 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% load i18n %}
{% 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">
{% if error %}
<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>
</p>
</section>
</div>
</main>
{% endblock %}

View file

@ -1,7 +1,9 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% 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" %}
</div>
</main>
{% endblock %}

View file

@ -1,8 +1,10 @@
from django.contrib.auth import views as django_views
from django.urls import reverse_lazy
from newsreader.utils.views import NavListMixin
class LoginView(django_views.LoginView):
class LoginView(NavListMixin, django_views.LoginView):
template_name = "accounts/views/login.html"
success_url = reverse_lazy("index")

View file

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

View file

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

View file

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

View file

@ -4,9 +4,10 @@ from django.views.generic.edit import FormView, ModelFormMixin
from newsreader.accounts.forms import UserSettingsForm
from newsreader.accounts.models import User
from newsreader.utils.views import NavListMixin
class SettingsView(ModelFormMixin, FormView):
class SettingsView(NavListMixin, ModelFormMixin, FormView):
template_name = "accounts/views/settings.html"
success_url = reverse_lazy("accounts:settings:home")
form_class = UserSettingsForm

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_RESET_ON_SUCCESS = True
# TODO: verify parser works correctly
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication",
@ -235,7 +236,13 @@ REST_FRAMEWORK = {
"rest_framework.permissions.IsAuthenticated",
"newsreader.accounts.permissions.IsOwner",
),
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
"DEFAULT_RENDERER_CLASSES": (
"djangorestframework_camel_case.render.CamelCaseJSONRenderer",
),
"DEFAULT_PARSER_CLASSES": (
"djangorestframework_camel_case.parser.CamelCaseJSONParser",
),
}
SWAGGER_SETTINGS = {

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/categories/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 CategoryModal from './components/CategoryModal.js';
import Messages from '../../components/Messages.js';
import Sidebar from '../../components/Sidebar.js';
class App extends React.Component {
selectCategory = ::this.selectCategory;
@ -90,6 +91,9 @@ class App extends React.Component {
return (
<>
{this.state.message && <Messages messages={[this.state.message]} />}
<Sidebar navLinks={this.props.navLinks} />
<div className="main__container">
<Card header={pageHeader} />
{cards}
{selectedCategory && (
@ -99,6 +103,7 @@ class App extends React.Component {
handleDelete={this.deleteCategory}
/>
)}
</div>
</>
);
}

View file

@ -12,11 +12,15 @@ if (page) {
let createUrl = document.getElementById('createUrl').textContent;
let updateUrl = document.getElementById('updateUrl').textContent;
let linkScript = document.getElementById('Links');
let navLinks = JSON.parse(linkScript.textContent);
ReactDOM.render(
<App
categories={categories}
createUrl={createUrl.substring(1, createUrl.length - 2)}
updateUrl={updateUrl.substring(1, updateUrl.length - 4)}
navLinks={navLinks}
/>,
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 { fetchCategories } from './actions/categories';
import { filterPosts } from './components/postlist/filters.js';
import ScrollTop from './components/ScrollTop.js';
import Sidebar from './components/sidebar/Sidebar.js';
import HomepageSidebar from './components/sidebar/Sidebar.js';
import PostList from './components/postlist/PostList.js';
import PostModal from './components/PostModal.js';
import Messages from '../../components/Messages.js';
class App extends React.Component {
state = { postListNode: null }
constructor(props) {
super(props);
this.postListRef = node => {
this.setState({ postListNode: node });
};
}
componentDidMount() {
this.props.fetchCategories();
}
@ -19,11 +30,13 @@ class App extends React.Component {
render() {
return (
<>
<Sidebar />
<HomepageSidebar navLinks={this.props.navLinks} />
<PostList
feedUrl={this.props.feedUrl}
subredditUrl={this.props.subredditUrl}
timezone={this.props.timezone}
forwardedRef={this.postListRef}
postsByType={this.props.postsByType}
/>
{!isEqual(this.props.post, {}) && (
@ -40,7 +53,7 @@ class App extends React.Component {
/>
)}
<ScrollTop />
<ScrollTop postListNode={this.state.postListNode} />
{this.props.error && (
<Messages messages={[{ type: 'error', text: this.props.error.message }]} />
@ -52,6 +65,7 @@ class App extends React.Component {
const mapStateToProps = state => {
const { error } = state.error;
const postsByType = filterPosts(state)
if (!isEqual(state.selected.post, {})) {
const ruleId = state.selected.post.rule.id;
@ -65,10 +79,11 @@ const mapStateToProps = state => {
rule,
post: state.selected.post,
selectedType: state.selected.item.type,
postsByType: postsByType,
};
}
return { error, post: state.selected.post };
return { error, post: state.selected.post, postsByType: postsByType, };
};
const mapDispatchToProps = dispatch => ({

View file

@ -47,7 +47,7 @@ class PostModal extends React.Component {
const titleClassName = post.read ? 'post__title post__title--read' : 'post__title';
const readButtonDisabled =
post.read || this.props.isUpdating || this.props.selectedType === SAVED_TYPE;
const savedIconClass = post.saved ? 'saved-icon saved-icon--saved' : 'saved-icon';
const savedIconClass = post.saved ? 'post__save post__save--saved saved-icon saved-icon--saved' : 'post__save saved-icon';
let ruleUrl = '';
@ -63,6 +63,7 @@ class PostModal extends React.Component {
return (
<div className="modal post-modal">
<div className="post">
<div className="post__container">
<div className="post__header">
<div className="post__actions">
<button
@ -83,11 +84,13 @@ class PostModal extends React.Component {
</div>
<div className="post__heading">
<h2 className={titleClassName}>{`${post.title} `}</h2>
<div className="post__meta-info">
<span className="post__date">
{publicationDate} {this.props.timezone}
</span>
<div className="post__meta">
<div className="post__text">
<span className="post__date">{publicationDate}</span>
{post.author && <span className="post__author">{post.author}</span>}
</div>
<div className="post__buttons">
{this.props.category && (
<span className="badge post__category" title={this.props.category.name}>
<a
@ -119,6 +122,8 @@ class PostModal extends React.Component {
</div>
</div>
</div>
</div>
</div>
{/* HTML is sanitized by the collectors */}
<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 {
scrollListener = ::this.scrollListener;
state = { showTop: false, showBottom: false };
state = {
listenerAttached: false,
showTop: false,
showBottom: false
};
componentDidMount() {
window.addEventListener('scroll', this.scrollListener);
componentDidUpdate() {
if (this.props.postListNode && !this.state.listenerAttached) {
this.props.postListNode.addEventListener('scroll', this.scrollListener);
this.setState({ listenerAttached: true });
}
}
scrollListener() {
const showBottom = window.innerHeight + window.scrollY < document.body.offsetHeight;
const postList = this.props.postListNode;
const elementEnd = (
postList.scrollTop + postList.offsetHeight>= postList.scrollHeight
);
this.setState({
showTop: window.pageYOffset > 0 ? true : false,
showBottom: showBottom,
showTop: postList.scrollTop > window.innerHeight,
showBottom: !elementEnd,
});
}
render() {
return (
const postList = this.props.postListNode;
return postList && (
<div className="scroll-to-top">
{this.state.showTop && (
<i
className="scroll-to-top__icon scroll-to-top__icon--top"
onClick={() => window.scrollTo(0, 0)}
onClick={() => postList.scroll({ top: 0 })}
/>
)}
{this.state.showBottom && (
<i
className="scroll-to-top__icon scroll-to-top__icon--bottom"
onClick={() => window.scrollTo(0, document.body.scrollHeight)}
onClick={() => postList.scroll({ top: postList.scrollHeight })}
/>
)}
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,13 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% load static %}
{% block content %}
<main id="rule--page" class="main">
{% 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" %}
</div>
</main>
{% endblock %}

View file

@ -1,14 +1,17 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% load static i18n %}
{% block content %}
<main id="rule--page" class="main">
{% if feed.error %}
{% url "news:collection:rules" as cancel_url %}
{% 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 %}
{% 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" %}
<div>
</main>
{% endblock %}

View file

@ -1,9 +1,13 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% load static %}
{% block content %}
<main id="import--page" class="main">
{% 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" %}
</div>
</main>
{% endblock %}

View file

@ -1,8 +1,9 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% load i18n static filters %}
{% 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">
{% csrf_token %}
@ -29,40 +30,68 @@
<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" %}
</th>
<th class="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--url">{% trans "URL" %}</th>
<th class="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--name">{% trans "Name" %}</th>
<th class="table__heading rules-table__heading rules-table__heading--category">{% trans "Category" %}</th>
<th class="table__heading rules-table__heading rules-table__heading--url">{% trans "URL" %}</th>
<th class="table__heading rules-table__heading rules-table__heading--succeeded">{% trans "Successfuly ran" %}</th>
<th class="table__heading rules-table__heading rules-table__heading--enabled">{% trans "Enabled" %}</th>
</tr>
</thead>
<tbody class="table__body">
<tbody class="table__body rules-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">
<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>
<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 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 %}
<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 %}
</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 class="table__item rules-table__item">
<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">
<td class="table__item rules-table__item rules-table__item--enabled">
{% if rule.enabled %}
<i class="fas fa-check"></i>
{% else %}
@ -81,7 +110,9 @@
<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>
<a class="link button" href="?page={{ page_obj.previous_page_number }}">
{% trans "previous" %}
</a>
{% endif %}
</span>
@ -93,11 +124,17 @@
<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>
<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>
</main>
{% endblock %}

View file

@ -1,9 +1,13 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% load static %}
{% block content %}
<main id="subreddit--page" class="main">
{% 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" %}
</div>
</main>
{% 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.core.models import Category
from newsreader.utils.views import NavListMixin
class CollectionRuleViewMixin:
class CollectionRuleViewMixin(NavListMixin):
queryset = CollectionRule.objects.order_by("name")
def get_queryset(self):

View file

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

View file

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

View file

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

View file

@ -1,9 +1,13 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% load static %}
{% block content %}
<main id="category--page" class="main">
{% 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" %}
</div>
</main>
{% endblock %}

View file

@ -1,9 +1,13 @@
{% extends "base.html" %}
{% extends "sidebar.html" %}
{% load static %}
{% block content %}
<main id="category--page" class="main">
{% 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" %}
</div>
</main>
{% endblock %}

View file

@ -7,6 +7,7 @@
{% block scripts %}
{{ homepageSettings|json_script:"homepageSettings" }}
{{ sidebar_links|json_script:"Links" }}
{{ block.super }}
{% 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.core.forms import CategoryForm
from newsreader.news.core.models import Category
from newsreader.utils.views import NavListMixin
class NewsView(TemplateView):
class NewsView(NavListMixin, TemplateView):
template_name = "news/core/views/homepage.html"
def get_context_data(self, **kwargs):
@ -29,7 +30,7 @@ class NewsView(TemplateView):
}
class CategoryViewMixin:
class CategoryViewMixin(NavListMixin):
queryset = Category.objects.prefetch_related("rules").order_by("name")
def get_queryset(self):

View file

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

View file

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

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 {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 10px 0;
padding: 15px;
border: none;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@
position: absolute;
left: 6px;
width: 13px;
background-color: $lavendal-pink;
background-color: var(--font-color);
animation: loading-indicator 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite;
&:nth-child(1){

View file

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

View file

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

View file

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

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 {
display: flex;
justify-content: center;
@ -5,34 +8,43 @@
padding: 10px 0;
width: 100%;
height: map-deep-get($nav, height);
position: sticky;
top: 0;
background-color: var(--background-color);
background-color: var(--lightest-accent-color);
border-bottom: 2px var(--border-color) solid;
ol {
@media (max-width: $mobile-breakpoint) {
justify-content: space-between;
height: map-deep-get($nav, mobile, height);
padding: 10px;
font-size: map-deep-get($nav, mobile, font-size);
}
&__list {
display: flex;
justify-content: flex-start;
width: 80%;
list-style-type: none;
@media (max-width: $mobile-breakpoint) {
display: none;
}
}
&__item {
margin: 0px 10px;
& .nav-list {
width: 80%;
& a {
@extend .button;
font-size: 0.9em !important;
font-weight: 600;
}
@media (max-width: $mobile-breakpoint) {
display: none;
}
&__item:last-child {
margin: 0 10px 0 auto;
border-right: 2px solid var(--lighter-accent-color);
border-right: 2px solid var(--border-color);
}
}
}

View file

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

View file

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

View file

@ -7,11 +7,27 @@
&__date {
align-self: center;
@media (max-width: $mobile-breakpoint){
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
&__link, .saved-icon {
@media (max-width: $mobile-breakpoint){
display: none;
}
}
& .badge {
& .link {
color: var(--font-color);
color: inherit;
}
@media (max-width: $mobile-breakpoint){
display: none;
}
}
}

View file

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

View file

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

View file

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

View file

@ -1,31 +1,77 @@
@import '../../partials/variables';
@import '../../lib/functions';
.sidebar {
display: flex;
flex-direction: column;
align-items: center;
align-self: start;
display: none; // hide the sidebar by default, homepage enables it by default
position: sticky;
top: 50px;
--easeOutExpo: cubic-bezier(0.16, 1, 0.3, 1);
--duration: .6s;
width: 20%;
height: calc(100vh - map-deep-get($nav, height));
font-size: map-deep-get($nav, font-size);
@media (max-width: $mobile-breakpoint) {
display: grid;
grid-template-columns: [nav] 5fr [escape] 1fr;
height: calc(100vh - map-deep-get($nav, mobile, height));
font-size: map-deep-get($sidebar, mobile, font-size);
overflow: hidden auto;
overscroll-behavior: contain;
visibility: hidden;
transform: translateX(-110vw);
will-change: transform;
transition:
transform var(--duration) var(--easeOutExpo),
visibility 0s linear var(--duration);
}
&__nav {
width: 100%;
max-height: 80vh;
overflow: auto;
display: flex;
flex-direction: column;
overflow-y: scroll;
background-color: var(--background-color);
padding: 10px;
}
&__close {
display: none;
@media (max-width: $mobile-breakpoint) {
display: initial;
}
}
&__list {
list-style: none;
}
&__item {
@media (max-width: $mobile-breakpoint) {
margin: 10px 0;
}
}
&__container {
display: flex;
align-items: center;
border-bottom-right-radius: .25em;
border-top-right-radius: .25em;
padding: 5px;
@media (max-width: $mobile-breakpoint) {
padding: 25px 5px;
}
&--selected, &:hover {
background-color: var(--lighter-accent-color);
background-color: var(--selected-color);
}
}
@ -51,6 +97,60 @@
}
.read-button {
margin: 20px 0 0 0;
margin: 20px 0 0 10px;
width: max-content;
@media (max-width: $mobile-breakpoint) {
margin: auto 0 20px 10px;
font-size: inherit;
}
}
& .nav-list {
display: none;
&--bordered {
border-bottom: 2px var(--border-color) solid;
}
@media (max-width: $mobile-breakpoint) {
display: flex;
flex-direction: column;
width: 100%;
&__item {
margin: 0;
padding: 25px 15px;
border-bottom-right-radius: 0.25em;
border-bottom-left-radius: 0.25em;
& a {
color: inherit;
font-size: inherit;
align-items: initial;
justify-content: initial;
padding: 0;
&:before {
@include font-awesome;
content: "\f35d";
padding: 0 20px 0 0;
}
}
}
}
}
}
@media (max-width: $mobile-breakpoint) {
#menu-input:checked ~ * .sidebar {
visibility: visible;
transform: translateX(0);
transition: transform var(--duration) var(--easeOutExpo);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
.label {
@include text-padding;
padding: 10px 0;
}
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 {
display: flex;
flex-direction: row;
align-items: initial;
width: 100%;
margin: 20px 0 0 0;
background-color: initial;
display: grid;
@media (min-width: $mobile-breakpoint) {
grid: [stack] 1fr/20% [stack] auto; // TODO: remove this line?
}
@media (min-width: $hd-breakpoint) {
grid: [stack] 1fr/15% [stack] auto;
}
@media (min-width: $wqhd-breakpoint) {
grid: [stack] 1fr/12% [stack] auto;
}
@media (min-width: $uhd-breakpoint) {
grid: [stack] 1fr/10% [stack] auto;
}
& .sidebar {
display: grid;
grid-template-columns: [nav] 100% [escape] 0;
}
}

View file

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

View file

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

View file

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

View file

@ -1,58 +1,64 @@
$orange: rgba(255, 212, 153, 1);
$green: rgba(89, 181, 128, 1);
$red: lighten(rgba(231, 76, 60, 1), 10%);
$gray: rgba(227, 227, 227, 1);
$blue: rgba(111, 164, 196, 1);
$orange: #ff2a51;
$green: #007936;
$red: #d30038;
$blue: #0085f2;
$white: rgba(255, 255, 255, 1);
$black: rgba(0, 0, 0, 1);
$dark: rgba(0, 0, 0, 0.4);
$reddit-orange: rgba(255, 69, 0, 1);
$white: #fff;
$black: #000;
$transparant-red: transparentize($red, 0.8);
$transparant-blue: transparentize($blue, 0.8);
$transparant-orange: transparentize($orange, 0.4);
$transparant-green: transparentize($green, 0.4);
$azureish-white: rgba(205, 230, 245, 1);
$gainsboro: rgba(238, 238, 238, 1);
$nickel: rgba(112, 112, 120, 1);
$lavendal-pink: rgba(162, 155, 254, 1);
$focus-blue: darken($azureish-white, +10%);
$checkbox-blue: rgba(34, 170, 253, 1);
// White theme
$background-color: rgba(255, 249, 176, 1);
$background-color: $white;
$background-color-secondary: #f9f9fb;
$font-color: rgba(83, 87, 91, 1);
$link-color: rgba(45, 142, 202, 1);
$read-color: darken($gainsboro, 10%);
$confirm-button-font-color: rgba(255, 255, 255, 1);
$font-color: #1b1b1b;
$accent-color: rgba(255, 171, 115, 1);
$lighter-accent-color: rgba(255, 211, 132, 1);
$lightest-accent-color: rgba(255, 174, 192, 1);
$link-color: #0069c2;
$selected-color: #0085f230;
$read-color: darken($font-color, 10%);
$confirm-color: rgba(117, 207, 184, 1);
$danger-color: rgba(237, 118, 105, 1);
$warning-color: rgba(255, 218, 119, 1);
$info-color: rgba(162, 213, 242, 1);
$confirm-color: $green;
$confirm-font-color: $white;
$danger-color: $red;
$danger-font-color: $white;
$warning-color: $orange;
$warning-font-color: $white;
$info-color: $blue;
$info-font-color: $white;
$sidebar-background-color: $background-color-secondary;
$border-color: #cdcdcd;
// Dark theme
$dark-background-color: rgba(29, 45, 80, 1);
$dark-background-color: #1b1b1b;
$dark-background-color-secondary: #313131;
$dark-font-color: #cccccc;
$dark-font-color: darken($gray, 10%);
$dark-link-color: $link-color;
$dark-read-color: darken($dark-font-color, 20%);
$dark-confirm-button-font-color: $dark-font-color;
$dark-read-color: darken($dark-font-color, 5%);
$dark-accent-color: rgba(19, 59, 92, 1);
$dark-lighter-accent-color: rgba(30, 95, 116, 1);
$dark-lightest-accent-color: rgba(88, 61, 114, 1);
$dark-confirm-color: $green;
$dark-confirm-font-color: $white;
$dark-confirm-color: rgba(0, 121, 101, 1);
$dark-danger-color: rgba(175, 45, 45, 1);
$dark-warning-color: rgba(238, 187, 77, 1);
$dark-info-color: rgba(31, 111, 139, 1);
$dark-danger-color: $red;
$dark-danger-font-color: $white;
$dark-warning-color: $orange;
$dark-warning-font-color: $white;
$dark-info-color: $blue;
$dark-info-font-color: $white;
$dark-sidebar-background-color: $dark-background-color-secondary;
// Third party
$reddit-orange: rgba(255, 69, 0, 1);

View file

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

View file

@ -1,33 +1,37 @@
:root {
--background-color: #{$background-color};
--background-color-secondary: #{$background-color-secondary};
--font-color: #{$font-color};
--link-color: #{$link-color};
--selected-color: #{$selected-color};
--read-color: #{$read-color};
--confirm-button-font-color: #{$confirm-button-font-color};
--accent-color: #{$accent-color};
--lighter-accent-color: #{$lighter-accent-color};
--lightest-accent-color: #{$lightest-accent-color};
--confirm-color: #{$confirm-color};
--confirm-font-color: #{$confirm-font-color};
--danger-color: #{$danger-color};
--danger-font-color: #{$danger-color};
--warning-color: #{$warning-color};
--warning-font-color: #{$warning-color};
--info-color: #{$info-color};
--info-font-color: #{$info-color};
--border-color: #{$border-color};
&.dark-theme {
--background-color: #{$dark-background-color};
--background-color-secondary: #{$dark-background-color-secondary};
--font-color: #{$dark-font-color};
--link-color: #{$dark-link-color};
--read-color: #{$dark-read-color};
--confirm-button-font-color: #{$dark-confirm-button-font-color};
--accent-color: #{$dark-accent-color};
--lighter-accent-color: #{$dark-lighter-accent-color};
--lightest-accent-color: #{$dark-lightest-accent-color};
--confirm-color: #{$dark-confirm-color};
--confirm-font-color: #{$dark-confirm-font-color};
--danger-color: #{$dark-danger-color};
--warning-color: #{$dark-warning-color};
--info-color: #{$dark-info-color};

View file

@ -1,4 +1,36 @@
$fa-padding: 7px;
$header-size: 1.2em;
// Fonts
$font-size: 1.1em;
$font-size-small: 0.833em;
// Dimensions
$mobile-breakpoint: 540px;
$tablet-breakpoint: 1280px;
$hd-breakpoint: 1920px;
$wqhd-breakpoint: 2560px;
$uhd-breakpoint: 3840px;
$nav: (
height: 50px,
font-size: $font-size-small,
mobile: (
height: 75px,
font-size: 1.5em
)
);
// Post
$post: (
header-font-size: 1.2em
);
// Sidebar
$sidebar: (
font-size: $font-size,
mobile: (
font-size: 1.2em,
)
);

View file

@ -4,6 +4,7 @@
<html>
<head>
<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" />
{% block head %}
<link href="{% static 'css/main.css' %}" rel="stylesheet" />
@ -11,22 +12,14 @@
</head>
<body class="body">
<input id="menu-input" type="checkbox" />
<nav class="nav">
<ol>
{% if request.user.is_authenticated %}
<li class="nav__item"><a href="{% url 'index' %}">Home</a></li>
<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>
<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>
<div class="menu">
<label class="menu__icon fas fa-bars" for="menu-input" />
</div>
{% include "components/nav-list/nav-list.html" with request=request only %}
<i class="theme-switcher fas fa-adjust"></i>
</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