mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-26 04:02:46 +01:00
banners/offline banner
This commit is contained in:
parent
2f30c9bb1b
commit
698ac81ed4
12
src/App.tsx
12
src/App.tsx
@ -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";
|
||||
@ -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();
|
||||
|
||||
@ -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>
|
||||
|
68
src/components/Banner.tsx
Normal file
68
src/components/Banner.tsx
Normal file
@ -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
src/components/banners/OfflineBanner.tsx
Normal file
24
src/components/banners/OfflineBanner.tsx
Normal file
@ -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
src/contexts/BannerContext.tsx
Normal file
31
src/contexts/BannerContext.tsx
Normal file
@ -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>;
|
||||
};
|
@ -6,7 +6,6 @@ h5,
|
||||
h6,
|
||||
p,
|
||||
span {
|
||||
color: var(--text);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -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";
|
||||
@ -25,7 +26,9 @@ root.render(
|
||||
<BrowserRouter>
|
||||
<ModalStack renderModals={ModalRenderer}>
|
||||
<ContextMenuContextProvider>
|
||||
<BannerContextProvider>
|
||||
<App />
|
||||
</BannerContextProvider>
|
||||
</ContextMenuContextProvider>
|
||||
<Theme />
|
||||
</ModalStack>
|
||||
|
@ -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;
|
||||
@ -27,12 +37,15 @@ function ChannelPage() {
|
||||
const channel = guild?.channels.get(channelId!);
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
@ -42,6 +42,9 @@ export default class AppStore {
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
|
||||
window.addEventListener("online", () => this.setNetworkConnected(true));
|
||||
window.addEventListener("offline", () => this.setNetworkConnected(false));
|
||||
}
|
||||
|
||||
@action
|
||||
@ -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 */;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user