A personal portfolio website built with React and TypeScript.
This project focuses on building a maintainable UI architecture and practicing:
- component design & responsibility boundaries
- state vs derived state
- data-driven UI patterns
- responsive layout for real devices (tested down to 320px)
- Demo: https://stuartchendev.github.io/
- Repo: https://github.com/stuartchendev/stuartchendev.github.io
- React
- TypeScript
- CSS (responsive layout)
- Deployment: GitHub Pages
- Multi-language support (English / Traditional Chinese / Japanese)
- Projects list + project detail view (modal)
- Data-driven project structure (single source of truth)
- Reusable layout components (navigation / sections)
- Toolbar space reserved for future UI features (e.g. theme toggle)
I store only user-driven UI state (e.g. active language, active project id). Project content is treated as data, and feature components derive what they need.
This keeps the state model small and prevents duplicated or conflicting UI state.
I use activeProjectId to represent user selection.
The selected project is derived from this id.
Why not a boolean isOpen?
Because the core interaction is selecting a project entity, not toggling a UI panel.
The UI presentation (modal/drawer) is a rendering detail.
The overlay and modal are rendered as siblings instead of nesting the modal inside the overlay.
This avoids accidental dismissal and prevents relying on event propagation hacks.
- Desktop: 2-column project grid for comparison
- Mobile: 1-column layout for readability and tap targets (tested at 320px)
- Large screens: constrain content width to keep visual density and scanning comfortable
(Implemented in ProjectsPage component)
What
- I model fetching as an explicit async state machine:
idle → loading → success | error
Why
- Avoids scattered boolean flags (
isLoading,hasError, etc.) - Keeps UI predictable and makes retry behavior explicit
- Scales better when more async flows are added
UI strategy
- Treat
idleas a loading state so the page shows a consistent loading UI immediately. errorrenders a visible fallback message and aRetrybutton.Retryre-runs the same async task and re-enters the state machine (loading → success/error).
Implementation notes
- The async task is derived from
activeLanguageId, so it only re-fetches when language changes. - Guard against stale responses (race conditions) so only the latest request can update the state.
Trade-off
- Slightly more verbose than a single
isLoadingflag, but clearer and more maintainable for real-world async UI.
- Tag filtering is not implemented yet because the current project count is small. It becomes valuable when the list grows (see Future Improvements).
npm install
npm run devnpm run build
npm run preview-
Add tag filtering when the project list grows
-
Improve project detail preview (lighter content, richer media such as GIF/video)
-
Add theme toggle (dark mode)、back-to-top button
-
Accessibility pass (focus states / aria labels / contrast)


