Skip to content

Commit

Permalink
banners/offline banner
Browse files Browse the repository at this point in the history
  • Loading branch information
Puyodead1 committed Sep 5, 2023
1 parent 2f30c9b commit 698ac81
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 12 deletions.
12 changes: 12 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import RegistrationPage from "./pages/RegistrationPage";

import { reaction } from "mobx";
import Loader from "./components/Loader";
import OfflineBanner from "./components/banners/OfflineBanner";
import { UnauthenticatedGuard } from "./components/guards/UnauthenticatedGuard";
import { BannerContext } from "./contexts/BannerContext";
import useLogger from "./hooks/useLogger";
import AppPage from "./pages/AppPage";
import LogoutPage from "./pages/LogoutPage";
Expand All @@ -18,6 +20,7 @@ import { Globals } from "./utils/Globals";

function App() {
const app = useAppStore();
const bannerContext = React.useContext(BannerContext);
const logger = useLogger("App");
const navigate = useNavigate();

Expand Down Expand Up @@ -54,6 +57,15 @@ function App() {
return dispose;
}, []);

React.useEffect(() => {
if (!app.isNetworkConnected)
bannerContext.setContent({
forced: true,
element: <OfflineBanner />,
});
else bannerContext.close();
}, [app.isNetworkConnected]);

return (
<Loader>
<Routes>
Expand Down
68 changes: 68 additions & 0 deletions src/components/Banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { AnimatePresence, motion } from "framer-motion";
import React from "react";
import styled from "styled-components";
import { BannerContext } from "../contexts/BannerContext";
import Icon from "./Icon";
import IconButton from "./IconButton";

const Container = styled(motion.div)`
display: flex;
justify-content: center;
align-items: center;
`;

const CloseWrapper = styled(IconButton)`
position: absolute;
right: 1%;
`;

function Banner() {
const bannerContext = React.useContext(BannerContext);

return (
<AnimatePresence>
{bannerContext.content && (
<Container
variants={{
show: {
// slide down
y: 0,
transition: {
delayChildren: 0.3,
staggerChildren: 0.2,
},
},
hide: {
// slide up
y: "-100%",
transition: {
delayChildren: 0.3,
staggerChildren: 0.2,
},
},
}}
initial="hide"
animate="show"
exit="hide"
onAnimationComplete={() => {
console.log("animation complete");
}}
style={bannerContext.content.style}
>
{bannerContext.content.element}
{!bannerContext.content.forced && (
<CloseWrapper
onClick={() => {
bannerContext.close();
}}
>
<Icon icon="mdiClose" color="var(--text)" size="24px" />
</CloseWrapper>
)}
</Container>
)}
</AnimatePresence>
);
}

export default Banner;
24 changes: 24 additions & 0 deletions src/components/banners/OfflineBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import styled from "styled-components";
import Icon from "../Icon";

const Wrapper = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`;

const Text = styled.span`
padding: 10px;
color: var(--warning);
`;

function OfflineBanner() {
return (
<Wrapper>
<Text>You are offline</Text>
<Icon icon="mdiWifiStrengthOff" color="var(--warning)" size="24px" />
</Wrapper>
);
}

export default OfflineBanner;
31 changes: 31 additions & 0 deletions src/contexts/BannerContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// context to handle banner open/close state

import { MotionStyle } from "framer-motion";
import React from "react";

export interface BannerContent {
element: React.ReactNode;
style?: MotionStyle;
forced?: boolean;
}

export type BannerContextType = {
content?: BannerContent;
setContent: React.Dispatch<React.SetStateAction<BannerContent | undefined>>;
close: () => void;
};

// @ts-expect-error not specifying a default value here
export const BannerContext = React.createContext<BannerContextType>();

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const BannerContextProvider: React.FC<any> = ({ children }) => {
const [content, setContent] = React.useState<BannerContent>();

const close = () => {
// clear content
setContent(undefined);
};

return <BannerContext.Provider value={{ content, setContent, close }}>{children}</BannerContext.Provider>;
};
1 change: 0 additions & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ h5,
h6,
p,
span {
color: var(--text);
padding: 0;
margin: 0;
}
Expand Down
5 changes: 4 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import ModalRenderer from "./components/modals/ModalRenderer";
import { BannerContextProvider } from "./contexts/BannerContext";
import { ContextMenuContextProvider } from "./contexts/ContextMenuContext";
import Theme from "./contexts/Theme";
import "./index.css";
Expand All @@ -25,7 +26,9 @@ root.render(
<BrowserRouter>
<ModalStack renderModals={ModalRenderer}>
<ContextMenuContextProvider>
<App />
<BannerContextProvider>
<App />
</BannerContextProvider>
</ContextMenuContextProvider>
<Theme />
</ModalStack>
Expand Down
29 changes: 21 additions & 8 deletions src/pages/subpages/ChannelPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,32 @@ import { observer } from "mobx-react-lite";
import React from "react";
import { useParams } from "react-router-dom";
import styled from "styled-components";
import Banner from "../../components/Banner";
import ChannelSidebar from "../../components/ChannelSidebar";
import Container from "../../components/Container";
import ContainerComponent from "../../components/Container";
import ContextMenu from "../../components/ContextMenu";
import GuildSidebar from "../../components/GuildSidebar";
import Chat from "../../components/messaging/Chat";
import { BannerContext } from "../../contexts/BannerContext";
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
import { useAppStore } from "../../stores/AppStore";

const Wrapper = styled(Container)`
const Container = styled(ContainerComponent)`
display: flex;
flex-direction: column;
`;

const Wrapper = styled.div`
display: flex;
flex-direction: row;
flex: 1;
overflow: hidden;
`;

function ChannelPage() {
const app = useAppStore();
const contextMenu = React.useContext(ContextMenuContext);
const bannerContext = React.useContext(BannerContext);

const { guildId, channelId } = useParams<{
guildId: string;
Expand All @@ -27,12 +37,15 @@ function ChannelPage() {
const channel = guild?.channels.get(channelId!);

return (
<Wrapper>
{contextMenu.visible && <ContextMenu {...contextMenu} />}
<GuildSidebar guildId={guildId!} />
<ChannelSidebar channel={channel} guild={guild} channelId={channelId} guildId={guildId} />
<Chat channel={channel} guild={guild} channelId={channelId} guildId={guildId} />
</Wrapper>
<Container>
<Banner />
<Wrapper>
{contextMenu.visible && <ContextMenu {...contextMenu} />}
<GuildSidebar guildId={guildId!} />
<ChannelSidebar channel={channel} guild={guild} channelId={channelId} guildId={guildId} />
<Chat channel={channel} guild={guild} channelId={channelId} guildId={guildId} />
</Wrapper>
</Container>
);
}

Expand Down
12 changes: 10 additions & 2 deletions src/stores/AppStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default class AppStore {
// whether the app is still loading
@observable isAppLoading = true;

@observable isNetworkConnected = true; // TODO: Implement this
@observable isNetworkConnected = true;
@observable tokenLoaded = false;
@observable token: string | null = null;

Expand All @@ -42,6 +42,9 @@ export default class AppStore {

constructor() {
makeAutoObservable(this);

window.addEventListener("online", () => this.setNetworkConnected(true));
window.addEventListener("offline", () => this.setNetworkConnected(false));
}

@action
Expand Down Expand Up @@ -91,12 +94,17 @@ export default class AppStore {
secureLocalStorage.removeItem("token");
}

@action
setNetworkConnected(value: boolean) {
this.isNetworkConnected = value;
}

@computed
/**
* Whether the app is done loading and ready to be displayed
*/
get isReady() {
return !this.isAppLoading && this.isGatewayReady && this.isNetworkConnected;
return !this.isAppLoading && this.isGatewayReady /* && this.isNetworkConnected */;
}
}

Expand Down

0 comments on commit 698ac81

Please sign in to comment.