mirror of
https://fast.feibisi.com/https://github.com/parcelvoy/platform.git
synced 2025-09-01 12:26:08 +08:00
Makes design less terrible on mobile (#110)
Adds profile image for OpenID
This commit is contained in:
parent
3cca06057d
commit
e9cca8b8ed
14 changed files with 127 additions and 25 deletions
|
@ -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')
|
||||
})
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -102,6 +102,7 @@ export interface Admin {
|
|||
first_name: string
|
||||
last_name: string
|
||||
email: string
|
||||
image_url: string
|
||||
}
|
||||
|
||||
export const projectRoles = [
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
|
||||
.ui-table .table-header-cell {
|
||||
padding: 10px 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-table .table-cell {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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' },
|
||||
]}
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -41,7 +41,7 @@ export default function UserDetailSubscriptions() {
|
|||
title="Subscriptions"
|
||||
actions={
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={async () => await unsubscribeAll()}
|
||||
>
|
||||
Unsubscribe From All
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue