From 7b232eafa17a361132219f77d1f5e17f60e16e2c Mon Sep 17 00:00:00 2001 From: Reza Rachmanuddin Date: Sat, 25 Dec 2021 15:53:06 +0700 Subject: [PATCH 1/7] feat: add dashboard/purchase page --- app/routes/dashboard/purchase.tsx | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 app/routes/dashboard/purchase.tsx diff --git a/app/routes/dashboard/purchase.tsx b/app/routes/dashboard/purchase.tsx new file mode 100644 index 0000000..86c6022 --- /dev/null +++ b/app/routes/dashboard/purchase.tsx @@ -0,0 +1,3 @@ +export default function Purchase() { + return

Purchase page

+} From 885eb17acc3da433d842b209b27b59bed60436f1 Mon Sep 17 00:00:00 2001 From: Reza Rachmanuddin Date: Sat, 25 Dec 2021 20:25:19 +0700 Subject: [PATCH 2/7] feat: implement outlet component in dashboard - add outlet component - move settings component to /dashboard/settings page --- app/routes/dashboard.tsx | 218 +----------------- app/routes/dashboard/settings.tsx | 236 ++++++++++++++++++++ e2e/{dashboard.spec.ts => settings.spec.ts} | 13 +- 3 files changed, 244 insertions(+), 223 deletions(-) create mode 100644 app/routes/dashboard/settings.tsx rename e2e/{dashboard.spec.ts => settings.spec.ts} (87%) diff --git a/app/routes/dashboard.tsx b/app/routes/dashboard.tsx index e572bb7..2c9b17a 100644 --- a/app/routes/dashboard.tsx +++ b/app/routes/dashboard.tsx @@ -13,22 +13,10 @@ import { XIcon, } from '@heroicons/react/outline' import { SearchIcon } from '@heroicons/react/solid' - -import type { User } from '@prisma/client' -import type { ActionFunction, LoaderFunction } from 'remix' -import { - redirect, - useActionData, - useTransition, - Form, - json, - useLoaderData, -} from 'remix' +import type { LoaderFunction } from 'remix' +import { Form, json, Outlet } from 'remix' import { auth } from '~/services/auth.server' import { LogoWithText } from '~/components/logo' -import { Button, Field } from '~/components/form-elements' -import { validatePhoneNumber, validateRequired } from '~/utils/validators' -import { db } from '~/utils/db.server' import { getUser } from '~/models/user' import { logout } from '~/services/session.server' @@ -47,71 +35,13 @@ export const loader: LoaderFunction = async ({ request }) => { return json({ user }) } -type ActionData = { - formError?: string - fieldErrors?: { - name: string | undefined - phoneNumber: string | undefined - instagram: string | undefined - telegram: string | undefined - } - fields?: { - name: string - phoneNumber: string - instagram: string - telegram: string - } -} - -export const action: ActionFunction = async ({ request }) => { - const user = await auth.isAuthenticated(request, { - failureRedirect: '/login', - }) - - const form = await request.formData() - const name = form.get('name') - const phoneNumber = form.get('phoneNumber') - const instagram = form.get('instagram') - const telegram = form.get('telegram') - // TODO: Use `zod` instead - if ( - typeof name !== 'string' || - typeof phoneNumber !== 'string' || - typeof instagram !== 'string' || - typeof telegram !== 'string' - ) { - return { formError: 'Form not submitted correctly.' } - } - - const fieldErrors = { - name: validateRequired('Nama Lengkap', name), - phoneNumber: validatePhoneNumber('Nomor WhatsApp', phoneNumber), - } - const fields = { name, phoneNumber, instagram, telegram } - if (Object.values(fieldErrors).some(Boolean)) { - return { fieldErrors, fields } - } - - await db.user.update({ where: { id: user.id }, data: fields }) - - return redirect('/dashboard') -} - const navigation = [ - { name: 'Home', href: '#', icon: HomeIcon, current: false }, + { name: 'Home', href: '#', icon: HomeIcon, current: true }, { name: 'Jobs', href: '#', icon: BriefcaseIcon, current: false }, { name: 'Applications', href: '#', icon: DocumentSearchIcon, current: false }, { name: 'Messages', href: '#', icon: ChatIcon, current: false }, { name: 'Team', href: '#', icon: UsersIcon, current: false }, - { name: 'Settings', href: '#', icon: CogIcon, current: true }, -] -const tabs = [ - { name: 'General', href: '#', current: true }, - { name: 'Password', href: '#', current: false }, - { name: 'Notifications', href: '#', current: false }, - { name: 'Plan', href: '#', current: false }, - { name: 'Billing', href: '#', current: false }, - { name: 'Team Members', href: '#', current: false }, + { name: 'Settings', href: '#', icon: CogIcon, current: false }, ] function classNames(...classes: string[]) { @@ -121,10 +51,6 @@ function classNames(...classes: string[]) { export default function Dashboard() { const [sidebarOpen, setSidebarOpen] = useState(false) - const { user } = useLoaderData<{ user: User }>() - const actionData = useActionData() - const { state } = useTransition() - return ( <>
@@ -338,141 +264,7 @@ export default function Dashboard() {
-
-
-
-

- Settings -

-
-
-
- {/* Tabs */} -
- - -
-
-
- -
-
-
- - {/* Description list with inline editing */} -
-
-
-
-

- Data Diri -

-

- Untuk keperluan proses administrasi akun Anda. -

-
-
-
-
-
-
-
- - - - - -
-
-
- -
-
-
-
-
-
-
-
-
+
diff --git a/app/routes/dashboard/settings.tsx b/app/routes/dashboard/settings.tsx new file mode 100644 index 0000000..58258f5 --- /dev/null +++ b/app/routes/dashboard/settings.tsx @@ -0,0 +1,236 @@ +import type { User } from '@prisma/client' +import type { ActionFunction, LoaderFunction } from 'remix' +import { + redirect, + useActionData, + useTransition, + Form, + json, + useLoaderData, +} from 'remix' +import { auth } from '~/services/auth.server' +import { Button, Field } from '~/components/form-elements' +import { validatePhoneNumber, validateRequired } from '~/utils/validators' +import { db } from '~/utils/db.server' +import { getUser } from '~/models/user' +import { logout } from '~/services/session.server' + +export const loader: LoaderFunction = async ({ request }) => { + // If the user is here, it's already authenticated, if not redirect them to + // the login page. + const { id } = await auth.isAuthenticated(request, { + failureRedirect: '/login', + }) + + // Get the user data from the database. + const user = await getUser(id) + if (!user) { + return logout(request) + } + return json({ user }) +} + +type ActionData = { + formError?: string + fieldErrors?: { + name: string | undefined + phoneNumber: string | undefined + instagram: string | undefined + telegram: string | undefined + } + fields?: { + name: string + phoneNumber: string + instagram: string + telegram: string + } +} + +export const action: ActionFunction = async ({ request }) => { + const user = await auth.isAuthenticated(request, { + failureRedirect: '/login', + }) + + const form = await request.formData() + const name = form.get('name') + const phoneNumber = form.get('phoneNumber') + const instagram = form.get('instagram') + const telegram = form.get('telegram') + // TODO: Use `zod` instead + if ( + typeof name !== 'string' || + typeof phoneNumber !== 'string' || + typeof instagram !== 'string' || + typeof telegram !== 'string' + ) { + return { formError: 'Form not submitted correctly.' } + } + + const fieldErrors = { + name: validateRequired('Nama Lengkap', name), + phoneNumber: validatePhoneNumber('Nomor WhatsApp', phoneNumber), + } + const fields = { name, phoneNumber, instagram, telegram } + if (Object.values(fieldErrors).some(Boolean)) { + return { fieldErrors, fields } + } + + await db.user.update({ where: { id: user.id }, data: fields }) + + return redirect('/dashboard') +} + +const tabs = [ + { name: 'General', href: '#', current: true }, + { name: 'Password', href: '#', current: false }, + { name: 'Notifications', href: '#', current: false }, + { name: 'Plan', href: '#', current: false }, + { name: 'Billing', href: '#', current: false }, + { name: 'Team Members', href: '#', current: false }, +] + +function classNames(...classes: string[]) { + return classes.filter(Boolean).join(' ') +} + +export default function Settings() { + const { user } = useLoaderData<{ user: User }>() + const actionData = useActionData() + const { state } = useTransition() + + return ( +
+
+
+

Settings

+
+
+
+ {/* Tabs */} +
+ + +
+
+
+ +
+
+
+ + {/* Description list with inline editing */} +
+
+
+
+

+ Data Diri +

+

+ Untuk keperluan proses administrasi akun Anda. +

+
+
+
+
+
+
+
+ + + + + +
+
+
+ +
+
+
+
+
+
+
+
+
+ ) +} diff --git a/e2e/dashboard.spec.ts b/e2e/settings.spec.ts similarity index 87% rename from e2e/dashboard.spec.ts rename to e2e/settings.spec.ts index 59b13e1..03788dd 100644 --- a/e2e/dashboard.spec.ts +++ b/e2e/settings.spec.ts @@ -7,21 +7,17 @@ test.use({ storageState: 'e2e/auth.json', }) -test('Dashboard', async ({ page, noscript, queries: { getByRole } }) => { - // Go to http://localhost:3000/dashboard - await page.goto('/dashboard') - +test('Settings', async ({ page, noscript, queries: { getByRole } }) => { + // Go to http://localhost:3000/dashboard/settings + await page.goto('/dashboard/settings') // Query phoneNumber const phoneNumber = await getByRole('textbox', { name: /nomor whatsapp/i, }) - // Fill phoneNumber await phoneNumber.fill('6512345678') - // Press Tab await phoneNumber.press('Tab') - // Expect visibility only when JavaScript is enabled if (!noscript) { await expect( @@ -32,16 +28,13 @@ test('Dashboard', async ({ page, noscript, queries: { getByRole } }) => { .first() ).toBeVisible() } - // Fill phoneNumber await phoneNumber.fill('+6512345678') - // Click text=Save await Promise.all([ page.waitForNavigation(/*{ url: 'http://localhost:3000/dashboard' }*/), page.click('text=Simpan'), ]) - // Expect invisibility await expect( page From 4d37ec6bdb35233f126f60a3bcd870102c9ac367 Mon Sep 17 00:00:00 2001 From: Reza Rachmanuddin Date: Sat, 25 Dec 2021 21:04:40 +0700 Subject: [PATCH 3/7] feat: add call to action component in dashboard --- app/routes/dashboard.tsx | 9 ++- app/routes/dashboard/index.tsx | 105 +++++++++++++++++++++++++++++++++ e2e/cta.spec.ts | 35 +++++++++++ 3 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 app/routes/dashboard/index.tsx create mode 100644 e2e/cta.spec.ts diff --git a/app/routes/dashboard.tsx b/app/routes/dashboard.tsx index 2c9b17a..4dd1a38 100644 --- a/app/routes/dashboard.tsx +++ b/app/routes/dashboard.tsx @@ -36,12 +36,17 @@ export const loader: LoaderFunction = async ({ request }) => { } const navigation = [ - { name: 'Home', href: '#', icon: HomeIcon, current: true }, + { name: 'Home', href: '/dashboard', icon: HomeIcon, current: true }, { name: 'Jobs', href: '#', icon: BriefcaseIcon, current: false }, { name: 'Applications', href: '#', icon: DocumentSearchIcon, current: false }, { name: 'Messages', href: '#', icon: ChatIcon, current: false }, { name: 'Team', href: '#', icon: UsersIcon, current: false }, - { name: 'Settings', href: '#', icon: CogIcon, current: false }, + { + name: 'Settings', + href: '/dashboard/settings', + icon: CogIcon, + current: false, + }, ] function classNames(...classes: string[]) { diff --git a/app/routes/dashboard/index.tsx b/app/routes/dashboard/index.tsx new file mode 100644 index 0000000..a77f18f --- /dev/null +++ b/app/routes/dashboard/index.tsx @@ -0,0 +1,105 @@ +import { CheckCircleIcon } from '@heroicons/react/solid' +import { Link } from 'remix' + +const includedFeatures = [ + 'Handout berupa catatan bergambar', + 'Akses kelas online melalui Zoom', + 'Printable planner', + 'Video rekaman kelas', +] + +export default function Dashboard() { + return ( +
+
+
+
+

+ Biaya kelas +

+

+ Biaya baru dibayarkan setelah Anda terkonfirmasi sebagai peserta + kelas +

+
+
+
+
+
+
+
+
+
+

+ Terbatas untuk 30 orang peserta +

+

+ Apabila Anda berubah pikiran, kabari kami setidaknya tiga hari + sebelum kelas dimulai supaya kami dapat mengembalikan dana + Anda sekaligus membuka slot untuk calon peserta kelas lainnya. +

+
+
+

+ Biaya termasuk +

+
+
+
    + {includedFeatures.map((feature) => ( +
  • +
    +
    +

    {feature}

    +
  • + ))} +
+
+
+
+

+ Sekali bayar +

+
+ + Rp + + XX0.000 + + ,- + +
+

+ + Kebijakan privasi + +

+
+
+ + Daftarkan diri + +
+
+
+
+
+
+
+
+ ) +} diff --git a/e2e/cta.spec.ts b/e2e/cta.spec.ts new file mode 100644 index 0000000..bd2fc6d --- /dev/null +++ b/e2e/cta.spec.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test' +import { test } from './base-test' + +test.use({ + storageState: 'e2e/auth.json', +}) + +test('Call to action has rendered in dashboard page', async ({ page }) => { + await page.goto('/dashboard') + + await expect(page.locator('text=Biaya kelas').first()).toBeVisible() + await expect( + page.locator('text=Terbatas untuk 30 orang peserta').first() + ).toBeVisible() + + const purchaseButton = page.locator('a:has-text("Daftarkan diri")').first() + await expect(purchaseButton).toBeVisible() + await expect(purchaseButton).toHaveAttribute('href', '/dashboard/purchase') +}) + +test('Redirect to /dashboard/purchase after click CTA button', async ({ + page, +}) => { + await page.goto('/dashboard') + + await expect(page.locator('text=Biaya kelas').first()).toBeVisible() + await expect( + page.locator('text=Terbatas untuk 30 orang peserta').first() + ).toBeVisible() + + await page.click('text=Daftarkan diri') + + await expect(page).toHaveURL('http://localhost:3000/dashboard/purchase') + await expect(page.locator('text=Purchase').first()).toBeVisible() +}) From 545421eae62c574d366b0a053dcac0fffb2543c5 Mon Sep 17 00:00:00 2001 From: Reza Rachmanuddin Date: Sat, 25 Dec 2021 21:55:16 +0700 Subject: [PATCH 4/7] fix: when instagram or telegram is null --- app/routes/dashboard/settings.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/routes/dashboard/settings.tsx b/app/routes/dashboard/settings.tsx index 58258f5..c6b0dd5 100644 --- a/app/routes/dashboard/settings.tsx +++ b/app/routes/dashboard/settings.tsx @@ -60,8 +60,8 @@ export const action: ActionFunction = async ({ request }) => { if ( typeof name !== 'string' || typeof phoneNumber !== 'string' || - typeof instagram !== 'string' || - typeof telegram !== 'string' + (typeof instagram !== 'string' && typeof instagram !== 'object') || + (typeof telegram !== 'string' && typeof telegram !== 'object') ) { return { formError: 'Form not submitted correctly.' } } @@ -77,7 +77,7 @@ export const action: ActionFunction = async ({ request }) => { await db.user.update({ where: { id: user.id }, data: fields }) - return redirect('/dashboard') + return redirect('/dashboard/settings') } const tabs = [ From d0fb018a2a91d123a6aee436a0f99ba050324c85 Mon Sep 17 00:00:00 2001 From: Reza Rachmanuddin Date: Sat, 25 Dec 2021 21:55:58 +0700 Subject: [PATCH 5/7] feat: add new scenario e2e testing for settings --- e2e/settings.spec.ts | 53 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/e2e/settings.spec.ts b/e2e/settings.spec.ts index 03788dd..2e3f1d1 100644 --- a/e2e/settings.spec.ts +++ b/e2e/settings.spec.ts @@ -7,7 +7,11 @@ test.use({ storageState: 'e2e/auth.json', }) -test('Settings', async ({ page, noscript, queries: { getByRole } }) => { +test('Validate phone number when updating data', async ({ + page, + noscript, + queries: { getByRole }, +}) => { // Go to http://localhost:3000/dashboard/settings await page.goto('/dashboard/settings') // Query phoneNumber @@ -44,3 +48,50 @@ test('Settings', async ({ page, noscript, queries: { getByRole } }) => { .first() ).not.toBeVisible() }) + +test('Validate name when updating data', async ({ + page, + noscript, + queries: { getByRole }, +}) => { + await page.goto('/dashboard/settings') + + const name = await getByRole('textbox', { + name: /nama lengkap/i, + }) + + await name.fill('') + await name.press('Tab') + + if (!noscript) { + await expect( + page.locator('text=Nama Lengkap wajib diisi').first() + ).toBeVisible() + } + + await name.fill('Lorem I') + await page.click('text=Simpan') + + await expect( + page.locator('text=Nama Lengkap wajib diisi').first() + ).not.toBeVisible() +}) + +test('Update profile', async ({ page }) => { + await page.goto('/dashboard/settings') + + await page.fill('[name="name"]', 'Lorem Ipsum') + await page.fill('[name="phoneNumber"]', '+6289123456') + await page.fill('[name="telegram"]', '@lorem_tl') + await page.fill('[name="instagram"]', '@lorem_ig') + + await page.click('text=Simpan') + + // Reload page to make sure getting the latest user data + await page.reload() + + await expect(page.locator('[value="Lorem Ipsum"]').first()).toBeVisible() + await expect(page.locator('[value="+6289123456"]').first()).toBeVisible() + await expect(page.locator('[value="@lorem_tl"]').first()).toBeVisible() + await expect(page.locator('[value="@lorem_ig"]').first()).toBeVisible() +}) From 8c579677f8835de2ff500e45bc876517bc60fd51 Mon Sep 17 00:00:00 2001 From: Reza Rachmanuddin Date: Sat, 25 Dec 2021 22:08:29 +0700 Subject: [PATCH 6/7] fix: error TS2322 with convert to string --- app/routes/dashboard/settings.tsx | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/app/routes/dashboard/settings.tsx b/app/routes/dashboard/settings.tsx index c6b0dd5..f9bdd65 100644 --- a/app/routes/dashboard/settings.tsx +++ b/app/routes/dashboard/settings.tsx @@ -30,6 +30,13 @@ export const loader: LoaderFunction = async ({ request }) => { return json({ user }) } +interface UserFields { + name: string + phoneNumber: string + instagram: string + telegram: string +} + type ActionData = { formError?: string fieldErrors?: { @@ -38,12 +45,7 @@ type ActionData = { instagram: string | undefined telegram: string | undefined } - fields?: { - name: string - phoneNumber: string - instagram: string - telegram: string - } + fields?: UserFields } export const action: ActionFunction = async ({ request }) => { @@ -70,7 +72,12 @@ export const action: ActionFunction = async ({ request }) => { name: validateRequired('Nama Lengkap', name), phoneNumber: validatePhoneNumber('Nomor WhatsApp', phoneNumber), } - const fields = { name, phoneNumber, instagram, telegram } + const fields: UserFields = { + name, + phoneNumber, + instagram: String(instagram), + telegram: String(telegram), + } if (Object.values(fieldErrors).some(Boolean)) { return { fieldErrors, fields } } From 70b763364c04acd6e177c00ed3a05736d33f7651 Mon Sep 17 00:00:00 2001 From: Reza Rachmanuddin Date: Sun, 26 Dec 2021 08:51:26 +0700 Subject: [PATCH 7/7] fix: change page.fill to getByRole --- e2e/settings.spec.ts | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/e2e/settings.spec.ts b/e2e/settings.spec.ts index 2e3f1d1..f9078ce 100644 --- a/e2e/settings.spec.ts +++ b/e2e/settings.spec.ts @@ -77,15 +77,33 @@ test('Validate name when updating data', async ({ ).not.toBeVisible() }) -test('Update profile', async ({ page }) => { +test('Update profile', async ({ page, queries: { getByRole } }) => { await page.goto('/dashboard/settings') - await page.fill('[name="name"]', 'Lorem Ipsum') - await page.fill('[name="phoneNumber"]', '+6289123456') - await page.fill('[name="telegram"]', '@lorem_tl') - await page.fill('[name="instagram"]', '@lorem_ig') + // Get element by role + const name = await getByRole('textbox', { + name: /nama lengkap/i, + }) + const phoneNumber = await getByRole('textbox', { + name: /nomor whatsapp/i, + }) + const telegram = await getByRole('textbox', { + name: /username telegram/i, + }) + const instagram = await getByRole('textbox', { + name: /username instagram/i, + }) + const saveButton = await getByRole('button', { + name: /simpan/i, + }) - await page.click('text=Simpan') + // Fill all input + await name.fill('Lorem Ipsum') + await phoneNumber.fill('+6289123456') + await telegram.fill('@lorem_tl') + await instagram.fill('@lorem_ig') + + await saveButton.click() // Reload page to make sure getting the latest user data await page.reload()