mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-26 11:31:08 +00:00
Redesign (front page, login page)
This commit is contained in:
parent
67e7eaaf85
commit
3efc8d45c3
30 changed files with 769 additions and 249 deletions
|
@ -28,15 +28,15 @@ class RegisterForm(ModelForm):
|
||||||
class ReviewForm(ModelForm):
|
class ReviewForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Review
|
model = models.Review
|
||||||
fields = ['name', 'content', 'rating']
|
fields = ['name', 'rating', 'content']
|
||||||
help_texts = {f: None for f in fields}
|
help_texts = {f: None for f in fields}
|
||||||
review_content = IntegerField(validators=[
|
content = IntegerField(validators=[
|
||||||
MinValueValidator(0), MaxValueValidator(5)
|
MinValueValidator(0), MaxValueValidator(5)
|
||||||
])
|
])
|
||||||
labels = {
|
labels = {
|
||||||
'name': 'Title',
|
'name': 'Title',
|
||||||
'review_content': 'Review',
|
|
||||||
'rating': 'Rating (out of 5)',
|
'rating': 'Rating (out of 5)',
|
||||||
|
'content': 'Review',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,6 @@ class Status(FedireadsModel):
|
||||||
return '%s/%s/%d' % (base_path, model_name, self.id)
|
return '%s/%s/%d' % (base_path, model_name, self.id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Review(Status):
|
class Review(Status):
|
||||||
''' a book review '''
|
''' a book review '''
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
|
|
@ -111,12 +111,14 @@ def handle_outgoing_accept(user, to_follow, follow_request):
|
||||||
broadcast(to_follow, activity, recipient)
|
broadcast(to_follow, activity, recipient)
|
||||||
|
|
||||||
def handle_outgoing_reject(user, to_follow, relationship):
|
def handle_outgoing_reject(user, to_follow, relationship):
|
||||||
|
''' a local user who managed follows rejects a follow request '''
|
||||||
relationship.delete()
|
relationship.delete()
|
||||||
|
|
||||||
activity = activitypub.get_reject(to_follow, relationship)
|
activity = activitypub.get_reject(to_follow, relationship)
|
||||||
recipient = get_recipients(to_follow, 'direct', direct_recipients=[user])
|
recipient = get_recipients(to_follow, 'direct', direct_recipients=[user])
|
||||||
broadcast(to_follow, activity, recipient)
|
broadcast(to_follow, activity, recipient)
|
||||||
|
|
||||||
|
|
||||||
def handle_shelve(user, book, shelf):
|
def handle_shelve(user, book, shelf):
|
||||||
''' a local user is getting a book put on their shelf '''
|
''' a local user is getting a book put on their shelf '''
|
||||||
# update the database
|
# update the database
|
||||||
|
@ -132,9 +134,10 @@ def handle_shelve(user, book, shelf):
|
||||||
'reading': 'started reading',
|
'reading': 'started reading',
|
||||||
'read': 'finished reading'
|
'read': 'finished reading'
|
||||||
}[shelf.identifier]
|
}[shelf.identifier]
|
||||||
name = user.name if user.name else user.localname
|
message = '%s %s' % (verb, book.title)
|
||||||
message = '%s %s %s' % (name, verb, book.title)
|
|
||||||
status = create_status(user, message, mention_books=[book])
|
status = create_status(user, message, mention_books=[book])
|
||||||
|
status.status_type = 'Update'
|
||||||
|
status.save()
|
||||||
|
|
||||||
activity = activitypub.get_status(status)
|
activity = activitypub.get_status(status)
|
||||||
create_activity = activitypub.get_create(user, activity)
|
create_activity = activitypub.get_create(user, activity)
|
||||||
|
|
BIN
fedireads/static/fonts/icomoon.eot
Normal file
BIN
fedireads/static/fonts/icomoon.eot
Normal file
Binary file not shown.
36
fedireads/static/fonts/icomoon.svg
Normal file
36
fedireads/static/fonts/icomoon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
BIN
fedireads/static/fonts/icomoon.ttf
Normal file
BIN
fedireads/static/fonts/icomoon.ttf
Normal file
Binary file not shown.
BIN
fedireads/static/fonts/icomoon.woff
Normal file
BIN
fedireads/static/fonts/icomoon.woff
Normal file
Binary file not shown.
|
@ -10,13 +10,21 @@ html {
|
||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
body {
|
||||||
a {
|
padding-top: 90px;
|
||||||
color: #00F;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited {
|
|
||||||
color: #808;
|
a {
|
||||||
|
color: #247BA0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, button {
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4 {
|
h1, h2, h3, h4 {
|
||||||
|
@ -29,16 +37,21 @@ h1 {
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
background-color: #B2DBBF;
|
|
||||||
padding: 0.5rem 0.2rem;
|
padding: 0.5rem 0.2rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
height: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#top-bar {
|
#top-bar {
|
||||||
background-color: #70C1B3;
|
overflow: visible;
|
||||||
overflow: hidden;
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
border-bottom: 3px solid #247BA0;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #FFF;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
height: 47px;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#warning {
|
#warning {
|
||||||
|
@ -46,62 +59,183 @@ h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#branding, #actions {
|
|
||||||
margin: 0 1rem;
|
|
||||||
}
|
|
||||||
#branding {
|
|
||||||
flex-grow: 1;
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
#branding a {
|
#branding a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: black;
|
|
||||||
}
|
}
|
||||||
#actions {
|
#actions {
|
||||||
flex-grow: 0;
|
margin-top: 1em;
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#main, header > div {
|
#actions > * {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
button .icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
#search button {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main, header {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
max-width: 55rem;
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
max-width: 75rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
#main {
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#feed, #content, #sidebar {
|
ul.menu {
|
||||||
|
list-style: none;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
ul.menu li {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: white;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
ul.menu a {
|
||||||
|
color: #555;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulldown-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.pulldown {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
list-style: none;
|
||||||
|
background: white;
|
||||||
|
padding: 1em;
|
||||||
|
text-align: right;
|
||||||
|
right: 0;
|
||||||
|
box-shadow: 0 5px 10px rgba(0,0,0,0.15);
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
.pulldown-container:hover .pulldown {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#feed {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0 1rem 1rem 0;
|
padding-top: 70px;
|
||||||
|
position: relative;
|
||||||
|
top: -50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
.row {
|
||||||
min-width: 20rem;
|
display: flex;
|
||||||
margin-right: 0;
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.row > * {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: min-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
form input {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
border: 2px solid #247BA0;
|
||||||
|
color: #247BA0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login h2 {
|
||||||
|
border-bottom: 3px solid #B2DBBF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
border-bottom: 3px solid #FF1654;
|
||||||
|
padding-left: 1em;
|
||||||
}
|
}
|
||||||
.tab.active {
|
.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;
|
background-color: #FF1654;
|
||||||
}
|
}
|
||||||
|
.tabs.secondary .tab.active {
|
||||||
|
background-color: #247BA0;
|
||||||
|
}
|
||||||
|
.tab.active a {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
.user-pic {
|
.user-pic {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: auto;
|
height: 2rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
position: relative;
|
position: relative;
|
||||||
bottom: 0.5em;
|
bottom: 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-form label {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-ago {
|
.time-ago {
|
||||||
|
@ -110,7 +244,7 @@ h2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-preview {
|
.book-preview {
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-preview img {
|
.book-preview img {
|
||||||
|
@ -122,17 +256,89 @@ h2 {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
.content-container > * {
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-shelves {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow-y: hidden;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.all-shelves h2 {
|
||||||
|
border-bottom: 3px solid #B2DBBF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.covers-shelf {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.covers-shelf .book-preview {
|
||||||
|
margin-right: 1em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
overflow: unset;
|
||||||
|
width: min-content;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.covers-shelf .book-preview button {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.covers-shelf .book-preview:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
.covers-shelf .book-preview:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.covers-shelf .book-preview:hover img {
|
||||||
|
box-shadow: #F3FFBD 0em 0em 1em 1em;
|
||||||
|
}
|
||||||
|
.covers-shelf .book-preview img {
|
||||||
|
float: none;
|
||||||
|
height: 11rem;
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
float: right;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compose-suggestion {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.compose-suggestion.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.book-cover.small {
|
.book-cover.small {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
.compose-suggestion .book-preview {
|
||||||
#content > div, #feed > div, #sidebar > div {
|
background-color: #EEE;
|
||||||
background-color: #EFEFEF;
|
padding: 1em;
|
||||||
margin: 1rem 0 0 1rem;
|
|
||||||
}
|
|
||||||
#content > div > *, #feed > div > *, #sidebar > div > * {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
|
@ -150,22 +356,14 @@ h2 {
|
||||||
width: 30rem;
|
width: 30rem;
|
||||||
height: 10rem;
|
height: 10rem;
|
||||||
}
|
}
|
||||||
.review {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
small {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.interaction {
|
.interaction {
|
||||||
background-color: #F3FFBD;
|
background-color: #B2DBBF;
|
||||||
clear: both;
|
border-radius: 0 0 0.5em 0.5em;
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.interaction textarea {
|
.interaction textarea {
|
||||||
|
@ -201,27 +399,44 @@ th, td {
|
||||||
.comment-thread .reply h2 {
|
.comment-thread .reply h2 {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
.post.main {
|
|
||||||
background-color: #F3FFBD;
|
|
||||||
}
|
|
||||||
.post {
|
.post {
|
||||||
|
background-color: #EFEFEF;
|
||||||
|
padding-top: 1em;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
.post h2, .compose-suggestion h2 {
|
||||||
|
position: relative;
|
||||||
|
right: 2em;
|
||||||
|
}
|
||||||
|
.post .user-pic, .compose-suggestion .user-pic {
|
||||||
|
right: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-thread .post {
|
||||||
margin-left: 4em;
|
margin-left: 4em;
|
||||||
border-left: 2px solid #247BA0;
|
border-left: 2px solid #247BA0;
|
||||||
}
|
}
|
||||||
.post.depth-1 {
|
.comment-thread .post.depth-1 {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
.post.depth-2 {
|
.comment-thread .post.depth-2 {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
}
|
}
|
||||||
.post.depth-3 {
|
.comment-thread .post.depth-3 {
|
||||||
margin-left: 2em;
|
margin-left: 2em;
|
||||||
}
|
}
|
||||||
.post.depth-4 {
|
.comment-thread .post.depth-4 {
|
||||||
margin-left: 3em;
|
margin-left: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unread {
|
.unread {
|
||||||
background-color: #F3FFBD;
|
background-color: #DDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-text {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
position: absolute;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
138
fedireads/static/icons.css
Normal file
138
fedireads/static/icons.css
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'icomoon';
|
||||||
|
src: url('fonts/icomoon.eot?v0wquk');
|
||||||
|
src: url('fonts/icomoon.eot?v0wquk#iefix') format('embedded-opentype'),
|
||||||
|
url('fonts/icomoon.ttf?v0wquk') format('truetype'),
|
||||||
|
url('fonts/icomoon.woff?v0wquk') format('woff'),
|
||||||
|
url('fonts/icomoon.svg?v0wquk#icomoon') format('svg');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[class^="icon-"], [class*=" icon-"] {
|
||||||
|
/* use !important to prevent issues with browser extensions that change fonts */
|
||||||
|
font-family: 'icomoon' !important;
|
||||||
|
speak: none;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
text-transform: none;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
/* Better Font Rendering =========== */
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-arrow-right:before {
|
||||||
|
content: "\e900";
|
||||||
|
}
|
||||||
|
.icon-arrow-left:before {
|
||||||
|
content: "\e910";
|
||||||
|
}
|
||||||
|
.icon-arrow-up:before {
|
||||||
|
content: "\e911";
|
||||||
|
}
|
||||||
|
.icon-arrow-down:before {
|
||||||
|
content: "\e912";
|
||||||
|
}
|
||||||
|
.icon-x:before {
|
||||||
|
content: "\e902";
|
||||||
|
}
|
||||||
|
.icon-cancel:before {
|
||||||
|
content: "\e902";
|
||||||
|
}
|
||||||
|
.icon-close:before {
|
||||||
|
content: "\e902";
|
||||||
|
}
|
||||||
|
.icon-search:before {
|
||||||
|
content: "\e986";
|
||||||
|
}
|
||||||
|
.icon-star-empty:before {
|
||||||
|
content: "\e9d7";
|
||||||
|
}
|
||||||
|
.icon-star-half:before {
|
||||||
|
content: "\e9d8";
|
||||||
|
}
|
||||||
|
.icon-star-full:before {
|
||||||
|
content: "\e9d9";
|
||||||
|
}
|
||||||
|
.icon-heart:before {
|
||||||
|
content: "\e9da";
|
||||||
|
}
|
||||||
|
.icon-local:before {
|
||||||
|
content: "\e914";
|
||||||
|
}
|
||||||
|
.icon-home:before {
|
||||||
|
content: "\e913";
|
||||||
|
}
|
||||||
|
.icon-quote-close:before {
|
||||||
|
content: "\e903";
|
||||||
|
}
|
||||||
|
.icon-quote-open:before {
|
||||||
|
content: "\e904";
|
||||||
|
}
|
||||||
|
.icon-image:before {
|
||||||
|
content: "\e905";
|
||||||
|
}
|
||||||
|
.icon-photo:before {
|
||||||
|
content: "\e905";
|
||||||
|
}
|
||||||
|
.icon-picture-o:before {
|
||||||
|
content: "\e905";
|
||||||
|
}
|
||||||
|
.icon-pencil:before {
|
||||||
|
content: "\e906";
|
||||||
|
}
|
||||||
|
.icon-list:before {
|
||||||
|
content: "\e907";
|
||||||
|
}
|
||||||
|
.icon-unlock:before {
|
||||||
|
content: "\e908";
|
||||||
|
}
|
||||||
|
.icon-unlisted:before {
|
||||||
|
content: "\e908";
|
||||||
|
}
|
||||||
|
.icon-globe:before {
|
||||||
|
content: "\e909";
|
||||||
|
}
|
||||||
|
.icon-global:before {
|
||||||
|
content: "\e909";
|
||||||
|
}
|
||||||
|
.icon-federated:before {
|
||||||
|
content: "\e909";
|
||||||
|
}
|
||||||
|
.icon-public:before {
|
||||||
|
content: "\e909";
|
||||||
|
}
|
||||||
|
.icon-lock:before {
|
||||||
|
content: "\e90a";
|
||||||
|
}
|
||||||
|
.icon-private:before {
|
||||||
|
content: "\e90a";
|
||||||
|
}
|
||||||
|
.icon-chain-broken:before {
|
||||||
|
content: "\e90b";
|
||||||
|
}
|
||||||
|
.icon-unlink:before {
|
||||||
|
content: "\e90b";
|
||||||
|
}
|
||||||
|
.icon-chain:before {
|
||||||
|
content: "\e90c";
|
||||||
|
}
|
||||||
|
.icon-link:before {
|
||||||
|
content: "\e90c";
|
||||||
|
}
|
||||||
|
.icon-comments:before {
|
||||||
|
content: "\e90d";
|
||||||
|
}
|
||||||
|
.icon-comment:before {
|
||||||
|
content: "\e90e";
|
||||||
|
}
|
||||||
|
.icon-boost:before {
|
||||||
|
content: "\e90f";
|
||||||
|
}
|
||||||
|
.icon-bell:before {
|
||||||
|
content: "\e901";
|
||||||
|
}
|
BIN
fedireads/static/images/logo-small.png
Normal file
BIN
fedireads/static/images/logo-small.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
BIN
fedireads/static/images/logo.png
Normal file
BIN
fedireads/static/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
11
fedireads/static/js/feed.js
Normal file
11
fedireads/static/js/feed.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
function show_compose(element) {
|
||||||
|
var visible_compose_boxes = document.getElementsByClassName('visible');
|
||||||
|
for (var i = 0; i < visible_compose_boxes.length; i++) {
|
||||||
|
visible_compose_boxes[i].className = 'compose-suggestion';
|
||||||
|
}
|
||||||
|
|
||||||
|
var target_id = 'compose-' + element.id;
|
||||||
|
var target = document.getElementById(target_id);
|
||||||
|
target.className += ' visible';
|
||||||
|
}
|
||||||
|
|
8
fedireads/static/js/shared.js
Normal file
8
fedireads/static/js/shared.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
function hide_element(element) {
|
||||||
|
var classes = element.parentElement.className;
|
||||||
|
element.parentElement.className = classes.replace('visible', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function favorite(element) {
|
||||||
|
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
<div>
|
<div>
|
||||||
<h2><q>{{ book.title }}</q> and You</h2>
|
<h2><q>{{ book.title }}</q> and You</h2>
|
||||||
<p>{% if shelf %}On shelf <q>{{ shelf.name }}</q>{% endif %}</p>
|
<p>{% if shelf %}On shelf <q>{{ shelf.name }}</q>{% endif %}</p>
|
||||||
{% include 'snippets/shelve-button.html' with book=book pulldown=True %}
|
{% include 'snippets/shelve_button.html' with book=book pulldown=True %}
|
||||||
|
|
||||||
<div id="tag-cloud">
|
<div id="tag-cloud">
|
||||||
{% for tag in user_tags %}
|
{% for tag in user_tags %}
|
||||||
|
|
|
@ -2,44 +2,52 @@
|
||||||
{% load fr_display %}
|
{% load fr_display %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div id="sidebar">
|
<div class="all-shelves content-container">
|
||||||
|
{% for shelf in shelves %}
|
||||||
|
{% if shelf.books %}
|
||||||
<div>
|
<div>
|
||||||
{% if shelves %}
|
<h2>{{ shelf.name }}
|
||||||
{% for shelf in shelves %}
|
{% if shelf.size > shelf.books|length %}
|
||||||
<h2>{{ shelf.name }}</h2>
|
<small>(<a href="/shelf/{{ user | username }}/{{ shelf.identifier }}">See all {{ shelf.size }}</a>)</small>
|
||||||
{% for book in shelf.books %}
|
{% endif %}
|
||||||
<div class="book-preview">
|
</h2>
|
||||||
{% include 'snippets/book.html' with book=book size="small" %}
|
<div class="covers-shelf {{ shelf.identifier }}">
|
||||||
|
{% for book in shelf.books %}
|
||||||
|
<div class="book-preview" onclick="show_compose(this)" id="book-{{ book.id }}">
|
||||||
|
{% include 'snippets/book_cover.html' with book=book %}
|
||||||
|
{% include 'snippets/shelve_button.html' with book=book %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
{% if shelf.size > shelf.books.count %}
|
|
||||||
<a href="/shelf/{{ user | username }}/{{ shelf.identifier }}">See all {{ shelf.size }}</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
<h2>Reading Activity</h2>
|
|
||||||
<p>Start a book!</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div>
|
|
||||||
<h2>Recently Added Books</h2>
|
|
||||||
{% for book in recent_books %}
|
|
||||||
<div class="book-preview">
|
|
||||||
{% include 'snippets/book.html' with book=book size="small" %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div id="feed">
|
|
||||||
{% include 'snippets/tabs.html' with tabs=feed_tabs active_tab=active_tab %}
|
|
||||||
|
|
||||||
{% for activity in activities %}
|
|
||||||
{% include 'snippets/status.html' with status=activity depth=1 description=True %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% for shelf in shelves %}
|
||||||
|
{% for book in shelf.books %}
|
||||||
|
<div class="compose-suggestion" id="compose-book-{{ book.id }}">
|
||||||
|
<span class="close icon icon-close" onclick="hide_element(this)">
|
||||||
|
<span class="hidden-text">Close</span>
|
||||||
|
</span>
|
||||||
|
<div class="content-container">
|
||||||
|
{% include 'snippets/create_status.html' with book=book user=request.user %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div id="feed">
|
||||||
|
<div class="content-container tabs">
|
||||||
|
{% include 'snippets/tabs.html' with tabs=feed_tabs active_tab=active_tab %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for activity in activities %}
|
||||||
|
<div class="content-container">
|
||||||
|
{% include 'snippets/status.html' with status=activity %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/js/feed.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -2,15 +2,16 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>FediReads</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/format.css">
|
||||||
|
<link type="text/css" rel="stylesheet" href="/static/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">
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
<meta name="twitter:title" content="FediReads">
|
<meta name="twitter:title" content="BookWyrm">
|
||||||
<meta name="og:title" content="FediReads">
|
<meta name="og:title" content="BookWyrm">
|
||||||
<meta name="twitter:description" content="Federated Social Reading">
|
<meta name="twitter:description" content="Federated Social Reading">
|
||||||
<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">
|
||||||
|
@ -22,40 +23,43 @@
|
||||||
|
|
||||||
<div id="top-bar">
|
<div id="top-bar">
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div id="branding"><a href="/"><img id="logo" src="/static/images/logo-small.png" alt="BookWyrm"></img></a></div>
|
||||||
<div id="branding"><a href="/">📚FediReads</a></div>
|
|
||||||
<div id="actions">
|
<ul class="menu">
|
||||||
<div id="account">
|
<li><a href="/user/{{request.user.localname}}">Your shelves</a></li>
|
||||||
{% if request.user.is_authenticated %}
|
<li><a href="/#feed">Updates</a></li>
|
||||||
<form name="logout" action="/logout/" method="post">
|
<li><a href="/books">Discover Books</a></li>
|
||||||
{% csrf_token %}
|
</ul>
|
||||||
Welcome, {% include 'snippets/username.html' with user=request.user %}
|
|
||||||
<input type="submit" value="Log out"></input>
|
<div id="actions">
|
||||||
</form>
|
<div id="search">
|
||||||
{% else %}
|
<form action="/search/">
|
||||||
<form name="login" action="/login/" method="post">
|
<input type="text" name="q" placeholder="Search for a book or user"></input>
|
||||||
{% csrf_token %}
|
<button type="submit">
|
||||||
{% for field in login_form %}
|
<span class="icon icon-search">
|
||||||
{{ field }}
|
<span class="hidden-text">search</span>
|
||||||
{% endfor %}
|
</span>
|
||||||
<input type="submit" value="Log in"></input>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div id="search">
|
|
||||||
<form action="/search/">
|
|
||||||
<input type="text" name="q" placeholder="search"></input>
|
|
||||||
<input type="submit" value="🔍"></input>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% if request.user.is_authenticated %}
|
|
||||||
<div id="notification">
|
|
||||||
<a href="/notifications">
|
|
||||||
🔔 ({{ request.user | notification_count }})
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<div id="notifications">
|
||||||
|
<a href="/notifications">
|
||||||
|
<span class="icon icon-bell">
|
||||||
|
<span class="hidden-text">Notitications</span>
|
||||||
|
</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="/logout/">Log out</a></li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
|
@ -65,6 +69,10 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var csrf_token = {{ csrf_token }};
|
||||||
|
</script>
|
||||||
|
<script src="/static/js/shared.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,34 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content">
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="content-container login">
|
||||||
|
<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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<form name="login" method="post">
|
<form name="register" method="post" action="/register">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ register_form.as_p }}
|
||||||
|
<button type="submit">Create account</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-container login">
|
||||||
|
<h2>Log in</h2>
|
||||||
|
<div>
|
||||||
|
<form name="login" method="post" action="/user-login">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ login_form.as_p }}
|
{{ login_form.as_p }}
|
||||||
<button type="submit">Log in</button>
|
<button type="submit">Log in</button>
|
||||||
</form>
|
</form>
|
||||||
<a href="/register/">Create a new account</a>
|
<p><small><a href="/reset-password">Forgot your password?</a></small></p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
{% extends 'layout.html' %}
|
|
||||||
{% block content %}
|
|
||||||
<div id="content">
|
|
||||||
<div>
|
|
||||||
<form name="register" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ register_form.as_p }}
|
|
||||||
<button type="submit">Create account</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<a href="/login/">Log in with existing account</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
|
@ -1,10 +0,0 @@
|
||||||
{% load humanize %}
|
|
||||||
<h2>
|
|
||||||
{% include 'snippets/avatar.html' with user=activity.user %}
|
|
||||||
{% include 'snippets/username.html' with user=activity.user %}
|
|
||||||
{{ content | safe }}
|
|
||||||
<span class="time-ago">
|
|
||||||
<a href="{{ activity.absolute_id }}">{{ activity.published_date | naturaltime }}</a>
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
|
|
|
@ -15,4 +15,4 @@
|
||||||
<blockquote>{{ book.description | description }}</blockquote>
|
<blockquote>{{ book.description | description }}</blockquote>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% include 'snippets/shelve-button.html' with book=book pulldown=shelf_pulldown %}
|
{% include 'snippets/shelve_button.html' with book=book pulldown=shelf_pulldown %}
|
||||||
|
|
6
fedireads/templates/snippets/covers_shelf.html
Normal file
6
fedireads/templates/snippets/covers_shelf.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% load fr_display %}
|
||||||
|
{% for book in books %}
|
||||||
|
<div class="book-preview">
|
||||||
|
{% include 'snippets/book.html' with rating=rating %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
32
fedireads/templates/snippets/create_status.html
Normal file
32
fedireads/templates/snippets/create_status.html
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{% load humanize %}
|
||||||
|
{% load fr_display %}
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
{% include 'snippets/avatar.html' with user=user %}
|
||||||
|
Your thoughts on
|
||||||
|
<a href="/book/{{ book.openlibrary_key }}">{{ book.title }}</a>
|
||||||
|
by {% include 'snippets/authors.html' with book=book %}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="tabs secondary">
|
||||||
|
<div class="tab active">
|
||||||
|
Review
|
||||||
|
</div>
|
||||||
|
<div class="tab">
|
||||||
|
Comment
|
||||||
|
</div>
|
||||||
|
<div class="tab">
|
||||||
|
Quote
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="book-preview">
|
||||||
|
{% include 'snippets/book_cover.html' with book=book %}
|
||||||
|
<form class="review-form" name="review" action="/review/" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{# TODO: this shouldn't use the openlibrary key #}
|
||||||
|
<input type="hidden" name="book" value="{{ book.openlibrary_key }}"></input>
|
||||||
|
{{ review_form.as_p }}
|
||||||
|
<button type="submit">Post review</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -5,7 +5,7 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ book.id }}"></input>
|
<input type="hidden" name="book" value="{{ book.id }}"></input>
|
||||||
<input type="hidden" name="shelf" value="{% shelve_button_identifier book %}"></input>
|
<input type="hidden" name="shelf" value="{% shelve_button_identifier book %}"></input>
|
||||||
<button type="submit">{% shelve_button_text book %}</button>
|
<button class="secondary" type="submit">{% shelve_button_text book %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button type="submit">Shelve</button>
|
<button class="secondary" type="submit">Shelve</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
|
@ -1,21 +1,48 @@
|
||||||
|
{% 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="post {{ status.status_type|lower }} depth-{{ depth }} {% if main %}main{% else %}reply{% endif %}">
|
||||||
{% include 'snippets/activity_banner.html' with activity=status %}
|
<h2>
|
||||||
|
{% include 'snippets/avatar.html' with user=status.user %}
|
||||||
|
{% include 'snippets/username.html' with user=status.user %}
|
||||||
|
{% if status.status_type == 'Update' %}
|
||||||
|
{{ status.content | safe }}
|
||||||
|
{% elif status.status_type == 'Review' %}
|
||||||
|
reviewed {{ status.book.title }}
|
||||||
|
{% elif status.reply_parent %}
|
||||||
|
{% with parent_status=status|parent %}
|
||||||
|
replied to {% include 'snippets/username.html' with user=parent_status.user possessive=True %} <a href="{{parent_status.absolute_id }}">{{ parent_status.status_type|lower }}</a>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
<span class="time-ago">
|
||||||
|
<a href="{{ status.absolute_id }}">{{ status.published_date | naturaltime }}</a>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
{% if not hide_book and status.mention_books.count %}
|
{% if not hide_book and status.mention_books.count %}
|
||||||
<div class="book-preview">
|
<div class="book-preview">
|
||||||
{% include 'snippets/book.html' with book=status.mention_books.first %}
|
{% if status.status_type == 'Review' %}
|
||||||
|
{% include 'snippets/book.html' with book=status.mention_books.first %}
|
||||||
|
{% else %}
|
||||||
|
{% include 'snippets/book.html' with book=status.mention_books.first description=True %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not hide_book and status.book%}
|
{% if not hide_book and status.book%}
|
||||||
<div class="book-preview">
|
<div class="book-preview">
|
||||||
{% include 'snippets/book.html' with book=status.book %}
|
{% if status.status_type == 'Review' %}
|
||||||
|
{% include 'snippets/book.html' with book=status.book %}
|
||||||
|
{% else %}
|
||||||
|
{% include 'snippets/book.html' with book=status.book description=True %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if status.status_type == 'Review' %}<h4>{{ status.name }}
|
{% if status.status_type == 'Review' %}<h4>{{ status.name }}
|
||||||
<small>{{ status.rating | stars }} stars, by {% include 'snippets/username.html' with user=status.user %}</small>
|
<small>{{ status.rating | stars }} stars, by {% include 'snippets/username.html' with user=status.user %}</small>
|
||||||
</h4>{% endif %}
|
</h4>{% endif %}
|
||||||
|
{% if status.status_type != 'Update' %}
|
||||||
<blockquote>{{ status.content | safe }}</blockquote>
|
<blockquote>{{ status.content | safe }}</blockquote>
|
||||||
|
{% endif %}
|
||||||
{% if not max_depth and status.reply_parent or status|replies %}<p><a href="{{ status.absolute_id }}">Thread</a>{% endif %}
|
{% if not max_depth and status.reply_parent or status|replies %}<p><a href="{{ status.absolute_id }}">Thread</a>{% endif %}
|
||||||
{% include 'snippets/interaction.html' with activity=status %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'snippets/interaction.html' with activity=status %}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<div class="tabs">
|
{% for tab in tabs %}
|
||||||
{% for tab in tabs %}
|
<div class="tab {% if tab.id == active_tab %}active{% endif %}">
|
||||||
<div class="tab {% if tab == active_tab %}active{% endif %}">
|
<a href="{{ path }}/{{ tab.id }}">{{ tab.display }}</a>
|
||||||
<a href="{{ path }}/{{ tab }}">{{ tab }}</a>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
{% load fr_display %}
|
{% load fr_display %}
|
||||||
<a href="/user/{{ user | username }}" class="user">{% if user.name %}{{ user.name }}{% else %}{{ user | username }}{% endif %}</a> {% if show_full and user.name or show_full and user.localname %} ({{ user.username }}){% endif %}
|
<a href="/user/{{ user | username }}" class="user">{% if user.name %}{{ user.name }}{% else %}{{ user | username }}{% endif %}</a>{% if possessive %}'s{% endif %}{% if show_full and user.name or show_full and user.localname %} ({{ user.username }}){% endif %}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
{% if is_self %}
|
{% if is_self %}
|
||||||
<div class="interaction">
|
<div class="interaction">
|
||||||
<a href="/edit_profile_page/">Edit profile</a>
|
<a href="/user-edit/">Edit profile</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.urls import path, re_path
|
||||||
from fedireads import incoming, outgoing, views, settings, wellknown
|
from fedireads import incoming, outgoing, views, settings, wellknown
|
||||||
from fedireads import view_actions as actions
|
from fedireads import view_actions as actions
|
||||||
|
|
||||||
username_regex = r'(?P<username>[\w@\-_]+)'
|
username_regex = r'(?P<username>[\w@\-_\.]+)'
|
||||||
localname_regex = r'(?P<username>[\w\-_]+)'
|
localname_regex = r'(?P<username>[\w\-_]+)'
|
||||||
user_path = r'^user/%s' % username_regex
|
user_path = r'^user/%s' % username_regex
|
||||||
local_user_path = r'^user/%s' % localname_regex
|
local_user_path = r'^user/%s' % localname_regex
|
||||||
|
@ -28,9 +28,7 @@ urlpatterns = [
|
||||||
# TODO: re_path(r'^.well-known/host-meta/?$', incoming.host_meta),
|
# TODO: re_path(r'^.well-known/host-meta/?$', incoming.host_meta),
|
||||||
|
|
||||||
# ui views
|
# ui views
|
||||||
re_path(r'^register/?$', views.register),
|
re_path(r'^login/?$', views.login_page),
|
||||||
re_path(r'^login/?$', views.user_login),
|
|
||||||
re_path(r'^logout/?$', views.user_logout),
|
|
||||||
|
|
||||||
# should return a ui view or activitypub json blob as requested
|
# should return a ui view or activitypub json blob as requested
|
||||||
path('', views.home),
|
path('', views.home),
|
||||||
|
@ -40,7 +38,7 @@ urlpatterns = [
|
||||||
# users
|
# users
|
||||||
re_path(r'%s/?$' % user_path, views.user_page),
|
re_path(r'%s/?$' % user_path, views.user_page),
|
||||||
re_path(r'%s\.json$' % local_user_path, views.user_page),
|
re_path(r'%s\.json$' % local_user_path, views.user_page),
|
||||||
re_path(r'edit_profile_page/?$', views.edit_profile_page),
|
re_path(r'user-edit/?$', views.edit_profile_page),
|
||||||
re_path(r'%s/followers/?$' % local_user_path, views.followers_page),
|
re_path(r'%s/followers/?$' % local_user_path, views.followers_page),
|
||||||
re_path(r'%s/followers.json$' % local_user_path, views.followers_page),
|
re_path(r'%s/followers.json$' % local_user_path, views.followers_page),
|
||||||
re_path(r'%s/following/?$' % local_user_path, views.following_page),
|
re_path(r'%s/following/?$' % local_user_path, views.following_page),
|
||||||
|
@ -61,6 +59,9 @@ urlpatterns = [
|
||||||
re_path(r'^shelf/%s/(?P<shelf_identifier>[\w-]+)/?$' % username_regex, views.shelf_page),
|
re_path(r'^shelf/%s/(?P<shelf_identifier>[\w-]+)/?$' % username_regex, views.shelf_page),
|
||||||
|
|
||||||
# internal action endpoints
|
# internal action endpoints
|
||||||
|
re_path(r'^logout/?$', actions.user_logout),
|
||||||
|
re_path(r'^user-login/?$', actions.user_login),
|
||||||
|
re_path(r'^register/?$', actions.register),
|
||||||
re_path(r'^review/?$', actions.review),
|
re_path(r'^review/?$', actions.review),
|
||||||
re_path(r'^tag/?$', actions.tag),
|
re_path(r'^tag/?$', actions.tag),
|
||||||
re_path(r'^untag/?$', actions.untag),
|
re_path(r'^untag/?$', actions.untag),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
''' views for actions you can take in the application '''
|
''' views for actions you can take in the application '''
|
||||||
|
from django.contrib.auth import authenticate, login, logout
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.http import HttpResponseBadRequest
|
from django.http import HttpResponseBadRequest
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
@ -6,9 +7,54 @@ from django.template.response import TemplateResponse
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from fedireads import forms, models, books_manager, outgoing
|
from fedireads import forms, models, books_manager, outgoing
|
||||||
|
from fedireads.settings import DOMAIN
|
||||||
from fedireads.views import get_user_from_username
|
from fedireads.views import get_user_from_username
|
||||||
|
|
||||||
|
|
||||||
|
def user_login(request):
|
||||||
|
''' authenticate user login '''
|
||||||
|
if request.method == 'GET':
|
||||||
|
return redirect('/login')
|
||||||
|
|
||||||
|
form = forms.LoginForm(request.POST)
|
||||||
|
if not form.is_valid():
|
||||||
|
return TemplateResponse(request, 'login.html', {'login_form': form})
|
||||||
|
|
||||||
|
username = form.data['username']
|
||||||
|
username = '%s@%s' % (username, DOMAIN)
|
||||||
|
password = form.data['password']
|
||||||
|
user = authenticate(request, username=username, password=password)
|
||||||
|
if user is not None:
|
||||||
|
login(request, user)
|
||||||
|
return redirect(request.GET.get('next', '/'))
|
||||||
|
return TemplateResponse(request, 'login.html', {'login_form': form})
|
||||||
|
|
||||||
|
|
||||||
|
def register(request):
|
||||||
|
''' join the server '''
|
||||||
|
if request.method == 'GET':
|
||||||
|
return redirect('/login')
|
||||||
|
|
||||||
|
form = forms.RegisterForm(request.POST)
|
||||||
|
if not form.is_valid():
|
||||||
|
return redirect('/register/')
|
||||||
|
|
||||||
|
username = form.data['username']
|
||||||
|
email = form.data['email']
|
||||||
|
password = form.data['password']
|
||||||
|
|
||||||
|
user = models.User.objects.create_user(username, email, password)
|
||||||
|
login(request, user)
|
||||||
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def user_logout(request):
|
||||||
|
''' done with this place! outa here! '''
|
||||||
|
logout(request)
|
||||||
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def edit_profile(request):
|
def edit_profile(request):
|
||||||
''' les get fancy with images '''
|
''' les get fancy with images '''
|
||||||
|
@ -23,7 +69,8 @@ def edit_profile(request):
|
||||||
if 'avatar' in form.files:
|
if 'avatar' in form.files:
|
||||||
request.user.avatar = form.files['avatar']
|
request.user.avatar = form.files['avatar']
|
||||||
request.user.summary = form.data['summary']
|
request.user.summary = form.data['summary']
|
||||||
request.user.manually_approves_followers = form.cleaned_data['manually_approves_followers']
|
request.user.manually_approves_followers = \
|
||||||
|
form.cleaned_data['manually_approves_followers']
|
||||||
request.user.save()
|
request.user.save()
|
||||||
return redirect('/user/%s' % request.user.localname)
|
return redirect('/user/%s' % request.user.localname)
|
||||||
|
|
||||||
|
@ -160,11 +207,14 @@ def search(request):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def clear_notifications(request):
|
def clear_notifications(request):
|
||||||
|
''' permanently delete notification for user '''
|
||||||
request.user.notification_set.filter(read=True).delete()
|
request.user.notification_set.filter(read=True).delete()
|
||||||
return redirect('/notifications')
|
return redirect('/notifications')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def accept_follow_request(request):
|
def accept_follow_request(request):
|
||||||
|
''' a user accepts a follow request '''
|
||||||
username = request.POST['user']
|
username = request.POST['user']
|
||||||
try:
|
try:
|
||||||
requester = get_user_from_username(username)
|
requester = get_user_from_username(username)
|
||||||
|
@ -172,7 +222,10 @@ def accept_follow_request(request):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
follow_request = models.UserFollowRequest.objects.get(user_subject=requester, user_object=request.user)
|
follow_request = models.UserFollowRequest.objects.get(
|
||||||
|
user_subject=requester,
|
||||||
|
user_object=request.user
|
||||||
|
)
|
||||||
except models.UserFollowRequest.DoesNotExist:
|
except models.UserFollowRequest.DoesNotExist:
|
||||||
# Request already dealt with.
|
# Request already dealt with.
|
||||||
pass
|
pass
|
||||||
|
@ -181,8 +234,10 @@ def accept_follow_request(request):
|
||||||
|
|
||||||
return redirect('/user/%s' % request.user.localname)
|
return redirect('/user/%s' % request.user.localname)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def delete_follow_request(request):
|
def delete_follow_request(request):
|
||||||
|
''' a user rejects a follow request '''
|
||||||
username = request.POST['user']
|
username = request.POST['user']
|
||||||
try:
|
try:
|
||||||
requester = get_user_from_username(username)
|
requester = get_user_from_username(username)
|
||||||
|
@ -190,9 +245,13 @@ def delete_follow_request(request):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
follow_request = models.UserFollowRequest.objects.get(user_subject=requester, user_object=request.user)
|
follow_request = models.UserFollowRequest.objects.get(
|
||||||
|
user_subject=requester,
|
||||||
|
user_object=request.user
|
||||||
|
)
|
||||||
except models.UserFollowRequest.DoesNotExist:
|
except models.UserFollowRequest.DoesNotExist:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
outgoing.handle_outgoing_reject(requester, request.user, follow_request)
|
outgoing.handle_outgoing_reject(requester, request.user, follow_request)
|
||||||
return redirect('/user/%s' % request.user.localname)
|
return redirect('/user/%s' % request.user.localname)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
''' views for pages you can go to in the application '''
|
''' views for pages you can go to in the application '''
|
||||||
from django.contrib.auth import authenticate, login, logout
|
|
||||||
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, Q
|
||||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound, \
|
from django.http import HttpResponseBadRequest, HttpResponseNotFound, \
|
||||||
|
@ -10,7 +9,6 @@ from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from fedireads import activitypub
|
from fedireads import activitypub
|
||||||
from fedireads import forms, models, books_manager
|
from fedireads import forms, models, books_manager
|
||||||
from fedireads.settings import DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_from_username(username):
|
def get_user_from_username(username):
|
||||||
|
@ -22,7 +20,6 @@ def get_user_from_username(username):
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def is_api_request(request):
|
def is_api_request(request):
|
||||||
''' check whether a request is asking for html or data '''
|
''' check whether a request is asking for html or data '''
|
||||||
# TODO: this should probably be the full content type? maybe?
|
# TODO: this should probably be the full content type? maybe?
|
||||||
|
@ -40,28 +37,44 @@ def home(request):
|
||||||
def home_tab(request, tab):
|
def home_tab(request, tab):
|
||||||
''' user's homepage with activity feed '''
|
''' user's homepage with activity feed '''
|
||||||
shelves = []
|
shelves = []
|
||||||
for identifier in ['reading', 'to-read']:
|
book_count = 6
|
||||||
|
for (identifier, count) in [('reading', 3), ('read', 1), ('to-read', 3)]:
|
||||||
|
if book_count <= 0:
|
||||||
|
break
|
||||||
shelf = models.Shelf.objects.get(
|
shelf = models.Shelf.objects.get(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
identifier=identifier,
|
identifier=identifier,
|
||||||
)
|
)
|
||||||
if not shelf.books.count():
|
if not shelf.books.count():
|
||||||
continue
|
continue
|
||||||
|
books = models.ShelfBook.objects.filter(
|
||||||
|
shelf=shelf,
|
||||||
|
).order_by(
|
||||||
|
'-updated_date'
|
||||||
|
)[:count]
|
||||||
|
|
||||||
|
book_count -= len(books)
|
||||||
|
|
||||||
shelves.append({
|
shelves.append({
|
||||||
'name': shelf.name,
|
'name': shelf.name,
|
||||||
'identifier': shelf.identifier,
|
'identifier': shelf.identifier,
|
||||||
'books': shelf.books.all()[:3],
|
'books': [b.book for b in books],
|
||||||
'size': shelf.books.count(),
|
'size': shelf.books.count(),
|
||||||
})
|
})
|
||||||
|
# books new to the instance, for discovery
|
||||||
|
if book_count > 0:
|
||||||
|
shelves.append({
|
||||||
|
'name': 'Recently added',
|
||||||
|
'identifier': None,
|
||||||
|
'books': models.Book.objects.order_by(
|
||||||
|
'-created_date'
|
||||||
|
)[:book_count],
|
||||||
|
'count': book_count,
|
||||||
|
})
|
||||||
|
|
||||||
# allows us to check if a user has shelved a book
|
# allows us to check if a user has shelved a book
|
||||||
user_books = models.Book.objects.filter(shelves__user=request.user).all()
|
user_books = models.Book.objects.filter(shelves__user=request.user).all()
|
||||||
|
|
||||||
# books new to the instance, for discovery
|
|
||||||
recent_books = models.Book.objects.order_by(
|
|
||||||
'-created_date'
|
|
||||||
)[:5]
|
|
||||||
|
|
||||||
# status updates for your follow network
|
# status updates for your follow network
|
||||||
following = models.User.objects.filter(
|
following = models.User.objects.filter(
|
||||||
Q(followers=request.user) | Q(id=request.user.id)
|
Q(followers=request.user) | Q(id=request.user.id)
|
||||||
|
@ -89,65 +102,27 @@ def home_tab(request, tab):
|
||||||
data = {
|
data = {
|
||||||
'user': request.user,
|
'user': request.user,
|
||||||
'shelves': shelves,
|
'shelves': shelves,
|
||||||
'recent_books': recent_books,
|
|
||||||
'user_books': user_books,
|
'user_books': user_books,
|
||||||
'activities': activities,
|
'activities': activities,
|
||||||
'feed_tabs': ['home', 'local', 'federated'],
|
'feed_tabs': [
|
||||||
|
{'id': 'home', 'display': 'Home'},
|
||||||
|
{'id': 'local', 'display': 'Local'},
|
||||||
|
{'id': 'federated', 'display': 'Federated'}
|
||||||
|
],
|
||||||
'active_tab': tab,
|
'active_tab': tab,
|
||||||
|
'review_form': forms.ReviewForm(),
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, 'feed.html', data)
|
return TemplateResponse(request, 'feed.html', data)
|
||||||
|
|
||||||
|
|
||||||
def user_login(request):
|
def login_page(request):
|
||||||
''' authentication '''
|
''' authentication '''
|
||||||
# send user to the login page
|
# send user to the login page
|
||||||
if request.method == 'GET':
|
data = {
|
||||||
form = forms.LoginForm()
|
'login_form': forms.LoginForm(),
|
||||||
return TemplateResponse(request, 'login.html', {'login_form': form})
|
'register_form': forms.RegisterForm(),
|
||||||
|
}
|
||||||
# authenticate user
|
return TemplateResponse(request, 'login.html', data)
|
||||||
form = forms.LoginForm(request.POST)
|
|
||||||
if not form.is_valid():
|
|
||||||
return TemplateResponse(request, 'login.html', {'login_form': form})
|
|
||||||
|
|
||||||
username = form.data['username']
|
|
||||||
username = '%s@%s' % (username, DOMAIN)
|
|
||||||
password = form.data['password']
|
|
||||||
user = authenticate(request, username=username, password=password)
|
|
||||||
if user is not None:
|
|
||||||
login(request, user)
|
|
||||||
return redirect(request.GET.get('next', '/'))
|
|
||||||
return TemplateResponse(request, 'login.html', {'login_form': form})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def user_logout(request):
|
|
||||||
''' done with this place! outa here! '''
|
|
||||||
logout(request)
|
|
||||||
return redirect('/')
|
|
||||||
|
|
||||||
|
|
||||||
def register(request):
|
|
||||||
''' join the server '''
|
|
||||||
if request.method == 'GET':
|
|
||||||
form = forms.RegisterForm()
|
|
||||||
return TemplateResponse(
|
|
||||||
request,
|
|
||||||
'register.html',
|
|
||||||
{'register_form': form}
|
|
||||||
)
|
|
||||||
|
|
||||||
form = forms.RegisterForm(request.POST)
|
|
||||||
if not form.is_valid():
|
|
||||||
return redirect('/register/')
|
|
||||||
|
|
||||||
username = form.data['username']
|
|
||||||
email = form.data['email']
|
|
||||||
password = form.data['password']
|
|
||||||
|
|
||||||
user = models.User.objects.create_user(username, email, password)
|
|
||||||
login(request, user)
|
|
||||||
return redirect('/')
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|
Loading…
Reference in a new issue