Skip to content

Commit

Permalink
fix(apps/frontend-pwa): check username availability before participan…
Browse files Browse the repository at this point in the history
…t account creation (#3903)

Co-authored-by: sjschlapbach <julius.schlapbach@gmx.net>
  • Loading branch information
BShaq and sjschlapbach authored Oct 9, 2023
1 parent 3c4b9c3 commit 62844ad
Show file tree
Hide file tree
Showing 28 changed files with 288 additions and 59 deletions.
2 changes: 1 addition & 1 deletion apps/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"@klicker-uzh/prisma": "workspace:*",
"@klicker-uzh/shared-components": "workspace:*",
"@next-auth/prisma-adapter": "1.0.7",
"@uzh-bf/design-system": "2.1.11",
"@uzh-bf/design-system": "2.1.12",
"axios": "1.4.0",
"bcryptjs": "2.4.3",
"js-cookie": "3.0.5",
Expand Down
2 changes: 1 addition & 1 deletion apps/backend-docker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@types/passport": "^1.0.10",
"@types/passport-jwt": "^3.0.6",
"@types/ramda": "^0.29.3",
"@uzh-bf/design-system": "2.1.11",
"@uzh-bf/design-system": "2.1.12",
"axios": "1.4.0",
"cross-env": "7.0.3",
"dotenv": "16.0.3",
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@gabrielcsapo/docusaurus-plugin-matomo": "0.1.2",
"@mdx-js/react": "2.3.0",
"@tsconfig/docusaurus": "1.0.5",
"@uzh-bf/design-system": "2.1.11",
"@uzh-bf/design-system": "2.1.12",
"autoprefixer": "10.4.14",
"cross-env": "7.0.3",
"nodemon": "3.0.1",
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend-control/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@klicker-uzh/shared-components": "workspace:*",
"@sentry/nextjs": "7.61.1",
"@socialgouv/matomo-next": "1.6.1",
"@uzh-bf/design-system": "2.1.11",
"@uzh-bf/design-system": "2.1.12",
"body-parser": "1.20.2",
"cross-env": "7.0.3",
"dayjs": "1.11.9",
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend-manage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@klicker-uzh/prisma": "workspace:*",
"@klicker-uzh/shared-components": "workspace:*",
"@socialgouv/matomo-next": "1.6.1",
"@uzh-bf/design-system": "2.1.11",
"@uzh-bf/design-system": "2.1.12",
"d3": "7.8.4",
"dayjs": "1.11.9",
"deepmerge": "4.3.1",
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend-pwa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@klicker-uzh/shared-components": "workspace:*",
"@radix-ui/react-tabs": "1.0.3",
"@socialgouv/matomo-next": "1.6.1",
"@uzh-bf/design-system": "2.1.11",
"@uzh-bf/design-system": "2.1.12",
"body-parser": "1.20.2",
"dayjs": "1.11.9",
"deepmerge": "4.3.1",
Expand Down
37 changes: 25 additions & 12 deletions apps/frontend-pwa/src/components/forms/CreateAccountForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useState } from 'react'
import { twMerge } from 'tailwind-merge'
import * as yup from 'yup'
import DynamicMarkdown from '../learningElements/DynamicMarkdown'
import DebouncedUsernameField from './DebouncedUsernameField'

interface Props {
initialUsername?: string
Expand All @@ -40,7 +41,14 @@ function CreateAccountForm({
.string()
.required(t('pwa.profile.usernameRequired'))
.min(5, t('pwa.profile.usernameMinLength', { length: '5' }))
.max(10, t('pwa.profile.usernameMaxLength', { length: '10' })),
.max(15, t('pwa.profile.usernameMaxLength', { length: '15' }))
.test(
'isUsernameAvailable',
t('pwa.createAccount.usernameAvailability'),
() =>
typeof isUsernameAvailable === 'undefined' ||
isUsernameAvailable === true
),
password: yup
.string()
.required()
Expand All @@ -61,8 +69,10 @@ function CreateAccountForm({
})

const [tosChecked, setTosChecked] = useState<boolean>(false)

const [openCollapsibleIx, setOpenCollapsibleIx] = useState<number>(0)
const [isUsernameAvailable, setIsUsernameAvailable] = useState<
boolean | undefined
>(true)

return (
<Formik
Expand All @@ -77,10 +87,10 @@ function CreateAccountForm({
validationSchema={createAccountSchema}
onSubmit={handleSubmit}
>
{({ isSubmitting, isValid, values }) => (
{({ isSubmitting, isValid, values, validateField }) => (
<Form>
<div className="flex flex-col md:grid md:grid-cols-2 md:w-full md:max-w-[1090px] md:mx-auto gap-2">
<div className="order-3 md:col-span-2 gap-2 md:gap-4 flex flex-col justify-between md:flex-row bg-slate-100 rounded p-4 md:px-4 py-2 items-center">
<div className="flex flex-col items-center justify-between order-3 gap-2 p-4 py-2 rounded md:col-span-2 md:gap-4 md:flex-row bg-slate-100 md:px-4">
<div className="flex flex-row items-center gap-4">
<div className="flex-1 text-slate-600">
{/* <FontAwesomeIcon icon={faWarning} /> */}
Expand Down Expand Up @@ -123,11 +133,11 @@ function CreateAccountForm({
<Button.Label>{t('pwa.profile.createProfile')}</Button.Label>
</Button>
</div>
<div className="order-1 md:order-1 gap-3 md:bg-slate-50 md:p-4 rounded">
<div className="order-1 gap-3 rounded md:order-1 md:bg-slate-50 md:p-4">
<H3 className={{ root: 'border-b mb-0' }}>
{t('shared.generic.profile')}
</H3>
<div className="space-y-3 mb-2">
<div className="mb-2 space-y-3">
<FormikTextField
disabled={!!initialEmail}
name="email"
Expand All @@ -137,12 +147,15 @@ function CreateAccountForm({
label: 'font-bold text-md text-black',
}}
/>
<FormikTextField
<DebouncedUsernameField
name="username"
label={t('shared.generic.username')}
labelType="small"
className={{
label: 'font-bold text-md text-black',
valid={isUsernameAvailable}
setValid={(usernameAvailable: boolean | undefined) =>
setIsUsernameAvailable(usernameAvailable)
}
validateField={async () => {
await validateField('username')
}}
/>
<FormikTextField
Expand All @@ -168,7 +181,7 @@ function CreateAccountForm({
<div className="font-bold">
{t('pwa.profile.publicProfile')}
</div>
<div className="flex flex-row space-between gap-4">
<div className="flex flex-row gap-4 space-between">
<div className="flex flex-col items-center gap-1">
<FormikSwitchField name="isProfilePublic" />
{values.isProfilePublic
Expand All @@ -184,7 +197,7 @@ function CreateAccountForm({
</div>
</div>
</div>
<div className="order-2 md:order-2 md:bg-slate-50 md:p-4 rounded md:justify-between space-y-2">
<div className="order-2 space-y-2 rounded md:order-2 md:bg-slate-50 md:p-4 md:justify-between">
<H3 className={{ root: 'border-b mb-0' }}>
{t('pwa.createAccount.dataProcessingTitle')}
</H3>
Expand Down
99 changes: 99 additions & 0 deletions apps/frontend-pwa/src/components/forms/DebouncedUsernameField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useLazyQuery } from '@apollo/client'
import { faCheck, faSpinner, faX } from '@fortawesome/free-solid-svg-icons'
import { CheckUsernameAvailabilityDocument } from '@klicker-uzh/graphql/dist/ops'
import { FormikTextField } from '@uzh-bf/design-system'
import { useField } from 'formik'
import { useTranslations } from 'next-intl'
import { useCallback, useEffect, useRef } from 'react'

interface DebouncedUsernameFieldProps {
name: string
label: string
valid: boolean | undefined
setValid: (isAvailable: boolean | undefined) => void
validateField: () => void
}

function DebouncedUsernameField({
name,
label,
valid,
setValid,
validateField,
}: DebouncedUsernameFieldProps) {
const t = useTranslations()
const [field, meta, helpers] = useField(name)
const [checkUsernameAvailable] = useLazyQuery(
CheckUsernameAvailabilityDocument
)

// validate field when valid value changes
useEffect(() => {
validateField()
}, [valid])

// check if initial username is valid
useEffect(() => {
const check = async () => {
const value = await checkUsernameAvailable({
variables: { username: field.value },
})
setValid(value.data?.checkUsernameAvailability)
}
check()
}, [])

const usernameValidationTimeout = useRef<any>()
const debouncedUsernameCheck = useCallback(
({ username }: { username: string }) => {
clearTimeout(usernameValidationTimeout.current)
setValid(undefined)
usernameValidationTimeout.current = setTimeout(async () => {
const { data: result } = await checkUsernameAvailable({
variables: { username },
})
const isAvailable = result?.checkUsernameAvailability
setValid(isAvailable)
if (!isAvailable) {
helpers.setError(t('pwa.createAccount.usernameAvailability'))
}
}, 1000)
},
[]
)

return (
<FormikTextField
value={field.value}
label={label}
error={meta.error}
labelType="small"
className={{
label: 'font-bold text-md text-black',
icon:
typeof valid === 'undefined'
? 'animate-spin !py-0 bg-transparent'
: !valid || typeof meta.error !== 'undefined'
? 'text-red-600 bg-red-50'
: 'text-green-600',
input:
valid === false || typeof meta.error !== 'undefined'
? 'border-red-600 bg-red-50'
: '',
}}
onChange={async (username: string) => {
await helpers.setValue(username)
debouncedUsernameCheck({ username })
}}
icon={
typeof valid === 'undefined'
? faSpinner
: valid === false || typeof meta.error !== 'undefined'
? faX
: faCheck
}
/>
)
}

export default DebouncedUsernameField
26 changes: 21 additions & 5 deletions apps/frontend-pwa/src/pages/editProfile.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMutation, useQuery } from '@apollo/client'
import { BigHead } from '@bigheads/core'
import DebouncedUsernameField from '@components/forms/DebouncedUsernameField'
import { faSave } from '@fortawesome/free-regular-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
Expand Down Expand Up @@ -45,6 +46,9 @@ function EditProfile() {
const [decodedRedirectPath, setDecodedRedirectPath] = useState('/profile')
const [showError, setShowError] = useState(false)
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const [isUsernameAvailable, setIsUsernameAvailable] = useState<
boolean | undefined
>(true)

useEffect(() => {
const urlParams = new URLSearchParams(window?.location?.search)
Expand Down Expand Up @@ -80,7 +84,14 @@ function EditProfile() {
.string()
.required(t('pwa.profile.usernameRequired'))
.min(5, t('pwa.profile.usernameMinLength', { length: '5' }))
.max(10, t('pwa.profile.usernameMaxLength', { length: '10' })),
.max(15, t('pwa.profile.usernameMaxLength', { length: '15' }))
.test(
'isUsernameAvailable',
t('pwa.createAccount.usernameAvailability'),
() =>
typeof isUsernameAvailable === 'undefined' ||
isUsernameAvailable === true
),
password: yup
.string()
.optional()
Expand Down Expand Up @@ -180,7 +191,7 @@ function EditProfile() {
}
}}
>
{({ values, isSubmitting, isValid }) => {
{({ values, isSubmitting, isValid, validateField }) => {
return (
<Form>
<div className="flex flex-col md:w-full md:max-w-5xl md:mx-auto gap-4">
Expand All @@ -204,11 +215,16 @@ function EditProfile() {
labelType="small"
className={{ label: 'font-bold text-md text-black' }}
/>
<FormikTextField
<DebouncedUsernameField
name="username"
label={t('shared.generic.username')}
labelType="small"
className={{ label: 'font-bold text-md text-black' }}
valid={isUsernameAvailable}
setValid={(usernameAvailable: boolean | undefined) =>
setIsUsernameAvailable(usernameAvailable)
}
validateField={async () => {
await validateField('username')
}}
/>
<FormikTextField
name="password"
Expand Down
2 changes: 1 addition & 1 deletion apps/func-incoming-responses/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@tsconfig/recommended": "^1.0.2",
"@types/jsonwebtoken": "^9.0.2",
"@types/node": "^18.17.4",
"@uzh-bf/design-system": "2.1.11",
"@uzh-bf/design-system": "2.1.12",
"azure-functions-core-tools": "4.0.5390",
"cross-env": "7.0.3",
"dotenv": "16.0.3",
Expand Down
2 changes: 1 addition & 1 deletion apps/func-response-processor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@types/md5": "2.3.2",
"@types/node": "^18.17.4",
"@types/ramda": "^0.29.3",
"@uzh-bf/design-system": "2.1.11",
"@uzh-bf/design-system": "2.1.12",
"azure-functions-core-tools": "4.0.5390",
"cross-env": "7.0.3",
"dotenv": "16.0.3",
Expand Down
2 changes: 1 addition & 1 deletion apps/office-addin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"license": "AGPL-3.0",
"dependencies": {
"@fluentui/react": "8.109.5",
"@uzh-bf/design-system": "2.1.11",
"@uzh-bf/design-system": "2.1.12",
"core-js": "3.30.2",
"es6-promise": "4.2.8",
"formik": "2.4.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
query CheckUsernameAvailability($username: String!) {
checkUsernameAvailability(username: $username)
}
33 changes: 33 additions & 0 deletions packages/graphql/src/ops.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9635,6 +9635,39 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "checkUsernameAvailability",
"description": null,
"args": [
{
"name": "username",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "cockpitSession",
"description": null,
Expand Down
Loading

0 comments on commit 62844ad

Please sign in to comment.