feat:✨ Add banner #305
jump to
@@ -2,11 +2,13 @@ {
"kind": "singleType", "collectionName": "settings", "info": { - "name": "settings" + "name": "settings", + "description": "" }, "options": { "increments": true, - "timestamps": true + "timestamps": true, + "draftAndPublish": false }, "attributes": { "gtm_id": {@@ -15,6 +17,9 @@ "regex": "GTM-.*"
}, "about_link": { "type": "string" + }, + "announcement": { + "type": "richtext" } } }
@@ -0,0 +1,81 @@
+import {Icon} from '@material-ui/core'; +import Button from '@material-ui/core/Button'; +import {makeStyles} from '@material-ui/core/styles'; +import {useEffect} from 'react'; +import {useElementSize, useEventListener} from 'usehooks-ts'; +import Markdown from '../Markdown'; +import useBannerStore from '../../stores/useBannerStore'; + +interface Props { + message: string; + open: boolean; + onClear?: () => void; +} + +const Banner = (props: Props) => { + const {message, open, onClear} = props; + const classes = useStyles(); + const [bannerRef, {height}] = useElementSize(); + const setBannerHeight = useBannerStore(s => s.setBannerHeight); + const setBannerOffset = useBannerStore(s => s.setBannerOffset); + useEffect(() => setBannerHeight({height}), [height]); + + if (typeof document != 'undefined') { + useEventListener('scroll', () => { + const y = window.scrollY; + if (y > height) { + setBannerOffset({offset: 0}) + } + if (y <= height) { + setBannerOffset({offset: height - y}); + } + }) + } + + if (!open) return null; + + return ( + <div className={classes.banner} ref={bannerRef}> + <Markdown className={classes.htmlReset}>{message}</Markdown> + <Button + className={classes.clear} + onClick={e => { + e.stopPropagation(); + onClear(); + }} + > + <Icon>close</Icon> + </Button> + </div> + ); +}; + +const useStyles = makeStyles(theme => ({ + banner: { + position: 'relative', + background: `linear-gradient(90deg, ${theme.palette.secondary.main} 20%, ${theme.palette.primary.main} 90%)`, + width: '100%', + padding: '12px 60px', + textAlign: 'center', + zIndex: theme.zIndex.appBar - 1, + }, + clear: { + position: 'absolute', + right: '12px', + bottom: '0', + minWidth: '44px', + padding: '12px', + lineHeight: '1.4em', + }, + htmlReset: { + '& a': { + color: 'inherit', + margin: 0, + }, + '& p': { + margin: 0, + }, + }, +})); + +export default Banner;
@@ -0,0 +1,24 @@
+import { styled } from '@material-ui/core/styles'; +import Typography, { TypographyProps } from '@material-ui/core/Typography'; +import {marked} from 'marked'; + +const Markdown = (props: TypographyProps) => { + const {children, ...typographyProps} = props; + + if (!children) return null; + + return ( + <Text + {...typographyProps} + dangerouslySetInnerHTML={{__html: marked(children)}} + /> + ); +}; + +const Text = styled(Typography)(({theme}) => ({ + '& > *:first-child': { + marginTop: 0, + }, +})); + +export default Markdown;
@@ -18,12 +18,18 @@ import useProfile from '../../hooks/useProfile';
import useSettings from '../../hooks/useSettings'; import GenericMenu from '../GenericMenu'; import EventDetails from '../EventDetails'; +import useBannerStore from '../../stores/useBannerStore'; +import Banner from '../../components/Banner'; + +let persistedLastAnnouncementSeen = ''; +if (typeof localStorage !== 'undefined') { + persistedLastAnnouncementSeen = localStorage.getItem('lastAnnouncementSeen'); +} const EventBar = ({event, onAdd, onSave, onShare}) => { const {t} = useTranslation(); const router = useRouter(); const [detailsOpen, toggleDetails] = useReducer(i => !i, false); - const classes = useStyles({detailsOpen}); const [anchorEl, setAnchorEl] = useState(null); const isEditing = useEventStore(s => s.isEditing); const setIsEditing = useEventStore(s => s.setIsEditing);@@ -32,6 +38,22 @@ const {user} = useProfile();
const settings = useSettings(); const setTour = useTourStore(s => s.setTour); const tourStep = useTourStore(s => s.step); + const bannerOffset = useBannerStore(s => s.offset); + const bannerHeight = useBannerStore(s => s.height); + const classes = useStyles({detailsOpen, bannerOffset, bannerHeight}); + const announcement = settings?.announcement || ''; + const [lastAnnouncementSeen, setLastAnnouncementSeen] = useState( + persistedLastAnnouncementSeen + ); + const showAnnouncement = + announcement !== '' && announcement !== lastAnnouncementSeen; + + const onBannerClear = () => { + if (typeof announcement != 'undefined') { + localStorage.setItem('lastAnnouncementSeen', String(announcement)); + } + setLastAnnouncementSeen(announcement); + }; useEffect(() => { onTourChange(toggleDetails);@@ -122,10 +144,15 @@
return ( <AppBar className={classes.appbar} - position="static" + position="fixed" color="primary" id={(isEditing && 'EditEvent') || (detailsOpen && 'Details') || 'Menu'} > + <Banner + message={announcement} + open={showAnnouncement} + onClear={onBannerClear} + /> <Toolbar> <div className={classes.name}> <Link href={appLink}>@@ -241,14 +268,13 @@ } else if (fromTo(2, 3) || fromTo(3, 2) || fromTo(3, 4)) toggleDetails();
}; const useStyles = makeStyles(theme => ({ - appbar: ({detailsOpen}) => ({ + appbar: ({detailsOpen, bannerOffset, bannerHeight}) => ({ overflow: 'hidden', - height: detailsOpen ? '100vh' : theme.mixins.toolbar.minHeight, + minHeight: detailsOpen ? '100vh' : theme.mixins.toolbar.minHeight, overflowY: detailsOpen ? 'scroll' : 'hidden', transition: 'height 0.3s ease', zIndex: theme.zIndex.appBar, - position: 'fixed', - top: 0, + marginTop: bannerOffset - bannerHeight , }), logo: { marginRight: theme.spacing(2),
@@ -10,6 +10,14 @@ import Icon from '@material-ui/core/Icon';
import useProfile from '../../hooks/useProfile'; import GenericMenu from '../GenericMenu'; import {ActionType} from '../GenericMenu/Action'; +import useBannerStore from '../../stores/useBannerStore'; +import Banner from '../../components/Banner'; +import useSettings from '../../hooks/useSettings'; + +let persistedLastAnnouncementSeen = ''; +if (typeof localStorage !== 'undefined') { + persistedLastAnnouncementSeen = localStorage.getItem('lastAnnouncementSeen'); +} const GenericToolbar = ({ title,@@ -22,8 +30,24 @@ goBack: () => void | null;
}) => { const router = useRouter(); const [anchorEl, setAnchorEl] = useState(null); - const classes = useStyles(); + const bannerOffset = useBannerStore(s => s.offset); + const bannerHeight = useBannerStore(s => s.height); + const classes = useStyles({bannerOffset, bannerHeight}); const {user} = useProfile(); + const settings = useSettings(); + const announcement = settings?.announcement || ''; + const [lastAnnouncementSeen, setLastAnnouncementSeen] = useState( + persistedLastAnnouncementSeen + ); + const showAnnouncement = + announcement !== '' && announcement !== lastAnnouncementSeen; + + const onBannerClear = () => { + if (typeof announcement != 'undefined') { + localStorage.setItem('lastAnnouncementSeen', String(announcement)); + } + setLastAnnouncementSeen(announcement); + }; const userInfos = user ? [{label: user.username, id: 'Email'}, {divider: true}]@@ -40,13 +64,20 @@ color="primary"
className={classes.appbar} id="Menu" > + <Banner + message={announcement} + open={showAnnouncement} + onClear={onBannerClear} + /> <Toolbar> {goBack && ( <IconButton edge="start" className={classes.goBack} onClick={() => - router.basePath.split('/').length > 2 ? router.back() : router.push('/dashboard') + router.basePath.split('/').length > 2 + ? router.back() + : router.push('/dashboard') } > <Icon>arrow_back</Icon>@@ -87,14 +118,13 @@ );
}; const useStyles = makeStyles(theme => ({ - container: { - padding: theme.spacing(2), - }, - appbar: { - height: theme.mixins.toolbar.minHeight, + appbar: ({bannerHeight, bannerOffset}) => ({ + minHeight: theme.mixins.toolbar.minHeight, transition: 'height 0.3s ease', zIndex: theme.zIndex.appBar, - }, + display: 'block', + marginTop: bannerOffset - bannerHeight, + }), name: { flexGrow: 1, display: 'flex',
@@ -9,6 +9,7 @@ import {Enum_Userspermissionsuser_Lang} from '../../generated/graphql';
import withLanguagesSelection, { LanguageSelectionComponentProps, } from './withLanguagesSelection'; +import useBannerStore from '../../stores/useBannerStore'; const IconLanguageSelection = ({ language,@@ -17,6 +18,8 @@ onConfirmCallback,
}: LanguageSelectionComponentProps) => { const {t} = useTranslation(); const [anchorEl, setAnchorEl] = useState(null); + const bannerHeight = useBannerStore(s => s.height); + const bannerOffset = useBannerStore(s => s.offset); const handleClick = event => { setAnchorEl(event.currentTarget);@@ -31,7 +34,7 @@ };
return ( <> - <Box position="fixed" top={0} right={0} zIndex={1050} p={1}> + <Box position="absolute" top={bannerOffset - bannerHeight} right={0} zIndex={1050} p={1}> <IconButton color="primary" aria-label="Languages"
@@ -8,11 +8,13 @@ import {useTranslation} from 'react-i18next';
import EditPassword from './EditPassword'; import ProfileField from './ProfileField'; import useToastStore from '../../stores/useToastStore'; +import useBannerStore from '../../stores/useBannerStore'; const Profile = ({profile, updateProfile, logout}) => { const {t} = useTranslation(); const addToast = useToastStore(s => s.addToast); - const classes = useStyles(); + const bannerHeight = useBannerStore(s => s.height); + const classes = useStyles({bannerHeight}); const [isEditing, setIsEditing] = useState(false); const [isEditingPassword, setIsEditingPassword] = useState(false); const [firstName, setFirstName] = useState(profile.firstName);@@ -69,7 +71,7 @@ );
return ( <form> - <Card> + <Card className={classes.container}> <CardContent> <ProfileField name="firstName"@@ -146,9 +148,13 @@ );
}; const useStyles = makeStyles(theme => ({ + container: ({bannerHeight}) => ({ + marginTop: bannerHeight + theme.mixins.toolbar.minHeight, + }), actions: { marginTop: theme.spacing(2), justifyContent: 'flex-end', }, })); + export default Profile;
@@ -164,7 +164,6 @@
const useStyles = makeStyles(theme => ({ container: { minHeight: '100vh', - paddingTop: theme.mixins.toolbar.minHeight, paddingLeft: theme.spacing(6), paddingRight: theme.spacing(6), [theme.breakpoints.down('sm')]: {@@ -173,8 +172,6 @@ paddingRight: theme.spacing(),
}, display: 'flex', flexDirection: 'column', - overflowX: 'hidden', - overflowY: 'auto', }, dots: { height: '56px',
@@ -779,6 +779,7 @@
export type SettingInput = { gtm_id?: Maybe<Scalars['String']>; about_link?: Maybe<Scalars['String']>; + announcement?: Maybe<Scalars['String']>; created_by?: Maybe<Scalars['ID']>; updated_by?: Maybe<Scalars['ID']>; };@@ -790,6 +791,7 @@ created_at: Scalars['DateTime'];
updated_at: Scalars['DateTime']; gtm_id?: Maybe<Scalars['String']>; about_link?: Maybe<Scalars['String']>; + announcement?: Maybe<Scalars['String']>; };@@ -1740,6 +1742,7 @@
export type EditSettingInput = { gtm_id?: Maybe<Scalars['String']>; about_link?: Maybe<Scalars['String']>; + announcement?: Maybe<Scalars['String']>; created_by?: Maybe<Scalars['ID']>; updated_by?: Maybe<Scalars['ID']>; };@@ -2016,7 +2019,7 @@ export type SettingQuery = (
{ __typename?: 'Query' } & { setting?: Maybe<( { __typename?: 'Settings' } - & Pick<Settings, 'id' | 'gtm_id' | 'about_link'> + & Pick<Settings, 'id' | 'gtm_id' | 'about_link' | 'announcement'> )> } );@@ -2518,6 +2521,7 @@ setting {
id gtm_id about_link + announcement } } `;
@@ -3,5 +3,6 @@ setting {
id gtm_id about_link + announcement } }
@@ -1,9 +1,12 @@
import {makeStyles} from '@material-ui/core/styles'; import Container from '@material-ui/core/Container'; import DefaultLayout from './Default'; +import useBannerStore from '../stores/useBannerStore'; const CenteredLayout = ({children, ...props}) => { - const classes = useStyles(); + const bannerHeight = useBannerStore(s => s.height); + const bannerOffset = useBannerStore(s => s.offset) + const classes = useStyles({bannerHeight, bannerOffset}); return ( <DefaultLayout className={classes.layout} {...props}>@@ -12,13 +15,14 @@ </DefaultLayout>
); }; -const useStyles = makeStyles(() => ({ - layout: { - minHeight: '100vh', +const useStyles = makeStyles((theme) => ({ + layout: ({bannerHeight, bannerOffset}) => ({ + minHeight: `calc(100vh - ${bannerHeight})`, + paddingTop: theme.mixins.toolbar.minHeight + bannerOffset - bannerHeight, display: 'flex', alignItems: 'center', justifyContent: 'center', - }, + }), })); export default CenteredLayout;
@@ -19,6 +19,7 @@ "@types/react": "^17.0.0",
"deepmerge": "^4.2.2", "graphql": "^15.5.0", "i18next": "^20.3.1", + "marked": "^4.0.16", "moment": "^2.29.1", "next": "^11.0.0", "next-pwa": "^5.2.21",@@ -29,6 +30,7 @@ "react-i18next": "^11.11.0",
"react-joyride": "^2.3.2", "react-slick": "^0.28.1", "typescript": "^4.1.3", + "usehooks-ts": "^2.5.3", "zustand": "^3.3.1" }, "devDependencies": {
@@ -13,7 +13,7 @@ const Confirm = () => {
const {t} = useTranslation(); return ( - <Layout> + <Layout displayMenu={false}> <Card> <CardMedia component={Logo} /> <CardContent>
@@ -6,6 +6,7 @@ import Layout from '../../layouts/Centered';
import SignUpForm from '../../containers/SignUpForm'; import LoginGoogle from '../../containers/LoginGoogle'; import Logo from '../../components/Logo'; +import LanguagesIcon from '../../containers/Languages/Icon'; const SignUp = () => { const {t} = useTranslation();@@ -16,6 +17,7 @@ <CardMedia component={Logo} />
<SignUpForm /> <Divider /> <LoginGoogle /> + <LanguagesIcon /> </Card> </Layout> );
@@ -10,6 +10,7 @@ import DashboardEvents from '../containers/DashboardEvents';
import DashboardEmpty from '../containers/DashboardEmpty'; import Loading from '../containers/Loading'; import Fab from '../containers/Fab'; +import useBannerStore from '../stores/useBannerStore'; const Dashboard = () => { const {t} = useTranslation();@@ -17,7 +18,8 @@ const router = useRouter();
const isAuth = useAuthStore(s => !!s.token); const {profile, isReady} = useProfile(); const {events} = profile || {}; - const classes = useStyles(); + const bannerOffset = useBannerStore(s => s.offset); + const classes = useStyles({bannerOffset}); useEffect(() => { if (!isAuth) router.push('/');@@ -41,9 +43,10 @@ .sort(sortDesc),
[events] ); - const noDateEvents = useMemo(() => events?.filter(({date}) => !date), [ - events, - ]); + const noDateEvents = useMemo( + () => events?.filter(({date}) => !date), + [events] + ); const menuActions = [ {@@ -90,10 +93,10 @@
const sortDesc = ({date: dateA}, {date: dateB}) => dateB.localeCompare(dateA); const useStyles = makeStyles(theme => ({ - root: { + root: ({bannerOffset}) => ({ minHeight: '100vh', - paddingTop: theme.mixins.toolbar.minHeight, - }, + paddingTop: theme.mixins.toolbar.minHeight + bannerOffset, + }), })); export default Dashboard;
@@ -26,6 +26,7 @@ import ErrorPage from '../_error';
import useProfile from '../../hooks/useProfile'; import Fab from '../../containers/Fab'; import useMediaQuery from '@material-ui/core/useMediaQuery'; +import useBannerStore from '../../stores/useBannerStore'; const POLL_INTERVAL = 10000;@@ -43,14 +44,13 @@ };
const Event = (props: Props) => { const {eventUUID} = props; - const classes = useStyles(); + const bannerOffset = useBannerStore(s => s.offset) + const classes = useStyles({bannerOffset}); const theme = useTheme(); const {t} = useTranslation(); const {user} = useProfile(); - const { - data: {me: {profile: {vehicles = []} = {}} = {}} = {}, - loading - } = useFindUserVehiclesQuery(); + const {data: {me: {profile: {vehicles = []} = {}} = {}} = {}, loading} = + useFindUserVehiclesQuery(); const addToast = useToastStore(s => s.addToast); const setEvent = useEventStore(s => s.setEvent); const eventUpdate = useEventStore(s => s.event);@@ -65,7 +65,7 @@ variables: {uuid: eventUUID},
}); const matches = useMediaQuery(theme.breakpoints.down('sm')); const addCarClasses = matches ? 'tour_travel_add' : ''; - + useEffect(() => { if (event) setEvent(event as EventType); }, [event]);@@ -94,6 +94,7 @@ if (!event || loading) return <Loading />;
return ( <Layout + className={classes.layout} pageTitle={t('event.title', {title: event.name})} menuTitle={t('event.title', {title: event.name})} displayMenu={false}@@ -158,6 +159,9 @@ };
} const useStyles = makeStyles(theme => ({ + layout: ({bannerOffset}) => ({ + paddingTop: theme.mixins.toolbar.minHeight + bannerOffset, + }), bottomRight: { position: 'absolute', bottom: theme.spacing(1),
@@ -7,6 +7,7 @@ import useProfile from '../hooks/useProfile';
import Loading from '../containers/Loading'; import Profile from '../containers/Profile'; import Layout from '../layouts/Centered'; +import Banner from '../components/Banner'; const ProfilePage = () => { const router = useRouter();
@@ -0,0 +1,24 @@
+import create from 'zustand'; + +type BannerState = { + height: number; + offset: number; +}; + +type BannerStore = BannerState & { + setBannerHeight: (store: Partial<BannerState>) => void; + setBannerOffset: (store: Partial<BannerState>) => void; +}; + +const useBannerStore = create<BannerStore>(set => ({ + height: 0, + setBannerHeight: store => { + set(s => ({...s, offset:store.height ,height: store.height})); + }, + offset: 0, + setBannerOffset: store => { + set(s => ({offset: store.offset})); + } +})); + +export default useBannerStore;
@@ -5127,6 +5127,11 @@ version "0.2.2"
resolved "https://npm-8ee.hidora.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= +marked@^4.0.16: + version "4.0.16" + resolved "https://npm-8ee.hidora.com/marked/-/marked-4.0.16.tgz#9ec18fc1a723032eb28666100344d9428cf7a264" + integrity sha512-wahonIQ5Jnyatt2fn8KqF/nIqZM8mh3oRu2+l5EANGMhu6RFjiSG52QNE2eWzFMI94HqYSgN184NurgNG6CztA== + md5.js@^1.3.4: version "1.3.5" resolved "https://npm-8ee.hidora.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"@@ -7277,6 +7282,11 @@ resolved "https://npm-8ee.hidora.com/use-subscription/-/use-subscription-1.5.1.tgz#73501107f02fad84c6dd57965beb0b75c68c42d1"
integrity sha512-Xv2a1P/yReAjAbhylMfFplFKj9GssgTwN7RlcTxBujFQcloStWNDQdc4g4NRWH9xS4i/FDk04vQBptAXoF3VcA== dependencies: object-assign "^4.1.1" + +usehooks-ts@^2.5.3: + version "2.5.3" + resolved "https://npm-8ee.hidora.com/usehooks-ts/-/usehooks-ts-2.5.3.tgz#379738eda9dc4058431a6fb6c0252ed7144102b8" + integrity sha512-9R6gMpMSjW7twa1wpW67xYWhEB7UGyKj+8/CEN6RHPlJsdv4fmzDe8mB+Dn5ABcTWtFajqqm1uVpSPGTUQ715g== util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2"