all repos — caroster @ 2d426f3ead783465e40efb9d91142388b5ec43da

[Octree] Group carpool to your event https://caroster.io

feat:✨ Add banner

#305
Simon Mulquin simon@octree.ch
Mon, 23 May 2022 13:11:16 +0000
commit

2d426f3ead783465e40efb9d91142388b5ec43da

parent

14ff149677768b81ffc4721bcaa06242ff86d300

M backend/api/settings/models/settings.settings.jsonbackend/api/settings/models/settings.settings.json

@@ -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" } } }
A frontend/components/Banner/index.tsx

@@ -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;
A frontend/components/Markdown/index.tsx

@@ -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;
M frontend/containers/EventBar/index.tsxfrontend/containers/EventBar/index.tsx

@@ -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),
M frontend/containers/GenericToolbar/index.tsxfrontend/containers/GenericToolbar/index.tsx

@@ -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',
M frontend/containers/Languages/Icon.tsxfrontend/containers/Languages/Icon.tsx

@@ -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"
M frontend/containers/Profile/index.jsfrontend/containers/Profile/index.js

@@ -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;
M frontend/containers/TravelColumns/index.tsxfrontend/containers/TravelColumns/index.tsx

@@ -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',
M frontend/generated/graphql.tsxfrontend/generated/graphql.tsx

@@ -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 } } `;
M frontend/graphql/setting.gqlfrontend/graphql/setting.gql

@@ -3,5 +3,6 @@ setting {

id gtm_id about_link + announcement } }
M frontend/layouts/Centered.tsxfrontend/layouts/Centered.tsx

@@ -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;
M frontend/package.jsonfrontend/package.json

@@ -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": {
M frontend/pages/auth/confirm.tsxfrontend/pages/auth/confirm.tsx

@@ -13,7 +13,7 @@ const Confirm = () => {

const {t} = useTranslation(); return ( - <Layout> + <Layout displayMenu={false}> <Card> <CardMedia component={Logo} /> <CardContent>
M frontend/pages/auth/register.tsxfrontend/pages/auth/register.tsx

@@ -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> );
M frontend/pages/dashboard.tsxfrontend/pages/dashboard.tsx

@@ -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;
M frontend/pages/e/[uuid].tsxfrontend/pages/e/[uuid].tsx

@@ -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),
M frontend/pages/profile.tsxfrontend/pages/profile.tsx

@@ -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();
A frontend/stores/useBannerStore.ts

@@ -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;
M frontend/yarn.lockfrontend/yarn.lock

@@ -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"