Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LBGG-564: Account confirmation #580

Merged
merged 11 commits into from
Nov 29, 2023
1 change: 1 addition & 0 deletions assets/sprite/svg/circle-check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/sprite/svg/circle-exclamation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/sprite/svg/circle-info.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/sprite/svg/spinner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/sprite/svg/triangle-exclamation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ export {}

declare module 'vue' {
export interface GlobalComponents {
ISvgCircleCheck: typeof import('~icons/svg/circle-check')['default']
ISvgCircleExclamation: typeof import('~icons/svg/circle-exclamation')['default']
ISvgCircleInfo: typeof import('~icons/svg/circle-info')['default']
ISvgClock: typeof import('~icons/svg/clock')['default']
ISvgClose: typeof import('~icons/svg/close')['default']
ISvgEyeHidden: typeof import('~icons/svg/eye-hidden')['default']
ISvgEyeVisible: typeof import('~icons/svg/eye-visible')['default']
ISvgMenu: typeof import('~icons/svg/menu')['default']
ISvgSearch: typeof import('~icons/svg/search')['default']
ISvgSpinner: typeof import('~icons/svg/spinner')['default']
ISvgTriangleExclamation: typeof import('~icons/svg/triangle-exclamation')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
Expand Down
31 changes: 31 additions & 0 deletions components/blocks/Loader/Loader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts"></script>

<template>
<div class="loader__container">
<i-svg-spinner class="loader__spinner" />
</div>
</template>

<style lang="postcss" scoped>
.loader__container {
@apply flex justify-center items-center h-screen w-full;
}

.loader__spinner {
@apply h-20 w-20 fill-current;
animation: spin 1.25s steps(9, end) infinite;

@media (prefers-reduced-motion) {
animation-duration: 9s;
}
zysim marked this conversation as resolved.
Show resolved Hide resolved

@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
</style>
60 changes: 60 additions & 0 deletions components/blocks/cards/BasicAlert/BasicAlert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { mount, enableAutoUnmount } from '@vue/test-utils'
import { useModalAlert } from 'composables/useModalAlert'
import { getByClass, getByTestId, getHTMLElement } from 'root/testUtils'
import BasicAlert from './BasicAlert.vue'

function getBasicAlertWrapper() {
return mount(BasicAlert)
}

beforeEach(() => {
const modalAlertState = useModalAlert()
modalAlertState.value = {
body: 'This is a test',
show: true,
title: 'A test alert?',
type: 'info',
}
})

enableAutoUnmount(afterEach)

afterEach(() => {
fetchMock.resetMocks()
vi.restoreAllMocks()
})

describe('<BasicAlert />', () => {
it('should render without crashing', () => {
const wrapper = getBasicAlertWrapper()

expect(wrapper.isVisible()).toBe(true)
})

it('renders with the correct information', () => {
const wrapper = getBasicAlertWrapper()

expect(
getHTMLElement(getByClass(wrapper, 'basic-modal-alert__header'))
.childElementCount,
).toEqual(3)
expect(getByClass(wrapper, 'basic-modal-alert__title').text()).toEqual(
'A test alert?',
)
expect(getByClass(wrapper, 'basic-modal-alert__body').text()).toEqual(
'This is a test',
)
})

describe('when the close button is clicked', () => {
it('should emit the close event', async () => {
const wrapper = getBasicAlertWrapper()

await getByTestId(wrapper, 'basic-modal-alert-close-button').trigger(
'click',
)

expect(wrapper.isVisible()).toBe(false)
})
})
})
99 changes: 99 additions & 0 deletions components/blocks/cards/BasicAlert/BasicAlert.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script setup lang="ts">
import BaseModal from 'elements/modals/BaseModal/BaseModal.vue'
import CloseButton from 'elements/buttons/CloseButton/CloseButton.vue'
import Card from 'elements/cards/Card/Card.vue'
import CardHeader from 'elements/cards/CardHeader/CardHeader.vue'
import CardBody from 'elements/cards/CardBody/CardBody.vue'
import { useModalAlert } from 'composables/useModalAlert'

const modalAlertState = useModalAlert()?.value

const close = () => {
erunks marked this conversation as resolved.
Show resolved Hide resolved
modalAlertState.body = ''
modalAlertState.show = false
modalAlertState.title = ''
modalAlertState.type = ''
}
</script>

<template>
<transition
v-if="modalAlertState.show"
enter-active-class="transition-opacity duration-200"
leave-active-class="transition-opacity duration-200"
enter-to-class="opacity-100"
leave-to-class="opacity-0"
>
<BaseModal v-show="modalAlertState.show" @close="close">
<Card
id="basicModalAlert"
:class="['basic-modal-alert', modalAlertState.type]"
data-testid="basic-modal-alert"
>
<CardHeader class="basic-modal-alert__header">
<i-svg-circle-info v-if="modalAlertState.type === 'info'" />
<i-svg-circle-check v-if="modalAlertState.type === 'success'" />
<i-svg-circle-exclamation v-if="modalAlertState.type === 'error'" />
<i-svg-triangle-exclamation
v-if="modalAlertState.type === 'warning'"
/>
<h2 class="basic-modal-alert__title">
{{ modalAlertState.title }}
</h2>
<CloseButton
class="basic-modal-alert__close-button"
data-testid="basic-modal-alert-close-button"
@click.prevent="close"
/>
</CardHeader>
<CardBody class="basic-modal-alert__body">
{{ modalAlertState.body }}
</CardBody>
</Card>
</BaseModal>
</transition>
</template>

<style lang="postcss" scoped>
.basic-modal-alert {
@apply bg-white w-full max-w-xl;

svg {
@apply h-5 w-5 mr-2;
}

&.info svg {
@apply fill-blue-500;
}

&.success svg {
@apply fill-green-500;
}

&.error svg {
@apply fill-red-500;
}

&.warning svg {
@apply fill-yellow-500;
}

& .basic-modal-alert__header {
@apply flex flex-row items-center;
}

& .basic-modal-alert__title,
& .basic-modal-alert__body {
@apply flex flex-1;
}

& .basic-modal-alert__title {
@apply text-lg font-medium py-2;
@apply flex items-center;
}

& .basic-modal-alert__body {
@apply justify-center m-8;
}
}
</style>
11 changes: 5 additions & 6 deletions components/blocks/cards/SignUpCard/SignUpCard.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { type Ref, ref } from 'vue'
import { sentenceCase } from 'lib/helpers'
import BaseInput from 'elements/inputs/BaseInput/BaseInput.vue'
import HideShowPassword from 'elements/buttons/HideShowPassword/HideShowPassword.vue'
import BaseButton from 'elements/buttons/BaseButton/BaseButton.vue'
Expand Down Expand Up @@ -98,12 +99,10 @@ async function signup() {
},
{
onError: (val: any) => {
errorText.value = `Error(s): ${(
Object.values(val.error.errors) as string[]
).reduce(
(accumulator: string, val: string) => `${accumulator} ${val}`,
'',
)}`
const errors = Object.values(val.error?.errors) as string[][]
errorText.value = `Error(s): ${errors
.map((errorType) => errorType.map(sentenceCase))
.join(', ')}`
showErrorsText.value = true
},
onOkay: () => {
Expand Down
1 change: 1 addition & 0 deletions composables/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as useConfirmAccount } from './useConfirmAccount'
export { default as useGetLeaderboardDetail } from './useGetLeaderboardDetail'
export { default as useGetUserDetail } from './useGetUserDetail'
export { default as useLoginUser } from './useLoginUser'
Expand Down
20 changes: 20 additions & 0 deletions composables/api/useConfirmAccount/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ApiResponse, optionalParameters, useApi } from 'composables/useApi'
import { Account } from 'lib/api/Account'

export const useConfirmAccount = async (
erunks marked this conversation as resolved.
Show resolved Hide resolved
confirmationToken: string,
opts: optionalParameters<void> = {},
): Promise<ApiResponse<void>> => {
const { onError, onOkay } = opts

const account = new Account({
baseUrl: useRuntimeConfig().public.BACKEND_BASE_URL,
})

return await useApi<void>(
async () => await account.confirmUpdate(confirmationToken),
{ onError, onOkay },
)
}

export default useConfirmAccount
24 changes: 24 additions & 0 deletions composables/api/useConfirmAccount/useConfirmAccount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useConfirmAccount } from '.'

const mockSuccessAccountConfirmation = vi.fn(() =>
Promise.resolve({ ok: true }),
)

describe('useConfirmUser', () => {
describe('when everything is successful', () => {
const confirmationCode = '123'

it('creates a PUT request to confirm the account', async () => {
vi.mock('lib/api/Account', () => ({
Account: function Account() {
this.confirmUpdate = mockSuccessAccountConfirmation
},
}))

await useConfirmAccount(confirmationCode)

expect(mockSuccessAccountConfirmation).toBeCalledTimes(1)
expect(mockSuccessAccountConfirmation).toBeCalledWith(confirmationCode)
})
})
})
16 changes: 16 additions & 0 deletions composables/useModalAlert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
interface ModalAlertState {
body: string
show: boolean
title: string
type: string
}

export const useModalAlert = () =>
useState<ModalAlertState>('modal_alert', () => ({
body: '',
show: false,
title: '',
type: '',
}))

export default useModalAlert
2 changes: 2 additions & 0 deletions layouts/default.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<script setup lang="ts">
import BasicAlert from 'blocks/cards/BasicAlert/BasicAlert.vue'
import SiteFooter from 'blocks/SiteFooter/SiteFooter.vue'
import SiteNavbar from 'blocks/nav/SiteNavbar/SiteNavbar.vue'
</script>

<template>
<div class="mx-auto flex h-screen flex-col">
<BasicAlert />
<SiteNavbar />
<slot />
<SiteFooter />
Expand Down
Loading