Merge pull request #206 from mouse-reeve/ui-overhaul

Ui overhaul
This commit is contained in:
Mouse Reeve 2020-09-30 17:15:22 -07:00 committed by GitHub
commit 4fda5c8e22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1140 additions and 1755 deletions

View file

@ -57,7 +57,7 @@ def broadcast_task(sender_id, activity, recipients):
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
# TODO: maybe keep track of users who cause errors # TODO: maybe keep track of users who cause errors
errors.append({ errors.append({
'error': e, 'error': str(e),
'recipient': recipient, 'recipient': recipient,
'activity': activity, 'activity': activity,
}) })

View file

@ -1,13 +1,33 @@
''' using django model forms ''' ''' using django model forms '''
import datetime import datetime
from collections import defaultdict
from django.forms import ModelForm, PasswordInput, widgets
from django import forms from django import forms
from django.forms import ModelForm, PasswordInput, widgets
from django.forms.widgets import Textarea
from bookwyrm import models from bookwyrm import models
class LoginForm(ModelForm): class CustomForm(ModelForm):
''' add css classes to the forms '''
def __init__(self, *args, **kwargs):
css_classes = defaultdict(lambda: '')
css_classes['text'] = 'input'
css_classes['password'] = 'input'
css_classes['email'] = 'input'
css_classes['number'] = 'input'
css_classes['checkbox'] = 'checkbox'
css_classes['textarea'] = 'textarea'
super(CustomForm, self).__init__(*args, **kwargs)
for visible in self.visible_fields():
if hasattr(visible.field.widget, 'input_type'):
input_type = visible.field.widget.input_type
if isinstance(visible.field.widget, Textarea):
input_type = 'textarea'
visible.field.widget.attrs['class'] = css_classes[input_type]
class LoginForm(CustomForm):
class Meta: class Meta:
model = models.User model = models.User
fields = ['username', 'password'] fields = ['username', 'password']
@ -17,7 +37,7 @@ class LoginForm(ModelForm):
} }
class RegisterForm(ModelForm): class RegisterForm(CustomForm):
class Meta: class Meta:
model = models.User model = models.User
fields = ['username', 'email', 'password'] fields = ['username', 'email', 'password']
@ -27,13 +47,13 @@ class RegisterForm(ModelForm):
} }
class RatingForm(ModelForm): class RatingForm(CustomForm):
class Meta: class Meta:
model = models.Review model = models.Review
fields = ['rating'] fields = ['rating']
class ReviewForm(ModelForm): class ReviewForm(CustomForm):
class Meta: class Meta:
model = models.Review model = models.Review
fields = ['name', 'content'] fields = ['name', 'content']
@ -44,7 +64,7 @@ class ReviewForm(ModelForm):
} }
class CommentForm(ModelForm): class CommentForm(CustomForm):
class Meta: class Meta:
model = models.Comment model = models.Comment
fields = ['content'] fields = ['content']
@ -54,7 +74,7 @@ class CommentForm(ModelForm):
} }
class QuotationForm(ModelForm): class QuotationForm(CustomForm):
class Meta: class Meta:
model = models.Quotation model = models.Quotation
fields = ['quote', 'content'] fields = ['quote', 'content']
@ -65,7 +85,7 @@ class QuotationForm(ModelForm):
} }
class ReplyForm(ModelForm): class ReplyForm(CustomForm):
class Meta: class Meta:
model = models.Status model = models.Status
fields = ['content'] fields = ['content']
@ -73,14 +93,14 @@ class ReplyForm(ModelForm):
labels = {'content': 'Comment'} labels = {'content': 'Comment'}
class EditUserForm(ModelForm): class EditUserForm(CustomForm):
class Meta: class Meta:
model = models.User model = models.User
fields = ['avatar', 'name', 'summary', 'manually_approves_followers'] fields = ['avatar', 'name', 'summary', 'manually_approves_followers']
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
class TagForm(ModelForm): class TagForm(CustomForm):
class Meta: class Meta:
model = models.Tag model = models.Tag
fields = ['name'] fields = ['name']
@ -88,17 +108,18 @@ class TagForm(ModelForm):
labels = {'name': 'Add a tag'} labels = {'name': 'Add a tag'}
class CoverForm(ModelForm): class CoverForm(CustomForm):
class Meta: class Meta:
model = models.Book model = models.Book
fields = ['cover'] fields = ['cover']
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
class EditionForm(ModelForm): class EditionForm(CustomForm):
class Meta: class Meta:
model = models.Edition model = models.Edition
exclude = [ exclude = [
'remote_id',
'created_date', 'created_date',
'updated_date', 'updated_date',
'last_sync_date', 'last_sync_date',
@ -135,7 +156,7 @@ class ExpiryWidget(widgets.Select):
return datetime.datetime.now() + interval return datetime.datetime.now() + interval
class CreateInviteForm(ModelForm): class CreateInviteForm(CustomForm):
class Meta: class Meta:
model = models.SiteInvite model = models.SiteInvite
exclude = ['code', 'user', 'times_used'] exclude = ['code', 'user', 'times_used']

View file

@ -91,6 +91,11 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
to write this so it's just a property ''' to write this so it's just a property '''
return cls.objects.filter(reply_parent=status).select_subclasses() return cls.objects.filter(reply_parent=status).select_subclasses()
@property
def status_type(self):
''' expose the type of status for the ui using activity type '''
return self.activity_serializer.__name__
def to_replies(self, **kwargs): def to_replies(self, **kwargs):
''' helper function for loading AP serialized replies to a status ''' ''' helper function for loading AP serialized replies to a status '''
return self.to_ordered_collection( return self.to_ordered_collection(
@ -211,7 +216,7 @@ class Boost(Status):
ActivityMapping('object', 'boosted_status'), ActivityMapping('object', 'boosted_status'),
] ]
activity_serializer = activitypub.Like activity_serializer = activitypub.Boost
# This constraint can't work as it would cross tables. # This constraint can't work as it would cross tables.
# class Meta: # class Meta:

View file

@ -43,17 +43,20 @@ def handle_account_search(query):
except models.User.DoesNotExist: except models.User.DoesNotExist:
url = 'https://%s/.well-known/webfinger?resource=acct:%s' % \ url = 'https://%s/.well-known/webfinger?resource=acct:%s' % \
(domain, query) (domain, query)
try:
response = requests.get(url) response = requests.get(url)
except requests.exceptions.ConnectionError:
return None
if not response.ok: if not response.ok:
response.raise_for_status() return None
data = response.json() data = response.json()
for link in data['links']: for link in data['links']:
if link['rel'] == 'self': if link['rel'] == 'self':
try: try:
user = get_or_create_remote_user(link['href']) user = get_or_create_remote_user(link['href'])
except KeyError: except KeyError:
return HttpResponseNotFound() return None
return user return [user]
def handle_follow(user, to_follow): def handle_follow(user, to_follow):

View file

@ -44,8 +44,7 @@ def make_signature(sender, destination, date, digest):
def make_digest(data): def make_digest(data):
''' creates a message digest for signing ''' ''' creates a message digest for signing '''
return 'SHA-256=' + b64encode(hashlib.sha256(data.encode('utf-8'))\ return 'SHA-256=' + b64encode(hashlib.sha256(data).digest()).decode('utf-8')
.digest()).decode('utf-8')
def verify_digest(request): def verify_digest(request):

File diff suppressed because one or more lines are too long

1
bookwyrm/static/css/bulma.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,115 @@
/* --- ICONS --- */
/* --- TOGGLES --- */
input.toggle-control {
display: none;
}
.hidden {
display: none;
}
input.toggle-control:checked ~ .toggle-content {
display: block;
}
/* --- STARS --- */
.rate-stars button.icon {
background: none;
border: none;
padding: 0;
margin: 0;
display: inline;
}
.rate-stars:hover .icon:before {
content: '\e9d9';
}
.rate-stars form:hover ~ form .icon:before{
content: '\e9d7';
}
/* stars in a review form */
.form-rate-stars:hover .icon:before {
content: '\e9d9';
}
.form-rate-stars input + .icon:before {
content: '\e9d9';
}
.form-rate-stars input:checked + .icon:before {
content: '\e9d9';
}
.form-rate-stars input:checked + * ~ .icon:before {
content: '\e9d7';
}
.form-rate-stars:hover label.icon:before {
content: '\e9d9';
}
.form-rate-stars label.icon:hover:before {
content: '\e9d9';
}
.form-rate-stars label.icon:hover ~ label.icon:before{
content: '\e9d7';
}
/* --- BOOK COVERS --- */
.cover-container {
height: 250px;
width: max-content;
}
.cover-container.is-medium {
height: 150px;
}
.cover-container.is-medium .no-cover div {
font-size: 0.9em;
padding: 0.3em;
}
.cover-container.is-small {
height: 100px;
}
.cover-container.is-small .no-cover div {
font-size: 0.7em;
padding: 0.1em;
}
.book-cover {
height: 100%;
object-fit: scale-down;
}
.no-cover {
position: relative;
white-space: normal;
}
.no-cover div {
position: absolute;
padding: 1em;
color: white;
top: 0;
left: 0;
text-align: center;
}
/* --- AVATAR --- */
.avatar {
vertical-align: middle;
display: inline;
}
/* --- QUOTES --- */
.quote blockquote {
position: relative;
padding-left: 2em;
}
.quote blockquote:before, .quote blockquote:after {
font-family: 'icomoon';
position: absolute;
}
.quote blockquote:before {
content: "\e904";
top: 0;
left: 0;
}
.quote blockquote:after {
content: "\e903";
right: 0;
}

View file

@ -1,822 +0,0 @@
/* some colors that are okay: #247BA0 #70C1B2 #B2DBBF #F3FFBD #FF1654 */
/* general override */
* {
margin: 0;
padding: 0;
line-height: 1.3em;
font-family: sans-serif;
}
html {
background-color: #FFF;
color: black;
}
a {
color: #247BA0;
}
h1 {
font-weight: normal;
font-size: 1.5rem;
}
h2 {
font-weight: normal;
font-size: 1rem;
padding: 0.5rem 0.2rem;
margin-bottom: 1rem;
border-bottom: 3px solid #B2DBBF;
}
h2 .edit-link {
text-decoration: none;
font-size: 0.9em;
float: right;
}
h2 .edit-link .icon {
font-size: 1.2em;
}
h3 {
font-size: 1rem;
font-weight: bold;
margin-bottom: 0.5em;
}
h3 small {
font-weight: normal;
}
section {
margin-bottom: 1em;
}
/* fixed display top bar */
body {
padding-top: 90px;
}
#top-bar {
overflow: visible;
padding: 0.5rem;
border-bottom: 3px solid #247BA0;
margin-bottom: 1em;
width: 100%;
background-color: #FFF;
position: fixed;
top: 0;
height: 47px;
z-index: 2;
}
/* --- header bar content */
#branding {
flex-grow: 0;
}
#menu {
list-style: none;
text-align: center;
margin-top: 1.5rem;
flex-grow: 2;
font-size: 0.9em;
}
#menu li {
display: inline-block;
padding: 0 0.5em;
text-transform: uppercase;
}
#menu a {
color: #555;
text-decoration: none;
font-size: 0.9em;
}
#actions {
margin-top: 1em;
}
#actions > * {
display: inline-block;
}
#actions > *:last-child {
margin-left: 0.5em;
}
#notifications .icon {
font-size: 1.1rem;
}
#notifications a {
color: black;
text-decoration: none;
position: relative;
top: 0.2rem;
}
#notifications .count {
background-color: #FF1654;
color: white;
font-size: 0.85rem;
border-radius: 50%;
display: block;
position: absolute;
text-align: center;
top: -0.65rem;
right: -0.5rem;
height: 1rem;
width: 1rem;
}
.notification {
margin-bottom: 1em;
padding: 1em 0;
background-color: #EEE;
}
.notification.unread {
background-color: #DDD;
}
#search button {
border: none;
background: none;
}
#main, header {
margin: 0 auto;
max-width: 55rem;
padding-right: 1em;
}
/* pulldown */
.pulldown-container {
position: relative;
display: inline;
}
.pulldown {
display: none;
position: absolute;
list-style: none;
background: white;
padding: 1em;
right: 0;
font-size: 0.9rem;
box-shadow: 0 5px 10px rgba(0,0,0,0.15);
width: max-content;
text-align: left;
z-index: 1;
}
.pulldown-container:hover .pulldown {
display: block;
}
.pulldown li a {
display: block;
margin-bottom: 0.5em;
text-decoration: none;
padding: 0.3em 0.8em
}
div.pulldown-button {
background-color: #eee;
border-radius: 0.3em;
color: #247BA0;
width: max-content;
margin: 0 auto;
white-space: nowrap;
}
.post div.pulldown-button {
border: 2px solid #247BA0;
}
.pulldown-button form {
display: inline;
}
div.pulldown-button button {
display: inline;
border: none;
border-radius: 0;
background-color: inherit;
color: #247BA0;
}
div.pulldown-button .pulldown-toggle {
padding-right: 0;
padding-left: 0;
position: relative;
left: -0.5em;
}
ul.pulldown button {
display: block;
text-align: left;
width: 100%;
border: none;
border-radius: 0;
background-color: white;
color: #247BA0;
}
.pulldown button[disabled] {
color: #aaa;
}
.pulldown button[disabled]:hover {
background-color: white;
}
.pulldown button:hover, .pulldown li:hover {
background-color: #ddd;
}
/* content area */
.content-container {
margin: 1rem;
}
.content-container > * {
padding-left: 1em;
padding-right: 1em;
}
#feed {
display: flex;
flex-direction: column;
padding-top: 70px;
position: relative;
z-index: 0;
margin-top: -2em;
}
/* row component */
.row {
display: flex;
flex-direction: row;
}
.row > * {
flex-grow: 1;
width: min-content;
margin-right: 1em;
}
.row > *:last-child {
margin-right: 0;
}
.row.shrink > * {
flex-grow: 0;
width: max-content;
}
.row.wrap {
flex-wrap: wrap;
}
.column {
display: flex;
flex-direction: column;
}
.column > * {
margin-bottom: 1em;
}
/* discover books page grid of covers */
.book-grid .book-cover {
height: 176px;
width: auto;
margin: 0 auto;
}
.book-grid .no-cover {
width: 115px;
}
.book-grid > * {
margin-bottom: 2em;
}
/* special case forms */
.review-form label {
display: block;
}
.review-form textarea {
width: 30rem;
height: 10rem;
}
.review-form.quote-form textarea#id_content {
height: 4rem;
}
.follow-requests .row {
margin-bottom: 0.5em;
}
.follow-requests .row > *:first-child {
width: 20em;
}
.login form {
margin-top: 1em;
}
.login form p {
display: flex;
flex-direction: row;
padding: 0.5em 0;
}
.login form label {
width: 0;
flex-grow: 1;
display: inline-block;
}
.book-form textarea {
display: block;
width: 100%;
font-size: 0.9em;
}
.book-form label {
display: inline-block;
width: 8rem;
vertical-align: top;
}
.book-form .row label {
width: max-content;
}
/* general form stuff */
input, button {
padding: 0.2em 0.5em;
}
button, input[type="submit"] {
cursor: pointer;
width: max-content;
}
.content-container button {
border: none;
background-color: #247BA0;
color: white;
padding: 0.3em 0.8em;
font-size: 0.9em;
border-radius: 0.3em;
}
button.secondary {
background-color: #EEE;
color: #247BA0;
}
.post button.secondary {
border: 2px solid #247BA0;
}
button.warning {
background-color: #FF1654;
}
form input {
flex-grow: 1;
}
form div {
margin-bottom: 1em;
}
textarea {
padding: 0.5em;
}
/* icons */
a .icon {
color: black;
text-decoration: none;
}
button .icon {
font-size: 1.1rem;
vertical-align: sub;
}
.hidden-text {
height: 0;
width: 0;
position: absolute;
overflow: hidden;
}
/* star ratings */
.stars {
letter-spacing: -0.15em;
display: inline-block;
}
.rate-stars .icon {
cursor: pointer;
color: goldenrod;
}
.rate-stars label.icon {
color: black;
}
.rate-stars form {
display: inline;
width: min-content;
}
.rate-stars button.icon {
background: none;
border: none;
padding: 0;
margin: 0;
display: inline;
}
.cover-container .stars {
display: block;
text-align: center;
}
.rate-stars:hover .icon:before {
content: '\e9d9';
}
.rate-stars form:hover ~ form .icon:before{
content: '\e9d7';
}
.review-form .rate-stars:hover .icon:before {
content: '\e9d9';
}
.review-form .rate-stars label {
display: inline;
}
.review-form .rate-stars input + .icon:before {
content: '\e9d9';
}
.review-form .rate-stars input:checked + .icon:before {
content: '\e9d9';
}
.review-form .rate-stars input:checked + * ~ .icon:before {
content: '\e9d7';
}
.review-form .rate-stars:hover label.icon:before {
content: '\e9d9';
}
.review-form .rate-stars label.icon:hover:before {
content: '\e9d9';
}
.review-form .rate-stars label.icon:hover ~ label.icon:before{
content: '\e9d7';
}
.review-form .rate-stars input[type="radio"] {
display: none;
}
/* re-usable tab styles */
.tabs {
display: flex;
flex-direction: row;
border-bottom: 3px solid #FF1654;
padding-left: 1em;
}
.tabs.secondary {
border-bottom: 3px solid #247BA0;
}
.tab {
padding: 0.5em 1em;
border-radius: 0.25em 0.25em 0 0;
}
.secondary .tab {
padding: 0.25em 0.5em;
}
.tabs .tab.active {
background-color: #FF1654;
}
.tabs.secondary .tab.active {
background-color: #247BA0;
}
.tab.active a {
color: black;
}
.user-pic {
width: 2em;
height: 2em;
border-radius: 50%;
vertical-align: top;
position: relative;
bottom: 0.35em;
}
.user-pic.large {
width: 5em;
height: 5em;
}
.user-profile .row > * {
flex-grow: 0;
}
.user-profile .row > *:last-child {
flex-grow: 1;
margin-left: 2em;
}
/* general book display */
.book-preview {
overflow: hidden;
z-index: 1;
text-align: center;
}
.book-preview.grid {
float: left;
}
.cover-container {
flex-grow: 0;
}
.cover-container button {
display: block;
margin: 0 auto;
}
.book-cover {
width: 180px;
height: auto;
}
.book-cover.small {
width: 50px;
height: auto;
}
.no-cover {
position: relative;
}
.no-cover div {
position: absolute;
padding: 1em;
color: white;
top: 0;
left: 0;
text-align: center;
}
.no-cover .title {
text-transform: uppercase;
margin-bottom: 1em;
}
dl {
font-size: 0.9em;
margin-top: 0.5em;
}
dt {
float: left;
margin-right: 0.5em;
}
dd {
margin-bottom: 0.25em;
}
.all-shelves {
display: flex;
flex-direction: row;
margin-left: 0;
position: relative;
z-index: 1;
overflow-y: auto;
}
.all-shelves h2 {
white-space: nowrap;
}
.all-shelves > div {
flex-grow: 0;
}
.all-shelves > div:last-child {
padding-right: 0;
flex-grow: 1;
}
.all-shelves > div > * {
padding: 0;
}
.all-shelves > div:first-child > * {
padding-left: 1em;
}
.covers-shelf {
display: flex;
flex-direction: row;
}
.covers-shelf .cover-container {
margin-right: 1em;
font-size: 0.9em;
overflow: unset;
width: min-content;
}
.covers-shelf .cover-container:last-child {
margin-right: 0;
}
.covers-shelf .book-cover:hover {
cursor: pointer;
box-shadow: #F3FFBD 0em 0em 1em 1em;
}
.covers-shelf .book-cover {
height: 11rem;
width: auto;
margin: 0;
}
.close {
float: right;
cursor: pointer;
padding: 1rem;
}
.all-shelves input[type='radio'] {
display: none;
}
.compose-popout input[type="radio"] {
display: none;
}
.compose-suggestion {
display: none;
box-shadow: 0 5px 10px rgba(0,0,0,0.15);
padding-bottom: 1em;
margin-top: 2em;
}
input:checked ~ .compose-suggestion {
display: block;
}
.compose .book-preview {
background-color: #EEE;
padding: 1em;
}
.compose button {
margin: 0;
}
.compose .stars {
text-align: left;
}
.tag {
display: inline-block;
padding: 0.2em;
border-radius: 0.2em;
background-color: #EEE;
}
.tag form {
display: inline;
}
.tag a {
text-decoration: none;
}
blockquote {
white-space: pre-line;
}
blockquote .icon-quote-open, blockquote .icon-quote-close, .quote blockquote:before, .quote blockquote:after {
font-size: 2rem;
margin-right: 0.5rem;
color: #888;
}
blockquote .icon-quote-open {
float: left;
}
.quote {
margin-bottom: 2em;
position: relative;
}
.quote blockquote {
background-color: white;
margin: 1em;
padding: 1em;
}
.quote blockquote:before, .quote blockquote:after {
font-family: 'icomoon';
position: absolute;
}
.quote blockquote:before {
content: "\e904";
top: 0;
left: 0;
}
.quote blockquote:after {
content: "\e903";
bottom: 1em;
right: 0;
}
.interaction {
background-color: #B2DBBF;
border-radius: 0 0 0.5em 0.5em;
display: flex;
flex-direction: row;
padding: 0.5em;
}
.interaction > * {
margin-right: 0.5em;
}
.interaction button:hover {
box-shadow: #247BA0 0em 0em 1em 0em;
color: #247BA0;
}
.interaction button {
background: white;
height: 2em;
min-width: 3em;
padding: 0;
color: #888;
}
.interaction .active button .icon {
color: #FF1654;
}
.interaction textarea {
height: 2em;
width: 23em;
float: left;
padding: 0.25em;
margin-right: 0.5em;
}
.interaction textarea:valid, .interaction textarea:focus {
height: 4em;
}
.hidden {
display: none;
}
table {
border-collapse: collapse;
margin: 1em;
}
tr {
vertical-align: top;
}
tr:nth-child(even) {
background-color: #EEE;
}
th {
font-weight: bold;
}
th, td {
padding: 1em;
text-align: left;
}
.errorlist {
list-style: none;
font-size: 0.8em;
color: #FF1654;
}
/* status css */
.time-ago {
float: right;
display: block;
text-align: right;
}
.post {
background-color: #EFEFEF;
padding-top: 1em;
padding-bottom: 1em;
}
.post h2, .compose-suggestion h2 {
position: relative;
right: 2em;
border: none;
}
.post .time-ago {
position: relative;
left: 2em;
}
.post .user-pic, .compose-suggestion .user-pic {
right: 0.25em;
}
.post h2 .subhead {
display: block;
margin-left: 2em;
}
.post .subhead .time-ago {
display: none;
}
/* status page with replies */
.comment-thread .reply h2 {
background: none;
}
.comment-thread .post {
margin-left: 4em;
border-left: 2px solid #247BA0;
}
.comment-thread .post.depth-1 {
margin-left: 0;
border: none;
}
.comment-thread .post.depth-2 {
margin-left: 1em;
}
.comment-thread .post.depth-3 {
margin-left: 2em;
}
.comment-thread .post.depth-4 {
margin-left: 3em;
}
/* pagination */
.pagination a {
text-decoration: none;
}
.pagination .next {
text-align: right;
}
/* special one-off "delete all data" banner */
#warning {
background-color: #FF1654;
text-align: center;
}

View file

@ -32,29 +32,19 @@ function rate_stars(e) {
} }
function tabChange(e) { function tabChange(e) {
e.preventDefault(); var target = e.target.closest('li')
var target = e.target.parentElement;
var identifier = target.getAttribute('data-id'); var identifier = target.getAttribute('data-id');
var options_class = target.getAttribute('data-category');
var options = document.getElementsByClassName(options_class);
for (var i = 0; i < options.length; i++) {
if (!options[i].className.includes('hidden')) {
options[i].className += ' hidden';
}
}
var tabs = target.parentElement.children; var tabs = target.parentElement.children;
for (i = 0; i < tabs.length; i++) { for (i = 0; i < tabs.length; i++) {
if (tabs[i].getAttribute('data-id') == identifier) { if (tabs[i].getAttribute('data-id') == identifier) {
tabs[i].className += ' active'; tabs[i].className += ' is-active';
} else { } else {
tabs[i].className = tabs[i].className.replace('active', ''); tabs[i].className = tabs[i].className.replace('is-active', '');
} }
} }
var el = document.getElementById(identifier); var el = document.getElementById(identifier);
el.className = el.className.replace('hidden', '');
} }
function ajaxPost(form) { function ajaxPost(form) {

View file

@ -1,21 +1,20 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<div class="content-container">
<h2>About {{ site_settings.name }}</h2> <div class="columns">
<div class="column block">
<h2 class="title">About {{ site_settings.name }}</h2>
<p> <p>
{{ site_settings.instance_description }} {{ site_settings.instance_description }}
</p> </p>
</div>
<p> <div class="column block">
<small> <h2 class="title">Code of Conduct</h2>
<a href="/login/">Login or Create an Account</a>
</small>
</p>
<h2>Code of Conduct</h2>
<p> <p>
{{ site_settings.code_of_conduct }} {{ site_settings.code_of_conduct }}
</p> </p>
</div> </div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,28 +1,19 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load fr_display %} {% load fr_display %}
{% block content %} {% block content %}
<div class="content-container"> <div class="block">
<h2>{{ author.display_name }}</h2> <h2 class="title">{{ author.display_name }}</h2>
{% if author.bio %} {% if author.bio %}
<p> <p>
{{ author.bio | author_bio }} {{ author.bio }}
</p> </p>
{% endif %} {% endif %}
</div> </div>
<div class="content-container"> <div class="block">
<h2>Books by {{ author.display_name }}</h2> <h3 class="title is-4">Books by {{ author.display_name }}</h3>
<div class="book-grid row shrink wrap"> {% include 'snippets/book_tiles.html' with books=books %}
{% for book in books %}
<div class="book-preview">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/shelve_button.html' with book=book %}
</div>
{% endfor %}
</div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,22 +1,27 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load fr_display %} {% load fr_display %}
{% load humanize %}
{% block content %} {% block content %}
<div class="content-container">
<h2>
{% include 'snippets/book_titleby.html' with book=book %}
{% if request.user.is_authenticated %} <div class="block">
<a href="{{ book.id }}/edit" class="edit-link">edit <div class="level">
<span class="icon icon-pencil"> <h2 class="title level-left">
<span class="hidden-text">Edit Book</span> <span>{% include 'snippets/book_titleby.html' with book=book %}</span>
</span>
</a>
{% endif %}
</h2> </h2>
<div class="row"> {% if request.user.is_authenticated %}
<div class="level-right">
<a href="{{ book.id }}/edit">edit
<span class="icon icon-pencil">
<span class="is-sr-only">Edit Book</span>
</span>
</a>
</div>
{% endif %}
</div>
<div class="cover-container"> <div class="columns">
<div class="column is-narrow">
{% include 'snippets/book_cover.html' with book=book size=large %} {% include 'snippets/book_cover.html' with book=book size=large %}
{% include 'snippets/rate_action.html' with user=request.user book=book %} {% include 'snippets/rate_action.html' with user=request.user book=book %}
{% include 'snippets/shelve_button.html' %} {% include 'snippets/shelve_button.html' %}
@ -25,11 +30,11 @@
<form name="add-cover" method="POST" action="/upload_cover/{{ book.id }}" enctype="multipart/form-data"> <form name="add-cover" method="POST" action="/upload_cover/{{ book.id }}" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ cover_form.as_p }} {{ cover_form.as_p }}
<button type="submit">Add cover</button> <button class="button" type="submit">Add cover</button>
</form> </form>
{% endif %} {% endif %}
<dl> <dl class="content">
{% for field in info_fields %} {% for field in info_fields %}
{% if field.value %} {% if field.value %}
<dt>{{ field.name }}:</dt> <dt>{{ field.name }}:</dt>
@ -40,56 +45,81 @@
</div> </div>
<div class="column"> <div class="column">
<h3>{{ active_tab }} rating: {% include 'snippets/stars.html' with rating=rating %}</h3> <div class="block">
<h3 class="field is-grouped">{% include 'snippets/stars.html' with rating=rating %} ({{ reviews|length }} review{{ reviews|length|pluralize }})</h3>
{% include 'snippets/book_description.html' %} {% include 'snippets/book_description.html' %}
{% if book.parent_work.edition_set.count > 1 %} {% if book.parent_work.edition_set.count > 1 %}
<p><a href="/editions/{{ book.parent_work.id }}">{{ book.parent_work.edition_set.count }} editions</a></p> <p><a href="/editions/{{ book.parent_work.id }}">{{ book.parent_work.edition_set.count }} editions</a></p>
{% endif %} {% endif %}
</div>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="compose"> <div class="block">
{% include 'snippets/create_status.html' with book=book hide_cover=True %} {% include 'snippets/create_status.html' with book=book hide_cover=True %}
</div> </div>
<div> <div class="block">
<h3>Tags</h3> <h3>Tags</h3>
<form name="tag" action="/tag/" method="post"> <form name="tag" action="/tag/" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="text" name="name"> <input class="input" type="text" name="name">
<button type="submit">Add tag</button> <button class="button" type="submit">Add tag</button>
</form> </form>
</div> </div>
<div class="tag-cloud"> {% endif %}
<div class="block">
<div class="field is-grouped is-grouped-multiline">
{% for tag in tags %} {% for tag in tags %}
{% include 'snippets/tag.html' with book=book tag=tag user_tags=user_tags %} {% include 'snippets/tag.html' with book=book tag=tag user_tags=user_tags %}
{% endfor %} {% endfor %}
</div> </div>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
{% if request.user.is_authenticated %}
<div class="content-container tabs">
{% include 'snippets/tabs.html' with tabs=feed_tabs active_tab=active_tab path=path %}
</div>
{% endif %}
{% if not reviews %} {% if not reviews %}
<div class="content-container"> <div class="block">
<p>No reviews yet!</p> <p>No reviews yet!</p>
</div> </div>
{% endif %} {% endif %}
<div class="block">
{% for review in reviews %} {% for review in reviews %}
<div class="content-container"> <div class="block">
{% include 'snippets/status.html' with status=review hide_book=True depth=1 %} {% include 'snippets/status.html' with status=review hide_book=True depth=1 %}
</div> </div>
{% endfor %} {% endfor %}
<div class="block columns">
{% for rating in ratings %}
<div class="column">
<div class="media">
<div class="media-left">{% include 'snippets/avatar.html' %}</div>
<div class="media-content">
<div>
{% include 'snippets/username.html' %}
</div>
<div class="field is-grouped mb-0">
<div>rated it</div>
{% include 'snippets/stars.html' with rating=rating.rating %}
</div>
<div>
<a href="{{ rating.remote_id }}">{{ rating.published_date | naturaltime }}</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,17 +0,0 @@
{% extends 'layout.html' %}
{% load fr_display %}
{% block content %}
<div class="content-container">
<h2>Recently Added Books</h2>
<div class="book-grid row wrap shrink">
{% for book in books %}
<div class="cover-container">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/shelve_button.html' with book=book %}
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -1,71 +1,83 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load humanize %} {% load humanize %}
{% block content %} {% block content %}
<div class="content-container"> <div class="block">
<h2> <div class="level">
<h2 class="title level-left">
Edit "{{ book.title }}" Edit "{{ book.title }}"
</h2>
<div class="level-right">
<a href="/book/{{ book.id }}"> <a href="/book/{{ book.id }}">
<span class="edit-link icon icon-close"> <span class="edit-link icon icon-close">
<span class="hidden-text">Close</span> <span class="is-sr-only">Close</span>
</span> </span>
</a> </a>
</h2> </div>
<div class="book-preview row"> </div>
<div class="cover-container"> <div class="columns">
<div class="column is-narrow">
{% include 'snippets/book_cover.html' with book=book size="small" %} {% include 'snippets/book_cover.html' with book=book size="small" %}
</div> </div>
<div> <div class="column is-narrow">
<p>Added: {{ book.created_date | naturaltime }}</p> <p>Added: {{ book.created_date | naturaltime }}</p>
<p>Updated: {{ book.updated_date | naturaltime }}</p> <p>Updated: {{ book.updated_date | naturaltime }}</p>
</div> </div>
</div> </div>
</div> </div>
<form class="book-form content-container" name="edit-book" action="/edit_book/{{ book.id }}" method="post" enctype="multipart/form-data"> <form class="block" name="edit-book" action="/edit_book/{{ book.id }}" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<h3>Data sync <div class="block">
<small>If sync is enabled, any changes will be over-written</small> <h3 class="title is-4">Data sync</h3>
<h4 class="subtitle is-5">If sync is enabled, any changes will be over-written</h4>
</h3> </h3>
<div> <div class="columns">
<div class="row"> <div class="column is-narrow">
<p><label for="id_sync">Sync:</label> <input type="checkbox" name="sync" id="id_sync"></p> <label class="checkbox" for="id_sync"><input class="checkbox" type="checkbox" name="sync" id="id_sync"> Sync</label>
<p><label for="id_sync_cover">Sync cover:</label> <input type="checkbox" name="sync_cover" id="id_sync_cover"></p> </div>
<div class="column is-narrow">
<label class="checkbox" for="id_sync_cover"><input class="checkbox" type="checkbox" name="sync_cover" id="id_sync_cover"> Sync cover</label>
</div>
</div> </div>
</div> </div>
<h3>Cover</h3> <div class="columns">
<div class="image-form"> <div class="block column">
<h3 class="title is-4">Book Identifiers</h3>
<p class="fields is-grouped"><label class="label"for="id_isbn_13">ISBN 13:</label> {{ form.isbn_13 }} </p>
<p class="fields is-grouped"><label class="label"for="id_isbn_10">ISBN 10:</label> {{ form.isbn_10 }} </p>
<p class="fields is-grouped"><label class="label"for="id_openlibrary_key">Openlibrary key:</label> {{ form.openlibrary_key }} </p>
<p class="fields is-grouped"><label class="label"for="id_librarything_key">Librarything key:</label> {{ form.librarything_key }} </p>
<p class="fields is-grouped"><label class="label"for="id_goodreads_key">Goodreads key:</label> {{ form.goodreads_key }} </p>
</div>
<div class="column">
<div class="block">
<h3 class="title is-4">Cover</h3>
<p>{{ form.cover }} </p> <p>{{ form.cover }} </p>
</div> </div>
<h3>Book Identifiers</h2> <div class="block">
<div> <h3 class="title is-4">Physical Properties</h3>
<p><label for="id_isbn_13">ISBN 13:</label> {{ form.isbn_13 }} </p> <p class="fields is-grouped"><label class="label"for="id_physical_format">Format:</label> {{ form.physical_format }} </p>
<p><label for="id_isbn_10">ISBN 10:</label> {{ form.isbn_10 }} </p> <p class="fields is-grouped"><label class="label"for="id_pages">Pages:</label> {{ form.pages }} </p>
<p><label for="id_openlibrary_key">Openlibrary key:</label> {{ form.openlibrary_key }} </p> </div>
<p><label for="id_librarything_key">Librarything key:</label> {{ form.librarything_key }} </p> </div>
<p><label for="id_goodreads_key">Goodreads key:</label> {{ form.goodreads_key }} </p>
</div> </div>
<h3>Physical Properties</h3> <div class="block">
<div> <h3 class="title is-4">Metadata</h3>
<p><label for="id_physical_format">Format:</label> {{ form.physical_format }} </p> <p class="fields is-grouped"><label class="label"for="id_title">Title:</label> {{ form.title }} </p>
<p><label for="id_pages">Pages:</label> {{ form.pages }} </p> <p class="fields is-grouped"><label class="label"for="id_sort_title">Sort title:</label> {{ form.sort_title }} </p>
<p class="fields is-grouped"><label class="label"for="id_subtitle">Subtitle:</label> {{ form.subtitle }} </p>
<p class="fields is-grouped"><label class="label"for="id_description">Description:</label> {{ form.description }} </p>
<p class="fields is-grouped"><label class="label"for="id_series">Series:</label> {{ form.series }} </p>
<p class="fields is-grouped"><label class="label"for="id_series_number">Series number:</label> {{ form.series_number }} </p>
<p class="fields is-grouped"><label class="label"for="id_first_published_date">First published date:</label> {{ form.first_published_date }} </p>
<p class="fields is-grouped"><label class="label"for="id_published_date">Published date:</label> {{ form.published_date }} </p>
</div> </div>
<div class="block">
<h3>Metadata</h3> <button class="button is-primary" type="submit">Save</button>
<div>
<p><label for="id_title">Title:</label> {{ form.title }} </p>
<p><label for="id_sort_title">Sort title:</label> {{ form.sort_title }} </p>
<p><label for="id_subtitle">Subtitle:</label> {{ form.subtitle }} </p>
<p><label for="id_description">Description:</label> {{ form.description }} </p>
<p><label for="id_series">Series:</label> {{ form.series }} </p>
<p><label for="id_series_number">Series number:</label> {{ form.series_number }} </p>
<p><label for="id_first_published_date">First published date:</label> {{ form.first_published_date }} </p>
<p><label for="id_published_date">Published date:</label> {{ form.published_date }} </p>
</div>
<div>
<button type="submit">Save</button>
</div> </div>
</form> </form>

View file

@ -1,16 +1,11 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<div class="content-container"> <div class="block">
<div class="user-profile"> <h2 class="title">Edit Profile</h2>
<h2>Edit Profile</h2>
<p>{% include 'snippets/avatar.html' with user=user %} {% if user.localname %}{{ user.localname }}{% else %}{{ user.username }}{% endif %}</p>
<form name="avatar" action="/edit_profile/" method="post" enctype="multipart/form-data"> <form name="avatar" action="/edit_profile/" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<button type="submit">Update profile</button> <button class="button is-primary" type="submit">Update profile</button>
</form> </form>
</div> </div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,18 +1,10 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load fr_display %} {% load fr_display %}
{% block content %} {% block content %}
<div class="content-container"> <div class="block">
<h2>Editions of <a href="/book/{{ work.id }}">"{{ work.title }}"</a></h2> <h2 class="title">Editions of <a href="/book/{{ work.id }}">"{{ work.title }}"</a></h2>
<ol class="book-grid row wrap">
{% for book in editions %} {% include 'snippets/book_tiles.html' with books=editions %}
<li class="book-preview">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/shelve_button.html' with book=book %}
</li>
{% endfor %}
</ol>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,8 +1,8 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<div class="content-container"> <div class="block">
<h2>Server Error</h2> <h2 class="title">Server Error</h2>
<p>Something went wrong! Sorry about that.</p> <p>Something went wrong! Sorry about that.</p>
</div> </div>

View file

@ -2,23 +2,42 @@
{% load fr_display %} {% load fr_display %}
{% block content %} {% block content %}
{% include 'snippets/covers_shelf.html' with shelves=shelves user=request.user %} <div class="columns">
<div class="column is-one-third">
<h2 class="title is-4">Suggested books</h2>
<div id="feed"> <div class="tabs is-small is-toggle">
<div class="content-container tabs"> <ul>
{% include 'snippets/tabs.html' with tabs=feed_tabs active_tab=active_tab %} {% for book in suggested_books %}
<li class="{% if forloop.first %}is-active{% endif %}" data-id="tab-book-{{ book.id }}">
<label for="book-{{ book.id }}" onclick="tabChange(event)"><a>{% include 'snippets/book_cover.html' with book=book size="medium" %}</a></label>
</li>
{% endfor %}
</ul>
</div>
{% for book in suggested_books %}
<div>
<input class="toggle-control" type="radio" name="recent-books" id="book-{{ book.id }}" {% if forloop.first %}checked{% endif %}>
<div class="toggle-content hidden">
<div class="block">
{% include 'snippets/book_titleby.html' with book=book %}
{% include 'snippets/shelve_button.html' with book=book %}
</div>
{% include 'snippets/create_status.html' with book=book %}
</div>
</div>
{% endfor %}
</div> </div>
<div class="column is-two-thirds" id="feed">
{% for activity in activities %} {% for activity in activities %}
<div class="content-container"> <div class="block">
{% include 'snippets/status.html' with status=activity %} {% include 'snippets/status.html' with status=activity %}
</div> </div>
{% endfor %} {% endfor %}
<div class="content-container pagination row"> <nav class="pagination" role="navigation" aria-label="pagination">
{% if prev %} {% if prev %}
<p> <p class="pagination-previous">
<a href="{{ prev }}"> <a href="{{ prev }}">
<span class="icon icon-arrow-left"></span> <span class="icon icon-arrow-left"></span>
Previous Previous
@ -27,14 +46,14 @@
{% endif %} {% endif %}
{% if next %} {% if next %}
<p class="next"> <p class="pagination-next">
<a href="{{ next }}"> <a href="{{ next }}">
Next Next
<span class="icon icon-arrow-right"></span> <span class="icon icon-arrow-right"></span>
</a> </a>
</p> </p>
{% endif %} {% endif %}
</nav>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -3,20 +3,22 @@
{% block content %} {% block content %}
{% include 'user_header.html' with user=user %} {% include 'user_header.html' with user=user %}
<div class="content-container"> <div class="block">
<h2>Followers</h2> <h2 class="title">Followers</h2>
{% for followers in followers %} {% for followers in followers %}
<div class="row shrink"> <div class="block">
<div> <div class="field is-grouped">
<div class="control">
{% include 'snippets/avatar.html' with user=followers %} {% include 'snippets/avatar.html' with user=followers %}
</div> </div>
<div> <div class="control">
{% include 'snippets/username.html' with user=followers show_full=True %} {% include 'snippets/username.html' with user=followers show_full=True %}
</div> </div>
<div> <div class="control">
{% include 'snippets/follow_button.html' with user=followers %} {% include 'snippets/follow_button.html' with user=followers %}
</div> </div>
</div> </div>
</div>
{% endfor %} {% endfor %}
{% if not followers.count %} {% if not followers.count %}
<div>{{ user|username }} has no followers</div> <div>{{ user|username }} has no followers</div>

View file

@ -3,20 +3,22 @@
{% block content %} {% block content %}
{% include 'user_header.html' %} {% include 'user_header.html' %}
<div class="content-container"> <div class="block">
<h2>Following</h2> <h2 class="title">Following</h2>
{% for follower in user.following.all %} {% for follower in user.following.all %}
<div class="row shrink"> <div class="block">
<div> <div class="field is-grouped">
<div class="control">
{% include 'snippets/avatar.html' with user=follower %} {% include 'snippets/avatar.html' with user=follower %}
</div> </div>
<div> <div class="control">
{% include 'snippets/username.html' with user=follower show_full=True %} {% include 'snippets/username.html' with user=follower show_full=True %}
</div> </div>
<div> <div class="control">
{% include 'snippets/follow_button.html' with user=follower %} {% include 'snippets/follow_button.html' with user=follower %}
</div> </div>
</div> </div>
</div>
{% endfor %} {% endfor %}
{% if not following.count %} {% if not following.count %}
<div>No one is following {{ user|username }}</div> <div>No one is following {{ user|username }}</div>

View file

@ -1,17 +1,22 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load humanize %} {% load humanize %}
{% block content %} {% block content %}
<div class="content-container"> <div class="block">
<h2>Import Books from GoodReads</h2> <h2 class="title">Import Books from GoodReads</h2>
<form name="import" action="/import_data/" method="post" enctype="multipart/form-data"> <form name="import" action="/import_data/" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ import_form.as_p }} {{ import_form.as_p }}
<button type="submit">Import</button> <button class="button" type="submit">Import</button>
</form> </form>
<p> <p>
Imports are limited in size, and only the first {{ limit }} items will be imported. Imports are limited in size, and only the first {{ limit }} items will be imported.
</div>
<h2>Recent Imports</h2> <div class="content block">
<h2 class="title">Recent Imports</h2>
{% if not jobs %}
<p>No recent imports</p>
{% endif %}
<ul> <ul>
{% for job in jobs %} {% for job in jobs %}
<li><a href="/import_status/{{ job.id }}">{{ job.created_date | naturaltime }}</a></li> <li><a href="/import_status/{{ job.id }}">{{ job.created_date | naturaltime }}</a></li>

View file

@ -2,9 +2,8 @@
{% load fr_display %} {% load fr_display %}
{% load humanize %} {% load humanize %}
{% block content %} {% block content %}
<div id="content"> <div class="block">
<div> <h1 class="title">Import Status</h1>
<h1>Import Status</h1>
<p> <p>
Import started: {{ job.created_date | naturaltime }} Import started: {{ job.created_date | naturaltime }}
@ -16,7 +15,9 @@
<p> <p>
{{ task.info }} {{ task.info }}
{% endif %} {% endif %}
</div>
<div class="block">
{% if job.import_status %} {% if job.import_status %}
{% include 'snippets/status.html' with status=job.import_status %} {% include 'snippets/status.html' with status=job.import_status %}
{% endif %} {% endif %}
@ -25,8 +26,10 @@
<p> <p>
(Hit reload to update!) (Hit reload to update!)
{% endif %} {% endif %}
</div>
<table> <div class="block">
<table class="table">
<tr> <tr>
<th> <th>
Book Book

View file

@ -4,8 +4,9 @@
<head> <head>
<title>BookWyrm</title> <title>BookWyrm</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link type="text/css" rel="stylesheet" href="/static/format.css"> <link type="text/css" rel="stylesheet" href="/static/css/bulma.min.css">
<link type="text/css" rel="stylesheet" href="/static/icons.css"> <link type="text/css" rel="stylesheet" href="/static/css/format.css">
<link type="text/css" rel="stylesheet" href="/static/css/icons.css">
<link rel="shortcut icon" type="image/x-icon" href="/static/images/favicon.ico"> <link rel="shortcut icon" type="image/x-icon" href="/static/images/favicon.ico">
@ -16,63 +17,103 @@
<meta name="og:description" content="Federated Social Reading"> <meta name="og:description" content="Federated Social Reading">
<meta name="twitter:creator" content="@tripofmice"> <meta name="twitter:creator" content="@tripofmice">
<meta name="twitter:site" content="@tripofmice"> <meta name="twitter:site" content="@tripofmice">
</head> </head>
<body> <body>
<div id="top-bar"> <nav class="navbar" role="navigation" aria-label="main navigation">
<header class="row"> <div class="navbar-brand">
<div id="branding"> <a class="navbar-item" href="/">
<a href="/"> <img src="/static/images/logo-small.png" alt="BookWyrm" width="112" height="28">
<img id="logo" src="/static/images/logo-small.png" alt="BookWyrm"></img>
</a> </a>
</div> <form class="navbar-item" action="/search/">
<div class="field is-grouped">
<ul id="menu"> <input class="input" type="text" name="q" placeholder="Search for a book or user">
{% if request.user.is_authenticated %} <button class="button" type="submit">
<li><a href="/user/{{request.user.localname}}/shelves">Your shelves</a></li>
{% endif %}
<li><a href="/#feed">Updates</a></li>
<li><a href="/books">Discover Books</a></li>
</ul>
<div id="actions">
<div id="search">
<form action="/search/">
<input type="text" name="q" placeholder="Search for a book or user">
<button type="submit">
<span class="icon icon-search"> <span class="icon icon-search">
<span class="hidden-text">search</span> <span class="is-sr-only">search</span>
</span> </span>
</button> </button>
</div>
</form> </form>
</div>
{% if request.user.is_authenticated %} <label for="main-nav" role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="mainNav">
<div id="notifications"> <span aria-hidden="true"></span>
<a href="/notifications"> <span aria-hidden="true"></span>
<span class="icon icon-bell"> <span aria-hidden="true"></span>
<span class="hidden-text">Notitications</span> </label>
</span>
{% if request.user|notification_count %}<span class="count">{{ request.user | notification_count }}</span>{% endif %}
</a>
</div>
<div class="pulldown-container">
{% include 'snippets/avatar.html' with user=request.user %}
<ul class="pulldown">
<li><a href="/user/{{ request.user }}">Your profile</a></li>
<li><a href="/user-edit/">Settings</a></li>
<li><a href="/import">Import Books</a></li>
<li><a href="/manage_invites/">Invites</a></li>
<li><a href="/logout/">Log out</a></li>
</ul>
</p>
{% endif %}
</div>
</header>
</div> </div>
<div id="main"> <input class="toggle-control" type="checkbox" id="main-nav">
<div id="mainNav" class="navbar-menu toggle-content">
<div class="navbar-start">
{% if request.user.is_authenticated %}
<a href="" class="navbar-item">
Lists
</a>
<a href="" class="navbar-item">
Groups
</a>
{% endif %}
</div>
<div class="navbar-end">
{% if request.user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable">
<div class="navbar-link"><p>
{% include 'snippets/avatar.html' with user=user %}
{% include 'snippets/username.html' with user=request.user %}
</p></div>
<div class="navbar-dropdown">
<a href="/user/{{request.user.localname}}" class="navbar-item">
Profile
</a>
<a href="/user-edit" class="navbar-item">
Settings
</a>
<a href="/invite" class="navbar-item">
Invites
</a>
<a href="/import" class="navbar-item">
Import books
</a>
<hr class="navbar-divider">
<a href="/logout" class="navbar-item">
Log out
</a>
</div>
</div>
<div class="navbar-item">
<a href="/notifications">
<div class="tags has-addons">
<span class="tag is-medium">
<span class="icon icon-bell">
<span class="is-sr-only">Notitications</span>
</span>
</span>
{% if request.user|notification_count %}
<span class="tag is-danger is-medium">{{ request.user | notification_count }}</span>
{% endif %}
</div>
</a>
</div>
{% else %}
<div class="navbar-item">
<div class="buttons">
<a href="/register" class="button is-primary">
<strong>Sign up</strong>
</a>
<a href="/login" class="button is-light">
Log in
</a>
</div>
</div>
{% endif %}
</div>
</div>
</nav>
<div class="section">
{% block content %} {% block content %}
{% endblock %} {% endblock %}
</div> </div>

View file

@ -1,52 +1,45 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<div class="content-container"> <div class="columns">
<h2>About {{ site_settings.name }}</h2> <div class="column">
<p> <h2 class="title">About {{ site_settings.name }}</h2>
<p class="block">
{{ site_settings.instance_description }} {{ site_settings.instance_description }}
</p> </p>
<p> <p class="block">
<small>
<a href="/about/">More about this site</a> <a href="/about/">More about this site</a>
</small> </p>
<p class="block">
<a href="/register" class="button is-link">Create an Account</a>
</p> </p>
</div> </div>
<div class="row"> <div class="column">
<div class="content-container login"> <h2 class="title">Log in</h2>
<h2>Create an Account</h2>
<p><small>
With a BookWyrm account, you can track and share your reading activity with
friends here and on any other federated server, like Mastodon and PixelFed.
</small></p>
{% if site_settings.allow_registration %} <div class="block">
<div>
<form name="register" method="post" action="/register">
{% csrf_token %}
{{ register_form.as_p }}
<button type="submit">Create account</button>
</form>
</div>
{% else %}
<small>
This instance is not open for registration.
</small>
{% endif %}
</div>
<div class="content-container login">
<h2>Log in</h2>
<div>
<form name="login" method="post" action="/user-login"> <form name="login" method="post" action="/user-login">
{% csrf_token %} {% csrf_token %}
{{ login_form.as_p }} <div class="field">
<button type="submit">Log in</button> <label class="label" for="id_username">Username:</label>
<div class="control">{{ login_form.username }}</div>
</div>
<div class="field">
<label class="label" for="id_password">Password:</label>
<div class="control">{{ login_form.password }}</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" type="submit">Log in</button>
</div>
<div class="control">
<small><a href="/reset-password">Forgot your password?</a></small>
</div>
</div>
</form> </form>
<p><small><a href="/reset-password">Forgot your password?</a></small></p>
</div> </div>
</div> </div>

View file

@ -1,16 +1,18 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load humanize %} {% load humanize %}
{% block content %} {% block content %}
<div class="content-container"> <div class="block">
<div class="manage-invites"> <h2 class="title">Invites</h2>
<h2>Invites</h2> <table class="table is-striped">
<table>
<tr> <tr>
<th>Link</th> <th>Link</th>
<th>Expires</th> <th>Expires</th>
<th>Max uses</th> <th>Max uses</th>
<th>Times used</th> <th>Times used</th>
</tr> </tr>
{% if not invites %}
<tr><td colspan="4">No active invites</td></tr>
{% endif %}
{% for invite in invites %} {% for invite in invites %}
<tr> <tr>
<td><a href="{{ invite.link }}">{{ invite.link }}</td> <td><a href="{{ invite.link }}">{{ invite.link }}</td>
@ -20,13 +22,32 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<h2>Generate New Invite</h2> </div>
<form name="avatar" action="/create_invite/" method="post"> <div class="block">
<h2 class="title is-4">Generate New Invite</h2>
<form name="invite" action="/create_invite/" method="post">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} <div class="field">
<button type="submit">Create Invite</button> <div class="control">
<label class="label" for="id_expiry">Expiry:</label>
</div>
<div class="select">
{{ form.expiry }}
</div>
</div>
<div class="field">
<div class="control">
<label class="label" for="id_use_limit">Use limit:</label>
</div>
<div class="select">
{{ form.use_limit }}
</div>
</div>
<button class="button is-primary" type="submit">Create Invite</button>
</form> </form>
</div> </div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,8 +1,8 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<div class="content-container"> <div class="block">
<h2>Not Found</h2> <h2 class="title">Not Found</h2>
<p>The page your requested doesn't seem to exist!</p> <p>The page your requested doesn't seem to exist!</p>
</div> </div>

View file

@ -1,20 +1,22 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load humanize %}l {% load humanize %}l
{% block content %} {% block content %}
<div class="content-container"> <div class="block">
<h2>Notifications</h2> <h2 class="title">Notifications</h2>
<form name="clear" action="/clear-notifications" method="POST"> <form name="clear" action="/clear-notifications" method="POST">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="secondary">Delete notifications</button> <button class="button is-danger" type="submit" class="secondary">Delete notifications</button>
</form> </form>
</div> </div>
<div class="content-container"> <div class="block">
{% for notification in notifications %} {% for notification in notifications %}
<div class="notification{% if notification.id in unread %} unread{% endif %}"> <div class="notification level{% if notification.id in unread %} is-primary{% endif %}">
<small class="time-ago">{{ notification.created_date | naturaltime }}</small> <div class="level-left">
<p>
{% if notification.related_user %} {% if notification.related_user %}
{% include 'snippets/avatar.html' with user=notification.related_user %}
{% include 'snippets/username.html' with user=notification.related_user %} {% include 'snippets/username.html' with user=notification.related_user %}
{% if notification.notification_type == 'FAVORITE' %} {% if notification.notification_type == 'FAVORITE' %}
favorited your favorited your
@ -41,6 +43,10 @@
your <a href="/import_status/{{ notification.related_import.id }}">import</a> completed. your <a href="/import_status/{{ notification.related_import.id }}">import</a> completed.
{% endif %} {% endif %}
</p>
</div>
<p class="level-right">{{ notification.created_date | naturaltime }}</p>
</div> </div>
{% endfor %} {% endfor %}
{% if not notifications %} {% if not notifications %}

View file

@ -0,0 +1,49 @@
{% extends 'layout.html' %}
{% block content %}
<div class="columns">
<div class="column">
<h2 class="title">About {{ site_settings.name }}</h2>
<p class="block">
{{ site_settings.instance_description }}
</p>
<p class="block">
<a href="/about/">More about this site</a>
</p>
<p class="block">
<a href="/login" class="button is-link">Log In</a>
</p>
</div>
<div class="column">
<h2 class="title">Create an Account</h2>
<div class="block">
<form name="register" method="post" action="/user-register">
{% csrf_token %}
<div class="field">
<label class="label" for="id_username">Username:</label>
<div class="control">{{ register_form.username }}</div>
</div>
<div class="field">
<label class="label" for="id_email">Email address:</label>
<div class="control">{{ register_form.email }}</div>
</div>
<div class="field">
<label class="label" for="id_password">Password:</label>
<div class="control">{{ register_form.password }}</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" type="submit">Sign Up</button>
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -1,9 +1,19 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<div id="content"> <div class="block">
<nav class="breadcrumb has-succeeds-separator" aria-label="breadcrumbs">
<ul>
<li><a href="/user/{{ user.username }}">{% include 'snippets/username.html' with user=user %}</a></li>
<li><a href="/user/{{ user.username }}/shelves">Shelves</a></li>
<li class="is-active"><a href="#" aria-current="page">{{ shelf.name }}</a></li>
</ul>
</nav>
</div>
<div class="block">
<div> <div>
<h2>{% include 'snippets/username.html' with user=user %} > {{ shelf.name }}</h2> <h2 class="title">{{ shelf.name }}</h2>
{% include 'snippets/shelf.html' with shelf=shelf ratings=ratings %} {% include 'snippets/shelf.html' with shelf=shelf ratings=ratings %}
</div> </div>
</div> </div>

View file

@ -1,2 +1,2 @@
<img class="user-pic{% if large %} large{% endif %}" src="{% if user.avatar %}/images/{{ user.avatar }}{% else %}/static/images/default_avi.jpg{% endif %}"> <img class="avatar image {% if large %}is-96x96{% else %}is-32x32{% endif %}" src="{% if user.avatar %}/images/{{ user.avatar }}{% else %}/static/images/default_avi.jpg{% endif %}">

View file

@ -1,12 +1,14 @@
{% load fr_display %} {% load fr_display %}
<div class="cover-container is-{{ size }}">
{% if book.cover %} {% if book.cover %}
<img class="book-cover {{ size }}" src="/images/{{ book.cover }}" alt="{% include 'snippets/cover_alt.html' with book=book %}"> <img class="book-cover" src="/images/{{ book.cover }}" alt="{% include 'snippets/cover_alt.html' with book=book %}">
{% else %} {% else %}
<div class="no-cover book-cover {{ size }}"> <div class="no-cover book-cover">
<img class="book-cover {{ size }}" src="/static/images/no_cover.jpg" alt="No cover"> <img class="book-cover" src="/static/images/no_cover.jpg" alt="No cover">
<div> <div>
<p class="title">{{ book.title }}</p> <p>{{ book.title }}</p>
<p>({{ book|edition_info }})</p> <p>({{ book|edition_info }})</p>
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div>

View file

@ -1,7 +1,6 @@
{% load fr_display %}
{% if book.description %} {% if book.description %}
<blockquote>{{ book.description | description }}</blockquote> <blockquote class="content">{{ book.description }}</blockquote>
{% elif book.parent_work.description %} {% elif book.parent_work.description %}
<blockquote>{{ book.parent_work.description | description }}</blockquote> <blockquote>{{ book.parent_work.description }}</blockquote>
{% endif %} {% endif %}

View file

@ -0,0 +1,18 @@
<div class="columns">
{% for book in books %}
{% if forloop.counter0|divisibleby:"4" %}
</div>
<div class="columns">
{% endif %}
<div class="column is-narrow">
<div class="box">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/rate_action.html' with user=request.user book=book %}
{% include 'snippets/shelve_button.html' with book=book %}
</div>
</div>
{% endfor %}
</div>

View file

@ -1,8 +1,8 @@
<span class="title"> <span>
<a href="/book/{{ book.id }}">{{ book.title }}</a> <a href="/book/{{ book.id }}">{{ book.title }}</a>
</span> </span>
{% if book.authors %} {% if book.authors %}
<span class="author"> <span>
by {% include 'snippets/authors.html' with book=book %} by {% include 'snippets/authors.html' with book=book %}
</span> </span>
{% endif %} {% endif %}

View file

@ -1,49 +0,0 @@
{% load fr_display %}
<div class="all-shelves content-container">
{% for shelf in shelves %}
{% if shelf.books %}
<div>
<h2>{{ shelf.name }}
{% if shelf.size > shelf.books|length %}
<small>(<a href="/shelf/{{ user | username }}/{{ shelf.identifier }}">See all {{ shelf.size }}</a>)</small>
{% endif %}
</h2>
<div class="covers-shelf {{ shelf.identifier }} ">
{% for book in shelf.books %}
<div class="cover-container">
<label for="book-{{ book.id }}-radio">
{% include 'snippets/book_cover.html' with book=book %}
</label>
{% include 'snippets/shelve_button.html' with book=book hide_pulldown=True %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% for shelf in shelves %}
{% for book in shelf.books %}
<div class="compose-popout">
<input name="book-popout" type="radio" id="book-{{ book.id }}-radio">
<div class="compose compose-suggestion" id="compose-book-{{ book.id }}">
<label class="close icon icon-close" for="book-{{ book.id }}-radio-close" onclick="hide_element(this)">
<span class="hidden-text">Close</span>
</label>
<input name="book-popout" type="radio" id="book-{{ book.id }}-radio-close">
<div class="content-container">
<h2>
{% include 'snippets/avatar.html' with user=user %}
Your thoughts on
a <a href="/book/{{ book.id }}">{{ book.title }}</a>
by {% include 'snippets/authors.html' with book=book %}
</h2>
{% include 'snippets/create_status.html' with book=book user=request.user %}
</div>
</div>
</div>
{% endfor %}
{% endfor %}

View file

@ -1,43 +1,79 @@
{% load humanize %} {% load humanize %}
{% load fr_display %} {% load fr_display %}
<div class="tabs secondary"> <div class="columns">
<div class="tab active" data-id="tab-review-{{ book.id }}" data-category="tab-option-{{ book.id }}"> <div class="column">
<a href="/book/{{ book.id }}/review" onclick="tabChange(event)">Review</a> <div class="tabs is-boxed">
</div> <ul>
<div class="tab" data-id="tab-comment-{{ book.id }}" data-category="tab-option-{{ book.id }}"> <li class="is-active" data-id="tab-review-{{ book.id }}" data-category="tab-option-{{ book.id }}">
<a href="/book/{{ book.id }}/comment" onclick="tabChange(event)">Comment</a> <label for="review-{{ book.id }}" onclick="tabChange(event)"><a>Review</a></label>
</div> </li>
<div class="tab" data-id="tab-quotation-{{ book.id }}" data-category="tab-option-{{ book.id }}"> <li data-id="tab-comment-{{ book.id }}" data-category="tab-option-{{ book.id }}">
<a href="/book/{{ book.id }}/quotation" onclick="tabChange(event)">Quote</a> <label for="comment-{{ book.id}}" onclick="tabChange(event)"><a>Comment</a></label>
</div> </li>
<li data-id="tab-quotation-{{ book.id }}" data-category="tab-option-{{ book.id }}">
<label for="quote-{{ book.id }}" onclick="tabChange(event)"><a>Quote</a></label>
</li>
</ul>
</div> </div>
<div class="book-preview row"> <div>
{% if not hide_cover %} <input class="toggle-control" type="radio" name="status-tabs-{{ book.id }}" id="review-{{ book.id }}" checked>
<div class="cover-container"> <form class="toggle-content hidden tab-option-{{ book.id }}" name="review" action="/review/" method="post" id="tab-review-{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<div class="control">
<label class="label" for="id_name_{{ book.id }}_review">Title:</label>
<input type="text" name="name" maxlength="255" class="input" required="" id="id_name_{{ book.id }}_review" placeholder="My review of '{{ book.title }}'">
</div> </div>
{% endif %} <div class="control">
<form class="tab-option-{{ book.id }} review-form" name="review" action="/review/" method="post" id="tab-review-{{ book.id }}"> <label class="label" for="id_content_{{ book.id }}_review">Review:</label>
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
{% include 'snippets/rate_form.html' with book=book %}
{{ review_form.as_p }}
<button type="submit">post review</button>
</form>
<form class="hidden tab-option-{{ book.id }} review-form" name="comment" action="/comment/" method="post" id="tab-comment-{{ book.id }}"> <span class="is-sr-only">Rating</span>
{% csrf_token %} <div class="field is-grouped stars form-rate-stars">
<input type="hidden" name="book" value="{{ book.id }}"> <input class="hidden" type="radio" name="rating" value="" checked>
{{ comment_form.as_p }} {% for i in '12345'|make_list %}
<button type="submit">post comment</button> <input class="hidden" id="book{{book.id}}-star-{{ forloop.counter }}" type="radio" name="rating" value="{{ forloop.counter }}">
</form> <label class="icon icon-star-empty" for="book{{book.id}}-star-{{ forloop.counter }}">
<span class="is-sr-only">{{ forloop.counter }} star{{ forloop.counter | pluralize }}</span>
</label>
{% endfor %}
</div>
<form class="hidden tab-option-{{ book.id }} review-form quote-form" name="quotation" action="/quotate/" method="post" id="tab-quotation-{{ book.id }}"> <textarea name="content" class="textarea" id="id_content_{{ book.id }}_review"></textarea>
{% csrf_token %} </div>
<input type="hidden" name="book" value="{{ book.id }}"> <button class="button is-primary" type="submit">post review</button>
{{ quotation_form.as_p }}
<button typr="submit">post quote</button>
</form> </form>
</div> </div>
<div>
<input class="toggle-control" type="radio" name="status-tabs-{{ book.id }}" id="comment-{{ book.id }}">
<form class="toggle-content hidden tab-option-{{ book.id }}" name="comment" action="/comment/" method="post" id="tab-comment-{{ book.id }}">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<div class="control">
<label class="label" for="id_content_{{ book.id }}_comment">Comment:</label>
<textarea name="content" class="textarea" id="id_content_{{ book.id }}_comment" placeholder="Some thoughts on '{{ book.title }}'"></textarea>
</div>
<button class="button is-primary" type="submit">post comment</button>
</form>
</div>
<div>
<input class="toggle-control" type="radio" name="status-tabs-{{ book.id }}" id="quote-{{ book.id }}">
<form class="toggle-content hidden tab-option-{{ book.id }}" name="quotation" action="/quotate/" method="post" id="tab-quotation-{{ book.id }}">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<div class="control">
<label class="label" for="id_quote_{{ book.id }}_quote">Quote:</label>
<textarea name="quote" class="textarea" required="" id="id_quote_{{ book.id }}_quote" placeholder="An except from '{{ book.title }}'"></textarea>
</div>
<div class="control">
<label class="label" for="id_content_{{ book.id }}_quote">Comment:</label>
<textarea name="content" class="textarea is-small" id="id_content_{{ book.id }}_quote"></textarea>
</div>
<button class="button is-primary" type="submit">post quote</button>
</form>
</div>
</div>
</div>

View file

@ -11,14 +11,14 @@ Follow request already sent.
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
{% if user.manually_approves_followers %} {% if user.manually_approves_followers %}
<button type="submit">Send follow request</button> <button class="button is-small" type="submit">Send follow request</button>
{% else %} {% else %}
<button type="submit">Follow</button> <button class="button is-small" type="submit">Follow</button>
{% endif %} {% endif %}
</form> </form>
<form action="/unfollow/" method="POST" onsubmit="interact(event)" class="follow-{{ user.id }} {% if not request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}"> <form action="/unfollow/" method="POST" onsubmit="interact(event)" class="follow-{{ user.id }} {% if not request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
<button type="submit">Unfollow</button> <button class="button is-small" type="submit">Unfollow</button>
</form> </form>
{% endif %} {% endif %}

View file

@ -3,11 +3,11 @@
<form action="/accept_follow_request/" method="POST"> <form action="/accept_follow_request/" method="POST">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
<button type="submit">Accept</button> <button class="button is-small" type="submit">Accept</button>
</form> </form>
<form action="/delete_follow_request/" method="POST"> <form action="/delete_follow_request/" method="POST">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
<button type="submit" class="warning">Delete</button> <button class="button is-small" type="submit" class="warning">Delete</button>
</form> </form>
{% endif %} {% endif %}

View file

@ -1,64 +1,65 @@
{% load fr_display %} {% load fr_display %}
<div class="interaction"> <div class="card-footer-item">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<form name="reply" action="/reply" method="post" onsubmit="return reply(event)"> <form name="reply" action="/reply" method="post" onsubmit="return reply(event)">
<div class="field is-grouped">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="parent" value="{{ activity.id }}"> <input type="hidden" name="parent" value="{{ activity.id }}">
<textarea name="content" placeholder="Leave a comment..." id="id_content" required="true"></textarea> <textarea name="content" placeholder="Leave a comment..." id="id_content_{{ activity.id }}" required="true"></textarea>
<button type="submit"> <button class="button" type="submit">
<span class="icon icon-comment"> <span class="icon icon-comment">
<span class="hidden-text">Comment</span> <span class="is-sr-only">Comment</span>
</span> </span>
</button> </button>
</div>
</form> </form>
<form name="boost" action="/boost/{{ activity.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }} {% if request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}"> <form name="boost" action="/boost/{{ activity.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }} {% if request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}">
{% csrf_token %} {% csrf_token %}
<button type="submit"> <button class="button" type="submit">
<span class="icon icon-boost"> <span class="icon icon-boost">
<span class="hidden-text">Boost status</span> <span class="is-sr-only">Boost status</span>
</span> </span>
</button> </button>
</form> </form>
<form name="unboost" action="/unboost/{{ activity.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }} active {% if not request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}"> <form name="unboost" action="/unboost/{{ activity.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }} active {% if not request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}">
{% csrf_token %} {% csrf_token %}
<button type="submit"> <button class="button is-success" type="submit">
<span class="icon icon-boost"> <span class="icon icon-boost">
<span class="hidden-text">Un-boost status</span> <span class="is-sr-only">Un-boost status</span>
</span> </span>
</button> </button>
</form> </form>
<form name="favorite" action="/favorite/{{ activity.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }} {% if request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}"> <form name="favorite" action="/favorite/{{ activity.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }} {% if request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}">
{% csrf_token %} {% csrf_token %}
<button type="submit"> <button class="button" type="submit">
<span class="icon icon-heart"> <span class="icon icon-heart">
<span class="hidden-text">Like status</span> <span class="is-sr-only">Like status</span>
</span> </span>
</button> </button>
</form> </form>
<form name="unfavorite" action="/unfavorite/{{ activity.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }} active {% if not request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}"> <form name="unfavorite" action="/unfavorite/{{ activity.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }} active {% if not request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}">
{% csrf_token %} {% csrf_token %}
<button type="submit"> <button class="button is-success" type="submit">
<span class="icon icon-heart"> <span class="icon icon-heart">
<span class="hidden-text">Un-like status</span> <span class="is-sr-only">Un-like status</span>
</span> </span>
</button> </button>
</form> </form>
{% else %} {% else %}
<a href="/login"> <a href="/login">
<span class="icon icon-comment"> <span class="icon icon-comment">
<span class="hidden-text">Comment</span> <span class="is-sr-only">Comment</span>
</span> </span>
<span class="icon icon-boost"> <span class="icon icon-boost">
<span class="hidden-text">Boost status</span> <span class="is-sr-only">Boost status</span>
</span> </span>
<span class="icon icon-heart"> <span class="icon icon-heart">
<span class="hidden-text">Like status</span> <span class="is-sr-only">Like status</span>
</span> </span>
</a> </a>
{% endif %} {% endif %}
</div> </div>

View file

@ -1,13 +1,13 @@
{% load fr_display %} {% load fr_display %}
<span class="hidden-text">Leave a rating</span> <span class="is-sr-only">Leave a rating</span>
<div class="stars rate-stars"> <div class="field is-grouped stars rate-stars">
{% for i in '12345'|make_list %} {% for i in '12345'|make_list %}
<form name="rate" action="/rate/" method="POST" onsubmit="return rate_stars(event)"> <form name="rate" action="/rate/" method="POST" onsubmit="return rate_stars(event)">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="rating" value="{{ forloop.counter }}"> <input type="hidden" name="rating" value="{{ forloop.counter }}">
<button type="submit" class="icon icon-star-{% if book|rating:user < forloop.counter %}empty{% else %}full{% endif %}"> <button type="submit" class="icon icon-star-{% if book|rating:user < forloop.counter %}empty{% else %}full{% endif %}">
<span class="hidden-text">{{ forloop.counter }} star{{ forloop.counter | pluralize }}</span> <span class="is-sr-only">{{ forloop.counter }} star{{ forloop.counter | pluralize }}</span>
</button> </button>
</form> </form>
{% endfor %} {% endfor %}

View file

@ -1,12 +0,0 @@
{% load fr_display %}
<span class="hidden-text">Rating</span>
<div class="stars rate-stars">
<input type="radio" name="rating" value="" checked>
{% for i in '12345'|make_list %}
<input id="book{{book.id}}-star-{{ forloop.counter }}" type="radio" name="rating" value="{{ forloop.counter }}">
<label class="icon icon-star-empty" for="book{{book.id}}-star-{{ forloop.counter }}">
<span class="hidden-text">{{ forloop.counter }} star{{ forloop.counter | pluralize }}</span>
</label>
{% endfor %}
</div>

View file

@ -1,7 +1,8 @@
{% load humanize %} {% load humanize %}
{% load fr_display %} {% load fr_display %}
{% if shelf.books %} {% if shelf.books %}
<table> <table class="table is-striped is-fullwidth">
<tr class="book-preview"> <tr class="book-preview">
<th> <th>
Cover Cover

View file

@ -1,25 +1,27 @@
{% load fr_display %} {% load fr_display %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="pulldown-button"> <div class="field is-grouped">
<form name="shelve" action="/shelve/" method="post"> <form name="shelve" action="/shelve/" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="shelf" value="{% shelve_button_identifier book %}"> <input type="hidden" name="shelf" value="{% shelve_button_identifier book %}">
<button type="submit" style="">{% shelve_button_text book %}</button> <button class="button is-small" type="submit" style="">{% shelve_button_text book %}</button>
</form> </form>
<div class="dropdown is-hoverable">
{% if not hide_pulldown %} {% if not hide_pulldown %}
<div class="pulldown-container"> <div class="button dropdown-trigger is-small" >
<button class="pulldown-toggle"> <span class="icon icon-arrow-down"><span class="is-sr-only">More shelves</span></span>
<span class="icon icon-arrow-down"><span class="hidden-text">More shelves</span></span> </div>
</button>
<ul class="pulldown"> <div class="dropdown-menu">
<form name="shelve" action="/shelve/" method="post"> <ul class="dropdown-content">
<form class="dropdown-item" name="shelve" action="/shelve/" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
{% for shelf in request.user.shelf_set.all %} {% for shelf in request.user.shelf_set.all %}
<li> <li>
<button name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>{{ shelf.name }} {% if shelf in book.shelf_set.all %} ✓ {% endif %}</button> <button class="is-small" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>{{ shelf.name }} {% if shelf in book.shelf_set.all %} ✓ {% endif %}</button>
</li> </li>
{% endfor %} {% endfor %}
</form> </form>
@ -28,4 +30,5 @@
{% endif %} {% endif %}
</div> </div>
</div>
{% endif %} {% endif %}

View file

@ -1,5 +1,5 @@
<div class="stars"> <div class="stars">
<span class="hidden-text">{{ rating|floatformat }} star{{ rating|floatformat | pluralize }}</span> <span class="is-sr-only">{{ rating|floatformat }} star{{ rating|floatformat | pluralize }}</span>
{% for i in '12345'|make_list %} {% for i in '12345'|make_list %}
<span class="icon icon-star-{% if rating >= forloop.counter %}full{% elif rating|floatformat:0 >= forloop.counter|floatformat:0 %}half{% else %}empty{% endif %}"> <span class="icon icon-star-{% if rating >= forloop.counter %}full{% elif rating|floatformat:0 >= forloop.counter|floatformat:0 %}half{% else %}empty{% endif %}">
</span> </span>

View file

@ -1,24 +1,31 @@
{% load humanize %}
{% load fr_display %} {% load fr_display %}
<div class="post {{ status.status_type | lower }} depth-{{ depth }} {% if main %}main{% else %}reply{% endif %}"> <div class="card">
<header class="card-header">
<h2>
{% if status.boosted_status %}
{% include 'snippets/status_header.html' with status=status.boosted_status %}
<small class="subhead">{% include 'snippets/status_header.html' with status=status %}</small>
{% else %}
{% include 'snippets/status_header.html' with status=status %} {% include 'snippets/status_header.html' with status=status %}
{% endif %} </header>
</h2>
<div class="status-content"> <div class="card-content">
{% include 'snippets/status_content.html' with status=status %}
</div>
</div>
{% if status.status_type == 'Boost' %} {% if status.status_type == 'Boost' %}
{% include 'snippets/interaction.html' with activity=status|boosted_status %} {% include 'snippets/status_content.html' with status=status.boosted_status %}
{% else %}
{% include 'snippets/status_content.html' with status=status %}
{% endif %}
</div>
<footer class="card-footer">
{% if status.status_type == 'Boost' %}
{% include 'snippets/interaction.html' with activity=status.boosted_status %}
{% else %} {% else %}
{% include 'snippets/interaction.html' with activity=status %} {% include 'snippets/interaction.html' with activity=status %}
{% endif %} {% endif %}
<div class="card-footer-item">
<span class="icon icon-public">
<span class="is-sr-only">Public post</span>
</span>
<a href="{{ status.remote_id }}">{{ status.published_date | naturaltime }}</a>
</div>
</footer>
</div>

View file

@ -1,38 +1,32 @@
{% load fr_display %} {% load fr_display %}
<div class="media">
{% if not hide_book and status.mention_books.count %} {% if not hide_book and status.mention_books.count %}
<div class="row"> <div class="media-left">
<div class="columns">
{% for book in status.mention_books.all|slice:"0:4" %} {% for book in status.mention_books.all|slice:"0:4" %}
<div class="row"> <div class="column">
<div class="cover-container"> <a href="/book/{{ book.id }}">{% include 'snippets/book_cover.html' with book=book %}</a>
{% include 'snippets/book_cover.html' with book=book %}
{% if status.mention_books.count > 1 %} {% if status.mention_books.count > 1 %}
<p>{% include 'snippets/book_titleby.html' with book=book %}</p> <p>{% include 'snippets/book_titleby.html' with book=book %}</p>
{% endif %} {% endif %}
{% include 'snippets/rate_action.html' with book=book user=request.user %}
{% include 'snippets/shelve_button.html' with book=book %} {% include 'snippets/shelve_button.html' with book=book %}
</div> </div>
{% if status.mention_books.count == 1 %}
<div>
<p>{% include 'snippets/book_titleby.html' with book=book %}</p>
{% include 'snippets/book_description.html' with book=book %}
</div>
{% endif %}
</div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %}
<div class="row">
{% if not hide_book and status.book %}
<div class="cover-container">
{% include 'snippets/book_cover.html' with book=status.book %}
{% include 'snippets/rate_action.html' with book=status.book user=request.user %}
{% include 'snippets/shelve_button.html' with book=status.book %}
</div> </div>
{% endif %} {% endif %}
{% if not hide_book and status.book %}
<div class="media-left">
<div> <div>
<a href="/book/{{ status.book.id }}">{% include 'snippets/book_cover.html' with book=status.book %}</a>
{% include 'snippets/shelve_button.html' with book=status.book %}
</div>
</div>
{% endif %}
<div class="media-content">
{% if status.status_type == 'Review' %} {% if status.status_type == 'Review' %}
<h3> <h3>
{% if status.name %}{{ status.name }}<br>{% endif %} {% if status.name %}{{ status.name }}<br>{% endif %}
@ -41,17 +35,21 @@
{% endif %} {% endif %}
{% if status.quote %} {% if status.quote %}
<div class="quote"> <div class="quote block">
<blockquote>{{ status.quote }}</blockquote> <blockquote>{{ status.quote }}</blockquote>
<p> &mdash; {% include 'snippets/book_titleby.html' with book=status.book %}</p> <p> &mdash; {% include 'snippets/book_titleby.html' with book=status.book %}</p>
</div> </div>
{% endif %} {% endif %}
{% if status.content and status.status_type != 'Update' and status.status_type != 'Boost' %} {% if status.content and status.status_type != 'GeneratedNote' and status.status_type != 'Boost' %}
<blockquote>{{ status.content | safe }}</blockquote> <blockquote>{{ status.content | safe }}</blockquote>
{% endif %} {% endif %}
{% if status.mention_books.count == 1 and not status.book %}
{% include 'snippets/book_description.html' with book=status.mention_books.first %}
{% endif %}
{% if not status.content and status.book and not hide_book and status.status_type != 'Boost' %} {% if not status.content and status.book and not hide_book and status.status_type != 'Boost' %}
{% include 'snippets/book_description.html' with book=status.book %} {% include 'snippets/book_description.html' with book=status.book %}
{% endif %} {% endif %}

View file

@ -1,10 +1,13 @@
{% load humanize %}
{% load fr_display %} {% load fr_display %}
<div class="card-header-title">
<p>
{% include 'snippets/avatar.html' with user=status.user %} {% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %}
{% if status.status_type == 'Update' %} {% include 'snippets/username.html' with user=status.user %}
{{ status.content | safe }} {% if status.status_type == 'GeneratedNote' %}
{{ status.content | safe }} {% include 'snippets/book_titleby.html' with book=status.mention_books.first %}
{% elif status.status_type == 'Boost' %}
boosted {% include 'snippets/avatar.html' with user=status.boosted_status.user %}{% include 'snippets/username.html' with user=status.boosted_status.user possessive=True %} status
{% elif status.status_type == 'Review' and not status.name and not status.content%} {% elif status.status_type == 'Review' and not status.name and not status.content%}
rated <a href="/book/{{ status.book.id }}">{{ status.book.title }}</a> rated <a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>
{% elif status.status_type == 'Review' %} {% elif status.status_type == 'Review' %}
@ -13,13 +16,10 @@
commented on <a href="/book/{{ status.book.id }}">{{ status.book.title }}</a> commented on <a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>
{% elif status.status_type == 'Quotation' %} {% elif status.status_type == 'Quotation' %}
quoted <a href="/book/{{ status.book.id }}">{{ status.book.title }}</a> quoted <a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>
{% elif status.status_type == 'Boost' %}
boosted
{% elif status.reply_parent %} {% elif status.reply_parent %}
{% with parent_status=status|parent %} {% with parent_status=status|parent %}
replied to {% include 'snippets/username.html' with user=parent_status.user possessive=True %} <a href="{{parent_status.remote_id }}">{{ parent_status.status_type | lower }}</a> replied to {% include 'snippets/username.html' with user=parent_status.user possessive=True %} <a href="{{parent_status.remote_id }}">{% if parent_status.status_type == 'GeneratedNote' %}update{% else %}{{ parent_status.status_type | lower }}{% endif %}</a>
{% endwith %} {% endwith %}
{% endif %} {% endif %}
<span class="time-ago"> </p>
<a href="{{ status.remote_id }}">{{ status.published_date | naturaltime }}</a> </div>
</span>

View file

@ -1,6 +0,0 @@
{% for tab in tabs %}
<div class="tab {% if tab.id == active_tab %}active{% endif %}">
<a href="{{ path }}/{{ tab.id }}">{{ tab.display }}</a>
</div>
{% endfor %}

View file

@ -1,20 +1,24 @@
<div class="control">
<div class="tags has-addons">
<a class="tag is-link" href="/tag/{{ tag.identifier|urlencode }}">
{{ tag.name }}
</a>
<div class="tag"> <div class="tag">
<a href="/tag/{{ tag.identifier|urlencode }}">{{ tag.name }}</a>
{% if tag.identifier in user_tags %} {% if tag.identifier in user_tags %}
<form class="tag-form" name="tag" action="/untag/" method="post"> <form name="tag" action="/untag/" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="name" value="{{ tag.name }}"> <input type="hidden" name="name" value="{{ tag.name }}">
<button type="submit">x</button> <button type="submit">x<span class="is-sr-only"> remove tag</span></button>
</form> </form>
{% else %} {% else %}
<form class="tag-form" name="tag" action="/tag/" method="post"> <form name="tag" action="/tag/" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="name" value="{{ tag.name }}"> <input type="hidden" name="name" value="{{ tag.name }}">
<button type="submit">+</button> <button type="submit">+<span class="is-sr-only"> add tag</span></button>
</form> </form>
{% endif %} {% endif %}
</div> </div>
</div>
</div>

View file

@ -1,4 +1,6 @@
{% load fr_display %} {% load fr_display %}
<div class="block">
{% with depth=depth|add:1 %} {% with depth=depth|add:1 %}
{% if depth <= max_depth and status.reply_parent and direction <= 0 %} {% if depth <= max_depth and status.reply_parent and direction <= 0 %}
{% with direction=-1 %} {% with direction=-1 %}
@ -16,3 +18,5 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endwith %} {% endwith %}
</div>

View file

@ -1,11 +0,0 @@
<div>
<div>
{% include 'snippets/avatar.html' with user=user %}
{% include 'snippets/username.html' with user=user %}
<small>{{ user.username }}</small>
</div>
{% if not is_self %}
{% include 'snippets/follow_button.html' with user=user %}
{% endif %}
</div>

View file

@ -1,11 +1,9 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<div id="content"> <div class="block">
<div class="comment-thread">
{% include 'snippets/thread.html' with status=status depth=0 max_depth=6 is_root=True direction=0 %} {% include 'snippets/thread.html' with status=status depth=0 max_depth=6 is_root=True direction=0 %}
</div> </div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,20 +1,12 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load fr_display %} {% load fr_display %}
{% block content %} {% block content %}
<div class="content-container">
<h2>Books tagged "{{ tag.name }}"</h2> <div class="block">
<div class="book-grid row wrap shrink"> <h2 class="title">Books tagged "{{ tag.name }}"</h2>
{% for book in books.all %} {% include 'snippets/book_tiles.html' with books=books.all %}
<div class="cover-container">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/rate_action.html' with user=request.user book=book %}
{% include 'snippets/shelve_button.html' with book=book %}
</div>
{% endfor %}
</div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,18 +1,43 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<div class="block">
{% include 'user_header.html' with user=user %} {% include 'user_header.html' with user=user %}
{% include 'snippets/covers_shelf.html' with shelves=shelves user=user %} </div>
<div class="block">
<h2 class="title">Shelves</h2>
<div class="columns">
{% for shelf in shelves %}
<div class="column is-narrow">
<h3>{{ shelf.name }}
{% if shelf.size > 3 %}<small>(<a href="{{ shelf.remote_id }}">See all {{ shelf.size }}</a>)</small>{% endif %}</h3>
<div class="is-mobile field is-grouped">
{% for book in shelf.books %}
<div class="control">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book size="medium" %}
</a>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<small><a href="/user/{{ user.localname }}/shelves">See all {{ shelf_count }} shelves</a></small>
</div>
<div> <div>
<div class="content-container"><h2>User Activity</h2></div> <div class="block">
<h2 class="title">User Activity</h2>
</div>
{% for activity in activities %} {% for activity in activities %}
<div class="content-container"> <div class="block">
{% include 'snippets/status.html' with status=activity %} {% include 'snippets/status.html' with status=activity %}
</div> </div>
{% endfor %} {% endfor %}
{% if not activities %} {% if not activities %}
<div class="content-container"> <div class="block">
<p>No activities yet!</a> <p>No activities yet!</a>
</div> </div>
{% endif %} {% endif %}

View file

@ -1,22 +1,26 @@
{% load humanize %} {% load humanize %}
{% load fr_display %} {% load fr_display %}
<div class="content-container user-profile"> <div class="block">
<h2>User Profile <div class="level">
<h2 class="title">User Profile</h2>
{% if is_self %} {% if is_self %}
<div class="level-right">
<a href="/user-edit/" class="edit-link">edit <a href="/user-edit/" class="edit-link">edit
<span class="icon icon-pencil"> <span class="icon icon-pencil">
<span class="hidden-text">Edit profile</span> <span class="is-sr-only">Edit profile</span>
</span> </span>
</a> </a>
</div>
{% endif %} {% endif %}
</h2>
<div class="row">
<div class="pic-container">
{% include 'snippets/avatar.html' with user=user large=True %}
</div> </div>
<div> <div class="columns">
<div class="column is-narrow">
<div class="media">
<div class="media-left">
{% include 'snippets/avatar.html' with user=user large=True %}
</div>
<div class="media-content">
<p>{% if user.name %}{{ user.name }}{% else %}{{ user.localname }}{% endif %}</p> <p>{% if user.name %}{{ user.name }}{% else %}{{ user.localname }}{% endif %}</p>
<p>{{ user.username }}</p> <p>{{ user.username }}</p>
<p>Joined {{ user.created_date | naturaltime }}</p> <p>Joined {{ user.created_date | naturaltime }}</p>
@ -24,11 +28,15 @@
<a href="/user/{{ user|username }}/followers">{{ user.followers.count }} follower{{ user.followers.count | pluralize }}</a>, <a href="/user/{{ user|username }}/followers">{{ user.followers.count }} follower{{ user.followers.count | pluralize }}</a>,
<a href="/user/{{ user|username }}/following">{{ user.following.count }} following</a></p> <a href="/user/{{ user|username }}/following">{{ user.following.count }} following</a></p>
</div> </div>
</div>
</div>
<div class="column">
{% if user.summary %} {% if user.summary %}
<blockquote><span class="icon icon-quote-open"></span>{{ user.summary | safe }}</blockquote> <blockquote><span class="icon icon-quote-open"></span>{{ user.summary | safe }}</blockquote>
{% endif %} {% endif %}
</div> </div>
</div>
{% if not is_self %} {% if not is_self %}
{% include 'snippets/follow_button.html' with user=user %} {% include 'snippets/follow_button.html' with user=user %}
{% endif %} {% endif %}

View file

@ -1,9 +1,14 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<div class="content-container"> <div class="block">
<h2 class="title">User search results</h2>
{% if not results %}
<p>No results found for "{{ query }}"</p>
{% endif %}
{% for result in results %} {% for result in results %}
<div> <div class="block">
<h2>{{ result.username }}</h2> {% include 'snippets/avatar.html' with user=result %}</h2>
{% include 'snippets/username.html' with user=result show_full=True %}</h2>
{% include 'snippets/follow_button.html' with user=result %} {% include 'snippets/follow_button.html' with user=result %}
</div> </div>
{% endfor %} {% endfor %}

View file

@ -4,8 +4,8 @@
{% include 'user_header.html' with user=user %} {% include 'user_header.html' with user=user %}
{% for shelf in shelves %} {% for shelf in shelves %}
<div class="content-container"> <div class="block">
<h2>{{ shelf.name }}</h2> <h2 class="title">{{ shelf.name }}</h2>
{% include 'snippets/shelf.html' with shelf=shelf ratings=ratings %} {% include 'snippets/shelf.html' with shelf=shelf ratings=ratings %}
</div> </div>
{% endfor %} {% endfor %}

View file

@ -25,27 +25,6 @@ def get_rating(book, user):
return 0 return 0
@register.filter(name='description')
def description_format(description):
''' handle the various OL description formats '''
if not description:
return ''
if '----------' in description:
description = description.split('----------')[0]
return description.strip()
@register.filter(name='author_bio')
def bio_format(bio):
''' clean up OL author bios '''
if isinstance(bio, dict) and 'value' in bio:
bio = bio['value']
bio = bio.split('\n')
return bio[0].strip()
@register.filter(name='username') @register.filter(name='username')
def get_user_identifier(user): def get_user_identifier(user):
''' use localname for local users, username for remote ''' ''' use localname for local users, username for remote '''
@ -67,12 +46,6 @@ def get_replies(status):
).select_subclasses().all()[:10] ).select_subclasses().all()[:10]
@register.filter(name='reply_count')
def get_reply_count(status):
''' how many replies does a status have? '''
return models.Status.objects.filter(reply_parent=status).count()
@register.filter(name='parent') @register.filter(name='parent')
def get_parent(status): def get_parent(status):
''' get the reply parent for a status ''' ''' get the reply parent for a status '''
@ -170,18 +143,6 @@ def shelve_button_text(context, book):
return 'Want to read' return 'Want to read'
@register.simple_tag(takes_context=True)
def current_shelf(context, book):
''' check what shelf a user has a book on, if any '''
try:
shelf = models.ShelfBook.objects.get(
shelf__user=context['user'],
book=book
)
except models.ShelfBook.DoesNotExist:
return None
return shelf.name
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)
def latest_read_through(book, user): def latest_read_through(book, user):
''' the most recent read activity ''' ''' the most recent read activity '''

View file

@ -38,14 +38,13 @@ urlpatterns = [
# ui views # ui views
re_path(r'^login/?$', views.login_page), re_path(r'^login/?$', views.login_page),
re_path(r'^register/?$', views.register_page),
re_path(r'^about/?$', views.about_page), re_path(r'^about/?$', views.about_page),
re_path(r'^invite/?$', views.manage_invites),
re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', views.invite_page), re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', views.invite_page),
re_path(r'^manage_invites/?$', views.manage_invites),
path('', views.home), path('', views.home),
re_path(r'^(?P<tab>home|local|federated)/?$', views.home_tab),
re_path(r'^notifications/?', views.notifications_page), re_path(r'^notifications/?', views.notifications_page),
re_path(r'books/?$', views.books_page),
re_path(r'import/?$', views.import_page), re_path(r'import/?$', views.import_page),
re_path(r'import_status/(\d+)/?$', views.import_status), re_path(r'import_status/(\d+)/?$', views.import_status),
re_path(r'user-edit/?$', views.edit_profile_page), re_path(r'user-edit/?$', views.edit_profile_page),
@ -66,8 +65,6 @@ urlpatterns = [
# books # books
re_path(r'%s(.json)?/?$' % book_path, views.book_page), re_path(r'%s(.json)?/?$' % book_path, views.book_page),
re_path(r'%s/(?P<tab>friends|local|federated)?$' % \
book_path, views.book_page),
re_path(r'%s/edit/?$' % book_path, views.edit_book_page), re_path(r'%s/edit/?$' % book_path, views.edit_book_page),
re_path(r'^editions/(?P<work_id>\d+)/?$', views.editions_page), re_path(r'^editions/(?P<work_id>\d+)/?$', views.editions_page),
@ -84,7 +81,7 @@ urlpatterns = [
# internal action endpoints # internal action endpoints
re_path(r'^logout/?$', actions.user_logout), re_path(r'^logout/?$', actions.user_logout),
re_path(r'^user-login/?$', actions.user_login), re_path(r'^user-login/?$', actions.user_login),
re_path(r'^register/?$', actions.register), re_path(r'^user-register/?$', actions.register),
re_path(r'^edit_profile/?$', actions.edit_profile), re_path(r'^edit_profile/?$', actions.edit_profile),
re_path(r'^import_data/?', actions.import_data), re_path(r'^import_data/?', actions.import_data),

View file

@ -144,7 +144,7 @@ def resolve_book(request):
def edit_book(request, book_id): def edit_book(request, book_id):
''' edit a book cool ''' ''' edit a book cool '''
if not request.method == 'POST': if not request.method == 'POST':
return redirect('/book/%s' % request.user.localname) return redirect('/book/%s' % book_id)
try: try:
book = models.Edition.objects.get(id=book_id) book = models.Edition.objects.get(id=book_id)
@ -444,4 +444,4 @@ def create_invite(request):
invite.user = request.user invite.user = request.user
invite.save() invite.save()
return redirect('/manage_invites') return redirect('/invite')

View file

@ -2,7 +2,7 @@
import re import re
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Avg, Q from django.db.models import Avg, Count, Q
from django.http import HttpResponseBadRequest, HttpResponseNotFound,\ from django.http import HttpResponseBadRequest, HttpResponseNotFound,\
JsonResponse JsonResponse
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
@ -43,60 +43,54 @@ def not_found_page(request, _):
@login_required @login_required
def home(request): def home(request):
''' this is the same as the feed on the home tab '''
return home_tab(request, 'home')
@login_required
def home_tab(request, tab):
''' user's homepage with activity feed ''' ''' user's homepage with activity feed '''
# TODO: why on earth would this be where the pagination is set
page_size = 15 page_size = 15
try: try:
page = int(request.GET.get('page', 1)) page = int(request.GET.get('page', 1))
except ValueError: except ValueError:
page = 1 page = 1
shelves = [] count = 5
shelves = get_user_shelf_preview( querysets = [
request.user, # recemt currently reading
[('reading', 3), ('read', 1), ('to-read', 3)] models.Edition.objects.filter(
) shelves__user=request.user,
size = sum(len(s['books']) for s in shelves) shelves__identifier='reading'
# books new to the instance, for discovery ),
if size < 6: # read
recent_books = models.Work.objects.order_by( models.Edition.objects.filter(
'-created_date' shelves__user=request.user,
)[:6 - size] shelves__identifier='read'
recent_books = [b.default_edition for b in recent_books] )[:2],
shelves.append({ # to-read
'name': 'Recently added', models.Edition.objects.filter(
'identifier': None, shelves__user=request.user,
'books': recent_books, shelves__identifier='to-read'
'count': 6 - size, ),
}) # popular books
models.Edition.objects.annotate(
shelf_count=Count('shelves')
).order_by('-shelf_count')
]
suggested_books = []
for queryset in querysets:
length = count - len(suggested_books)
suggested_books += list(queryset[:length])
if len(suggested_books) >= count:
break
activities = get_activity_feed(request.user, 'home')
# allows us to check if a user has shelved a book
user_books = models.Edition.objects.filter(shelves__user=request.user).all()
activities = get_activity_feed(request.user, tab)
activity_count = activities.count() activity_count = activities.count()
activities = activities[(page - 1) * page_size:page * page_size] activities = activities[(page - 1) * page_size:page * page_size]
next_page = '/?page=%d' % (page + 1) next_page = '/?page=%d#feed' % (page + 1)
prev_page = '/?page=%d' % (page - 1) prev_page = '/?page=%d#feed' % (page - 1)
data = { data = {
'user': request.user, 'user': request.user,
'shelves': shelves, 'suggested_books': suggested_books,
'user_books': user_books,
'activities': activities, 'activities': activities,
'feed_tabs': [
{'id': 'home', 'display': 'Home'},
{'id': 'local', 'display': 'Local'},
{'id': 'federated', 'display': 'Federated'}
],
'active_tab': tab,
'review_form': forms.ReviewForm(), 'review_form': forms.ReviewForm(),
'quotation_form': forms.QuotationForm(), 'quotation_form': forms.QuotationForm(),
'comment_form': forms.CommentForm(), 'comment_form': forms.CommentForm(),
@ -147,9 +141,9 @@ def search(request):
query = request.GET.get('q') query = request.GET.get('q')
if re.match(r'\w+@\w+.\w+', query): if re.match(r'\w+@\w+.\w+', query):
# if something looks like a username, search with webfinger # if something looks like a username, search with webfinger
results = [outgoing.handle_account_search(query)] results = outgoing.handle_account_search(query)
return TemplateResponse( return TemplateResponse(
request, 'user_results.html', {'results': results} request, 'user_results.html', {'results': results, 'query': query}
) )
# or just send the question over to book search # or just send the question over to book search
@ -163,23 +157,6 @@ def search(request):
return TemplateResponse(request, 'book_results.html', {'results': results}) return TemplateResponse(request, 'book_results.html', {'results': results})
def books_page(request):
''' discover books '''
recent_books = models.Work.objects
recent_books = recent_books.order_by('-created_date')[:50]
recent_books = [b.default_edition for b in recent_books]
if request.user.is_authenticated:
recent_books = models.Edition.objects.filter(
~Q(shelfbook__shelf__user=request.user),
id__in=[b.id for b in recent_books if b],
)
data = {
'books': recent_books,
}
return TemplateResponse(request, 'books.html', data)
@login_required @login_required
def import_page(request): def import_page(request):
''' import history from goodreads ''' ''' import history from goodreads '''
@ -216,6 +193,16 @@ def login_page(request):
return TemplateResponse(request, 'login.html', data) return TemplateResponse(request, 'login.html', data)
def register_page(request):
''' authentication '''
# send user to the login page
data = {
'site_settings': models.SiteSettings.get(),
'register_form': forms.RegisterForm(),
}
return TemplateResponse(request, 'register.html', data)
def about_page(request): def about_page(request):
''' more information about the instance ''' ''' more information about the instance '''
data = { data = {
@ -276,8 +263,6 @@ def user_page(request, username, subpage=None):
return JsonResponse(user.to_activity(), encoder=ActivityEncoder) return JsonResponse(user.to_activity(), encoder=ActivityEncoder)
# otherwise we're at a UI view # otherwise we're at a UI view
# TODO: change display with privacy and authentication considerations
data = { data = {
'user': user, 'user': user,
'is_self': request.user.id == user.id, 'is_self': request.user.id == user.id,
@ -292,10 +277,22 @@ def user_page(request, username, subpage=None):
data['shelves'] = user.shelf_set.all() data['shelves'] = user.shelf_set.all()
return TemplateResponse(request, 'user_shelves.html', data) return TemplateResponse(request, 'user_shelves.html', data)
shelves = get_user_shelf_preview(user) data['shelf_count'] = user.shelf_set.count()
shelves = []
for shelf in user.shelf_set.all():
if not shelf.books.count():
continue
shelves.append({
'name': shelf.name,
'remote_id': shelf.remote_id,
'books': shelf.books.all()[:3],
'size': shelf.books.count(),
})
if len(shelves) > 2:
break
data['shelves'] = shelves data['shelves'] = shelves
activities = get_activity_feed(user, 'self')[:15] data['activities'] = get_activity_feed(user, 'self')[:15]
data['activities'] = activities
return TemplateResponse(request, 'user.html', data) return TemplateResponse(request, 'user.html', data)
@ -398,7 +395,7 @@ def edit_profile_page(request):
return TemplateResponse(request, 'edit_user.html', data) return TemplateResponse(request, 'edit_user.html', data)
def book_page(request, book_id, tab='friends'): def book_page(request, book_id):
''' info about a book ''' ''' info about a book '''
book = models.Book.objects.select_subclasses().get(id=book_id) book = models.Book.objects.select_subclasses().get(id=book_id)
if is_api_request(request): if is_api_request(request):
@ -413,33 +410,15 @@ def book_page(request, book_id, tab='friends'):
if not work: if not work:
return HttpResponseNotFound() return HttpResponseNotFound()
book_reviews = models.Review.objects.filter(book__in=work.edition_set.all()) reviews = models.Review.objects.filter(
book__in=work.edition_set.all(),
).order_by('-published_date')
user_tags = []
if request.user.is_authenticated: if request.user.is_authenticated:
user_reviews = book_reviews.filter(
user=request.user,
).all()
reviews = get_activity_feed(request.user, tab, model=book_reviews)
try:
# TODO: books can be on multiple shelves
shelf = models.Shelf.objects.filter(
user=request.user,
edition=book
).first()
except models.Shelf.DoesNotExist:
shelf = None
user_tags = models.Tag.objects.filter( user_tags = models.Tag.objects.filter(
book=book, user=request.user book=book, user=request.user
).values_list('identifier', flat=True) ).values_list('identifier', flat=True)
else:
tab = 'public'
reviews = book_reviews.filter(privacy='public')
shelf = None
user_reviews = []
user_tags = []
rating = reviews.aggregate(Avg('rating')) rating = reviews.aggregate(Avg('rating'))
tags = models.Tag.objects.filter( tags = models.Tag.objects.filter(
@ -450,20 +429,15 @@ def book_page(request, book_id, tab='friends'):
data = { data = {
'book': book, 'book': book,
'shelf': shelf, 'reviews': reviews.filter(content__isnull=False),
'user_reviews': user_reviews, 'ratings': reviews.filter(content__isnull=True),
'reviews': reviews.distinct(),
'rating': rating['rating__avg'], 'rating': rating['rating__avg'],
'tags': tags, 'tags': tags,
'user_tags': user_tags, 'user_tags': user_tags,
'review_form': forms.ReviewForm(), 'review_form': forms.ReviewForm(),
'quotation_form': forms.QuotationForm(),
'comment_form': forms.CommentForm(),
'tag_form': forms.TagForm(), 'tag_form': forms.TagForm(),
'feed_tabs': [
{'id': 'friends', 'display': 'Friends'},
{'id': 'local', 'display': 'Local'},
{'id': 'federated', 'display': 'Federated'}
],
'active_tab': tab,
'path': '/book/%s' % book_id, 'path': '/book/%s' % book_id,
'cover_form': forms.CoverForm(instance=book), 'cover_form': forms.CoverForm(instance=book),
'info_fields': [ 'info_fields': [
@ -555,42 +529,3 @@ def shelf_page(request, username, shelf_identifier):
'user': user, 'user': user,
} }
return TemplateResponse(request, 'shelf.html', data) return TemplateResponse(request, 'shelf.html', data)
def get_user_shelf_preview(user, shelf_proportions=None):
''' data for the covers shelf (user page and feed page) '''
shelves = []
shelf_max = 6
if not shelf_proportions:
shelf_proportions = [('reading', 3), ('read', 2), ('to-read', -1)]
for (identifier, count) in shelf_proportions:
if shelf_max <= 0:
break
if count > shelf_max or count < 0:
count = shelf_max
try:
shelf = models.Shelf.objects.get(
user=user,
identifier=identifier,
)
except models.Shelf.DoesNotExist:
continue
if not shelf.books.count():
continue
books = models.ShelfBook.objects.filter(
shelf=shelf,
).order_by(
'-updated_date'
)[:count]
shelf_max -= len(books)
shelves.append({
'name': shelf.name,
'identifier': shelf.identifier,
'books': [b.book for b in books],
'size': shelf.books.count(),
})
return shelves

3
fr-dev
View file

@ -44,6 +44,9 @@ case "$1" in
test_report) test_report)
docker-compose exec web coverage report docker-compose exec web coverage report
;; ;;
collectstatic)
docker-compose exec web python manage.py collectstatic
;;
*) *)
echo "Unrecognised command. Try: up, initdb, resetdb, makemigrations, migrate, shell, dbshell, restart_celery, test, test_report" echo "Unrecognised command. Try: up, initdb, resetdb, makemigrations, migrate, shell, dbshell, restart_celery, test, test_report"
;; ;;