Skip to content
Snippets Groups Projects
Unverified Commit 180b0303 authored by Samuel Couillard's avatar Samuel Couillard Committed by GitHub
Browse files

New Homepage (#4528)

* Initial commit: rework navbar

* Clean up Header

* Add upper layer of Homepage layout

* Initial work on lower layout, Homepage Card

* Add Features Cards, re-work the homepage texts

* Add regular-height class if user is logged in for room/join page

* esf

* Fix header avatar
parent a64f398e
Branches
Tags
No related merge requests found
...@@ -25,15 +25,18 @@ ...@@ -25,15 +25,18 @@
"created_at": "Created at", "created_at": "Created at",
"homepage": { "homepage": {
"welcome_bbb": "Welcome to BigBlueButton.", "welcome_bbb": "Welcome to BigBlueButton.",
"greenlight_description": "Greenlight is a simple front-end for your BigBlueButton open-source web conferencing server. You can create your own rooms to host sessions, or join others using a short and convenient link.", "bigbluebutton_description": "BigBlueButton is an open source web conferencing system for online classes. The platform maximizes time for applied learning by enabling students to collaborate and receive feedback in real-time.",
"greenlight_description": "Create your own rooms to host sessions, or join others using a short and convenient link.",
"learn_more": "Learn more about BigBlueButton",
"explore_features": "Explore our features",
"meeting_title": "Launch a meeting", "meeting_title": "Launch a meeting",
"meeting_description": "Launch a virtual class that maximizes applied learning.", "meeting_description": "Launch a virtual class with video, audio, screen sharing, chat, and all the tools required for applied learning.",
"recording_title": "Record your meetings", "recording_title": "Record your meetings",
"recording_description": "BigBlueButton meetings can be recorded and shared to the students.", "recording_description": "Record the BigBlueButton meetings and share them with the students to review and reflect on the material.",
"settings_title": "Manage your rooms", "settings_title": "Manage your rooms",
"settings_description": "Configure a room to help you run an effective virtual classroom.", "settings_description": "Configure the rooms, the meeting settings, to be in charge an effective classroom.",
"learning_tools_title": "And more!", "and_more_title": "And more!",
"learning_tools_description": "BigBlueButton gives you built-in tools to improve the online learning & teaching experience." "and_more_description": "BigBlueButton offers built-in tools for applied learning and is designed to save you time during class."
}, },
"authentication": { "authentication": {
"sign_in": "Sign In", "sign_in": "Sign In",
......
...@@ -556,16 +556,27 @@ input[type="range"]:focus::-moz-range-thumb { ...@@ -556,16 +556,27 @@ input[type="range"]:focus::-moz-range-thumb {
min-height: 2.5em; min-height: 2.5em;
} }
.homepage-icon { // Homepage
width: 300px; #homepage-hero {
margin-top: 5rem;
margin-bottom: 5rem;
img { @include media-breakpoint-down(md) {
max-width: 100px; margin-top: 3rem;
margin-bottom: 3rem;
}
} }
h4 { .homepage-card {
color: var(--brand-color) !important; min-height: 300px;
min-width: 300px;
} }
.homepage-card-icon-circle {
width: 50px;
height: 50px;
background-color: var(--brand-color);
box-shadow: 0 0 0 8px whitesmoke;
} }
// Table Placeholders length // Table Placeholders length
......
...@@ -10,10 +10,13 @@ import useSiteSetting from './hooks/queries/site_settings/useSiteSetting'; ...@@ -10,10 +10,13 @@ import useSiteSetting from './hooks/queries/site_settings/useSiteSetting';
export default function App() { export default function App() {
const currentUser = useAuth(); const currentUser = useAuth();
const pageHeight = currentUser?.signed_in ? 'regular-height' : 'no-header-height';
const location = useLocation(); const location = useLocation();
// //i18n // Pages that do not need a header: SignIn, SignUp and JoinMeeting (if the user is not signed in)
const headerPage = location.pathname !== '/signin' && location.pathname !== '/signup' && !location.pathname.includes('/join');
const pageHeight = (headerPage || currentUser.signed_in) ? 'regular-height' : 'no-header-height';
// i18n
const { i18n } = useTranslation(); const { i18n } = useTranslation();
useEffect(() => { useEffect(() => {
i18n.changeLanguage(currentUser?.language); i18n.changeLanguage(currentUser?.language);
...@@ -29,7 +32,7 @@ export default function App() { ...@@ -29,7 +32,7 @@ export default function App() {
return ( return (
<> <>
{location.pathname !== '/' && currentUser?.signed_in && <Header /> } {headerPage && <Header /> }
<Container className={pageHeight}> <Container className={pageHeight}>
<Outlet /> <Outlet />
</Container> </Container>
......
import React, { useEffect } from 'react';
import { Form, Stack } from 'react-bootstrap';
import Button from 'react-bootstrap/Button';
import { useLocation, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import ButtonLink from '../shared_components/utilities/ButtonLink';
import useSiteSetting from '../../hooks/queries/site_settings/useSiteSetting';
import useEnv from '../../hooks/queries/env/useEnv';
export default function AuthButtons() {
const { data: env } = useEnv();
const { t } = useTranslation();
const { search } = useLocation();
const { data: registrationMethod } = useSiteSetting('RegistrationMethod');
const [searchParams] = useSearchParams();
const inviteToken = searchParams.get('inviteToken');
useEffect(() => {
document.cookie = `inviteToken=${inviteToken};path=/;`;
}, [inviteToken]);
function showSignUp() {
return registrationMethod !== 'invite' || !!inviteToken;
}
if (env?.OPENID_CONNECT) {
return (
<Form action="/auth/openid_connect" method="POST" data-turbo="false">
<input type="hidden" name="authenticity_token" value={document.querySelector('meta[name="csrf-token"]').content} />
<Button variant="brand-outline-color" className="btn m-2" type="submit">{t('authentication.sign_up')}</Button>
<Button variant="brand" className="btn m-2" type="submit">{t('authentication.sign_in')}</Button>
</Form>
);
}
return (
<Stack direction="horizontal">
{ showSignUp()
&& (
<ButtonLink to={`/signup${search}`} variant="brand-outline-color" className="btn me-2">
{t('authentication.sign_up')}
</ButtonLink>
) }
<ButtonLink to="/signin" variant="brand" className="btn">{t('authentication.sign_in')}</ButtonLink>
</Stack>
);
}
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { import {
Col, Row, Form, Col, Row,
} from 'react-bootstrap'; } from 'react-bootstrap';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Button from 'react-bootstrap/Button'; import {
import ButtonLink from '../shared_components/utilities/ButtonLink'; ArrowRightIcon, Cog8ToothIcon, ComputerDesktopIcon, VideoCameraIcon, WrenchScrewdriverIcon,
import useEnv from '../../hooks/queries/env/useEnv'; } from '@heroicons/react/24/outline';
import Logo from '../shared_components/Logo';
import useSiteSetting from '../../hooks/queries/site_settings/useSiteSetting';
import { useAuth } from '../../contexts/auth/AuthProvider'; import { useAuth } from '../../contexts/auth/AuthProvider';
import HomepageFeatureCard from './HomepageFeatureCard';
import MeetingIcon from '../../../assets/images/desktop-computer.png';
import RecordingIcon from '../../../assets/images/webcam.png';
import SettingsIcon from '../../../assets/images/setting.png';
import LearningToolsIcon from '../../../assets/images/paint-brush.png';
import HomepageIcon from './HomepageIcon';
export default function HomePage() { export default function HomePage() {
const { data: env } = useEnv();
const { t } = useTranslation(); const { t } = useTranslation();
const { search } = useLocation();
const currentUser = useAuth(); const currentUser = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams();
const inviteToken = searchParams.get('inviteToken');
const { data: registrationMethod } = useSiteSetting('RegistrationMethod');
useEffect(() => {
document.cookie = `inviteToken=${inviteToken};path=/;`;
}, [inviteToken]);
// redirect user to correct page based on signed in status and CreateRoom permission // Redirects the user to the proper page based on signed in status and CreateRoom permission
useEffect( useEffect(
() => { () => {
// Todo: Use PermissionChecker. // Todo: Use PermissionChecker.
...@@ -44,60 +28,54 @@ export default function HomePage() { ...@@ -44,60 +28,54 @@ export default function HomePage() {
[currentUser.signed_in], [currentUser.signed_in],
); );
function showSignUp() {
return registrationMethod !== 'invite' || !!inviteToken;
}
// TODO - samuel: OPENID signup and signin are both pointing at the same endpoint
return ( return (
<> <>
<Row className="wide-white"> <Row className="wide-white">
<Col className="mx-auto"> <Col lg={10}>
<div className="text-center pt-xl-5 my-3"> <div id="homepage-hero">
<Logo />
</div>
<div className="text-center">
<h1 className="my-4"> {t('homepage.welcome_bbb')} </h1> <h1 className="my-4"> {t('homepage.welcome_bbb')} </h1>
<p className="text-muted fs-5 px-xxl-5"> <p className="text-muted fs-5">
{t('homepage.bigbluebutton_description')}
</p>
<p className="text-muted fs-5">
{t('homepage.greenlight_description')} {t('homepage.greenlight_description')}
</p> </p>
<a href="https://bigbluebutton.org/" className="pt-5 fs-5 text-link"> Learn more about BigBlueButton. </a> <a href="https://bigbluebutton.org/" className="pt-5 fs-5 text-link fw-bolder">
</div> {t('homepage.learn_more')}
<div className="text-center my-5"> <ArrowRightIcon className="hi-s ms-2" />
{ </a>
env?.OPENID_CONNECT ? (
<Form action="/auth/openid_connect" method="POST" data-turbo="false">
<input type="hidden" name="authenticity_token" value={document.querySelector('meta[name="csrf-token"]').content} />
<Button variant="brand-outline-color" className="btn btn-xlg m-2" type="submit">{t('authentication.sign_up')}</Button>
<Button variant="brand" className="btn btn-xlg m-2" type="submit">{t('authentication.sign_in')}</Button>
</Form>
) : (
<>
{ showSignUp()
&& (
<ButtonLink to={`/signup${search}`} variant="brand-outline-color" className="btn btn-xlg m-2">
{t('authentication.sign_up')}
</ButtonLink>
) }
<ButtonLink to="/signin" variant="brand" className="btn btn-xlg m-2">{t('authentication.sign_in')}</ButtonLink>
</>
)
}
</div> </div>
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Col> <h4 className="text-muted text-uppercase my-5">{t('homepage.explore_features')}</h4>
<HomepageIcon title={t('homepage.meeting_title')} description={t('homepage.meeting_description')} icon={MeetingIcon} /> <Col className="mb-3">
<HomepageFeatureCard
title={t('homepage.meeting_title')}
description={t('homepage.meeting_description')}
icon={<ComputerDesktopIcon className="hi-s text-white" />}
/>
</Col> </Col>
<Col> <Col className="mb-3">
<HomepageIcon title={t('homepage.recording_title')} description={t('homepage.recording_description')} icon={RecordingIcon} /> <HomepageFeatureCard
title={t('homepage.recording_title')}
description={t('homepage.recording_description')}
icon={<VideoCameraIcon className="hi-s text-white" />}
/>
</Col> </Col>
<Col> <Col className="mb-3">
<HomepageIcon title={t('homepage.settings_title')} description={t('homepage.settings_description')} icon={SettingsIcon} /> <HomepageFeatureCard
title={t('homepage.settings_title')}
description={t('homepage.settings_description')}
icon={<Cog8ToothIcon className="hi-s text-white" />}
/>
</Col> </Col>
<Col> <Col className="mb-3">
<HomepageIcon title={t('homepage.learning_tools_title')} description={t('homepage.learning_tools_description')} icon={LearningToolsIcon} /> <HomepageFeatureCard
title={t('homepage.and_more_title')}
description={t('homepage.and_more_description')}
icon={<WrenchScrewdriverIcon className="hi-s text-white" />}
/>
</Col> </Col>
</Row> </Row>
</> </>
......
import React from 'react';
import { Card } from 'react-bootstrap';
import PropTypes from 'prop-types';
export default function HomepageFeatureCard({ title, description, icon }) {
return (
<Card className="homepage-card h-100 shadow-sm border-0">
<Card.Body className="p-4">
<div className="homepage-card-icon-circle rounded-circle mb-5 d-flex align-items-center justify-content-center">
{ icon }
</div>
<Card.Title> { title } </Card.Title>
<Card.Text className="text-muted"> { description } </Card.Text>
</Card.Body>
</Card>
);
}
HomepageFeatureCard.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
icon: PropTypes.element.isRequired,
};
import React from 'react';
import { Stack } from 'react-bootstrap';
import PropTypes from 'prop-types';
export default function HomepageIcon({ title, description, icon }) {
return (
<Stack className="text-center my-3 my-md-5 homepage-icon d-block mx-auto">
<div className="mb-3">
{/* Icons are created by Pixel perfect on Flaticons.com */}
<img src={icon} alt="" />
</div>
<h4> {title} </h4>
<span> {description} </span>
</Stack>
);
}
HomepageIcon.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
};
import React from 'react';
import { Nav, Navbar } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import AuthButtons from './AuthButtons';
export default function NavbarNotSignedIn() {
const { t } = useTranslation();
return (
<>
<Navbar.Toggle aria-controls="responsive-navbar-nav" className="border-0" />
{/* Hidden Mobile */}
<Navbar.Collapse id="navbar-menu" className="bg-white w-100 position-absolute">
<Nav className="d-block d-sm-none text-black px-2">
<Nav.Link eventKey={1} as={Link} to="/signin">
{t('authentication.sign_in')}
</Nav.Link>
<Nav.Link eventKey={2} as={Link} to="/signup">
{t('authentication.sign_up')}
</Nav.Link>
</Nav>
</Navbar.Collapse>
{/* Mobile Navbar Toggle */}
<div className="justify-content-end d-none d-sm-block">
<AuthButtons />
</div>
</>
);
}
import React from 'react';
import {
Button, Nav, Navbar, NavDropdown,
} from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { IdentificationIcon, QuestionMarkCircleIcon, StarIcon } from '@heroicons/react/24/outline';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import useDeleteSession from '../../hooks/mutations/sessions/useDeleteSession';
import Avatar from '../users/user/Avatar';
export default function NavbarSignedIn({ currentUser }) {
const { t } = useTranslation();
const deleteSession = useDeleteSession();
const adminAccess = () => {
const { permissions } = currentUser;
const {
ManageUsers, ManageRooms, ManageRecordings, ManageSiteSettings, ManageRoles,
} = permissions;
// Todo: Use PermissionChecker.
if (ManageUsers === 'true'
|| ManageRooms === 'true'
|| ManageRecordings === 'true'
|| ManageSiteSettings === 'true'
|| ManageRoles === 'true') {
return true;
}
return false;
};
return (
<>
{/* Mobile Navbar Toggle */}
<Navbar.Toggle aria-controls="responsive-navbar-nav" className="border-0">
<Avatar avatar={currentUser?.avatar} size="small" />
</Navbar.Toggle>
<Navbar.Collapse id="navbar-menu" className="bg-white w-100 position-absolute">
<Nav className="d-block d-sm-none text-black px-2">
<Nav.Link eventKey={1} as={Link} to="/profile">
<IdentificationIcon className="hi-s me-3" />
{t('user.profile.profile')}
</Nav.Link>
<Nav.Link eventKey={2} href="https://docs.bigbluebutton.org/greenlight/gl-overview.html">
<QuestionMarkCircleIcon className="hi-s me-3" />
{t('help_center')}
</Nav.Link>
{
adminAccess()
&& (
<Nav.Link eventKey={3} as={Link} to="/admin">
<StarIcon className="hi-s me-3 mb-1" />
{ t('admin.admin_panel') }
</Nav.Link>
)
}
<NavDropdown.Divider />
<Button
onClick={deleteSession.mutate}
variant="brand"
className="btn btn-sm mt-2 mb-3 py-2 w-100"
>{t('authentication.sign_out')}
</Button>
</Nav>
</Navbar.Collapse>
{/* Hidden on Mobile */}
<div className="justify-content-end d-none d-sm-block">
<div className="d-inline-block">
<Avatar avatar={currentUser?.avatar} size="small" />
</div>
<NavDropdown title={currentUser?.name} id="nav-user-dropdown" className="d-inline-block" align="end">
<NavDropdown.Item as={Link} to="/profile">
<IdentificationIcon className="hi-s me-3" />
{ t('user.profile.profile') }
</NavDropdown.Item>
<NavDropdown.Item href="https://docs.bigbluebutton.org/greenlight/gl-overview.html">
<QuestionMarkCircleIcon className="hi-s me-3" />
{t('help_center')}
</NavDropdown.Item>
{
adminAccess()
&& (
<NavDropdown.Item as={Link} to="/admin">
<StarIcon className="hi-s me-3 mb-1" />
{ t('admin.admin_panel') }
</NavDropdown.Item>
)
}
<NavDropdown.Divider />
<div className="px-2">
<Button onClick={deleteSession.mutate} variant="brand" className="btn btn-sm w-100 my-2">{t('authentication.sign_out')}</Button>
</div>
</NavDropdown>
</div>
</>
);
}
NavbarSignedIn.propTypes = {
currentUser: PropTypes.shape({
avatar: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
permissions: PropTypes.shape({
ManageUsers: PropTypes.string.isRequired,
ManageRooms: PropTypes.string.isRequired,
ManageRecordings: PropTypes.string.isRequired,
ManageSiteSettings: PropTypes.string.isRequired,
ManageRoles: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
};
...@@ -36,7 +36,7 @@ export default function RecordingRow({ ...@@ -36,7 +36,7 @@ export default function RecordingRow({
<tr key={recording.id} className="align-middle text-muted border border-2"> <tr key={recording.id} className="align-middle text-muted border border-2">
<td className="border-end-0 text-dark"> <td className="border-end-0 text-dark">
<Stack direction="horizontal" className="py-2"> <Stack direction="horizontal" className="py-2">
<div className="recording-icon-circle rounded-circle me-3 d-flex align-items-center justify-content-center"> <div className="recording-icon-circle rounded-circle me-3 d-flex justify-content-center">
<VideoCameraIcon className="hi-s text-brand" /> <VideoCameraIcon className="hi-s text-brand" />
</div> </div>
<Stack> <Stack>
......
import React from 'react'; import React from 'react';
import Container from 'react-bootstrap/Container'; import Container from 'react-bootstrap/Container';
import { import {
Nav, NavDropdown, Navbar, Button, Navbar,
} from 'react-bootstrap'; } from 'react-bootstrap';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { StarIcon, IdentificationIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/outline';
import { useAuth } from '../../contexts/auth/AuthProvider'; import { useAuth } from '../../contexts/auth/AuthProvider';
import useDeleteSession from '../../hooks/mutations/sessions/useDeleteSession';
import Avatar from '../users/user/Avatar';
import Logo from './Logo'; import Logo from './Logo';
import NavbarSignedIn from '../home/NavbarSignedIn';
import NavbarNotSignedIn from '../home/NavbarNotSignedIn';
export default function Header() { export default function Header() {
const { t } = useTranslation();
const currentUser = useAuth(); const currentUser = useAuth();
const deleteSession = useDeleteSession();
const adminAccess = () => { let homePath = '/';
const { permissions } = currentUser;
const {
ManageUsers, ManageRooms, ManageRecordings, ManageSiteSettings, ManageRoles,
} = permissions;
// Todo: Use PermissionChecker.
if (ManageUsers === 'true'
|| ManageRooms === 'true'
|| ManageRecordings === 'true'
|| ManageSiteSettings === 'true'
|| ManageRoles === 'true') {
return true;
}
return false;
};
let homePath = '/rooms';
if (currentUser?.permissions?.CreateRoom === 'false') { if (currentUser?.permissions?.CreateRoom === 'false') {
homePath = '/home'; homePath = '/home';
} }
...@@ -45,65 +23,14 @@ export default function Header() { ...@@ -45,65 +23,14 @@ export default function Header() {
<Navbar.Brand as={Link} to={homePath} className="ps-2"> <Navbar.Brand as={Link} to={homePath} className="ps-2">
<Logo size="small" /> <Logo size="small" />
</Navbar.Brand> </Navbar.Brand>
<Navbar.Toggle aria-controls="responsive-navbar-nav" className="border-0">
<Avatar avatar={currentUser?.avatar} size="small" />
</Navbar.Toggle>
{/* /!* Visible only on mobile *!/ */}
<Navbar.Collapse id="navbar-menu" className="bg-white w-100 position-absolute">
<Nav className="d-block d-sm-none text-black px-2">
<Nav.Link eventKey={1} as={Link} to="/profile">
<IdentificationIcon className="hi-s me-3" />
{t('user.profile.profile')}
</Nav.Link>
<Nav.Link eventKey={2} href="https://docs.bigbluebutton.org/greenlight/gl-overview.html">
<QuestionMarkCircleIcon className="hi-s me-3" />
{t('help_center')}
</Nav.Link>
{
adminAccess()
&& (
<Nav.Link eventKey={3} as={Link} to="/admin">
<StarIcon className="hi-s me-3 mb-1" />
{ t('admin.admin_panel') }
</Nav.Link>
)
}
<NavDropdown.Divider />
<Button onClick={deleteSession.mutate} variant="brand" className="btn btn-sm mt-2 mb-3 py-2 w-100">{t('authentication.sign_out')}</Button>
</Nav>
</Navbar.Collapse>
{/* Not visible on mobile */}
<div className="justify-content-end d-none d-sm-block">
<div className="d-inline-block">
<Avatar avatar={currentUser?.avatar} size="small" />
</div>
<NavDropdown title={currentUser?.name} id="nav-user-dropdown" className="d-inline-block" align="end">
<NavDropdown.Item as={Link} to="/profile">
<IdentificationIcon className="hi-s me-3" />
{ t('user.profile.profile') }
</NavDropdown.Item>
<NavDropdown.Item href="https://docs.bigbluebutton.org/greenlight/gl-overview.html">
<QuestionMarkCircleIcon className="hi-s me-3" />
{t('help_center')}
</NavDropdown.Item>
{ {
adminAccess() currentUser.signed_in
&& ( ? (
<NavDropdown.Item as={Link} to="/admin"> <NavbarSignedIn currentUser={currentUser} />
<StarIcon className="hi-s me-3 mb-1" /> ) : (
{ t('admin.admin_panel') } <NavbarNotSignedIn />
</NavDropdown.Item>
) )
} }
<NavDropdown.Divider />
<div className="px-2">
<Button onClick={deleteSession.mutate} variant="brand" className="btn btn-sm w-100 my-2">{t('authentication.sign_out')}</Button>
</div>
</NavDropdown>
</div>
</Container> </Container>
</Navbar> </Navbar>
); );
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment