- Render work list via Pagination / Infinity Scroll
- Search by title
- Filter by category
- Config route list to specify which menu requires login
- Render route list based on login status
- Integrate with login and test
- Target to have type suggestion when using profile from useAuth()
- TIP: Organize Imports (Option + Shift + O)
Current flow:
- On init: useAuth with default data =
undefined
- Call API profile and update data = logged in user
Updated flow:
- Save user's info to local storage if login successfully
- On init: useAuth with default data =
user info from local storage
- Call API profile and update data = logged in user
- Remove local storage if logout
key: user_info
Unexpected cases handling:
- What if fetch profile failed?
Process:
- Server side generate HTML (A) and send to client
- Client get HTML (A) to display on UI and download JS in the background
- Once JS downloaded, it will be executed. Hydration process take place and generate a new DOM (B). Then it try to match B and A and attach event listener to it.
If A = B --> OK If A <> B --> Show error text content did not match
Solutions:
- Make sure the first render on client side should be the same with server side.
- Use client side rendering via dynamic feature of NextJS (not SEO friendly)
Refs:
- https://nextjs.org/docs/messages/react-hydration-error
- https://www.joshwcomeau.com/react/the-perils-of-rehydration/
- https://blog.saeloun.com/2021/12/16/hydration
- https://thanhle.blog/blog/server-side-rendering-voi-hydration-lang-phi-tai-nguyen-nhu-the-nao
- https://nextjs.org/docs/advanced-features/dynamic-import
- Can't change tsconfig from
jsx: preserve
tojsx: react
- Can safely remove React import due to this post
- Can't use named export with ssr: false --> change to use default import instead
Refs:
- Mock response to return Error instead of success
- How to extract error body from API response
- Throw error response in axios interceptor
- Retrieve error message in catch statement
- Do whatever you want with the message (show toast, log, report error, ...)
- add react-toastify package
- toast error message
Ref: https://kentcdodds.com/blog/get-a-catch-block-error-message-with-typescript
- Works API
- OpenAPI (Swagger) Editor
- Where to get API Schema file? --> from my github repo
- Demo how to use Swagger directly in VSCode
- add work-api.ts
- add type definition for work api: ListResponse, ListParams
- call API and log data on component
export interface Pagination {
_page: number
_limit: number
_totalRows: number
}
- update Pagination type:
_total
to_totalRows
- update
swr
lib to latest v2.1.2 - implement useWorkList() hook using useSWR()
- try to use the hook in component
- Show Work List UI
- demo decouple interval
- List State
- Loading
- No data
- Has data
- Component:
<Skeleton />
docs - Update
<WorkList />
to show loading status
- Using Component
<Pagination />
docs - Integrate our API response with Pagination component
- Disable Prev / Next when page is at min / max.
export default function PaginationControlled() {
const [page, setPage] = React.useState(1);
const handleChange = (event: React.ChangeEvent<unknown>, value: number) => {
setPage(value);
};
return (
<Stack spacing={2}>
<Typography>Page: {page}</Typography>
<Pagination count={10} page={page} onChange={handleChange} />
</Stack>
);
}
- Add
<WorkFilters />
component (clone from<LoginForm />
)- take care of all filters: search, categories select, ...
- take initial value to set default value for each filter
- Support
externalOnChange
for<InputField />
- Add debounce on search change
- Log form submission value
- Log data received at page-level component
PageA: control form submit logic |__ WorkFilters: manage filters form and notify parent via callback if any changes | |__ InputField: search | |__ SelectField: category select
PageB: control form submit logic |__ WorkFilters: manage filters form and notify parent via callback if any changes | |__ InputField: search | |__ SelectField: category select
Current flow: refetch work list whenever filters state
changes
Current flow: when filters change --> update state
(setFilters)
New flow: refetch work list whenever router query
changes
New flow: when filters change --> update router query
- remove filters state
- using Shallow Routing
- how to set default values for
WorkFilters
- how to ignore the first render with empty query?
- fixed: API resolved without sending a response for /api/works?_page=1&_limit=3, this may result in stalled requests.
- Goal: will
show page loading
by default (both on server and client) - Using tool: Performance Insights
Process:
- Server return a HTML
A
- Client render first time
B
- router.isReady = false
, router.query = undefined - Client render second time - router.
isReady = true
, router.query = data
<Skeleton
variant="rectangular"
height={40}
sx={{
display: 'inline-block',
width: '100%',
mt: 2,
mb: 1,
verticalAlign: 'middle', // fix layout shift
}}
/>
A Form Control includes 2 main parts:
- UI control
- Bind form state to UI control (react-hook-form) --> integrate with form logic
- Add Autocomplete UI control
- Show it on UI to see how it looks like
- Add new component:
AutocompleteField
(cloned fromInputField
)
- Add type definition for AutocompleteField
- Add new key to WorkFiltersPayload: tagList_like
- Add generic type for InputField
- Add generic type for AutocompleteField
- Integrate with react hook form control
- Binding form state: onChange, onBlur, ref, value, error
-
add new api file: api-client/tag-api.ts GET: /api/tags?_page=1&_limit=30
-
new hook file: hooks/use-tag-list.ts
-
Populate data to AutocompleteField
- filter works by tags (either tag1 or tag2 or tag3):
GET /api/works?_page=1&_limit=10&
tagList_like=tag1|tag2|tag3
- transform form data into api payload
const formData = { search: '', selectedTagList: ['Design', 'Dashboard'] }
// 1. turn selectedTagList into tagList_like (array to string using join())
// 2. remove unused attr selectedTagList
const apiPayload = { search: '', tagList_like: 'Design|Dashboard' }
- set initial value for auto complete field
- Demo idea via behance.net
- Clone works/index.tsx page into works/infinity-scroll.tsx
- Remove pagination approach / change to new hook
- Implement new hook useWorkListInfinity() using useSWRInfinite() from SWR
- Add new lib: qs to parse query
Approach:
- Clone page
- Change data source by new hook
- Convert new data to workList
- Pass worklist to
- Handle load more
- handle
enabled
params in the hook - Convert new data to workList
- Pass worklist to
- Add simple load more
yarn add react-intersection-observer
import React from 'react';
import { useInView } from 'react-intersection-observer';
const Component = () => {
const { ref, inView, entry } = useInView({
/* Optional options */
threshold: 0,
});
return (
<div ref={ref}>
<h2>{`Header inside viewport ${inView}.`}</h2>
</div>
);
};
- Setup Page / Form
- Rich Text Editor control
- Form Validation
- Setup works/[workId].tsx - AddEditWorkPage
- Detect Add or Edit mode
- Add new hook: hooks/use-work-details.ts
- Integrate the new hook into AddEditWorkPage, make sure it only calls in edit mode
- Add new form: components/work/work-form.tsx
- Show form on Page
- Add title control:
- Add shortDescription control:
- Add tagList control:
- Validation array of string
- New form control:
<PhotoField />
- UI: preview image + upload file (hidden) + error message
- Data:
{ file: File, previewUrl: string }
- Logic:
- init show previewUrl || default placeholder
- onChange: update data
- Setup PhotoField and handle onChange
- Custom validation logic via yup.test()
- 1MB = 1024 * 1024 Bytes (MB -1024-> KB -1024-> BYTES)
- Limit upload file less than 3MB
- Conditional validation, required when add, optional when edit
- Build failed bugfix: photo-field.tsx value?.previewUrl --> value?.['previewUrl']
- React Quill Github
- Add new form control:
EditorField
- Fix issue that React Quill not able to render on server side via dynamic ssr: false
yarn add react-quill
-
- Toolbar: show / hide controls + custom handlers
- Keyboard: key binding in a context
- History: undo, redo
- Clipboard: copy / paste / cut between quill and other apps
- Syntax: highlight for code block
-
- Default to enable all
- Define a list of enable some only
- Allow to add custom format
const modules = {
toolbar: {
container: [
[{ 'header': [1, 2, false] }],
['bold', 'italic', 'underline','strike', 'blockquote'],
[{'list': 'ordered'}, {'list': 'bullet'}, {'indent': '-1'}, {'indent': '+1'}],
['link', 'image'],
['clean']
],
handlers: {
image: imageHandler,
}
},
}
const formats = [
'header',
'bold',
'italic',
'underline',
'strike',
'blockquote',
'list',
'bullet',
'indent',
'link',
'image',
]
Deltas or HTML?
Option 1: use HTML
- Init with HTML string from fullDescription
- onChange --> update HTML string
- render: use HTML string to render on UI
Option 2: use Deltas
- Init with Deltas object
- onChange --> get full deltas object and update to form state
- render: need to convert from Deltas to HTML first
--> prefer to go with option 1
if no special reason to use Deltas
.
Read more about React Quill Props
- why we need to get editorRef? --> to get current selection index to insert image
- can we just use ref directly? --> let's debug to see
getSelection()
: Returns the current selection range, or null if the editor is unfocused.
- Setup cloudinary - upload preset
- Import widget script from src: https://widget.cloudinary.com/v2.0/global/all.js
- Setup upload widget and save to ref
- When user click image --> open the widget from ref
- Handle image function: must be a static function, otherwise it may have error
- Get current selection:
quill.getEditorSelection()
- Insert image: https://quilljs.com/docs/api/#insertembed
- Intro to update work API
- API
work-api
: Implement work add / update API - hook
use-work-details
: Add update function useWorkDetails() hook - Form level
work-form
: Transform form values --> payload to submit API - Page level
[workId]
: handle form submission for update
- fix issue that cloudinary widget not ready
- root cause: init add form but cloudinary not ready
- solution: retry if failed (0.5 - 1s)
- hook
use-add-work
: Implement new hook useAddWork() hook - Form level: same with update
- Page level
[workId]
: handle form submission for add mode
- Allow to config at page-level: require login or not
- Update to receive new props: requireLogin
- Use replace instead of push for login redirect
- Only show Add button if user is logged in
- Fix blinking issue between add and edit mode
- Before redirect to login page, encode current path and attach to URL query params
back_to
- Once login successfully, extract
back_to
from query params --> redirect to it
Notes:
- Need to encode to avoid easily change
back_to
URL (btoa) - Need to escape special character on URL (encodeURIComponent)
const path = '/works?_page=1&_limit=3'
// encode
const base64 = window.btoa(path)
const safeStringOnURL = encodeURIComponent(base64)
// decode
const base64 = decodeURIComponent(safeStringOnURL)
const path = window.atob(base64)
- Add Edit Page: /works/:workId/index
- Details Page: /works/:workId/details
- Not required login to view
- Bind click event for work item (WorkCard.tsx)
- Fetch data on server
- Render rich text content (HTML string) from API
- Add new package:
yarn add sanitize-html
github - Use default options, but you can customize the way you want.
- Fix build failed due to type error of
@types/sanitize-html
ref- RUN
yarn add --dev typescript@latest
- RUN
yarn add --dev eslint@latest
- RUN
- Add edit button to details page, only show if logged in
- Add back_to param when click on Login item (header-desktop)