/* P.E.S.T Web App — root: async store provider over nc-proxy.php, routing, login gate */ const EMPTY_STATE = { projects: [], tokens: [], users: [], referralsByMember: {}, votes: [], myVotes: {}, user: { nick: "", username: "", isAdmin: false, pinned: [], watchTokens: [] }, loggedIn: false, bootstrapped: false, }; const ERR_MSG = { invalid_credentials: "Wrong username or password.", missing_fields: "Fill in all fields.", username_taken: "That username is taken.", username_too_short: "Username is too short.", password_too_short: "Use at least 6 characters.", wrong_password: "Current password is wrong.", pin_cap: "Dashboard is full (8 max).", watch_cap: "Watchlist is full (8 max).", ref_cap: "You can only share 3 referral links.", unknown_user: "That user no longer exists.", nick_taken: "That nickname is already taken.", cannot_delete_self: "You can't delete your own account.", cannot_demote_self: "You can't remove your own admin rights.", invalid_user: "Fill in nick, username and a password (6+ chars).", invalid_url: "Enter a valid URL (https://…).", unknown_project: "That project no longer exists.", unknown_token: "That token no longer exists.", duplicate_id: "An item with that id already exists.", duplicate_token: "A token with that symbol already exists.", already_voted: "You already voted.", vote_closed: "Voting is closed for this proposal.", admin_only: "Admin only.", unauthenticated: "Session expired. Please log in again.", file_too_large: "Image is too large (max 2 MB).", not_an_image: "That file isn't a valid image.", unsupported_image_type: "Use PNG, JPG, GIF or WEBP.", }; function errMsg(e) { const k = (e && e.message) || ""; return ERR_MSG[k] || ("Something went wrong (" + (k || "unknown") + ")"); } // Translate server response { db, nick } into local state slice. function deriveState(resp) { const db = resp.db || {}; const meNick = resp.nick || ""; const me = (db.users || []).find((u) => u.nick === meNick) || {}; return { projects: db.projects || [], tokens: db.tokens || [], users: db.users || [], referralsByMember: db.referralsByMember || {}, votes: db.votes || [], myVotes: (db.myVotes && typeof db.myVotes === "object") ? db.myVotes : {}, user: { nick: me.nick || meNick, username: me.username || "", isAdmin: !!me.isAdmin, pinned: me.pinned || [], watchTokens: me.watchTokens || [], }, loggedIn: true, bootstrapped: true, }; } function useStoreState() { const [state, setState] = useState(EMPTY_STATE); const members = useMemo(() => window.PEST_APP.getMembers(), []); // Initial: try to resume session from cookie. useEffect(() => { let alive = true; window.PEST_APP.api.fetchDb() .then((resp) => { if (alive) setState(deriveState(resp)); }) .catch((e) => { if (!alive) return; if (e.status === 401) setState((s) => ({ ...s, bootstrapped: true, loggedIn: false })); else { toast(errMsg(e), "err"); setState((s) => ({ ...s, bootstrapped: true, loggedIn: false })); } }); return () => { alive = false; }; }, []); const member = useMemo( () => members.find((m) => m.nick === state.user.nick) || members[0], [members, state.user.nick] ); const runOp = useCallback(async (type, payload, okMsg) => { try { const resp = await window.PEST_APP.api.op(type, payload); setState(deriveState(resp)); if (okMsg) toast(okMsg); return true; } catch (e) { if (e.status === 401) { setState({ ...EMPTY_STATE, bootstrapped: true }); toast("Session expired.", "err"); } else { toast(errMsg(e), "err"); } return false; } }, []); const api = useMemo(() => ({ projects: state.projects, tokens: state.tokens, users: state.users, referralsByMember: state.referralsByMember, votes: state.votes, myVotes: state.myVotes, user: state.user, members, member, loggedIn: state.loggedIn, bootstrapped: state.bootstrapped, login: async (username, password) => { try { const resp = await window.PEST_APP.api.login(username, password); setState(deriveState(resp)); return { ok: true }; } catch (e) { return { ok: false, error: errMsg(e) }; } }, logout: async () => { try { await window.PEST_APP.api.logout(); } catch (_) {} setState({ ...EMPTY_STATE, bootstrapped: true }); }, pinProject: (id) => runOp("pinProject", { id }), unpinProject: (id) => runOp("unpinProject", { id }), toggleToken: (id) => runOp("toggleToken", { id }), setUsername: (username) => runOp("setUsername", { username }, "Username updated"), changePassword:(current, next) => runOp("changePassword", { current, new: next }, "Password changed"), addReferral: (projectId, url) => runOp("addReferral", { projectId, url }, "Referral link added"), removeReferral:(idx) => runOp("removeReferral", { idx }, "Referral removed"), castVote: (voteId, optionId) => runOp("castVote", { voteId, optionId }, "Vote cast"), addProject: (proj) => runOp("addProject", proj, proj.name + " added"), updateProject: (proj) => runOp("updateProject", proj, proj.name + " updated"), deleteProject: (id, name) => runOp("deleteProject", { id }, (name || "Project") + " deleted"), addToken: (tok) => runOp("addToken", tok, tok.symbol + " added"), updateToken: (tok) => runOp("updateToken", tok, tok.symbol + " updated"), deleteToken: (id, symbol) => runOp("deleteToken", { id }, (symbol || "Token") + " deleted"), addVote: (vote) => runOp("addVote", vote, "Vote created"), closeVote: (id) => runOp("closeVote", { id }, "Vote closed"), deleteVote: (id) => runOp("deleteVote",{ id }, "Vote deleted"), createUser: (u) => runOp("createUser", u, u.nick + " created"), adminUpdateUser:(patch, okMsg) => runOp("adminUpdateUser", patch, okMsg || "User updated"), deleteUser: (nick) => runOp("deleteUser", { nick }, nick + " deleted"), }), [state, members, member, runOp]); return api; } function BootSplash() { return (
P.E.S.T
connecting…