diff --git a/assets/icons/spinner-icon.svg b/assets/icons/spinner-icon.svg new file mode 100644 index 0000000..cae8ea9 --- /dev/null +++ b/assets/icons/spinner-icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/icons/x-circle-icon.svg b/assets/icons/x-circle-icon.svg new file mode 100644 index 0000000..0d72ac5 --- /dev/null +++ b/assets/icons/x-circle-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/styles/_app-colors.scss b/assets/styles/_app-colors.scss index 724816f..ea29bb9 100644 --- a/assets/styles/_app-colors.scss +++ b/assets/styles/_app-colors.scss @@ -7,12 +7,16 @@ $background-primary-light: #252627; $background-primary-main: #151314; $background-primary-dark: #0C0D10; +$background-success-main: #161F18; + +$background-error-main: #1F1616; + $background-secondary-main: #181819; /* status colors */ -$success-light: #5EDDB7; +$success-light: #66EBC2; $success-main: #5EDDB7; -$success-dark: #5EDDB7; +$success-dark: #56CBA5; $error-light: #FF8383; $error-main: #FF8383; diff --git a/assets/styles/_variables.scss b/assets/styles/_variables.scss index e65a082..722f61c 100644 --- a/assets/styles/_variables.scss +++ b/assets/styles/_variables.scss @@ -16,6 +16,10 @@ --background-secondary-main: #{$background-secondary-main}; + --background-success-main: #{$background-success-main}; + + --background-error-main: #{$background-error-main}; + /* status colors */ --success-light: #{$success-light}; --success-main: #{$success-main}; diff --git a/components/AppAlert.vue b/components/AppAlert.vue new file mode 100644 index 0000000..8135285 --- /dev/null +++ b/components/AppAlert.vue @@ -0,0 +1,80 @@ + + + + + + {{ title }} + + + {{ message }} + + + + + + + + diff --git a/components/AppButton.vue b/components/AppButton.vue index a0c5ec1..e2d25bc 100644 --- a/components/AppButton.vue +++ b/components/AppButton.vue @@ -7,9 +7,9 @@ :to="route" > @@ -21,7 +21,7 @@ @@ -29,9 +29,9 @@ @@ -43,7 +43,7 @@ @@ -57,9 +57,9 @@ :type="buttonType" > @@ -71,7 +71,7 @@ @@ -91,12 +91,13 @@ const props = withDefaults( text?: string scheme?: 'filled' | 'flat' | 'none' modification?: 'border-circle' | 'border-rounded' | 'text' | 'none' - color?: 'primary' | 'secondary' | 'none' + color?: 'primary' | 'secondary' | 'success' | 'error' | 'none' size?: 'large' | 'medium' | 'none' route?: RouteLocationRaw href?: string iconLeft?: ICON_NAMES | '' iconRight?: ICON_NAMES | '' + isLoading?: boolean }>(), { text: '', @@ -108,14 +109,17 @@ const props = withDefaults( href: '', iconLeft: '', iconRight: '', + isLoading: false, }, ) const attrs = useAttrs() const slots = useSlots() -const isDisabled = computed((): boolean => - ['', 'disabled', true].includes(attrs.disabled as string | boolean), +const isDisabled = computed( + (): boolean => + ['', 'disabled', true].includes(attrs.disabled as string | boolean) || + props.isLoading, ) const buttonClasses = computed(() => [ @@ -130,8 +134,13 @@ const buttonClasses = computed(() => [ ...((props.iconLeft || props.iconRight) && !props.text && !slots.default ? ['app-button--icon-only'] : []), + ...(props.isLoading ? ['app-button--loading'] : []), ]) +const buttonIconLeft = computed(() => + props.isLoading ? ICON_NAMES.spinner : props.iconLeft, +) + const buttonType = computed( () => (attrs.type as ButtonType) || 'button', ) @@ -152,8 +161,8 @@ const buttonType = computed( background-color: var(--app-button-bg); color: var(--app-button-text); - &:disabled, - &--disabled { + &:disabled:not(.app-button--loading), + &--disabled:not(.app-button--loading) { cursor: not-allowed; pointer-events: none; --app-button-bg: var(--app-button-disabled-bg); @@ -161,6 +170,14 @@ const buttonType = computed( --app-button-border: var(--app-button-disabled-border); } + &.app-button--disabled, + .app-button--loading { + background-color: var(--app-button-bg); + color: var(--app-button-text); + border: var(--app-button-border); + opacity: 0.5; + } + &:not([disabled]):hover { text-decoration: none; transition-timing-function: var(--transition-timing-default); @@ -331,6 +348,90 @@ const buttonType = computed( } } + &--success { + --app-button-filled-bg: var(--success-main); + --app-button-filled-bg-hover: var(--success-dark); + --app-button-filled-bg-focused: var(--success-dark); + --app-button-filled-bg-active: var(--success-dark); + + --app-button-filled-text: var(--text-primary-invert-main); + --app-button-filled-text-hover: var(--text-primary-invert-main); + --app-button-filled-text-focused: var(--text-primary-invert-light); + --app-button-filled-text-active: var(--text-primary-invert-main); + + --app-button-flat-bg-hover: var(--background-success-light); + --app-button-flat-bg-focused: var(--background-success-light); + --app-button-flat-bg-active: var(--background-success-light); + + --app-button-flat-text: var(--success-main); + --app-button-flat-text-hover: var(--success-main); + --app-button-flat-text-focused: var(--success-light); + --app-button-flat-text-active: var(--success-light); + + --app-button-flat-border: #{toRem(1)} solid var(--success-main); + --app-button-flat-border-hover: #{toRem(1)} solid var(--success-main); + --app-button-flat-border-focused: #{toRem(1)} solid var(--success-light); + --app-button-flat-border-active: #{toRem(1)} solid var(--success-light); + + --app-button-none-bg-hover: var(--background-success-light); + --app-button-none-bg-focused: var(--background-success-light); + --app-button-none-bg-active: var(--background-success-light); + + --app-button-none-text: var(--success-main); + --app-button-none-text-hover: var(--success-main); + --app-button-none-text-focused: var(--success-light); + --app-button-none-text-active: var(--success-light); + + &.app-button--text { + --app-button-text: var(--text-primary-invert-main); + --app-button-text-hover: var(--primary-light); + --app-button-text-focused: var(--primary-dark); + --app-button-text-active: var(--primary-dark); + } + } + + &--error { + --app-button-filled-bg: var(--error-main); + --app-button-filled-bg-hover: var(--error-dark); + --app-button-filled-bg-focused: var(--error-dark); + --app-button-filled-bg-active: var(--error-dark); + + --app-button-filled-text: var(--text-primary-invert-main); + --app-button-filled-text-hover: var(--text-primary-invert-main); + --app-button-filled-text-focused: var(--text-primary-invert-light); + --app-button-filled-text-active: var(--text-primary-invert-main); + + --app-button-flat-bg-hover: var(--background-error-light); + --app-button-flat-bg-focused: var(--background-error-light); + --app-button-flat-bg-active: var(--background-error-light); + + --app-button-flat-text: var(--error-main); + --app-button-flat-text-hover: var(--error-main); + --app-button-flat-text-focused: var(--error-light); + --app-button-flat-text-active: var(--error-light); + + --app-button-flat-border: #{toRem(1)} solid var(--error-main); + --app-button-flat-border-hover: #{toRem(1)} solid var(--error-main); + --app-button-flat-border-focused: #{toRem(1)} solid var(--error-light); + --app-button-flat-border-active: #{toRem(1)} solid var(--error-light); + + --app-button-none-bg-hover: var(--background-error-light); + --app-button-none-bg-focused: var(--background-error-light); + --app-button-none-bg-active: var(--background-error-light); + + --app-button-none-text: var(--error-main); + --app-button-none-text-hover: var(--error-main); + --app-button-none-text-focused: var(--error-light); + --app-button-none-text-active: var(--error-light); + + &.app-button--text { + --app-button-text: var(--text-primary-invert-main); + --app-button-text-hover: var(--primary-light); + --app-button-text-focused: var(--primary-dark); + --app-button-text-active: var(--primary-dark); + } + } + &--none { $flat-border-hover: #{toRem(1)} solid var(--primary-light); @@ -406,11 +507,16 @@ const buttonType = computed( text-decoration: underline; } } +} - .app-button__icon-left, - .app-button__icon-right { - height: 1.6em; - width: 1.6em; +.app-button__icon { + height: toRem(20); + width: toRem(20); + + &--left { + .app-button--loading & { + animation: spin 2s linear infinite; + } } } @@ -423,4 +529,10 @@ const buttonType = computed( @include text-ellipsis; } + +@keyframes spin { + 100% { + transform: rotate(360deg); + } +} diff --git a/enums/icon-names.enum.ts b/enums/icon-names.enum.ts index 492ad5c..340ec25 100644 --- a/enums/icon-names.enum.ts +++ b/enums/icon-names.enum.ts @@ -22,4 +22,5 @@ export enum ICON_NAMES { xCircle = 'x-circle', x = 'x', squareRoot = 'square-root', + spinner = 'spinner', } diff --git a/fields/DatetimeField.vue b/fields/DatetimeField.vue index 63d8e61..cd5ca2a 100644 --- a/fields/DatetimeField.vue +++ b/fields/DatetimeField.vue @@ -192,6 +192,7 @@ $z-index-btn: 1; right: 0; padding-bottom: var(--app-padding-bottom); width: min-content; + z-index: $z-index-btn; @include respond-to(small) { position: static; diff --git a/forms/ConstantsForm.vue b/forms/ConstantsForm.vue index 5c33a96..6d90e2f 100644 --- a/forms/ConstantsForm.vue +++ b/forms/ConstantsForm.vue @@ -1,33 +1,40 @@ - + {{ $t('constants-form.title') }} - - - - {{ $t(`constants-form.random-bytes32-label`) }} - - - - - - + + - + - - - - - - + + + + + {{ $t(`constants-form.random-bytes32-label`) }} + + + + + + + + + + + + + @@ -58,6 +65,12 @@ const form = reactive({ @include solidity-tools-form-part; } +.constants-form__input-fields { + display: flex; + flex-direction: column; + gap: toRem(20); +} + .constants-form__label-wrp { display: flex; gap: toRem(8); diff --git a/forms/DateForm.vue b/forms/DateForm.vue index 5563785..0fa6a9e 100644 --- a/forms/DateForm.vue +++ b/forms/DateForm.vue @@ -1,7 +1,13 @@ - {{ $t('date-form.input-title') }} + + {{ $t('date-form.input-title') }} + + - @@ -162,6 +164,11 @@ const outputItems = computed(() => [ @include solidity-tools-form-part; } +.date-form__input-title-wrp { + display: flex; + justify-content: space-between; +} + .date-form__divider { @include solidity-tools-form-divider; } diff --git a/forms/UnitConverterForm.vue b/forms/UnitConverterForm.vue index 2ef9544..54a348b 100644 --- a/forms/UnitConverterForm.vue +++ b/forms/UnitConverterForm.vue @@ -35,16 +35,10 @@ type UnitConverterFormKeys = keyof typeof form const form = reactive({ wei: '', - kwei: '', - mwei: '', gwei: '', finney: '', szabo: '', ether: '', - kether: '', - mether: '', - gether: '', - tether: '', }) const { getFieldErrorMessage, touchField, isFormValid } = useFormValidation( diff --git a/forms/VerifySignatureForm.vue b/forms/VerifySignatureForm.vue index 459dc6b..4ebf735 100644 --- a/forms/VerifySignatureForm.vue +++ b/forms/VerifySignatureForm.vue @@ -1,7 +1,28 @@ - {{ $t('verify-signature-form.input-title') }} + + + {{ $t('verify-signature-form.input-title') }} + + + + + + + + + + - import { useFormValidation } from '@/composables' -import { InputField, TextareaField } from '@/fields' -import { ErrorHandler, address, hexadecimal, required } from '@/helpers' -import { verifyMessage, getAddress } from 'ethers' +import { InputField, TextareaField, RadioButtonField } from '@/fields' +import { address, required, hexadecimal, ErrorHandler } from '@/helpers' +import { type FieldOption, type DecodeType } from '@/types' +import { getAddress, recoverAddress, toUtf8Bytes } from 'ethers' import { i18n } from '~/plugins/localization' -import { reactive } from 'vue' -const { showToast } = useNotifications() +type VerifyingState = 'idle' | 'verified' | 'unverified' + const { t } = i18n.global +const decodeOptions = computed(() => [ + { title: t('hash-function-form.select-option-text'), value: 'text' }, + { title: t('hash-function-form.select-option-hex'), value: 'hex' }, +]) + const INITIAL_FORM_STATE = { accountAddress: '', signature: '', message: '', + messageMode: decodeOptions.value[0].value as DecodeType, } +const verifyingState = ref('idle') + const form = reactive({ ...INITIAL_FORM_STATE }) +const rules = computed(() => ({ + accountAddress: { required, address }, + signature: { required, hexadecimal }, + message: { + required, + ...(form.messageMode === 'hex' && { hexadecimal, required }), + }, +})) + const { isFormValid, getFieldErrorMessage, touchField, validationController } = - useFormValidation(form, { - accountAddress: { required, address }, - signature: { required, hexadecimal }, - message: { required }, - }) + useFormValidation(form, rules) -const verifySignature = async () => { +const verifySignature = () => { + if (!isFormValid()) return try { - if (!isFormValid()) return - const signerAddr = verifyMessage(form.message, form.signature) + const messageToVerify = + form.messageMode === 'hex' ? form.message : toUtf8Bytes(form.message) + const signerAddr = recoverAddress(messageToVerify, form.signature) if (signerAddr !== getAddress(form.accountAddress)) { - showToast('error', t('verify-signature-form.not-verified')) + verifyingState.value = 'unverified' return } - showToast('success', t('verify-signature-form.verified')) + verifyingState.value = 'verified' } catch (error) { - ErrorHandler.process(error) + verifyingState.value = 'unverified' + ErrorHandler.processWithoutFeedback(error) } } +const resetVerifyingState = () => { + verifyingState.value = 'idle' +} + const resetForm = () => { Object.assign(form, INITIAL_FORM_STATE) validationController.value.$reset() + resetVerifyingState() } + +watch(form, () => { + resetVerifyingState() +}) diff --git a/plugins/localization/resources/en.json b/plugins/localization/resources/en.json index be85985..951aeef 100644 --- a/plugins/localization/resources/en.json +++ b/plugins/localization/resources/en.json @@ -86,7 +86,7 @@ "duration-form-tab": "Duration" }, "duration-form": { - "info": "Please note that in the convertor 1 year = {daysInYear} days and 1 month = {daysInMonth} days", + "info": "1 year = {daysInYear} days and 1 month = {daysInMonth} days", "pasted-value": "{years} year(s) {months} month(s) {weeks} week(s) {days} day(s)\n {hours} hour(s) {minutes} minute(s)", "input-title": "Enter Duration", "duration-label": "Duration in following format", @@ -291,13 +291,15 @@ "input-title": "Verify signature", "verified": "Message signature verified", "not-verified": "The Message Signature Verification Failed!", - "account-address-label": "Address", + "account-address-label": "Signer address", "account-address-placeholder": "Enter the signer address", - "message-label": "Message", + "message-label": "Signing message", "message-placeholder": "Enter the original message", - "signature-label": "Hash", + "signature-label": "Signature hash", "signature-placeholder": "Enter the hash of signed message", "submit-btn": "Check signature", + "alert-success": "The message is succesfully verified!", + "alert-error": "The message is not verified!", "reset-btn": "Reset form" } } \ No newline at end of file
+ {{ title }} +
+ {{ message }} +