diff --git a/src/data/strings.js b/src/data/strings.js index fdc14e9c..a2658907 100644 --- a/src/data/strings.js +++ b/src/data/strings.js @@ -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", diff --git a/src/structs/psconnection.js b/src/structs/psconnection.js index 6a98e385..c8bf5a3d 100644 --- a/src/structs/psconnection.js +++ b/src/structs/psconnection.js @@ -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; } } diff --git a/src/styles/builtins/publicservers.css b/src/styles/builtins/publicservers.css index 9ebb8118..aa67887b 100644 --- a/src/styles/builtins/publicservers.css +++ b/src/styles/builtins/publicservers.css @@ -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; } \ No newline at end of file diff --git a/src/styles/index.css b/src/styles/index.css index 7425a063..7e1a3483 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -3,6 +3,7 @@ @import "./builtins/*"; @import "./ui/*"; @import "./buttons.css"; +@import "./spinner.css"; .bd-chat-badge { vertical-align: bottom; diff --git a/src/styles/spinner.css b/src/styles/spinner.css new file mode 100644 index 00000000..d2699f68 --- /dev/null +++ b/src/styles/spinner.css @@ -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); + } +} \ No newline at end of file diff --git a/src/ui/icons/next.jsx b/src/ui/icons/next.jsx new file mode 100644 index 00000000..50b1b993 --- /dev/null +++ b/src/ui/icons/next.jsx @@ -0,0 +1,11 @@ +import {React} from "modules"; + +export default class ArrowRight extends React.Component { + render() { + const size = this.props.size || "24px"; + return + + + ; + } +} \ No newline at end of file diff --git a/src/ui/icons/previous.jsx b/src/ui/icons/previous.jsx new file mode 100644 index 00000000..913705df --- /dev/null +++ b/src/ui/icons/previous.jsx @@ -0,0 +1,11 @@ +import {React} from "modules"; + +export default class ArrowLeft extends React.Component { + render() { + const size = this.props.size || "24px"; + return + + + ; + } +} \ No newline at end of file diff --git a/src/ui/publicservers/menu.js b/src/ui/publicservers/menu.js index 17f53f16..242cab02 100644 --- a/src/ui/publicservers/menu.js +++ b/src/ui/publicservers/menu.js @@ -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 ; + return ; } 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
+
+
+
+
+
; + } + + 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}, ), + 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}, ) + ); } get connection() {