mirror of
https://github.com/dnote/dnote
synced 2026-03-16 23:45:52 +01:00
Fix digest note review (#383)
* Fix issue where unreviewed note is displayed as reviewed * Show unreviewed first * Fix menu alignment on mobile * Show completion message * Display help on empty digest list * Fix email type
This commit is contained in:
parent
952e0e62f3
commit
ea51514c85
23 changed files with 330 additions and 67 deletions
|
|
@ -22,6 +22,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
|
@ -179,3 +180,27 @@ func (a *App) GetDigests(p GetDigestsParam) ([]database.Digest, error) {
|
|||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// PreloadDigest preloads associations for the given digest. It returns a new digest.
|
||||
func (a *App) PreloadDigest(d database.Digest) (database.Digest, error) {
|
||||
var ret database.Digest
|
||||
|
||||
conn := a.DB.Where("id = ?", d.ID).
|
||||
Preload("Notes", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("notes.created_at DESC")
|
||||
}).
|
||||
Preload("Notes.Book").
|
||||
Preload("Notes.NoteReview", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("note_reviews.digest_id = ?", d.ID)
|
||||
}).
|
||||
Preload("Rule").
|
||||
Preload("Receipts", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("digest_receipts.user_id = ?", d.UserID)
|
||||
}).First(&ret)
|
||||
|
||||
if err := conn.Error; err != nil {
|
||||
return ret, errors.Wrap(err, "preloading")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func createEmailPreference(user database.User, tx *gorm.DB) error {
|
|||
|
||||
func createDefaultRepetitionRule(user database.User, tx *gorm.DB) error {
|
||||
r := database.RepetitionRule{
|
||||
Title: "Default repetition - all book",
|
||||
Title: "Default repetition - all books",
|
||||
UserID: user.ID,
|
||||
Enabled: false,
|
||||
Hour: 20,
|
||||
|
|
|
|||
|
|
@ -29,23 +29,9 @@ import (
|
|||
"github.com/dnote/dnote/pkg/server/log"
|
||||
"github.com/dnote/dnote/pkg/server/presenters"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func preloadDigest(conn *gorm.DB, userID int) *gorm.DB {
|
||||
return conn.
|
||||
Preload("Notes", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("notes.created_at DESC")
|
||||
}).
|
||||
Preload("Notes.Book").
|
||||
Preload("Notes.NoteReview").
|
||||
Preload("Rule").
|
||||
Preload("Receipts", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("digest_receipts.user_id = ?", userID)
|
||||
})
|
||||
}
|
||||
|
||||
func (a *API) getDigest(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := r.Context().Value(helpers.KeyUser).(database.User)
|
||||
if !ok {
|
||||
|
|
@ -56,17 +42,18 @@ func (a *API) getDigest(w http.ResponseWriter, r *http.Request) {
|
|||
vars := mux.Vars(r)
|
||||
digestUUID := vars["digestUUID"]
|
||||
|
||||
db := a.App.DB
|
||||
|
||||
var digest database.Digest
|
||||
conn := db.Where("user_id = ? AND uuid = ? ", user.ID, digestUUID)
|
||||
conn = preloadDigest(conn, user.ID)
|
||||
conn = conn.First(&digest)
|
||||
|
||||
if conn.RecordNotFound() {
|
||||
d, err := a.App.GetUserDigestByUUID(user.ID, digestUUID)
|
||||
if d == nil {
|
||||
RespondNotFound(w)
|
||||
return
|
||||
} else if err := conn.Error; err != nil {
|
||||
}
|
||||
if err != nil {
|
||||
HandleError(w, "finding digest", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
digest, err := a.App.PreloadDigest(*d)
|
||||
if err != nil {
|
||||
HandleError(w, "finding digest", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ func (c Context) notify(now time.Time, user database.User, digest database.Diges
|
|||
return errors.Wrap(err, "getting sender email")
|
||||
}
|
||||
|
||||
if err := c.EmailBackend.Queue(subject, sender, []string{account.Email.String}, mailer.EmailKindHTML, body); err != nil {
|
||||
if err := c.EmailBackend.Queue(subject, sender, []string{account.Email.String}, mailer.EmailKindText, body); err != nil {
|
||||
return errors.Wrap(err, "queueing email")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ interface MenuProps {
|
|||
triggerClassName?: string;
|
||||
contentClassName?: string;
|
||||
alignment: Alignment;
|
||||
alignmentMd?: Alignment;
|
||||
direction: Direction;
|
||||
headerContent?: React.ReactNode;
|
||||
wrapperClassName?: string;
|
||||
|
|
@ -93,6 +94,7 @@ const Menu: React.FunctionComponent<MenuProps> = ({
|
|||
triggerClassName,
|
||||
contentClassName,
|
||||
alignment,
|
||||
alignmentMd,
|
||||
direction,
|
||||
headerContent,
|
||||
wrapperClassName,
|
||||
|
|
@ -181,6 +183,7 @@ const Menu: React.FunctionComponent<MenuProps> = ({
|
|||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
alignment={alignment}
|
||||
alignmentMd={alignmentMd}
|
||||
direction={direction}
|
||||
renderContent={() => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ interface Props {
|
|||
headerText: string;
|
||||
menuId: string;
|
||||
alignment: Alignment;
|
||||
alignmentMd?: Alignment;
|
||||
direction: Direction;
|
||||
|
||||
disabled?: boolean;
|
||||
wrapperClassName?: string;
|
||||
triggerClassName?: string;
|
||||
|
|
@ -53,6 +53,7 @@ const SelectMenu: React.FunctionComponent<Props> = ({
|
|||
triggerId,
|
||||
menuId,
|
||||
alignment,
|
||||
alignmentMd,
|
||||
direction,
|
||||
wrapperClassName,
|
||||
triggerClassName
|
||||
|
|
@ -82,6 +83,7 @@ const SelectMenu: React.FunctionComponent<Props> = ({
|
|||
contentClassName={styles.content}
|
||||
wrapperClassName={wrapperClassName}
|
||||
alignment={alignment}
|
||||
alignmentMd={alignmentMd}
|
||||
direction={direction}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -37,19 +37,66 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* alignment */
|
||||
.left-align {
|
||||
left: 0;
|
||||
right: auto;
|
||||
top: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
.right-align {
|
||||
right: 0;
|
||||
left: auto;
|
||||
top: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
.top-align {
|
||||
top: 0;
|
||||
left: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
.bottom-align {
|
||||
bottom: 0;
|
||||
left: auto;
|
||||
right: auto;
|
||||
top: auto;
|
||||
}
|
||||
// md
|
||||
.left-align-md {
|
||||
@include breakpoint(md) {
|
||||
left: 0;
|
||||
right: auto;
|
||||
top: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
.right-align-md {
|
||||
@include breakpoint(md) {
|
||||
right: 0;
|
||||
left: auto;
|
||||
top: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
.top-align-md {
|
||||
@include breakpoint(md) {
|
||||
top: 0;
|
||||
left: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
.bottom-align-md {
|
||||
@include breakpoint(md) {
|
||||
bottom: 0;
|
||||
left: auto;
|
||||
right: auto;
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* direction */
|
||||
.top-direction {
|
||||
bottom: calc(100% + 4px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ interface Props {
|
|||
closeOnEscapeKeydown?: boolean;
|
||||
contentClassName?: string;
|
||||
alignment?: Alignment;
|
||||
alignmentMd?: Alignment;
|
||||
triggerEl?: HTMLElement;
|
||||
wrapperEl?: any;
|
||||
hasBorder?: boolean;
|
||||
|
|
@ -45,6 +46,7 @@ const PopoverContent: React.FunctionComponent<Props> = ({
|
|||
wrapperEl,
|
||||
children,
|
||||
alignment,
|
||||
alignmentMd,
|
||||
direction,
|
||||
hasBorder,
|
||||
closeOnOutsideClick,
|
||||
|
|
@ -135,6 +137,12 @@ const PopoverContent: React.FunctionComponent<Props> = ({
|
|||
[styles['right-align']]: alignment === 'right',
|
||||
[styles['top-align']]: alignment === 'top',
|
||||
[styles['bottom-align']]: alignment === 'bottom',
|
||||
|
||||
[styles['left-align-md']]: alignmentMd === 'left',
|
||||
[styles['right-align-md']]: alignmentMd === 'right',
|
||||
[styles['top-align-md']]: alignmentMd === 'top',
|
||||
[styles['bottom-align-md']]: alignmentMd === 'bottom',
|
||||
|
||||
// direction
|
||||
[styles['top-direction']]: direction === 'top',
|
||||
[styles['bottom-direction']]: direction === 'bottom',
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import React, { useRef } from 'react';
|
|||
import classnames from 'classnames';
|
||||
|
||||
import PopoverContent from './PopoverContent';
|
||||
import styles from './Popover.module.scss';
|
||||
import styles from './Popover.scss';
|
||||
import { Alignment, Direction } from './types';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -29,6 +29,7 @@ interface Props {
|
|||
renderTrigger: (any) => React.ReactNode;
|
||||
renderContent: () => any;
|
||||
alignment: Alignment;
|
||||
alignmentMd?: Alignment;
|
||||
direction: Direction;
|
||||
contentHasBorder?: boolean;
|
||||
hasArrow?: boolean;
|
||||
|
|
@ -46,6 +47,7 @@ const Popover: React.FunctionComponent<Props> = ({
|
|||
isOpen,
|
||||
setIsOpen,
|
||||
alignment,
|
||||
alignmentMd,
|
||||
direction,
|
||||
renderTrigger,
|
||||
contentId,
|
||||
|
|
@ -83,6 +85,7 @@ const Popover: React.FunctionComponent<Props> = ({
|
|||
wrapperEl={wrapperRef.current}
|
||||
triggerEl={triggerRef.current}
|
||||
alignment={alignment}
|
||||
alignmentMd={alignmentMd}
|
||||
direction={direction}
|
||||
contentId={contentId}
|
||||
hasBorder={contentHasBorder}
|
||||
|
|
|
|||
29
web/src/components/Digest/Empty.scss
Normal file
29
web/src/components/Digest/Empty.scss
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import '../App/responsive';
|
||||
@import '../App/theme';
|
||||
@import '../App/variables';
|
||||
@import '../App/font';
|
||||
@import '../App/rem';
|
||||
|
||||
.wrapper {
|
||||
padding: rem(40px) rem(16px);
|
||||
text-align: center;
|
||||
color: $gray;
|
||||
}
|
||||
22
web/src/components/Digest/Empty.tsx
Normal file
22
web/src/components/Digest/Empty.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SearchParams, Status } from './types';
|
||||
import styles from './Empty.scss';
|
||||
|
||||
interface Props {
|
||||
params: SearchParams;
|
||||
}
|
||||
|
||||
const Empty: React.FunctionComponent<Props> = ({ params }) => {
|
||||
if (params.status === Status.Unreviewed) {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
You have completed reviewing this digest.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={styles.wrapper}>No results matched your filters.</div>;
|
||||
};
|
||||
|
||||
export default Empty;
|
||||
|
|
@ -26,11 +26,14 @@ import { useDispatch } from '../../store';
|
|||
import { setDigestNoteReviewed } from '../../store/digest';
|
||||
import Placeholder from '../Common/Note/Placeholder';
|
||||
import NoteItem from './NoteItem';
|
||||
import Empty from './Empty';
|
||||
import { SearchParams } from './types';
|
||||
import styles from './Digest.scss';
|
||||
|
||||
interface Props {
|
||||
notes: DigestNoteData[];
|
||||
digest: DigestData;
|
||||
params: SearchParams;
|
||||
isFetched: boolean;
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
|
@ -38,6 +41,7 @@ interface Props {
|
|||
const NoteList: React.FunctionComponent<Props> = ({
|
||||
isFetched,
|
||||
isFetching,
|
||||
params,
|
||||
notes,
|
||||
digest
|
||||
}) => {
|
||||
|
|
@ -62,6 +66,10 @@ const NoteList: React.FunctionComponent<Props> = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
if (notes.length === 0) {
|
||||
return <Empty params={params} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Helmet>
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const StatusMenu: React.FunctionComponent<Props> = ({
|
|||
|
||||
const options = [
|
||||
{
|
||||
name: 'all',
|
||||
name: 'unreviewed',
|
||||
value: (
|
||||
<Link
|
||||
role="menuitem"
|
||||
|
|
@ -57,26 +57,6 @@ const StatusMenu: React.FunctionComponent<Props> = ({
|
|||
}}
|
||||
ref={optRefs[0]}
|
||||
tabIndex={-1}
|
||||
>
|
||||
All
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'unreviewed',
|
||||
value: (
|
||||
<Link
|
||||
role="menuitem"
|
||||
className={selectMenuStyles.link}
|
||||
to={getDigestPath(digestUUID, {
|
||||
...searchObj,
|
||||
status: Status.Unreviewed
|
||||
})}
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
ref={optRefs[1]}
|
||||
tabIndex={-1}
|
||||
>
|
||||
Unreviewed
|
||||
</Link>
|
||||
|
|
@ -95,28 +75,48 @@ const StatusMenu: React.FunctionComponent<Props> = ({
|
|||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
ref={optRefs[2]}
|
||||
ref={optRefs[1]}
|
||||
tabIndex={-1}
|
||||
>
|
||||
Reviewed
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'all',
|
||||
value: (
|
||||
<Link
|
||||
role="menuitem"
|
||||
className={selectMenuStyles.link}
|
||||
to={getDigestPath(digestUUID, {
|
||||
...searchObj,
|
||||
status: Status.All
|
||||
})}
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
ref={optRefs[2]}
|
||||
tabIndex={-1}
|
||||
>
|
||||
All
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const isActive = status === Status.Reviewed || status === Status.Unreviewed;
|
||||
const isActive = status === Status.Reviewed || status === Status.All;
|
||||
|
||||
let defaultCurrentOptionIdx: number;
|
||||
let statusText: string;
|
||||
if (status === Status.Reviewed) {
|
||||
defaultCurrentOptionIdx = 2;
|
||||
statusText = 'Reviewed';
|
||||
} else if (status === Status.Unreviewed) {
|
||||
defaultCurrentOptionIdx = 1;
|
||||
statusText = 'Unreviewed';
|
||||
statusText = 'Reviewed';
|
||||
} else if (status === Status.All) {
|
||||
defaultCurrentOptionIdx = 2;
|
||||
statusText = 'All';
|
||||
} else {
|
||||
defaultCurrentOptionIdx = 0;
|
||||
statusText = 'All';
|
||||
statusText = 'Unreviewed';
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -134,8 +134,9 @@ const StatusMenu: React.FunctionComponent<Props> = ({
|
|||
triggerClassName={classnames('button-no-padding', {
|
||||
[styles['active-menu-trigger']]: isActive
|
||||
})}
|
||||
triggerText={` Status: ${statusText} `}
|
||||
alignment="right"
|
||||
triggerText={`Status: ${statusText}`}
|
||||
alignment="left"
|
||||
alignmentMd="right"
|
||||
direction="bottom"
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -79,24 +79,22 @@ function getNotes(notes: DigestNoteData[], p: SearchParams): DigestNoteData[] {
|
|||
});
|
||||
}
|
||||
|
||||
const statusMap = {
|
||||
[Status.All]: Status.All,
|
||||
[Status.Reviewed]: Status.Reviewed,
|
||||
[Status.Unreviewed]: Status.Unreviewed
|
||||
};
|
||||
|
||||
const sortMap = {
|
||||
[Sort.Newest]: Sort.Newest,
|
||||
[Sort.Oldest]: Sort.Oldest
|
||||
};
|
||||
|
||||
function parseSearchParams(location: Location): SearchParams {
|
||||
const searchObj = parseSearchString(location.search);
|
||||
|
||||
let sort: Sort;
|
||||
if (searchObj.sort === Sort.Oldest) {
|
||||
sort = Sort.Oldest;
|
||||
} else {
|
||||
sort = Sort.Newest;
|
||||
}
|
||||
|
||||
let status: Status;
|
||||
if (searchObj.status === Status.Unreviewed) {
|
||||
status = Status.Unreviewed;
|
||||
} else if (searchObj.status === Status.Reviewed) {
|
||||
status = Status.Reviewed;
|
||||
} else {
|
||||
status = Status.All;
|
||||
}
|
||||
const status = statusMap[searchObj.status] || Status.Unreviewed;
|
||||
const sort = sortMap[searchObj.sort] || Sort.Newest;
|
||||
|
||||
return {
|
||||
sort,
|
||||
|
|
@ -153,6 +151,7 @@ const Digest: React.FunctionComponent<Props> = ({ location, match }) => {
|
|||
<div className="container mobile-nopadding">
|
||||
<NoteList
|
||||
digest={digest.data}
|
||||
params={params}
|
||||
notes={notes}
|
||||
isFetched={digest.isFetched}
|
||||
isFetching={digest.isFetching}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ export enum Sort {
|
|||
|
||||
// Status is a set of possible values for status query parameters
|
||||
export enum Status {
|
||||
All = '',
|
||||
Unreviewed = 'unreviewed',
|
||||
Reviewed = 'reviewed'
|
||||
Unreviewed = '',
|
||||
Reviewed = 'reviewed',
|
||||
All = 'all'
|
||||
}
|
||||
|
||||
export interface SearchParams {
|
||||
|
|
|
|||
31
web/src/components/Digests/Digests.scss
Normal file
31
web/src/components/Digests/Digests.scss
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import '../App/responsive';
|
||||
@import '../App/theme';
|
||||
@import '../App/variables';
|
||||
@import '../App/font';
|
||||
@import '../App/rem';
|
||||
|
||||
.flash {
|
||||
margin-top: rem(20px);
|
||||
|
||||
@include breakpoint(lg) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
41
web/src/components/Digests/Empty.scss
Normal file
41
web/src/components/Digests/Empty.scss
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/* Copyright (C) 2019, 2020 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import '../App/responsive';
|
||||
@import '../App/theme';
|
||||
@import '../App/variables';
|
||||
@import '../App/font';
|
||||
@import '../App/rem';
|
||||
|
||||
.wrapper {
|
||||
padding: rem(40px) rem(16px);
|
||||
text-align: center;
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
.support {
|
||||
margin-top: rem(20px);
|
||||
}
|
||||
|
||||
.md-support {
|
||||
display: none;
|
||||
|
||||
@include breakpoint(md) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
26
web/src/components/Digests/Empty.tsx
Normal file
26
web/src/components/Digests/Empty.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { getRepetitionsPath } from 'web/libs/paths';
|
||||
import styles from './Empty.scss';
|
||||
|
||||
interface Props {}
|
||||
|
||||
const Empty: React.FunctionComponent<Props> = () => {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<h3>No digests were found.</h3>
|
||||
|
||||
<p className={styles.support}>
|
||||
You could <Link to={getRepetitionsPath()}>create repetition rules</Link>{' '}
|
||||
first.
|
||||
</p>
|
||||
|
||||
<p className={styles['md-support']}>
|
||||
Digests are automatically created based on your repetition rules.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Empty;
|
||||
|
|
@ -22,6 +22,7 @@ import classnames from 'classnames';
|
|||
import { DigestData } from 'jslib/operations/types';
|
||||
import { getRange } from 'jslib/helpers/arr';
|
||||
import Item from './Item';
|
||||
import Empty from './Empty';
|
||||
import Placeholder from './Placeholder';
|
||||
import styles from './List.scss';
|
||||
|
||||
|
|
@ -49,6 +50,10 @@ const List: React.FunctionComponent<Props> = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={classnames('list-unstyled', styles.wrapper)}>
|
||||
{items.map(item => {
|
||||
|
|
|
|||
|
|
@ -17,10 +17,11 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import { usePrevious } from 'web/libs/hooks';
|
||||
import { getSubscriptionPath } from 'web/libs/paths';
|
||||
import { parseSearchString } from 'jslib/helpers/url';
|
||||
import { useDispatch, useSelector } from '../../store';
|
||||
import { getDigests } from '../../store/digests';
|
||||
|
|
@ -28,6 +29,7 @@ import { Status } from './types';
|
|||
import Flash from '../Common/Flash';
|
||||
import List from './List';
|
||||
import Toolbar from './Toolbar';
|
||||
import styles from './Digests.scss';
|
||||
|
||||
function useFetchDigests(params: { page: number; status: Status }) {
|
||||
const dispatch = useDispatch();
|
||||
|
|
@ -48,9 +50,10 @@ function useFetchDigests(params: { page: number; status: Status }) {
|
|||
interface Props extends RouteComponentProps {}
|
||||
|
||||
const Digests: React.FunctionComponent<Props> = ({ location }) => {
|
||||
const { digests } = useSelector(state => {
|
||||
const { user, digests } = useSelector(state => {
|
||||
return {
|
||||
digests: state.digests
|
||||
digests: state.digests,
|
||||
user: state.auth.user.data
|
||||
};
|
||||
});
|
||||
const { page, status } = parseSearchString(location.search);
|
||||
|
|
@ -69,10 +72,21 @@ const Digests: React.FunctionComponent<Props> = ({ location }) => {
|
|||
<div className="page-header">
|
||||
<h1 className="page-heading">Digests</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Flash kind="danger" when={Boolean(digests.errorMessage)}>
|
||||
<div className="container mobile-nopadding">
|
||||
<Flash
|
||||
kind="danger"
|
||||
when={Boolean(digests.errorMessage)}
|
||||
wrapperClassName={styles.flash}
|
||||
>
|
||||
Error getting digests: {digests.errorMessage}
|
||||
</Flash>
|
||||
|
||||
<Flash when={!user.pro} kind="warning" wrapperClassName={styles.flash}>
|
||||
Digests are not enabled on your plan.{' '}
|
||||
<Link to={getSubscriptionPath()}>Upgrade here.</Link>
|
||||
</Flash>
|
||||
</div>
|
||||
|
||||
<div className="container mobile-nopadding">
|
||||
|
|
|
|||
|
|
@ -77,12 +77,16 @@ const Content: React.FunctionComponent = () => {
|
|||
</div>
|
||||
|
||||
<div className="container mobile-nopadding">
|
||||
<Flash when={!user.pro} kind="warning">
|
||||
<Flash when={!user.pro} kind="warning" wrapperClassName={styles.flash}>
|
||||
Repetitions are not enabled on your plan.{' '}
|
||||
<Link to={getSubscriptionPath()}>Upgrade here.</Link>
|
||||
</Flash>
|
||||
|
||||
<Flash when={user.pro && !user.emailVerified} kind="warning">
|
||||
<Flash
|
||||
when={user.pro && !user.emailVerified}
|
||||
kind="warning"
|
||||
wrapperClassName={styles.flash}
|
||||
>
|
||||
Please verify your email address in order to receive digests.{' '}
|
||||
<Link to={getSettingsPath(SettingSections.account)}>
|
||||
Go to settings.
|
||||
|
|
|
|||
|
|
@ -30,3 +30,11 @@
|
|||
.content {
|
||||
margin-top: rem(20px);
|
||||
}
|
||||
|
||||
.flash {
|
||||
margin-top: rem(20px);
|
||||
|
||||
@include breakpoint(lg) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue