1
Fork 0

Finish password protection ui

This commit is contained in:
viktorstrate 2021-06-08 13:46:51 +02:00
parent 9cb236df41
commit dc317d4489
No known key found for this signature in database
GPG Key ID: 3F855605109C1E8A
4 changed files with 219 additions and 28 deletions

View File

@ -1,5 +1,11 @@
import React, { useEffect } from 'react'
import { useMutation, useQuery, gql, useLazyQuery } from '@apollo/client'
import React, { useEffect, useState } from 'react'
import {
useMutation,
useQuery,
gql,
useLazyQuery,
DocumentNode,
} from '@apollo/client'
import copy from 'copy-to-clipboard'
import { useTranslation } from 'react-i18next'
import { Popover } from '@headlessui/react'
@ -23,6 +29,7 @@ import {
import {
sidebarGetAlbumShares,
sidebarGetAlbumSharesVariables,
sidebarGetAlbumShares_album_shares,
} from './__generated__/sidebarGetAlbumShares'
import { authToken } from '../../helpers/authentication'
import { SidebarSection, SidebarSectionTitle } from './SidebarComponents'
@ -35,6 +42,10 @@ import { ReactComponent as AddIcon } from './icons/shareAddIcon.svg'
import Checkbox from '../../primitives/form/Checkbox'
import { TextField } from '../../primitives/form/Input'
import styled from 'styled-components'
import {
sidebarProtectShare,
sidebarProtectShareVariables,
} from './__generated__/sidebarProtectShare'
const SHARE_PHOTO_QUERY = gql`
query sidebarGetPhotoShares($id: ID!) {
@ -78,14 +89,14 @@ const ADD_ALBUM_SHARE_MUTATION = gql`
}
`
// const PROTECT_SHARE_MUTATION = gql`
// mutation sidebarProtectShare($token: String!, $password: String) {
// protectShareToken(token: $token, password: $password) {
// token
// hasPassword
// }
// }
// `
const PROTECT_SHARE_MUTATION = gql`
mutation sidebarProtectShare($token: String!, $password: String) {
protectShareToken(token: $token, password: $password) {
token
hasPassword
}
}
`
const DELETE_SHARE_MUTATION = gql`
mutation sidebareDeleteShare($token: String!) {
@ -95,12 +106,6 @@ const DELETE_SHARE_MUTATION = gql`
}
`
// type ShareItemMoreDropdownProps = {
// id: string
// query: DocumentNode
// share: sidbarGetAlbumShares_album_shares
// }
// const ShareItemMoreDropdown = ({
// id,
// share,
@ -255,7 +260,121 @@ const ArrowPopoverPanel = styled.div.attrs({
}
`
const MorePopover = () => {
type MorePopoverSectionPasswordProps = {
share: sidebarGetAlbumShares_album_shares
query: DocumentNode
id: string
}
const MorePopoverSectionPassword = ({
share,
query,
id,
}: MorePopoverSectionPasswordProps) => {
const [addingPassword, setAddingPassword] = useState(false)
const activated = addingPassword || share.hasPassword
const [passwordInputValue, setPasswordInputValue] = useState(
share.hasPassword ? '**********' : ''
)
const [passwordHidden, setPasswordHidden] = useState(share.hasPassword)
const [setPassword, { loading: setPasswordLoading }] = useMutation<
sidebarProtectShare,
sidebarProtectShareVariables
>(PROTECT_SHARE_MUTATION, {
refetchQueries: [{ query: query, variables: { id } }],
onCompleted: data => {
hidePassword(data.protectShareToken.hasPassword)
},
// refetchQueries: [{ query: query, variables: { id } }],
variables: {
token: share.token,
},
})
const hidePassword = (hide: boolean) => {
if (hide) {
setPasswordInputValue('**********')
}
if (passwordHidden && !hide) {
setPasswordInputValue('')
}
setPasswordHidden(hide)
}
const checkboxChange = () => {
const enable = !activated
setAddingPassword(enable)
if (!enable) {
setPassword({
variables: {
token: share.token,
password: null,
},
})
setPasswordInputValue('')
}
}
const updatePasswordAction = () => {
if (!passwordHidden && passwordInputValue != '') {
setPassword({
variables: {
token: share.token,
password: passwordInputValue,
},
})
}
}
return (
<div className="px-4 py-2">
<Checkbox
label="Password protected"
checked={activated}
onChange={checkboxChange}
/>
<TextField
disabled={!activated}
type={passwordHidden ? 'password' : 'text'}
value={passwordInputValue}
sizeVariant="small"
className="mt-2"
onKeyDown={event => {
if (
event.shiftKey ||
event.altKey ||
event.ctrlKey ||
event.metaKey ||
event.key == 'Enter' ||
event.key == 'Tab' ||
event.key == 'Escape'
) {
return
}
hidePassword(false)
}}
onChange={event => {
setPasswordInputValue(event.target.value)
}}
action={updatePasswordAction}
loading={setPasswordLoading}
/>
</div>
)
}
type MorePopoverProps = {
id: string
query: DocumentNode
share: sidebarGetAlbumShares_album_shares
}
const MorePopover = ({ id, share, query }: MorePopoverProps) => {
const { t } = useTranslation()
return (
@ -269,10 +388,7 @@ const MorePopover = () => {
<Popover.Panel>
<ArrowPopoverPanel>
<div className="px-4 py-2">
<Checkbox label="Password protected" />
<TextField sizeVariant="small" className="mt-2" />
</div>
<MorePopoverSectionPassword id={id} share={share} query={query} />
<div className="px-4 py-2 border-t border-gray-200 mt-2 mb-2">
<Checkbox label="Expiration date" />
<TextField sizeVariant="small" className="mt-2" />
@ -439,7 +555,7 @@ const SidebarShare = ({
>
<DeleteIcon />
</button>
<MorePopover />
<MorePopover share={share} id={id} query={query} />
{/* <ShareItemMoreDropdown share={share} id={id} isPhoto={isPhoto} /> */}
</td>

View File

@ -1,18 +1,32 @@
import React, { forwardRef } from 'react'
import classNames, { Argument as ClassNamesArg } from 'classnames'
import { ReactComponent as ActionArrowIcon } from './icons/textboxActionArrow.svg'
import { ReactComponent as LoadingSpinnerIcon } from './icons/textboxLoadingSpinner.svg'
type TextFieldProps = {
label?: string
error?: string
className?: ClassNamesArg
sizeVariant?: 'default' | 'small'
action?: () => void
loading?: boolean
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className'>
export const TextField = forwardRef(
(
{ label, error, className, sizeVariant, ...inputProps }: TextFieldProps,
{
label,
error,
className,
sizeVariant,
action,
loading,
...inputProps
}: TextFieldProps,
ref: React.ForwardedRef<HTMLInputElement>
) => {
const disabled = !!inputProps.disabled
sizeVariant = sizeVariant ?? 'default'
let variant = 'bg-white border-gray-200 focus:border-blue-400'
@ -20,8 +34,23 @@ export const TextField = forwardRef(
variant =
'bg-red-50 border-red-200 focus:border-red-400 focus:ring-red-100'
const input = (
if (disabled) variant = 'bg-gray-100'
let keyUpEvent = undefined
if (action) {
keyUpEvent = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (inputProps.onKeyUp) inputProps.onKeyUp(event)
if (event.key == 'Enter') {
event.preventDefault()
action()
}
}
}
let input = (
<input
onKeyUp={keyUpEvent}
className={classNames(
'block border rounded-md w-full focus:ring-2 focus:outline-none px-2',
variant,
@ -32,12 +61,46 @@ export const TextField = forwardRef(
/>
)
if (loading) {
input = (
<div className="relative">
{input}
<LoadingSpinnerIcon
aria-label="Loading"
className="absolute right-[8px] top-[7px] animate-spin"
/>
</div>
)
} else if (action) {
input = (
<div className="relative">
{input}
<button
disabled={disabled}
aria-label="Submit"
className={classNames(
'absolute top-[1px] right-0 p-2',
disabled ? 'text-gray-400 cursor-default' : 'text-gray-600'
)}
onClick={() => action()}
>
<ActionArrowIcon />
</button>
</div>
)
}
let errorElm = null
if (error) errorElm = <div className="text-red-800">{error}</div>
const wrapperClasses = classNames(
className,
sizeVariant == 'small' && 'text-sm'
)
if (label) {
return (
<label className={classNames('block', className)}>
<label className={wrapperClasses}>
<span className="block text-xs uppercase font-semibold mb-1">
{label}
</span>
@ -48,9 +111,7 @@ export const TextField = forwardRef(
}
return (
<div
className={classNames(className, sizeVariant == 'small' && 'text-sm')}
>
<div className={wrapperClasses}>
{input}
{errorElm}
</div>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<line x1="1" y1="5.66666667" x2="10.3333333" y2="5.66666667"></line>
<polyline id="Path" points="5.66666667 1 10.3333333 5.66666667 5.66666667 10.3333333"></polyline>
</g>
</svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none">
<path d="M8,0 C12.418278,0 16,3.581722 16,8 C16,12.418278 12.418278,16 8,16 C3.581722,16 0,12.418278 0,8 C0,3.581722 3.581722,0 8,0 Z M8,2 C4.6862915,2 2,4.6862915 2,8 C2,11.3137085 4.6862915,14 8,14 C11.3137085,14 14,11.3137085 14,8 C14,4.6862915 11.3137085,2 8,2 Z" id="background" fill="#D8D8D8"></path>
<path d="M8,0 C12.418278,0 16,3.581722 16,8 L14,8 L14,8 C14,4.6862915 11.3137085,2 8,2 L8,0 L8,0 Z" id="foreground" fill="#818181"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 683 B