Introduces proper pagination
- Moves temporary strings to the string files. - Adds pagination to the PublicServers module. - Reduces the complexity and knowledge between view and connection. - Adds loading spinner to imrpove UX. - Removed unused CSS.
This commit is contained in:
parent
eecdcebb01
commit
e992e440dc
|
@ -261,6 +261,9 @@ export default {
|
|||
loadMore: "Load More",
|
||||
notConnected: "Not Connected",
|
||||
connectionRequired: "You must connect your account in order to join servers.",
|
||||
connectionError: "Connection Error",
|
||||
connectionErrorMessage: "There was an error connecting to DiscordServers.com, it's possible their website/api is down. Please try again later.",
|
||||
pagination: "Page {{page}} of {{count}}",
|
||||
search: "Search",
|
||||
connect: "Connect",
|
||||
reconnect: "Reconnect",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {WebpackModules} from "modules";
|
||||
import {Logger, WebpackModules} from "modules";
|
||||
|
||||
const SortedGuildStore = WebpackModules.getByProps("getSortedGuilds");
|
||||
const AvatarDefaults = WebpackModules.getByProps("getUserAvatarURL", "DEFAULT_AVATARS");
|
||||
|
@ -19,6 +19,8 @@ const betterDiscordServer = {
|
|||
insertDate: 1517806800
|
||||
};
|
||||
|
||||
const ITEMS_PER_PAGE = 50;
|
||||
|
||||
export default new class PublicServersConnection {
|
||||
|
||||
constructor() {
|
||||
|
@ -43,9 +45,10 @@ export default new class PublicServersConnection {
|
|||
return SortedGuildStore.getFlattenedGuildIds().includes(id);
|
||||
}
|
||||
|
||||
async search({term = "", keyword = "", from = 0} = {}) {
|
||||
if (this.cache.has(term + keyword + from)) return this.cache.get(term + keyword + from);
|
||||
async search({term = "", keyword = "", page = 1} = {}) {
|
||||
if (this.cache.has(term + keyword + page)) return this.cache.get(term + keyword + page);
|
||||
|
||||
const from = (page - 1) * ITEMS_PER_PAGE;
|
||||
const queries = [];
|
||||
if (keyword) queries.push(`keyword=${keyword.replace(/ /g, "%20").toLowerCase()}`);
|
||||
if (term) queries.push(`term=${term.replace(/ /g, "%20")}`);
|
||||
|
@ -55,19 +58,18 @@ export default new class PublicServersConnection {
|
|||
try {
|
||||
const response = await fetch(`${this.endPoint}${query}`, {method: "GET"});
|
||||
const data = await response.json();
|
||||
const next = data.size + data.from;
|
||||
const results = {
|
||||
servers: data.results,
|
||||
size: data.size,
|
||||
from: data.from,
|
||||
total: data.total,
|
||||
next: next >= data.total ? null : next
|
||||
page: Math.ceil(from / ITEMS_PER_PAGE) + 1,
|
||||
numPages: Math.ceil(data.total / ITEMS_PER_PAGE)
|
||||
};
|
||||
this.cache.set(term + keyword + from, results);
|
||||
this.cache.set(term + keyword + page, results);
|
||||
return results;
|
||||
}
|
||||
catch (error) {
|
||||
return null;
|
||||
Logger.stacktrace("PublicServers", "Could not reach search endpoint.", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,6 +93,7 @@ export default new class PublicServersConnection {
|
|||
return {featured: this.cache.get("featured"), popular: this.cache.get("popular"), keywords: this.cache.get("keywords")};
|
||||
}
|
||||
catch (error) {
|
||||
Logger.stacktrace("PublicServers", "Could not download dashboard.", error);
|
||||
return {featured: this.cache.get("featured"), popular: this.cache.get("popular"), keywords: this.cache.get("keywords")};
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +112,8 @@ export default new class PublicServersConnection {
|
|||
});
|
||||
return true;
|
||||
}
|
||||
catch (e) {
|
||||
catch (error) {
|
||||
Logger.warn("PublicServers", "Could not join server.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -130,6 +134,7 @@ export default new class PublicServersConnection {
|
|||
return data;
|
||||
}
|
||||
catch (error) {
|
||||
Logger.warn("PublicServers", "Could not verify connection.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,50 +14,6 @@
|
|||
height: 24px;
|
||||
}
|
||||
|
||||
.bd-layer {
|
||||
-ms-flex-direction: column;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
bottom: 0;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#pubslayer button {
|
||||
background: #7289da;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
padding: 2px 16px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.17s ease;
|
||||
}
|
||||
|
||||
#pubslayer button:hover {
|
||||
background-color: #677bc4;
|
||||
}
|
||||
|
||||
#pubslayer input {
|
||||
color: #f6f6f7;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 10px;
|
||||
height: 30px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
transition: background-color 0.15s ease, border 0.15s ease;
|
||||
}
|
||||
|
||||
#bd-connection {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
@ -249,4 +205,40 @@
|
|||
border-radius: 3px;
|
||||
background: #3e82e5;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bd-pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 15px;
|
||||
color: var(--header-primary);
|
||||
}
|
||||
|
||||
.bd-pagination span {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bd-pagination button {
|
||||
background: none;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid var(--background-tertiary);
|
||||
border-radius: 3px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bd-pagination button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bd-pagination button svg {
|
||||
fill: var(--header-primary);
|
||||
}
|
||||
|
||||
.bd-pagination button[disabled] {
|
||||
opacity: 0.2;
|
||||
cursor: not-allowed;
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
@import "./builtins/*";
|
||||
@import "./ui/*";
|
||||
@import "./buttons.css";
|
||||
@import "./spinner.css";
|
||||
|
||||
.bd-chat-badge {
|
||||
vertical-align: bottom;
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
.bd-spinner {
|
||||
margin: 100px auto;
|
||||
width: 57px;
|
||||
height: 57px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bd-cube1,
|
||||
.bd-cube2 {
|
||||
background-color: #3e82e5;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
animation: bd-sk-cubemove 1.8s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.bd-cube2 {
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
|
||||
@keyframes bd-sk-cubemove {
|
||||
25% {
|
||||
transform: translateX(42px) rotate(-90deg) scale(0.5);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(42px) translateY(42px) rotate(-179deg);
|
||||
}
|
||||
|
||||
50.1% {
|
||||
transform: translateX(42px) translateY(42px) rotate(-180deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateX(0) translateY(42px) rotate(-270deg) scale(0.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import {React} from "modules";
|
||||
|
||||
export default class ArrowRight extends React.Component {
|
||||
render() {
|
||||
const size = this.props.size || "24px";
|
||||
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
|
||||
<path d="M10 17l5-5-5-5v10z" />
|
||||
<path d="M0 24V0h24v24H0z" fill="none" />
|
||||
</svg>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import {React} from "modules";
|
||||
|
||||
export default class ArrowLeft extends React.Component {
|
||||
render() {
|
||||
const size = this.props.size || "24px";
|
||||
return <svg viewBox="0 0 24 24" style={{width: size, height: size}}>
|
||||
<path d="M14 7l-5 5 5 5V7z" />
|
||||
<path d="M24 0v24H0V0h24z" fill="none" />
|
||||
</svg>;
|
||||
}
|
||||
}
|
|
@ -5,12 +5,21 @@ import ServerCard from "./card";
|
|||
import EmptyResults from "./noresults";
|
||||
import Connection from "../../structs/psconnection";
|
||||
import Search from "../settings/components/search";
|
||||
|
||||
import Previous from "../icons/previous";
|
||||
import Next from "../icons/next";
|
||||
|
||||
const SettingsView = WebpackModules.getByDisplayName("SettingsView");
|
||||
const GuildActions = WebpackModules.getByProps("transitionToGuildSync");
|
||||
const LayerManager = WebpackModules.getByProps("popLayer");
|
||||
|
||||
const EMPTY_RESULTS = {
|
||||
servers: [],
|
||||
size: 0,
|
||||
total: 0,
|
||||
page: 0,
|
||||
numPages: 0
|
||||
};
|
||||
|
||||
export default class PublicServers extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
|
@ -20,13 +29,7 @@ export default class PublicServers extends React.Component {
|
|||
query: "",
|
||||
loading: true,
|
||||
user: null,
|
||||
results: {
|
||||
servers: [],
|
||||
size: 0,
|
||||
from: 0,
|
||||
total: 0,
|
||||
next: null
|
||||
}
|
||||
results: Object.assign({}, EMPTY_RESULTS)
|
||||
};
|
||||
|
||||
this.featured = [];
|
||||
|
@ -36,6 +39,7 @@ export default class PublicServers extends React.Component {
|
|||
this.changeTab = this.changeTab.bind(this);
|
||||
this.searchKeyDown = this.searchKeyDown.bind(this);
|
||||
this.connect = this.connect.bind(this);
|
||||
this.loadPreviousPage = this.loadPreviousPage.bind(this);
|
||||
this.loadNextPage = this.loadNextPage.bind(this);
|
||||
this.join = this.join.bind(this);
|
||||
this.navigateTo = this.navigateTo.bind(this);
|
||||
|
@ -62,7 +66,7 @@ export default class PublicServers extends React.Component {
|
|||
this.setState({loading: false});
|
||||
this.changeTab(this.state.tab);
|
||||
|
||||
if (!this.keywords || !this.keywords.length) Modals.showConfirmationModal("Connection Error", "There was an error connecting to DiscordServers.com, it's possible their website/api is down. Please try again later.");
|
||||
if (!this.keywords || !this.keywords.length) Modals.showConfirmationModal(Strings.PublicServers.connectionError, Strings.PublicServers.connectionErrorMessage);
|
||||
}
|
||||
|
||||
async connect() {
|
||||
|
@ -77,18 +81,10 @@ export default class PublicServers extends React.Component {
|
|||
else this.search(term);
|
||||
}
|
||||
|
||||
async search(term = "", from = 0) {
|
||||
async search(term = "", page = 1) {
|
||||
this.setState({query: term, loading: true});
|
||||
const results = await Connection.search({term, keyword: this.state.tab == "All" || this.state.tab == "Featured" || this.state.tab == "Popular" ? "" : this.state.tab, from});
|
||||
if (!results) {
|
||||
return this.setState({results: {
|
||||
servers: [],
|
||||
size: 0,
|
||||
from: 0,
|
||||
total: 0,
|
||||
next: null
|
||||
}});
|
||||
}
|
||||
const results = await Connection.search({term, keyword: this.state.tab == "All" || this.state.tab == "Featured" || this.state.tab == "Popular" ? "" : this.state.tab, page});
|
||||
if (!results) return this.setState({results: Object.assign({}, EMPTY_RESULTS)});
|
||||
|
||||
this.setState({loading: false, results});
|
||||
}
|
||||
|
@ -100,18 +96,26 @@ export default class PublicServers extends React.Component {
|
|||
return this.setState({results: {
|
||||
servers: this[this.state.tab.toLowerCase()],
|
||||
size: this[this.state.tab.toLowerCase()].length,
|
||||
from: 0,
|
||||
total: this[this.state.tab.toLowerCase()].length,
|
||||
next: null
|
||||
page: 1,
|
||||
numPages: 1
|
||||
}});
|
||||
}
|
||||
|
||||
this.search();
|
||||
}
|
||||
|
||||
get hasPrevious() {return this.state.results.page > 1;}
|
||||
get hasNext() {return this.state.results.page < this.state.results.numPages;}
|
||||
|
||||
loadPreviousPage() {
|
||||
if (this.state.loading || !this.hasPrevious) return;
|
||||
this.search(this.state.query, this.state.results.page - 1);
|
||||
}
|
||||
|
||||
loadNextPage() {
|
||||
if (this.state.loading) return;
|
||||
this.search(this.state.query, this.state.results.next);
|
||||
if (this.state.loading || !this.hasNext) return;
|
||||
this.search(this.state.query, this.state.results.page + 1);
|
||||
}
|
||||
|
||||
async join(id, native = false) {
|
||||
|
@ -133,17 +137,20 @@ export default class PublicServers extends React.Component {
|
|||
}
|
||||
|
||||
get searchBox() {
|
||||
return <Search onKeyDown={this.searchKeyDown} className="bd-server-search" placeholder={`${Strings.PublicServers.search}...`} />;
|
||||
return <Search onKeyDown={this.searchKeyDown} className="bd-server-search" placeholder={`${Strings.PublicServers.search}...`} value={this.state.query} />;
|
||||
}
|
||||
|
||||
get title() {
|
||||
if (this.state.loading) return `${Strings.PublicServers.loading}...`;
|
||||
const start = this.state.results.from + 1;
|
||||
const total = this.state.results.total;
|
||||
const end = this.state.results.next ? this.state.results.next : total;
|
||||
let title = Strings.PublicServers.results.format({start, end, total, category: this.state.tab});
|
||||
if (this.state.query) title += " " + Strings.PublicServers.query.format({query: this.state.query});
|
||||
return title;
|
||||
if (this.state.query) {
|
||||
const start = ((this.state.results.page - 1) * this.state.results.size) + 1;
|
||||
const total = this.state.results.total;
|
||||
const end = this.hasNext ? (start - 1) + this.state.results.size : total;
|
||||
let title = Strings.PublicServers.results.format({start, end, total, category: this.state.tab});
|
||||
if (this.state.query) title += " " + Strings.PublicServers.query.format({query: this.state.query});
|
||||
return title;
|
||||
}
|
||||
return this.state.tab;
|
||||
}
|
||||
|
||||
get content() {
|
||||
|
@ -151,14 +158,34 @@ export default class PublicServers extends React.Component {
|
|||
const servers = this.state.results.servers.map((server) => {
|
||||
return React.createElement(ServerCard, {key: server.identifier, server: server, joined: Connection.hasJoined(server.identifier), join: this.join, navigateTo: this.navigateTo, defaultAvatar: Connection.getDefaultAvatar});
|
||||
});
|
||||
|
||||
let content = React.createElement(EmptyResults);
|
||||
if (this.state.loading) content = this.loadingIndicator;
|
||||
else if (this.state.results.total) content = React.createElement("div", {className: "bd-card-list"}, servers);
|
||||
|
||||
return [React.createElement(SettingsTitle, {text: this.title, button: connectButton}),
|
||||
this.state.results.total ? React.createElement("div", {className: "bd-card-list"}, servers) : React.createElement(EmptyResults),
|
||||
this.state.results.next ? this.nextButton : null,
|
||||
this.state.results.servers.length > 0 && React.createElement(SettingsTitle, {text: this.title})];
|
||||
(this.state.tab !== "Featured" && this.state.tab !== "Popular") && this.pagination,
|
||||
content,
|
||||
(this.state.tab !== "Featured" && this.state.tab !== "Popular") && this.pagination,
|
||||
this.state.results.servers.length > 0 && React.createElement(SettingsTitle, {text: this.title})
|
||||
];
|
||||
}
|
||||
|
||||
get nextButton() {
|
||||
return React.createElement("button", {type: "button", className: "bd-button bd-button-next", onClick: this.loadNextPage}, this.state.loading ? Strings.PublicServers.loading : Strings.PublicServers.loadMore);
|
||||
get loadingIndicator() {
|
||||
return <div className="bd-loading">
|
||||
<div className="bd-spinner">
|
||||
<div className="bd-cube1"></div>
|
||||
<div className="bd-cube2"></div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
get pagination() {
|
||||
return React.createElement("div", {className: "bd-pagination"},
|
||||
React.createElement("button", {type: "button", className: "bd-button bd-pagination-previous", disabled: !this.hasPrevious, onClick: this.loadPreviousPage}, <Previous />),
|
||||
React.createElement("span", {className: "bd-pagination-info"}, Strings.PublicServers.pagination.format({page: this.state.results.page, count: this.state.results.numPages})),
|
||||
React.createElement("button", {type: "button", className: "bd-button bd-pagination-next", disabled: !this.hasNext, onClick: this.loadNextPage}, <Next />)
|
||||
);
|
||||
}
|
||||
|
||||
get connection() {
|
||||
|
|
Loading…
Reference in New Issue