Makes design less terrible on mobile (#110)

Adds profile image for OpenID
This commit is contained in:
Chris Anderson 2023-04-04 08:44:06 -05:00 committed by GitHub
parent 3cca06057d
commit e9cca8b8ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 127 additions and 25 deletions

View file

@ -0,0 +1,11 @@
exports.up = async function(knex) {
await knex.schema.table('admins', function(table) {
table.string('image_url', 255).after('email')
})
}
exports.down = async function(knex) {
await knex.schema.table('admins', function(table) {
table.dropColumn('image_url')
})
}

View file

@ -4,6 +4,7 @@ export class Admin extends Model {
email!: string
first_name?: string
last_name?: string
image_url?: string
}
export type AdminParams = Omit<Admin, ModelParams>

View file

@ -90,6 +90,7 @@ export default class OpenIDAuthProvider extends AuthProvider {
email: claims.email,
first_name: claims.given_name ?? claims.name,
last_name: claims.family_name,
image_url: claims.picture,
}
await this.login(admin, ctx, state)

View file

@ -179,4 +179,10 @@ label .switch .slider.round:before {
.icon {
width: 16px;
height: 16px;
}
@media only screen and (max-width: 600px) {
.page-content {
padding: 0 20px 20px;
}
}

View file

@ -102,6 +102,7 @@ export interface Admin {
first_name: string
last_name: string
email: string
image_url: string
}
export const projectRoles = [

View file

@ -67,7 +67,7 @@ const Button = forwardRef(function Button(props: ButtonProps, ref: Ref<HTMLButto
disabled={disabled ?? isLoading}
style={style}
>
{icon && (<span className="button-icon">{icon}</span>)}
{icon && (<span className="button-icon" aria-hidden="true">{icon}</span>)}
{children}
</button>
)

View file

@ -33,6 +33,7 @@
.ui-table .table-header-cell {
padding: 10px 5px;
white-space: nowrap;
}
.ui-table .table-cell {

View file

@ -39,7 +39,7 @@ export default function Menu({ children, variant, size, placement }: PropsWithCh
<Popover>
<Popover.Button as={Button}
ref={setReferenceElement}
variant={variant ?? 'plain'}
variant={variant ?? 'secondary'}
size={size}
icon={<ThreeDotsIcon />}></Popover.Button>

View file

@ -10,11 +10,11 @@
z-index: 1;
}
.sidebar + * {
main {
margin-left: 225px;
}
.sidebar .sidebar-header {
header, .sidebar .sidebar-header {
display: flex;
position: relative;
border-bottom: 1px solid var(--color-grey);
@ -23,12 +23,12 @@
align-items: center;
}
.sidebar-header .logo {
header .logo, .sidebar-header .logo {
display: flex;
align-items: center;
}
.sidebar-header .logo svg {
header .logo svg, .sidebar-header .logo svg {
height: 30px;
flex-shrink: 0;
fill: var(--color-primary);
@ -52,6 +52,10 @@
font-weight: 500;
}
header {
display: none;
}
nav {
padding: 20px;
flex-grow: 1;
@ -95,8 +99,9 @@ nav a .nav-icon {
grid-template-areas: "image name" "image role";
grid-template-columns: 40px 1fr;
grid-column-gap: 15px;
grid-row-gap: 5px;
align-items: center;
padding: 20px;
padding: 15px 20px;
}
.sidebar-profile .profile-image {
@ -105,14 +110,61 @@ nav a .nav-icon {
background: var(--color-grey);
border-radius: 20px;
grid-area: image;
overflow: hidden;
}
.sidebar-profile .profile-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.sidebar-profile .profile-name {
grid-area: name;
font-size: 14px;
font-size: 15px;
}
.profile .profile-role {
.sidebar-profile .profile-role {
grid-area: role;
font-size: 12px;
display: flex;
}
@media only screen and (max-width: 600px) {
header {
background: var(--color-background);
display: flex;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 10;
gap: 15px;
}
.sidebar {
width: 90%;
top: 61px;
margin-left: -90%;
transition: margin-left 100ms ease-in-out;
}
main {
padding-top: 61px;
background-color: var(--color-background);
margin-left: 0;
transition: margin-left 100ms ease-in-out;
}
.sidebar.is-open {
margin-left: 0%;
}
main.is-open {
margin-left: 90%;
}
.sidebar .sidebar-header {
display: none;
}
}

View file

@ -2,7 +2,7 @@ import './Sidebar.css'
import NavLink from './NavLink'
import { ReactComponent as Logo } from '../assets/logo.svg'
import { Link, NavLinkProps, useNavigate } from 'react-router-dom'
import { PropsWithChildren, ReactNode, useCallback, useContext } from 'react'
import { PropsWithChildren, ReactNode, useCallback, useContext, useState } from 'react'
import { AdminContext, ProjectContext } from '../contexts'
import api from '../api'
import { PreferencesContext } from './PreferencesContext'
@ -10,9 +10,10 @@ import { useResolver } from '../hooks'
import { SingleSelect } from './form/SingleSelect'
import Button from './Button'
import ButtonGroup from './ButtonGroup'
import { MoonIcon, SunIcon } from './icons'
import { MenuIcon, MoonIcon, SunIcon } from './icons'
import { checkProjectRole, getRecentProjects } from '../utils'
import { ProjectRole } from '../types'
import clsx from 'clsx'
interface SidebarProps {
links?: Array<NavLinkProps & {
@ -52,10 +53,17 @@ export default function Sidebar({ children, links }: PropsWithChildren<SidebarPr
},
]
}, [project]))
const [isOpen, setIsOpen] = useState(false)
return (
<>
<section className="sidebar">
<header className="header">
<Button onClick={() => setIsOpen(!isOpen)} icon={<MenuIcon />} aria-label="Menu" variant="secondary" size="small"/>
<Link className="logo" to="/">
<Logo />
</Link>
</header>
<section className={clsx('sidebar', { 'is-open': isOpen })}>
<div className="sidebar-header">
<Link className="logo" to="/">
<Logo />
@ -86,37 +94,39 @@ export default function Sidebar({ children, links }: PropsWithChildren<SidebarPr
links
?.filter(({ minRole }) => !minRole || checkProjectRole(minRole, project.role))
.map(({ key, minRole, ...props }) => (
<NavLink {...props} key={key} />
<NavLink {...props} key={key} onClick={() => setIsOpen(false)} />
))
}
</nav>
{
profile && (
<div className="sidebar-profile">
<div className="profile-image"></div>
<div className="profile-image">
<img src={profile.image_url} referrerPolicy="no-referrer" />
</div>
<span className="profile-name">{`${profile.first_name} ${profile.last_name}`}</span>
<span className="profile-role">
<div className="profile-role">
<ButtonGroup>
<Button
variant="plain"
variant="secondary"
size="small"
icon={preferences.mode === 'dark' ? <MoonIcon /> : <SunIcon />}
onClick={() => setPreferences({ ...preferences, mode: preferences.mode === 'dark' ? 'light' : 'dark' })}
/>
<Button
variant="plain"
variant="secondary"
size="small"
onClick={async () => await api.logout()}
>
{'Sign Out'}
</Button>
</ButtonGroup>
</span>
</div>
</div>
)
}
</section>
<main>
<main className={clsx({ 'is-open': isOpen })}>
{children}
</main>
</>

View file

@ -165,3 +165,9 @@ export const CopyIcon = () => (
<path strokeLinecap="round" strokeLinejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" />
</svg>
)
export const MenuIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="icon">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
)

View file

@ -1,15 +1,28 @@
import { useCallback, useContext } from 'react'
import api from '../../api'
import { CampaignContext, ProjectContext } from '../../contexts'
import { CampaignSendState } from '../../types'
import Alert from '../../ui/Alert'
import Heading from '../../ui/Heading'
import { InfoTable } from '../../ui/InfoTable'
import { PreferencesContext } from '../../ui/PreferencesContext'
import { SearchTable, useSearchTableState } from '../../ui/SearchTable'
import Tag from '../../ui/Tag'
import { formatDate } from '../../utils'
import Tag, { TagVariant } from '../../ui/Tag'
import { formatDate, snakeToTitle } from '../../utils'
import { useRoute } from '../router'
export const CampaignSendTag = ({ state }: { state: CampaignSendState }) => {
const variant: Record<CampaignSendState, TagVariant> = {
pending: 'info',
sent: 'success',
failed: 'error',
}
return <Tag variant={variant[state]}>
{snakeToTitle(state)}
</Tag>
}
export default function CampaignDelivery() {
const [project] = useContext(ProjectContext)
const [preferences] = useContext(PreferencesContext)
@ -39,7 +52,7 @@ export default function CampaignDelivery() {
{ key: 'phone' },
{
key: 'state',
cell: ({ item: { state } }) => <Tag>{state}</Tag>,
cell: ({ item: { state } }) => CampaignSendTag({ state }),
},
{ key: 'send_at' },
]}

View file

@ -67,10 +67,10 @@ export default function Teams() {
enableSearch
/>
<Modal
title={editing?.id ? 'Update Team Member Permissions' : 'Add Team Member'}
title={editing?.id ? 'Update Permissions' : 'Add Team Member'}
open={Boolean(editing)}
onClose={() => setEditing(null)}
size="regular"
size="small"
>
{
editing && (

View file

@ -41,7 +41,7 @@ export default function UserDetailSubscriptions() {
title="Subscriptions"
actions={
<Button
variant="secondary"
size="small"
onClick={async () => await unsubscribeAll()}
>
Unsubscribe From All