diff --git a/.shared/readme-thumbnail.png b/.shared/readme-thumbnail.png index 7ed18d7b..a77534aa 100644 Binary files a/.shared/readme-thumbnail.png and b/.shared/readme-thumbnail.png differ diff --git a/app/src/assets/images/app_hero.svg b/app/src/assets/images/app_hero.svg index acd739de..2cc8d1b9 100644 --- a/app/src/assets/images/app_hero.svg +++ b/app/src/assets/images/app_hero.svg @@ -1,6 +1,6 @@ - + = () => { href={inviteUrl(guild.id)} /> ))} + {guildsLoaded && guilds.length === 0 && ( + + )} {!guildsLoaded && ( // Show a spinner if the pool hasn't been completely loaded diff --git a/app/src/data/path-prefix.ts b/app/src/data/path-prefix.ts index f5826422..504c701f 100644 --- a/app/src/data/path-prefix.ts +++ b/app/src/data/path-prefix.ts @@ -1,6 +1,6 @@ import { graphql, useStaticQuery } from "gatsby"; -import { Option } from "@architus/lib/option"; +import { Option, None, Some } from "@architus/lib/option"; /** * Gets the site's `pathPrefix` if it is set, else None @@ -14,5 +14,7 @@ export function usePathPrefix(): Option { } `); - return Option.from(queryResult.site?.pathPrefix); + return Option.from(queryResult.site?.pathPrefix).flatMap((prefix) => + prefix.trim().length === 0 ? None : Some(prefix) + ); } diff --git a/app/src/layout.ts b/app/src/layout.ts index 70587735..228ec091 100644 --- a/app/src/layout.ts +++ b/app/src/layout.ts @@ -9,7 +9,8 @@ export const sitePaddingVariable = `--site-padding`; export const contentWidthVariable = `--content-width`; export const sitePadding = `var(${sitePaddingVariable})`; export const contentWidth = `var(${contentWidthVariable})`; -export const appHorizontalPadding = gap.milli; +export const appHorizontalPaddingVariable = "--app-horizontal-padding"; +export const appHorizontalPadding = `var(${appHorizontalPaddingVariable})`; export const appVerticalPadding = gap.milli; export const minimizeBreakpoint: BreakpointKey = "md"; @@ -18,6 +19,7 @@ export const layoutGlobal = css` :root { ${sitePaddingVariable}: ${gap.milli}; ${contentWidthVariable}: 1180px; + ${appHorizontalPaddingVariable}: ${gap.milli}; ${down("xl")} { ${contentWidthVariable}: 960px; @@ -29,6 +31,7 @@ export const layoutGlobal = css` ${down("md")} { ${sitePaddingVariable}: ${gap.micro}; + ${appHorizontalPaddingVariable}: ${sitePadding}; } ${down("vs")} { diff --git a/app/src/pages/app.tsx b/app/src/pages/app.tsx index 88b7f008..bfaf4d5b 100644 --- a/app/src/pages/app.tsx +++ b/app/src/pages/app.tsx @@ -19,6 +19,7 @@ import tabs from "@app/tabs/definitions"; import { TabDefinition, TabProps, BaseAppProps } from "@app/tabs/types"; import { useInitialRender } from "@app/utility"; import { Snowflake } from "@app/utility/types"; +import { up } from "@architus/facade/theme/media"; import { gap } from "@architus/facade/theme/spacing"; import { withPathPrefix } from "@architus/lib/path"; @@ -29,8 +30,10 @@ const headerClass = css` const Styled = { Layout: styled(Layout)` - /* Correct the global site padding */ - ${sitePaddingVariable}: ${gap.nano} !important; + ${up("md")} { + /* Correct the global site padding */ + ${sitePaddingVariable}: ${gap.nano} !important; + } `, }; diff --git a/app/src/store/saga/index.ts b/app/src/store/saga/index.ts index b0a2b43a..0fcd3e18 100644 --- a/app/src/store/saga/index.ts +++ b/app/src/store/saga/index.ts @@ -1,6 +1,5 @@ import { SagaIterator } from "@redux-saga/core"; -import { delay } from "@redux-saga/core/effects"; -import { takeEvery, put, fork } from "redux-saga/effects"; +import { takeEvery, put, fork, delay } from "redux-saga/effects"; import { navigate } from "@app/components/Router"; import { @@ -48,9 +47,22 @@ function* autoHideNotification( * Upon sign out, clears session storage, shows a toast, and navigates to the home */ function* handleSignOut(action: ReturnType): SagaIterator { - navigate("/"); setLocalStorage(LOCAL_STORAGE_KEY, ""); if (!action.payload.silent) { yield put(showToast({ message: "Signed out" })); } + + const retries = 8; + for (let i = 0; i < retries; ++i) { + try { + navigate("/"); + } catch (ex) { + if (i === retries - 1) { + // eslint-disable-next-line no-console + console.error(ex); + return; + } + yield delay(250); + } + } } diff --git a/app/src/tabs/AutoResponses/AutoResponses.tsx b/app/src/tabs/AutoResponses/AutoResponses.tsx index 57a0c630..0109a027 100644 --- a/app/src/tabs/AutoResponses/AutoResponses.tsx +++ b/app/src/tabs/AutoResponses/AutoResponses.tsx @@ -6,6 +6,7 @@ import MigrationAlert from "./MigrationAlert"; import { AuthorData, Author } from "./types"; import PageTitle from "@app/components/PageTitle"; import { getAvatarUrl } from "@app/components/UserDisplay"; +import { appHorizontalPadding, appVerticalPadding } from "@app/layout"; import { useDispatch } from "@app/store"; import { useCurrentUser } from "@app/store/actions"; import { usePool, usePoolEntities } from "@app/store/slices/pools"; @@ -25,10 +26,10 @@ const Styled = { flex-direction: column; height: 100%; - padding-top: ${gap.milli}; + padding-top: ${appVerticalPadding}; `, Header: styled.div` - padding: 0 ${gap.milli}; + padding: 0 ${appHorizontalPadding}; p { margin-bottom: ${gap.micro}; diff --git a/design/src/assets/empty.svg b/design/src/assets/empty.svg new file mode 100644 index 00000000..c1973b5b --- /dev/null +++ b/design/src/assets/empty.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/design/src/components/Empty.tsx b/design/src/components/Empty.tsx new file mode 100644 index 00000000..6c1cdd41 --- /dev/null +++ b/design/src/components/Empty.tsx @@ -0,0 +1,86 @@ +import { styled } from "linaria/react"; +import React from "react"; + +import EmptyIcon from "../assets/empty.svg"; +import { gap } from "../theme/spacing"; +import { MergeProps, Nil } from "@architus/lib/types"; +import { isDefined } from "@architus/lib/utility"; + +const Message = styled.p` + margin-bottom: 0; + color: currentColor; + text-align: center; +`; + +const Styled = { + Container: styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 300px; + + svg { + opacity: 0.9; + } + + &[data-size="normal"] { + svg { + width: 48px; + } + + ${Message} { + margin-top: ${gap.pico}; + font-size: 1rem; + max-width: 200px; + } + } + + &[data-size="large"] { + svg { + width: 76px; + } + + ${Message} { + margin-top: ${gap.nano}; + font-size: 1.08rem; + max-width: 300px; + } + } + `, + Message, +}; + +export type Size = "normal" | "large"; +export type EmptyProps = MergeProps< + { + message?: React.ReactNode | Nil; + size?: Size; + className?: string; + style?: React.CSSProperties; + }, + React.HTMLAttributes +>; + +/** + * Empty placeholder content, including an icon and an optional message + */ +const Empty: React.FC = ({ + message, + size = "normal", + className, + style, + ...rest +}) => ( + + + {isDefined(message) && {message}} + +); + +export default Empty; diff --git a/lib/src/types.ts b/lib/src/types.ts index a2c9a523..6ada4a3d 100644 --- a/lib/src/types.ts +++ b/lib/src/types.ts @@ -4,3 +4,5 @@ export type Mutable = { -readonly [K in keyof T]: T[K]; }; export type MutableArray = T extends readonly (infer I)[] ? I[] : never; + +export type MergeProps = A & Omit;