Add Privacy Policy related improvements

various thing to make sure we're GDPR, et al compliant
This commit is contained in:
Kurt Hutten
2020-12-25 17:29:01 +11:00
parent 6623939f78
commit 7d262e9f58
51 changed files with 1480 additions and 95 deletions

View File

@@ -2,7 +2,13 @@ import { getActiveClasses } from 'get-active-classes'
import InputText from 'src/components/InputText'
const Breadcrumb = ({ userName, partTitle, onPartTitleChange, className }) => {
const Breadcrumb = ({
userName,
partTitle,
onPartTitleChange,
className,
isInvalid,
}) => {
return (
<h3 className={getActiveClasses('text-2xl font-roboto', className)}>
<div className="w-1 inline-block text-indigo-800 bg-indigo-800 mr-2">
@@ -26,6 +32,7 @@ const Breadcrumb = ({ userName, partTitle, onPartTitleChange, className }) => {
className={getActiveClasses('text-indigo-800 text-2xl', {
'-ml-2': !onPartTitleChange,
})}
isInvalid={isInvalid}
/>
</h3>
)

View File

@@ -0,0 +1,70 @@
import { useMutation, useFlash } from '@redwoodjs/web'
import { navigate, routes } from '@redwoodjs/router'
import SubjectAccessRequestForm from 'src/components/SubjectAccessRequestForm'
export const QUERY = gql`
query FIND_SUBJECT_ACCESS_REQUEST_BY_ID($id: String!) {
subjectAccessRequest: subjectAccessRequest(id: $id) {
id
comment
payload
userId
createdAt
updatedAt
}
}
`
const UPDATE_SUBJECT_ACCESS_REQUEST_MUTATION = gql`
mutation UpdateSubjectAccessRequestMutation(
$id: String!
$input: UpdateSubjectAccessRequestInput!
) {
updateSubjectAccessRequest(id: $id, input: $input) {
id
comment
payload
userId
createdAt
updatedAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Success = ({ subjectAccessRequest }) => {
const { addMessage } = useFlash()
const [updateSubjectAccessRequest, { loading, error }] = useMutation(
UPDATE_SUBJECT_ACCESS_REQUEST_MUTATION,
{
onCompleted: () => {
navigate(routes.subjectAccessRequests())
addMessage('SubjectAccessRequest updated.', {
classes: 'rw-flash-success',
})
},
}
)
const onSave = (input, id) => {
updateSubjectAccessRequest({ variables: { id, input } })
}
return (
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
Edit SubjectAccessRequest {subjectAccessRequest.id}
</h2>
</header>
<div className="rw-segment-main">
<SubjectAccessRequestForm
subjectAccessRequest={subjectAccessRequest}
onSave={onSave}
error={error}
loading={loading}
/>
</div>
</div>
)
}

View File

@@ -32,7 +32,7 @@ export const Empty = () => <div>Empty</div>
export const Failure = ({ error }) => <div>Error: {error.message}</div>
export const Success = ({ user }) => {
export const Success = ({ user, variables: { isEditable } }) => {
const { addMessage } = useFlash()
const [updateUser, { loading, error }] = useMutation(UPDATE_USER_MUTATION, {
onCompleted: ({ updateUserByUserName }) => {
@@ -51,7 +51,7 @@ export const Success = ({ user }) => {
onSave={onSave}
loading={loading}
error={error}
isEditable
isEditable={isEditable}
/>
)
}

View File

@@ -0,0 +1,16 @@
import { Link, routes } from '@redwoodjs/router'
const Footer = () => {
return (
<div className="bg-indigo-900 text-indigo-200 font-roboto mt-20 text-sm">
<div className="flex h-16 justify-end items-center mx-16">
<Link className="mr-8" to={routes.codeOfConduct()}>
Code of Conduct
</Link>
<Link to={routes.privacyPolicy()}>Privacy Policy</Link>
</div>
</div>
)
}
export default Footer

View File

@@ -0,0 +1,7 @@
import Footer from './Footer'
export const generated = () => {
return <Footer />
}
export default { title: 'Components/Footer' }

View File

@@ -0,0 +1,11 @@
import { render } from '@redwoodjs/testing'
import Footer from './Footer'
describe('Footer', () => {
it('renders successfully', () => {
expect(() => {
render(<Footer />)
}).not.toThrow()
})
})

View File

@@ -59,7 +59,6 @@ const IdeToolbar = ({ canEdit, isChanges, onSave, onExport, userNamePart }) => {
<div className="h-8 w-8 ml-4">
<ImageUploader
className="rounded-full object-cover"
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={userNamePart?.image}
width={80}

View File

@@ -12,7 +12,7 @@ const CLOUDINARY_UPLOAD_PRESET = 'CadHub_project_images'
const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/irevdev/upload'
export default function ImageUploader({
onImageUpload,
onImageUpload = () => {},
imageUrl,
aspectRatio,
className,
@@ -67,11 +67,12 @@ export default function ImageUploader({
>
<div className="absolute w-full h-full" {...getRootProps()}>
{cloudinaryId && isEditable && (
<button className="absolute z-10 w-full inset-0 bg-indigo-900 opacity-50 flex justify-center items-center">
<button className="absolute z-10 bg-indigo-900 opacity-75 bottom-0 right-0 flex items-center p-1 mb-6 mr-2 rounded-lg">
<span className="text-gray-100 pr-2">Update</span>
<Svg
name="pencil"
strokeWidth={2}
className="text-gray-300 h-24 w-24"
className=" text-gray-100 h-6 w-6"
/>
</button>
)}

View File

@@ -1,6 +1,12 @@
import { getActiveClasses } from 'get-active-classes'
const InputText = ({ value, isEditable, onChange, className }) => {
const InputText = ({
value,
isEditable,
onChange,
className,
isInvalid = false,
}) => {
return (
<>
<div
@@ -10,7 +16,12 @@ const InputText = ({ value, isEditable, onChange, className }) => {
className
)}
>
<div className="absolute inset-0 mb-2 rounded bg-gray-200 shadow-inner" />
<div
className={getActiveClasses(
'absolute inset-0 mb-2 rounded bg-gray-200 shadow-inner',
{ 'border border-red-500': isInvalid }
)}
/>
<input
className="pl-2 pt-1 text-indigo-800 font-medium mb-px pb-px bg-transparent relative"
onChange={onChange}

View File

@@ -191,6 +191,14 @@ const SignUpForm = ({ onSubmitSignUp, checkBox, setCheckBox, onClose }) => (
>
Code of Conduct
</Link>
, and agree with our{' '}
<Link
onClick={onClose}
to={routes.privacyPolicy()}
className="underline"
>
Privacy Policy
</Link>
</span>
</div>
<HeroButton text="Sign Up" />

View File

@@ -21,8 +21,10 @@ const PartProfile = ({
}) => {
const [comment, setComment] = useState('')
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false)
const [isInvalid, setIsInvalid] = useState(false)
const { currentUser } = useAuth()
const canEdit = currentUser?.sub === userPart.id
const isImageEditable = !isEditable && canEdit // image is editable when not in profile edit mode in order to separate them as it's too hard too to upload an image to cloudinary temporarily until the use saves (and maybe have to clean up) for the time being
const part = userPart?.Part
const emotes = countEmotes(part?.Reaction)
const userEmotes = part?.userReactions.map(({ emote }) => emote)
@@ -48,11 +50,19 @@ const PartProfile = ({
setProperty('title', target.value.replace(/([^a-zA-Z\d_:])/g, '-'))
const onDescriptionChange = (description) =>
setProperty('description', description())
const onImageUpload = ({ cloudinaryPublicId }) =>
setProperty('mainImage', cloudinaryPublicId)
const onEditSaveClick = () => {
const onImageUpload = ({ cloudinaryPublicId }) => {
onSave(part?.id, { ...input, mainImage: cloudinaryPublicId })
}
// setProperty('mainImage', cloudinaryPublicId)
const onEditSaveClick = (hi) => {
// do a thing
if (isEditable) {
input.title && onSave(part?.id, input)
if (!input.title) {
setIsInvalid(true)
return
}
setIsInvalid(false)
onSave(part?.id, input)
return
}
navigate(
@@ -69,7 +79,6 @@ const PartProfile = ({
<aside className="col-start-2 relative">
<ImageUploader
className="rounded-half rounded-br-lg shadow-md border-2 border-gray-200 border-solid"
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={userPart?.image}
width={300}
@@ -144,13 +153,14 @@ const PartProfile = ({
onPartTitleChange={isEditable && onTitleChange}
userName={userPart?.userName}
partTitle={input?.title}
isInvalid={isInvalid}
/>
{!!(input?.mainImage || isEditable) && (
{!!input?.mainImage && !isEditable && part?.id && (
<ImageUploader
className="rounded-lg shadow-md border-2 border-gray-200 border-solid mt-8"
onImageUpload={onImageUpload}
aspectRatio={16 / 9}
isEditable={isEditable}
isEditable={isImageEditable}
imageUrl={input?.mainImage}
width={1010}
/>
@@ -183,7 +193,6 @@ const PartProfile = ({
<div className="w-8 h-8 overflow-hidden rounded-full border border-indigo-300 shadow flex-shrink-0">
<ImageUploader
className=""
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={user?.image}
width={50}

View File

@@ -36,7 +36,6 @@ const PartsList = ({ parts, shouldFilterPartsWithoutImage = false }) => {
<div className="w-8 h-8 overflow-hidden rounded-full border border-indigo-300 shadow">
<ImageUploader
className=""
onImageUpload={() => {}}
aspectRatio={1}
imageUrl={user?.image}
width={50}
@@ -49,7 +48,6 @@ const PartsList = ({ parts, shouldFilterPartsWithoutImage = false }) => {
<div className="w-full overflow-hidden relative rounded-b-lg">
<ImageUploader
className=""
onImageUpload={() => {}}
aspectRatio={1.4}
imageUrl={mainImage}
width={700}

View File

@@ -0,0 +1,119 @@
import { useMutation, useFlash } from '@redwoodjs/web'
import { Link, routes, navigate } from '@redwoodjs/router'
import { QUERY } from 'src/components/SubjectAccessRequestsCell'
const DELETE_SUBJECT_ACCESS_REQUEST_MUTATION = gql`
mutation DeleteSubjectAccessRequestMutation($id: String!) {
deleteSubjectAccessRequest(id: $id) {
id
}
}
`
const jsonDisplay = (obj) => {
return (
<pre>
<code>{JSON.stringify(obj, null, 2)}</code>
</pre>
)
}
const timeTag = (datetime) => {
return (
<time dateTime={datetime} title={datetime}>
{new Date(datetime).toUTCString()}
</time>
)
}
const checkboxInputTag = (checked) => {
return <input type="checkbox" checked={checked} disabled />
}
const SubjectAccessRequest = ({ subjectAccessRequest }) => {
const { addMessage } = useFlash()
const [deleteSubjectAccessRequest] = useMutation(
DELETE_SUBJECT_ACCESS_REQUEST_MUTATION,
{
onCompleted: () => {
navigate(routes.subjectAccessRequests())
addMessage('SubjectAccessRequest deleted.', {
classes: 'rw-flash-success',
})
},
// This refetches the query on the list page. Read more about other ways to
// update the cache over here:
// https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
refetchQueries: [{ query: QUERY }],
awaitRefetchQueries: true,
}
)
const onDeleteClick = (id) => {
if (
confirm(
'Are you sure you want to delete subjectAccessRequest ' + id + '?'
)
) {
deleteSubjectAccessRequest({ variables: { id } })
}
}
return (
<>
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
SubjectAccessRequest {subjectAccessRequest.id} Detail
</h2>
</header>
<table className="rw-table">
<tbody>
<tr>
<th>Id</th>
<td>{subjectAccessRequest.id}</td>
</tr>
<tr>
<th>Comment</th>
<td>{subjectAccessRequest.comment}</td>
</tr>
<tr>
<th>Payload</th>
<td>{subjectAccessRequest.payload}</td>
</tr>
<tr>
<th>User id</th>
<td>{subjectAccessRequest.userId}</td>
</tr>
<tr>
<th>Created at</th>
<td>{timeTag(subjectAccessRequest.createdAt)}</td>
</tr>
<tr>
<th>Updated at</th>
<td>{timeTag(subjectAccessRequest.updatedAt)}</td>
</tr>
</tbody>
</table>
</div>
<nav className="rw-button-group">
<Link
to={routes.editSubjectAccessRequest({ id: subjectAccessRequest.id })}
className="rw-button rw-button-blue"
>
Edit
</Link>
<a
href="#"
className="rw-button rw-button-red"
onClick={() => onDeleteClick(subjectAccessRequest.id)}
>
Delete
</a>
</nav>
</>
)
}
export default SubjectAccessRequest

View File

@@ -0,0 +1,22 @@
import SubjectAccessRequest from 'src/components/SubjectAccessRequest'
export const QUERY = gql`
query FIND_SUBJECT_ACCESS_REQUEST_BY_ID($id: String!) {
subjectAccessRequest: subjectAccessRequest(id: $id) {
id
comment
payload
userId
createdAt
updatedAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>SubjectAccessRequest not found</div>
export const Success = ({ subjectAccessRequest }) => {
return <SubjectAccessRequest subjectAccessRequest={subjectAccessRequest} />
}

View File

@@ -0,0 +1,83 @@
import {
Form,
FormError,
FieldError,
Label,
TextField,
Submit,
} from '@redwoodjs/forms'
const SubjectAccessRequestForm = (props) => {
const onSubmit = (data) => {
props.onSave(data, props?.subjectAccessRequest?.id)
}
return (
<div className="rw-form-wrapper">
<Form onSubmit={onSubmit} error={props.error}>
<FormError
error={props.error}
wrapperClassName="rw-form-error-wrapper"
titleClassName="rw-form-error-title"
listClassName="rw-form-error-list"
/>
<Label
name="comment"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Comment
</Label>
<TextField
name="comment"
defaultValue={props.subjectAccessRequest?.comment}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="comment" className="rw-field-error" />
<Label
name="payload"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Payload
</Label>
<TextField
name="payload"
defaultValue={props.subjectAccessRequest?.payload}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="payload" className="rw-field-error" />
<Label
name="userId"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
User id
</Label>
<TextField
name="userId"
defaultValue={props.subjectAccessRequest?.userId}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<FieldError name="userId" className="rw-field-error" />
<div className="rw-button-group">
<Submit disabled={props.loading} className="rw-button rw-button-blue">
Save
</Submit>
</div>
</Form>
</div>
)
}
export default SubjectAccessRequestForm

View File

@@ -0,0 +1,137 @@
import { useMutation, useFlash } from '@redwoodjs/web'
import { Link, routes } from '@redwoodjs/router'
import { QUERY } from 'src/components/SubjectAccessRequestsCell'
const DELETE_SUBJECT_ACCESS_REQUEST_MUTATION = gql`
mutation DeleteSubjectAccessRequestMutation($id: String!) {
deleteSubjectAccessRequest(id: $id) {
id
}
}
`
const MAX_STRING_LENGTH = 150
const truncate = (text) => {
let output = text
if (text && text.length > MAX_STRING_LENGTH) {
output = output.substring(0, MAX_STRING_LENGTH) + '...'
}
return output
}
const jsonTruncate = (obj) => {
return truncate(JSON.stringify(obj, null, 2))
}
const timeTag = (datetime) => {
return (
<time dateTime={datetime} title={datetime}>
{new Date(datetime).toUTCString()}
</time>
)
}
const checkboxInputTag = (checked) => {
return <input type="checkbox" checked={checked} disabled />
}
const SubjectAccessRequestsList = ({ subjectAccessRequests }) => {
const { addMessage } = useFlash()
const [deleteSubjectAccessRequest] = useMutation(
DELETE_SUBJECT_ACCESS_REQUEST_MUTATION,
{
onCompleted: () => {
addMessage('SubjectAccessRequest deleted.', {
classes: 'rw-flash-success',
})
},
// This refetches the query on the list page. Read more about other ways to
// update the cache over here:
// https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
refetchQueries: [{ query: QUERY }],
awaitRefetchQueries: true,
}
)
const onDeleteClick = (id) => {
if (
confirm(
'Are you sure you want to delete subjectAccessRequest ' + id + '?'
)
) {
deleteSubjectAccessRequest({ variables: { id } })
}
}
return (
<div className="rw-segment rw-table-wrapper-responsive">
<table className="rw-table">
<thead>
<tr>
<th>Id</th>
<th>Comment</th>
<th>Payload</th>
<th>User id</th>
<th>Created at</th>
<th>Updated at</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{subjectAccessRequests.map((subjectAccessRequest) => (
<tr key={subjectAccessRequest.id}>
<td>{truncate(subjectAccessRequest.id)}</td>
<td>{truncate(subjectAccessRequest.comment)}</td>
<td>{truncate(subjectAccessRequest.payload)}</td>
<td>{truncate(subjectAccessRequest.userId)}</td>
<td>{timeTag(subjectAccessRequest.createdAt)}</td>
<td>{timeTag(subjectAccessRequest.updatedAt)}</td>
<td>
<nav className="rw-table-actions">
<Link
to={routes.subjectAccessRequest({
id: subjectAccessRequest.id,
})}
title={
'Show subjectAccessRequest ' +
subjectAccessRequest.id +
' detail'
}
className="rw-button rw-button-small"
>
Show
</Link>
<Link
to={routes.editSubjectAccessRequest({
id: subjectAccessRequest.id,
})}
title={
'Edit subjectAccessRequest ' + subjectAccessRequest.id
}
className="rw-button rw-button-small rw-button-blue"
>
Edit
</Link>
<a
href="#"
title={
'Delete subjectAccessRequest ' + subjectAccessRequest.id
}
className="rw-button rw-button-small rw-button-red"
onClick={() => onDeleteClick(subjectAccessRequest.id)}
>
Delete
</a>
</nav>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default SubjectAccessRequestsList

View File

@@ -0,0 +1,24 @@
import SubjectAccessRequests from 'src/components/SubjectAccessRequests'
export const QUERY = gql`
query SUBJECT_ACCESS_REQUESTS {
subjectAccessRequests {
id
comment
payload
userId
createdAt
updatedAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => {
return <div className="rw-text-center">No subjectAccessRequests yet.</div>
}
export const Success = ({ subjectAccessRequests }) => {
return <SubjectAccessRequests subjectAccessRequests={subjectAccessRequests} />
}

View File

@@ -1,23 +0,0 @@
import UserProfile from 'src/components/UserProfile'
export const QUERY = gql`
query FIND_USER_BY_ID($userName: String!) {
user: userName(userName: $userName) {
id
userName
name
createdAt
updatedAt
image
bio
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>User not found</div>
export const Success = ({ user }) => {
return <UserProfile user={user} />
}

View File

@@ -10,6 +10,7 @@ import PartsOfUser from 'src/components/PartsOfUserCell'
const UserProfile = ({ user, isEditable, loading, onSave, error, parts }) => {
const { currentUser } = useAuth()
const canEdit = currentUser?.sub === user.id
const isImageEditable = !isEditable && canEdit // image is editable when not in profile edit mode in order to separate them as it's too hard too to upload an image to cloudinary temporarily until the use saves (and maybe have to clean up) for the time being
useEffect(() => {
isEditable && !canEdit && navigate(routes.user({ userName: user.userName }))
}, [currentUser])
@@ -25,21 +26,23 @@ const UserProfile = ({ user, isEditable, loading, onSave, error, parts }) => {
<>
<section className="max-w-2xl mx-auto mt-20 ">
<div className="flex">
<div className="w-40 flex-shrink-0">
<ImageUploader
className="rounded-half rounded-br-lg shadow-md border-2 border-gray-200 border-solid"
onImageUpload={({ cloudinaryPublicId: image }) =>
setInput({
...input,
image,
})
}
aspectRatio={1}
isEditable={isEditable}
imageUrl={user.image}
width={300}
/>
</div>
{!isEditable && (
<div className="w-40 flex-shrink-0">
<ImageUploader
className="rounded-half rounded-br-lg shadow-md border-2 border-gray-200 border-solid"
onImageUpload={({ cloudinaryPublicId: image }) => {
onSave(user.userName, {
...input,
image,
})
}}
aspectRatio={1}
isEditable={isImageEditable}
imageUrl={user.image}
width={300}
/>
</div>
)}
<div className="ml-6 flex flex-col justify-between">
<ProfileTextInput
fields={editableTextFields}