diff --git a/packages/design-system/atoms/dot-overlay/DotOverlay.stories.tsx b/packages/design-system/atoms/dot-overlay/DotOverlay.stories.tsx index 4047561..7f6618c 100644 --- a/packages/design-system/atoms/dot-overlay/DotOverlay.stories.tsx +++ b/packages/design-system/atoms/dot-overlay/DotOverlay.stories.tsx @@ -7,3 +7,4 @@ export default { export const Dark = () => ; export const Light = () => ; +export const Skeleton = () => ; diff --git a/packages/design-system/atoms/dot-overlay/DotOverlay.tsx b/packages/design-system/atoms/dot-overlay/DotOverlay.tsx index 1a84233..f503145 100644 --- a/packages/design-system/atoms/dot-overlay/DotOverlay.tsx +++ b/packages/design-system/atoms/dot-overlay/DotOverlay.tsx @@ -1,3 +1,4 @@ +import { animateOpacity } from '@roleypoly/design-system/atoms/placeholder'; import * as React from 'react'; import styled from 'styled-components'; @@ -33,6 +34,22 @@ const DotOverlayLight = styled(dotOverlayBase)` ); `; -export const DotOverlay = ({ light }: { light?: boolean }) => { - return light ? : ; +const DotOverlaySkeleton = styled(DotOverlayDark)` + ${animateOpacity} +`; + +export const DotOverlay = ({ + light, + skeleton, +}: { + light?: boolean; + skeleton?: boolean; +}) => { + return skeleton ? ( + + ) : light ? ( + + ) : ( + + ); }; diff --git a/packages/design-system/atoms/loading-text/Loading.stories.tsx b/packages/design-system/atoms/loading-text/Loading.stories.tsx new file mode 100644 index 0000000..8d776a3 --- /dev/null +++ b/packages/design-system/atoms/loading-text/Loading.stories.tsx @@ -0,0 +1,12 @@ +import { Hero } from '@roleypoly/design-system/atoms/hero'; +import { LoadingFill } from './Loading'; +export default { + title: 'Atoms/Loading Text', + component: LoadingFill, +}; + +export const loading = (args) => ( + + + +); diff --git a/packages/design-system/atoms/loading-text/Loading.tsx b/packages/design-system/atoms/loading-text/Loading.tsx new file mode 100644 index 0000000..ec93eb0 --- /dev/null +++ b/packages/design-system/atoms/loading-text/Loading.tsx @@ -0,0 +1,30 @@ +const loadingTexts = [ + 'Loading Roleypoly...', + 'Reticulating splines...', + 'Mining cryptocoins...', + 'Going to Mars...', + 'Building the Box Signature Edition...', + 'Hiring a new CEO...', + 'Firing the new CEO...', + 'Doing a calculation...', + 'Doin a fixy boi...', + 'Feeling like a plastic bag...', + 'Levelling up...', + 'Your Roleypoly is evolving!!!', + 'Adding subtitles...', + 'Rolling... Rolling...', +]; + +export const LoadingFill = (props: { forceIndex?: keyof typeof loadingTexts }) => { + const useEasterEgg = Math.floor(Math.random() * 10) === 0; + const index = + props.forceIndex !== undefined + ? props.forceIndex + : useEasterEgg + ? Math.floor(Math.random() * loadingTexts.length) + : 0; + + const text = loadingTexts[index]; + + return <>{text}>; +}; diff --git a/packages/design-system/atoms/loading-text/index.ts b/packages/design-system/atoms/loading-text/index.ts new file mode 100644 index 0000000..bc5115b --- /dev/null +++ b/packages/design-system/atoms/loading-text/index.ts @@ -0,0 +1 @@ +export * from './Loading'; diff --git a/packages/design-system/atoms/placeholder/Placeholder.stories.tsx b/packages/design-system/atoms/placeholder/Placeholder.stories.tsx new file mode 100644 index 0000000..2fc770b --- /dev/null +++ b/packages/design-system/atoms/placeholder/Placeholder.stories.tsx @@ -0,0 +1,13 @@ +import { palette } from '../colors'; +import { PlaceholderBox } from './Placeholder'; + +export default { + title: 'Atoms/Placeholder', + component: PlaceholderBox, + args: { + firstColor: palette.taupe100, + secondColor: palette.taupe300, + }, +}; + +export const placeholderBox = (args) => ; diff --git a/packages/design-system/atoms/placeholder/Placeholder.tsx b/packages/design-system/atoms/placeholder/Placeholder.tsx new file mode 100644 index 0000000..3663132 --- /dev/null +++ b/packages/design-system/atoms/placeholder/Placeholder.tsx @@ -0,0 +1,55 @@ +import styled, { css, keyframes } from 'styled-components'; +import { palette } from '../colors'; + +export const fadeInOut = keyframes` + from { + background-color: var(--placeholder-first-color); + } + to { + background-color: var(--placeholder-second-color); + } +`; + +export const animateFade = (firstColor?: string, secondColor?: string) => css` + @media (prefers-reduced-motion: no-preference) { + animation: ${fadeInOut} 2s ease-in-out infinite alternate; + } + + --placeholder-first-color: ${firstColor}; + --placeholder-second-color: ${secondColor}; +`; + +type PlaceholderProps = { + forceReduceMotion?: boolean; + firstColor?: string; + secondColor?: string; +}; + +export const PlaceholderBox = styled.div` + width: 7em; + height: 1em; + background-color: ${(props) => props.firstColor || palette.taupe200}; + display: inline-block; + border-radius: 2px; + position: relative; + top: 0.2em; + ${(props) => + props.secondColor && + !props.forceReduceMotion && + animateFade(props.firstColor, props.secondColor)} +`; + +export const opacityInOut = keyframes` + from { + opacity: 0.6; + } + to { + opacity: 0.3; + } +`; + +export const animateOpacity = css` + @media (prefers-reduced-motion: no-preference) { + animation: ${opacityInOut} 5s ease-in-out infinite alternate; + } +`; diff --git a/packages/design-system/atoms/placeholder/index.ts b/packages/design-system/atoms/placeholder/index.ts new file mode 100644 index 0000000..5c37725 --- /dev/null +++ b/packages/design-system/atoms/placeholder/index.ts @@ -0,0 +1 @@ +export * from './Placeholder'; diff --git a/packages/design-system/atoms/spinner/Spinner.stories.tsx b/packages/design-system/atoms/spinner/Spinner.stories.tsx new file mode 100644 index 0000000..530bf52 --- /dev/null +++ b/packages/design-system/atoms/spinner/Spinner.stories.tsx @@ -0,0 +1,13 @@ +import { Hero } from '@roleypoly/design-system/atoms/hero'; +import { Spinner } from './Spinner'; + +export default { + title: 'Atoms/Spinner', + component: Spinner, +}; + +export const spinner = (args) => ( + + + +); diff --git a/packages/design-system/atoms/spinner/Spinner.tsx b/packages/design-system/atoms/spinner/Spinner.tsx new file mode 100644 index 0000000..5b2aa2a --- /dev/null +++ b/packages/design-system/atoms/spinner/Spinner.tsx @@ -0,0 +1,62 @@ +import styled, { keyframes } from 'styled-components'; +import { palette } from '../colors'; + +type SpinnerProps = { + size?: number; + reverse?: boolean; + color?: string; + speed?: number; +}; + +const spinnerKeyframes = keyframes` + 0%, 100% { + border-width: 0; + border-right-width: 3px; + } + 25% { + border-width: 0; + border-top-width: 3px; + } + 50% { + border-width: 0; + border-left-width: 3px; + } + 75% { + border-width: 0; + border-bottom-width: 3px; + } +`; + +const SpinnerStyled = styled.div>` + @media (prefers-reduced-motion: no-preference) { + animation: ${spinnerKeyframes} ${(props) => props.speed}s linear infinite + ${(props) => (props.reverse ? `reverse` : '')}; + transform: rotateZ(0); + } + + border: 0 solid ${(props) => props.color}; + width: ${(props) => props.size}px; + height: ${(props) => props.size}px; + border-radius: ${(props) => props.size * 0.7}px; + display: flex; + align-items: center; + justify-content: center; +`; + +export const Spinner = ( + props: SpinnerProps & { innerColor?: string; outerColor?: string } +) => ( + + + +); diff --git a/packages/design-system/atoms/spinner/index.ts b/packages/design-system/atoms/spinner/index.ts new file mode 100644 index 0000000..b259397 --- /dev/null +++ b/packages/design-system/atoms/spinner/index.ts @@ -0,0 +1 @@ +export * from './Spinner'; diff --git a/packages/design-system/molecules/user-avatar-group/UserAvatarGroup.stories.tsx b/packages/design-system/molecules/user-avatar-group/UserAvatarGroup.stories.tsx index bf60186..b81d941 100644 --- a/packages/design-system/molecules/user-avatar-group/UserAvatarGroup.stories.tsx +++ b/packages/design-system/molecules/user-avatar-group/UserAvatarGroup.stories.tsx @@ -2,6 +2,7 @@ import { Hero } from '@roleypoly/design-system/atoms/hero'; import * as React from 'react'; import { user } from '../../fixtures/storyData'; import { UserAvatarGroup } from './UserAvatarGroup'; +import { UserAvatarGroupSkeleton } from './UserAvatarGroupSkeleton'; export default { title: 'Molecules/User Avatar Group', @@ -17,3 +18,9 @@ export const Default = (args) => ( ); + +export const skeleton = () => ( + + + +); diff --git a/packages/design-system/molecules/user-avatar-group/UserAvatarGroupSkeleton.tsx b/packages/design-system/molecules/user-avatar-group/UserAvatarGroupSkeleton.tsx new file mode 100644 index 0000000..b6a21aa --- /dev/null +++ b/packages/design-system/molecules/user-avatar-group/UserAvatarGroupSkeleton.tsx @@ -0,0 +1,17 @@ +import { Avatar } from '@roleypoly/design-system/atoms/avatar'; +import { palette } from '@roleypoly/design-system/atoms/colors'; +import { PlaceholderBox } from '@roleypoly/design-system/atoms/placeholder'; +import * as React from 'react'; +import { Collapse, Group, GroupText } from './UserAvatarGroup.styled'; + +export const UserAvatarGroupSkeleton = () => ( + + + + + + + + + +); diff --git a/packages/design-system/molecules/user-avatar-group/index.ts b/packages/design-system/molecules/user-avatar-group/index.ts index 52cf06c..1e1e2b2 100644 --- a/packages/design-system/molecules/user-avatar-group/index.ts +++ b/packages/design-system/molecules/user-avatar-group/index.ts @@ -1 +1,2 @@ export * from './UserAvatarGroup'; +export * from './UserAvatarGroupSkeleton'; diff --git a/packages/design-system/organisms/app-shell/AppShell.tsx b/packages/design-system/organisms/app-shell/AppShell.tsx index ab8b771..2e348cd 100644 --- a/packages/design-system/organisms/app-shell/AppShell.tsx +++ b/packages/design-system/organisms/app-shell/AppShell.tsx @@ -15,13 +15,35 @@ export type AppShellProps = { guilds?: GuildSlug[]; recentGuilds?: string[]; disableGuildPicker?: boolean; + skeleton?: boolean; +}; + +const OptionallyScroll = (props: { + shouldScroll: boolean; + children: React.ReactNode; +}) => { + if (props.shouldScroll) { + return ( + + {props.children} + + ); + } + + return <>{props.children}>; }; export const AppShell = (props: AppShellProps) => ( <> - {props.user ? ( + {props.skeleton ? ( + + ) : props.user ? ( ( ) : ( )} - - {props.children} - {props.showFooter && } - + + <> + {props.children} + {props.showFooter && } + > + > ); diff --git a/packages/design-system/organisms/masthead/Masthead.stories.tsx b/packages/design-system/organisms/masthead/Masthead.stories.tsx index 30c44d3..146cd3c 100644 --- a/packages/design-system/organisms/masthead/Masthead.stories.tsx +++ b/packages/design-system/organisms/masthead/Masthead.stories.tsx @@ -2,15 +2,20 @@ import * as React from 'react'; import { guild, mastheadSlugs, user } from '../../fixtures/storyData'; import { Authed } from './Authed'; import { Guest } from './Guest'; +import { Skeleton } from './Skeleton'; export default { title: 'Organisms/Masthead', }; -export const HasGuilds = () => ( - +export const hasGuilds = () => ( + ); -export const NoGuilds = () => ; +export const noGuilds = () => ( + +); -export const Guest_ = () => ; +export const guest = () => ; + +export const skeleton = () => ; diff --git a/packages/design-system/organisms/masthead/Skeleton.tsx b/packages/design-system/organisms/masthead/Skeleton.tsx new file mode 100644 index 0000000..9b8b8c0 --- /dev/null +++ b/packages/design-system/organisms/masthead/Skeleton.tsx @@ -0,0 +1,27 @@ +import { Logotype } from '@roleypoly/design-system/atoms/branding'; +import { palette } from '@roleypoly/design-system/atoms/colors'; +import { UserAvatarGroupSkeleton } from '@roleypoly/design-system/molecules/user-avatar-group'; +import { + MastheadAlignment, + MastheadBase, + MastheadLeft, + MastheadRight, +} from './Masthead.styled'; + +export const Skeleton = () => ( + + + + + + + + + + +); diff --git a/packages/design-system/organisms/masthead/index.ts b/packages/design-system/organisms/masthead/index.ts index d3f96a7..2fcce1b 100644 --- a/packages/design-system/organisms/masthead/index.ts +++ b/packages/design-system/organisms/masthead/index.ts @@ -1,2 +1,3 @@ export * from './Authed'; export * from './Guest'; +export * from './Skeleton'; diff --git a/packages/design-system/templates/generic-loading/GenericLoading.stories.tsx b/packages/design-system/templates/generic-loading/GenericLoading.stories.tsx new file mode 100644 index 0000000..f881477 --- /dev/null +++ b/packages/design-system/templates/generic-loading/GenericLoading.stories.tsx @@ -0,0 +1,7 @@ +import { GenericLoadingTemplate } from './GenericLoading'; +export default { + title: 'Templates/Generic Loading', + component: GenericLoadingTemplate, +}; + +export const genericLoading = (args) => ; diff --git a/packages/design-system/templates/generic-loading/GenericLoading.styled.ts b/packages/design-system/templates/generic-loading/GenericLoading.styled.ts new file mode 100644 index 0000000..a3f7420 --- /dev/null +++ b/packages/design-system/templates/generic-loading/GenericLoading.styled.ts @@ -0,0 +1,6 @@ +import { palette } from '@roleypoly/design-system/atoms/colors'; +import styled from 'styled-components'; + +export const TextStyle = styled.div` + color: ${palette.taupe500}; +`; diff --git a/packages/design-system/templates/generic-loading/GenericLoading.tsx b/packages/design-system/templates/generic-loading/GenericLoading.tsx new file mode 100644 index 0000000..57ee11b --- /dev/null +++ b/packages/design-system/templates/generic-loading/GenericLoading.tsx @@ -0,0 +1,28 @@ +import { DotOverlay } from '@roleypoly/design-system/atoms/dot-overlay'; +import { Hero } from '@roleypoly/design-system/atoms/hero'; +import { LoadingFill } from '@roleypoly/design-system/atoms/loading-text'; +import { Space } from '@roleypoly/design-system/atoms/space'; +import { Spinner } from '@roleypoly/design-system/atoms/spinner'; +import { AppShell } from '@roleypoly/design-system/organisms/app-shell'; +import styled from 'styled-components'; +import { TextStyle } from './GenericLoading.styled'; + +const Center = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; + +export const GenericLoadingTemplate = (props: { children?: React.ReactNode }) => ( + + + + + + + {props.children ? props.children : } + + + +); diff --git a/packages/design-system/templates/generic-loading/index.ts b/packages/design-system/templates/generic-loading/index.ts new file mode 100644 index 0000000..aa803c2 --- /dev/null +++ b/packages/design-system/templates/generic-loading/index.ts @@ -0,0 +1 @@ +export * from './GenericLoading'; diff --git a/packages/web/src/app-router/AppRouter.tsx b/packages/web/src/app-router/AppRouter.tsx index 1aebefe..d6892a4 100644 --- a/packages/web/src/app-router/AppRouter.tsx +++ b/packages/web/src/app-router/AppRouter.tsx @@ -1,11 +1,12 @@ import { Router } from '@reach/router'; +import { GenericLoadingTemplate } from '@roleypoly/design-system/templates/generic-loading'; import * as React from 'react'; +import AuthLogin from '../pages/auth/login'; +import LandingPage from '../pages/landing'; -const LandingPage = React.lazy(() => import('../pages/landing')); const ServersPage = React.lazy(() => import('../pages/servers')); const PickerPage = React.lazy(() => import('../pages/picker')); -const AuthLogin = React.lazy(() => import('../pages/auth/login')); const MachineryNewSession = React.lazy(() => import('../pages/machinery/new-session')); const MachineryLogout = React.lazy(() => import('../pages/machinery/logout')); const MachineryBotJoin = React.lazy(() => import('../pages/machinery/bot-join')); @@ -18,7 +19,7 @@ const RouteWrapper = (props: { path?: string; default?: boolean; }) => ( - Loading...}> + }> ); @@ -26,7 +27,7 @@ const RouteWrapper = (props: { export const AppRouter = () => { return ( - + @@ -37,7 +38,7 @@ export const AppRouter = () => { - + diff --git a/packages/web/src/pages/auth/login.tsx b/packages/web/src/pages/auth/login.tsx index 147bfd4..27fa0e0 100644 --- a/packages/web/src/pages/auth/login.tsx +++ b/packages/web/src/pages/auth/login.tsx @@ -1,12 +1,13 @@ import { redirectTo } from '@reach/router'; import { AuthLogin } from '@roleypoly/design-system/templates/auth-login'; +import { GenericLoadingTemplate } from '@roleypoly/design-system/templates/generic-loading'; import { GuildSlug } from '@roleypoly/types'; import React from 'react'; import { useApiContext } from '../../contexts/api/ApiContext'; import { useSessionContext } from '../../contexts/session/SessionContext'; import { Title } from '../../utils/metaTitle'; -const Login = () => { +const Login = (props: { path: string }) => { const { apiUrl, fetch } = useApiContext(); const { isAuthenticated } = useSessionContext(); // If ?r is in query, then let's render the slug page @@ -46,7 +47,7 @@ const Login = () => { }, [apiUrl, fetch, isAuthenticated]); if (guildSlug === null) { - return Loading...; + return Sending you to Discord...; } return ( diff --git a/packages/web/src/pages/landing.tsx b/packages/web/src/pages/landing.tsx index fecbfb3..cd46fa2 100644 --- a/packages/web/src/pages/landing.tsx +++ b/packages/web/src/pages/landing.tsx @@ -5,7 +5,7 @@ import { useAppShellProps } from '../contexts/app-shell/AppShellContext'; import { useSessionContext } from '../contexts/session/SessionContext'; import { Title } from '../utils/metaTitle'; -const Landing = () => { +const Landing = (props: { path: string }) => { const { isAuthenticated } = useSessionContext(); const appShellProps = useAppShellProps(); diff --git a/packages/web/src/pages/machinery/logout.tsx b/packages/web/src/pages/machinery/logout.tsx index 9c6e9f5..5bfb7ff 100644 --- a/packages/web/src/pages/machinery/logout.tsx +++ b/packages/web/src/pages/machinery/logout.tsx @@ -1,3 +1,4 @@ +import { GenericLoadingTemplate } from '@roleypoly/design-system/templates/generic-loading'; import React from 'react'; const Logout = () => { @@ -7,7 +8,7 @@ const Logout = () => { window.location.href = '/'; }, []); - return Logging you out...; + return Logging you out...; }; export default Logout; diff --git a/packages/web/src/pages/machinery/new-session.tsx b/packages/web/src/pages/machinery/new-session.tsx index 869e7a9..caf4da9 100644 --- a/packages/web/src/pages/machinery/new-session.tsx +++ b/packages/web/src/pages/machinery/new-session.tsx @@ -1,4 +1,6 @@ +import { palette } from '@roleypoly/design-system/atoms/colors'; import { Link } from '@roleypoly/design-system/atoms/typography'; +import { GenericLoadingTemplate } from '@roleypoly/design-system/templates/generic-loading'; import * as React from 'react'; import { useSessionContext } from '../../contexts/session/SessionContext'; import { Title } from '../../utils/metaTitle'; @@ -32,13 +34,15 @@ const NewSession = (props: { sessionID: string }) => { )(props.sessionID); return ( - <> + - Logging you into Roleypoly... - - If you aren't redirected soon, click here. + + Logging you into Roleypoly... + + If you aren't redirected soon, click here. + - > + ); };