From c63144dd4e58bf5f4239528aef53ae5f0251f876 Mon Sep 17 00:00:00 2001 From: Josiah Ilesanmi Date: Wed, 23 Nov 2022 11:47:11 +0100 Subject: [PATCH 1/2] user feedback form --- src/components.d.ts | 13 ++++++++ src/global/app.css | 6 ++++ src/helpers/routes.ts | 4 +++ src/pages/feedback/feedback.css | 5 +++ src/pages/feedback/feedback.tsx | 55 +++++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+) create mode 100644 src/pages/feedback/feedback.css create mode 100644 src/pages/feedback/feedback.tsx diff --git a/src/components.d.ts b/src/components.d.ts index da65a11..f610e31 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -16,6 +16,8 @@ export namespace Components { "to"?: string; "value": string | number; } + interface AppFeedback { + } interface AppLanding { } interface AppLayout { @@ -44,6 +46,12 @@ declare global { prototype: HTMLAppButtonElement; new (): HTMLAppButtonElement; }; + interface HTMLAppFeedbackElement extends Components.AppFeedback, HTMLStencilElement { + } + var HTMLAppFeedbackElement: { + prototype: HTMLAppFeedbackElement; + new (): HTMLAppFeedbackElement; + }; interface HTMLAppLandingElement extends Components.AppLanding, HTMLStencilElement { } var HTMLAppLandingElement: { @@ -76,6 +84,7 @@ declare global { }; interface HTMLElementTagNameMap { "app-button": HTMLAppButtonElement; + "app-feedback": HTMLAppFeedbackElement; "app-landing": HTMLAppLandingElement; "app-layout": HTMLAppLayoutElement; "app-result": HTMLAppResultElement; @@ -94,6 +103,8 @@ declare namespace LocalJSX { "to"?: string; "value"?: string | number; } + interface AppFeedback { + } interface AppLanding { } interface AppLayout { @@ -110,6 +121,7 @@ declare namespace LocalJSX { } interface IntrinsicElements { "app-button": AppButton; + "app-feedback": AppFeedback; "app-landing": AppLanding; "app-layout": AppLayout; "app-result": AppResult; @@ -122,6 +134,7 @@ declare module "@stencil/core" { export namespace JSX { interface IntrinsicElements { "app-button": LocalJSX.AppButton & JSXBase.HTMLAttributes; + "app-feedback": LocalJSX.AppFeedback & JSXBase.HTMLAttributes; "app-landing": LocalJSX.AppLanding & JSXBase.HTMLAttributes; "app-layout": LocalJSX.AppLayout & JSXBase.HTMLAttributes; "app-result": LocalJSX.AppResult & JSXBase.HTMLAttributes; diff --git a/src/global/app.css b/src/global/app.css index 4ecaa9c..47d6671 100644 --- a/src/global/app.css +++ b/src/global/app.css @@ -82,6 +82,12 @@ h2 { font-weight: 600; } +textarea, ion-select { + background: #ededed !important; + border-radius: 3px; + margin-bottom: 20px; +} + @media only screen and (max-width: 390px) { h2 { margin-top: 10px; diff --git a/src/helpers/routes.ts b/src/helpers/routes.ts index 616d367..5fc45e8 100644 --- a/src/helpers/routes.ts +++ b/src/helpers/routes.ts @@ -11,6 +11,10 @@ const routes = { url: '/page/result', component: 'app-result', }, + feedback: { + url: '/page/feedback', + component: 'app-feedback', + }, }; export default routes; diff --git a/src/pages/feedback/feedback.css b/src/pages/feedback/feedback.css new file mode 100644 index 0000000..d55b7f1 --- /dev/null +++ b/src/pages/feedback/feedback.css @@ -0,0 +1,5 @@ +.feedback-btn { + margin-top: 30px; + display: grid; + justify-content: center; +} diff --git a/src/pages/feedback/feedback.tsx b/src/pages/feedback/feedback.tsx new file mode 100644 index 0000000..3d6c2e0 --- /dev/null +++ b/src/pages/feedback/feedback.tsx @@ -0,0 +1,55 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'app-feedback', + styleUrl: 'feedback.css', + scoped: true, +}) +export class FeedbackPage { + + render() { + return ( + +
+

Ishihara Feedback form

+
+ + + Name + + + + + + Email + + + + + + Rating + + 1 + 2 + 3 + 4 + 5 + + + + + + Message + + + +
+ + +
+
+ ); + } +} From 54ed13d39380ba3ca1dbe1096f60335f6842ecb5 Mon Sep 17 00:00:00 2001 From: zahiruldu Date: Fri, 2 Dec 2022 23:59:42 +0600 Subject: [PATCH 2/2] feat: [FSET-1033] added form validation, rating design, success alert --- package-lock.json | 45 ++++---- src/pages/feedback/feedback.css | 63 +++++++++++ src/pages/feedback/feedback.tsx | 150 ++++++++++++++++++++++++--- src/pages/feedback/feedback.types.ts | 5 + src/pages/slider/slider.tsx | 13 +++ 5 files changed, 236 insertions(+), 40 deletions(-) create mode 100644 src/pages/feedback/feedback.types.ts diff --git a/package-lock.json b/package-lock.json index cb45f71..566af2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -805,7 +805,6 @@ "version": "5.36.0", "resolved": "https://registry.npmjs.org/@ionic-native/core/-/core-5.36.0.tgz", "integrity": "sha512-lOrkktadlKYbYf1LrDyAtsu1JnQ0oCCdkOU7iHQ8oXnNOkMwobFfD2m62F1CoOr0u9LIkpYnZSPjng8lZbmbNw==", - "dev": true, "dependencies": { "@types/cordova": "latest" }, @@ -816,8 +815,7 @@ "node_modules/@ionic-native/core/node_modules/@types/cordova": { "version": "0.0.34", "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", - "integrity": "sha512-rkiiTuf/z2wTd4RxFOb+clE7PF4AEJU0hsczbUdkHHBtkUmpWQpEddynNfJYKYtZFJKbq4F+brfekt1kx85IZA==", - "dev": true + "integrity": "sha512-rkiiTuf/z2wTd4RxFOb+clE7PF4AEJU0hsczbUdkHHBtkUmpWQpEddynNfJYKYtZFJKbq4F+brfekt1kx85IZA==" }, "node_modules/@ionic-native/screenshot": { "version": "5.36.0", @@ -1366,7 +1364,6 @@ "version": "2.17.0", "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.17.0.tgz", "integrity": "sha512-KJJH097K2TJlJWj2LANWO0yXCidxOpv1L2MmtYqFxE443t75fxOhtKBZq6zebDaZj2DFGlUtV4pcM/uapfd8TQ==", - "dev": true, "bin": { "stencil": "bin/stencil" }, @@ -6555,7 +6552,6 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", - "dev": true, "dependencies": { "tslib": "^2.1.0" } @@ -7300,7 +7296,6 @@ "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8219,7 +8214,8 @@ "@capacitor/android": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-3.6.0.tgz", - "integrity": "sha512-X6n0OLy7BE3c6qfVuL7UYyq/aIwEsqIAqtyDOwMdj5k+P1rLQVsGaWERXUtC0BGeoKBD5YgbWiyKwAwg5Spjdg==" + "integrity": "sha512-X6n0OLy7BE3c6qfVuL7UYyq/aIwEsqIAqtyDOwMdj5k+P1rLQVsGaWERXUtC0BGeoKBD5YgbWiyKwAwg5Spjdg==", + "requires": {} }, "@capacitor/cli": { "version": "3.6.0", @@ -8302,12 +8298,14 @@ "@capacitor/ios": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-3.6.0.tgz", - "integrity": "sha512-clauOkPj24COpAi1eIHSgi4OENIwuI8P6WKkfn3HVRX2lUPWyxczHTtg64inwECABy3zJdxnnwBHeok3bpoSCg==" + "integrity": "sha512-clauOkPj24COpAi1eIHSgi4OENIwuI8P6WKkfn3HVRX2lUPWyxczHTtg64inwECABy3zJdxnnwBHeok3bpoSCg==", + "requires": {} }, "@capacitor/keyboard": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-1.2.3.tgz", - "integrity": "sha512-KTSzJkMa6CrmDIDhWfswNPqS7gvYlL25E1gqXbY7w/EhyBjJhEdF0bc6QUFufS+ZVMlcPafc0/E5P+pTfLnKiA==" + "integrity": "sha512-KTSzJkMa6CrmDIDhWfswNPqS7gvYlL25E1gqXbY7w/EhyBjJhEdF0bc6QUFufS+ZVMlcPafc0/E5P+pTfLnKiA==", + "requires": {} }, "@capacitor/project": { "version": "2.0.10", @@ -8349,12 +8347,14 @@ "@capacitor/share": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@capacitor/share/-/share-1.1.2.tgz", - "integrity": "sha512-FUTdjA7MAiD1tkGVZ+C3gs7a4fyEhXojDO2HkZ954oupG1cQ51dEJ1xTNnR9BAmCwUJO4sa91cxy7SMyCDPuGg==" + "integrity": "sha512-FUTdjA7MAiD1tkGVZ+C3gs7a4fyEhXojDO2HkZ954oupG1cQ51dEJ1xTNnR9BAmCwUJO4sa91cxy7SMyCDPuGg==", + "requires": {} }, "@capacitor/splash-screen": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-1.2.2.tgz", - "integrity": "sha512-iGh9gc0rdS3R+Wr+bD/+tJl3qbmJZ5xVQAP5UsD6U6Y3ydCBo9KpA8DEqxaBZVsCDaLt67009hK5nxKBwjRn7Q==" + "integrity": "sha512-iGh9gc0rdS3R+Wr+bD/+tJl3qbmJZ5xVQAP5UsD6U6Y3ydCBo9KpA8DEqxaBZVsCDaLt67009hK5nxKBwjRn7Q==", + "requires": {} }, "@cspotcode/source-map-support": { "version": "0.8.1", @@ -8384,7 +8384,6 @@ "version": "5.36.0", "resolved": "https://registry.npmjs.org/@ionic-native/core/-/core-5.36.0.tgz", "integrity": "sha512-lOrkktadlKYbYf1LrDyAtsu1JnQ0oCCdkOU7iHQ8oXnNOkMwobFfD2m62F1CoOr0u9LIkpYnZSPjng8lZbmbNw==", - "dev": true, "requires": { "@types/cordova": "latest" }, @@ -8392,8 +8391,7 @@ "@types/cordova": { "version": "0.0.34", "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", - "integrity": "sha512-rkiiTuf/z2wTd4RxFOb+clE7PF4AEJU0hsczbUdkHHBtkUmpWQpEddynNfJYKYtZFJKbq4F+brfekt1kx85IZA==", - "dev": true + "integrity": "sha512-rkiiTuf/z2wTd4RxFOb+clE7PF4AEJU0hsczbUdkHHBtkUmpWQpEddynNfJYKYtZFJKbq4F+brfekt1kx85IZA==" } } }, @@ -8838,13 +8836,13 @@ "@stencil/core": { "version": "2.17.0", "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.17.0.tgz", - "integrity": "sha512-KJJH097K2TJlJWj2LANWO0yXCidxOpv1L2MmtYqFxE443t75fxOhtKBZq6zebDaZj2DFGlUtV4pcM/uapfd8TQ==", - "dev": true + "integrity": "sha512-KJJH097K2TJlJWj2LANWO0yXCidxOpv1L2MmtYqFxE443t75fxOhtKBZq6zebDaZj2DFGlUtV4pcM/uapfd8TQ==" }, "@stencil/store": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@stencil/store/-/store-1.5.0.tgz", - "integrity": "sha512-fe5fCF6dgVlDM1iLRkkJUyUh0Tfx305asVGgMAJjIs7Q+x/b1pGgTLROm9Ibr53PZuFwr5Kg+4h9p4FLbYqHgA==" + "integrity": "sha512-fe5fCF6dgVlDM1iLRkkJUyUh0Tfx305asVGgMAJjIs7Q+x/b1pGgTLROm9Ibr53PZuFwr5Kg+4h9p4FLbYqHgA==", + "requires": {} }, "@szmarczak/http-timer": { "version": "1.1.2", @@ -11178,7 +11176,8 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true + "dev": true, + "requires": {} }, "jest-regex-util": { "version": "27.5.1", @@ -12314,7 +12313,8 @@ "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", - "dev": true + "dev": true, + "requires": {} } } }, @@ -12715,7 +12715,6 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", - "dev": true, "requires": { "tslib": "^2.1.0" } @@ -13275,8 +13274,7 @@ "typescript": { "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "dev": true + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==" }, "uglify-js": { "version": "3.16.1", @@ -13520,7 +13518,8 @@ "version": "7.5.8", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", - "dev": true + "dev": true, + "requires": {} }, "xcode": { "version": "3.0.1", diff --git a/src/pages/feedback/feedback.css b/src/pages/feedback/feedback.css index d55b7f1..754b69a 100644 --- a/src/pages/feedback/feedback.css +++ b/src/pages/feedback/feedback.css @@ -3,3 +3,66 @@ display: grid; justify-content: center; } + +.rating-column { + display: flex; + justify-content: center; +} +.rating-message { + display: block; + text-align: center; + margin-bottom: 16px; +} +.error { + border: 1px solid red; +} + +ion-textarea { + --padding-start: 12px; +} + +.rate-area { + float: left; + display: revert; + padding: 0px; + border-style: none; + margin-bottom: 0px; +} + +.rate-area:not(:checked) > input { + position: absolute; + top: -9999px; + clip: rect(0, 0, 0, 0); +} + +.rate-area:not(:checked) > ion-label { + float: right; + width: 1em; + overflow: hidden; + white-space: nowrap; + cursor: pointer; + font-size: 180%; + color: lightgrey; +} + +.rate-area:not(:checked) > ion-label:before { + content: "★"; +} + +.rate-area > input:checked ~ ion-label { + color: gold; +} + +.rate-area:not(:checked) > ion-label:hover, +.rate-area:not(:checked) > ion-label:hover ~ ion-label { + color: gold; +} + +.rate-area > input:checked + ion-label:hover, +.rate-area > input:checked + ion-label:hover ~ ion-label, +.rate-area > input:checked ~ ion-label:hover, +.rate-area > input:checked ~ ion-label:hover ~ ion-label, +.rate-area > ion-label:hover ~ input:checked ~ ion-label { + color: gold; +} + diff --git a/src/pages/feedback/feedback.tsx b/src/pages/feedback/feedback.tsx index 3d6c2e0..cb0f2a3 100644 --- a/src/pages/feedback/feedback.tsx +++ b/src/pages/feedback/feedback.tsx @@ -1,4 +1,6 @@ -import { Component, h } from '@stencil/core'; +import { alertController } from '@ionic/core'; +import { Component, h, State } from '@stencil/core'; +import { FeedbackFormErrorType } from './feedback.types'; @Component({ tag: 'app-feedback', @@ -7,47 +9,161 @@ import { Component, h } from '@stencil/core'; }) export class FeedbackPage { + @State() selectEmail: string; + @State() selectName: string; + @State() selectRating: string = "3"; + @State() ratingMessage: string; + @State() feedbackMessage: string; + @State() formErrors: FeedbackFormErrorType = { + selectName: false, + selectEmail: false, + feedbackMessage: false, + }; + + + async handleSubmit(e) { + e.preventDefault(); + const feedbackData = { + name: this.selectName, + email: this.selectEmail, + rating: this.selectRating, + message: this.feedbackMessage + }; + // Validate form + if (this.formErrors.selectName || this.formErrors.selectEmail || this.formErrors.feedbackMessage) { + const alert = await alertController.create({ + header: 'Error', + message: 'Please check your form for errors', + buttons: ['OK'] + }); + await alert.present(); + return; + } + //validate form is empty + if (this.selectName === '' || this.selectEmail === '' || this.feedbackMessage === '') { + const alert = await alertController.create({ + header: 'Error', + message: 'Please fill out all fields', + buttons: ['OK'] + }); + await alert.present(); + return; + } + // TODO: send feedback data to server + console.log(feedbackData); + + // show an alert message from stencil + const alert = await alertController.create({ + header: 'Thank you!', + message: 'We have successfully received your feedback!', + buttons: [ + { + text: 'Ok', + handler: () => { + let transition = alert.dismiss(); + transition.then(() => { + // clear all data + this.selectName = ''; + this.selectEmail = ''; + this.selectRating = ''; + this.ratingMessage = ''; + this.feedbackMessage = ''; + }); + }, + }, + ], + }); + + await alert.present(); + + } + + handleName(event) { + this.selectName = event.target.value; + // validate name + if (event.target.validity.patternMismatch === true || event.target.value === '') { + event.target.setCustomValidity('Please enter a valid name'); + this.formErrors.selectName = true; + } else { + event.target.setCustomValidity(''); + this.formErrors.selectName = false; + } + } + + handleEmail(event) { + this.selectEmail = event.target.value; + // Validate email + if (event.target.validity.typeMismatch === true || event.target.value === '') { + event.target.setCustomValidity('Please enter a valid email'); + this.formErrors.selectEmail = true; + } else { + event.target.setCustomValidity(''); + this.formErrors.selectEmail = false; + } + } + + handleRating(event) { + this.selectRating = event.target.innerText; + this.ratingMessage = event.target.attributes.title.value; + } + + + handleMessage(event) { + this.feedbackMessage = event.target.value; + // validate message + if (event.target.validity.patternMismatch === true || event.target.value === '') { + event.target.setCustomValidity('Please enter a valid message'); + this.formErrors.feedbackMessage = true; + } else { + event.target.setCustomValidity(''); + this.formErrors.feedbackMessage = false; + } + } + render() { return (

Ishihara Feedback form

-
+ this.handleSubmit(e)}> Name - + this.handleName(e)}> Email - + this.handleEmail(e)} > - - Rating - - 1 - 2 - 3 - 4 - 5 - + +
    + this.handleRating(event)} title="Amazing">5 + this.handleRating(event)} title="Good">4 + this.handleRating(event)} title="Average">3 + this.handleRating(event)} title="Not Good">2 + this.handleRating(event)} title="Bad">1 +
+ + + {this.ratingMessage} + + Message - + this.handleMessage(e)}> -
- +
); diff --git a/src/pages/feedback/feedback.types.ts b/src/pages/feedback/feedback.types.ts new file mode 100644 index 0000000..717b3e8 --- /dev/null +++ b/src/pages/feedback/feedback.types.ts @@ -0,0 +1,5 @@ +export interface FeedbackFormErrorType{ + selectName: boolean; + selectEmail: boolean; + feedbackMessage: boolean; +} diff --git a/src/pages/slider/slider.tsx b/src/pages/slider/slider.tsx index b13372c..c1bafa7 100644 --- a/src/pages/slider/slider.tsx +++ b/src/pages/slider/slider.tsx @@ -230,6 +230,13 @@ export class SliderPage { (document.getElementById(`plate-${this.slideIndex}`).firstChild as HTMLIonInputElement).focus(); } + /** + * Navigate to feedback page + */ + navigateToFeedback = () => { + this.router.push(routes.feedback.url, 'root'); + }; + /** * Due some problems with real devices on "next/go/enter" keyboard, * where the keydown wasn't being triggered when the enterkey was with "next" @@ -241,6 +248,12 @@ export class SliderPage { render() { return ( + + + + + +

Ishihara Plates Challenge