diff --git a/web/src/pages/UserPage.tsx b/web/src/pages/UserPage.tsx
index 89348c35..9a550607 100644
--- a/web/src/pages/UserPage.tsx
+++ b/web/src/pages/UserPage.tsx
@@ -1,9 +1,70 @@
-/**
- * Placeholder for the ported `UserComponent`, implemented in Phase 2d.
- * The user id comes from the `/user/:id` route via `useParams`.
- */
+import { useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+
+import { fetchUser } from '../api/hackerNews';
+import { ErrorMessage } from '../components/ErrorMessage';
+import { Loader } from '../components/Loader';
+import { User } from '../models/user';
+import '../user/user.scss';
+
export function UserPage() {
- return null;
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const [user, setUser] = useState
+
+ Profile: {user.id}
+ Created {user.created} Y Combinatorindented
',
+};
+
+function renderUserPage(entries: string[] = ['/user/pg'], initialIndex = 0) {
+ return render(
+
Y Combinator
indented'); + }); + + it('omits the about section for a user without an about text', async () => { + fetchUserMock.mockResolvedValue({ id: 'lurker', created: '2 days ago', karma: 1 }); + + const { container } = renderUserPage(['/user/lurker']); + + await screen.findByText('Created 2 days ago'); + + expect(container.querySelector('.other-details')).toBeNull(); + }); + + it('shows an error message when the user could not be loaded', async () => { + fetchUserMock.mockRejectedValue(new Error('offline')); + + const { container } = renderUserPage(['/user/ghost']); + + expect(await screen.findByText('Could not load user ghost.')).toBeInTheDocument(); + expect(container.querySelector('.profile')).toBeNull(); + expect(container.querySelector('.loading-section')).toBeNull(); + }); + + it('goes back in the history when the back button is clicked', async () => { + fetchUserMock.mockResolvedValue(user); + + const { container } = renderUserPage(['/news/1', '/user/pg'], 1); + + await screen.findByText('Created 4230 days ago'); + + await userEvent.click(container.querySelector('.back-button') as HTMLElement); + + await waitFor(() => expect(screen.getByText('news feed')).toBeInTheDocument()); + }); +}); diff --git a/web/src/user/user.scss b/web/src/user/user.scss new file mode 100644 index 00000000..0288468e --- /dev/null +++ b/web/src/user/user.scss @@ -0,0 +1,89 @@ +@import '../styles/media'; +@import '../styles/theme_variables'; + +.profile pre { + white-space: pre-wrap; +} + +.profile { + padding: 30px; +} + +@media #{$mobile-only} { + .profile { + padding: 110px 15px 0 15px; + } + .title-block { + font-size: 15px; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + margin: 0 75px; + } + .back-button { + position: absolute; + top: 52%; + width: 0.6rem; + height: 0.6rem; + background: transparent; + box-shadow: 0 0 0 lightgray; + transition: all 200ms ease; + left: 4%; + transform: translate3d(0, -50%, 0) rotate(-135deg); + } + .item-header { + padding-bottom: 10px; + background-color: #fff; + padding: 10px 0 10px 0; + position: fixed; + width: 100%; + left: 0; + top: 62px; + height: 20px; + } +} + +@media #{$laptop-only} { + .mobile { + display: none; + } +} + +.main-details { + .name { + font-weight: bold; + font-size: 32px; + letter-spacing: 2px; + } + .age { + font-weight: bold; + color: #696969; + padding-bottom: 0; + } + .right { + float: right; + font-weight: bold; + font-size: 32px; + letter-spacing: 2px; + } +} + +@media #{$mobile-only} { + .main-details { + margin-top: 20px; + .name { + font-size: 18px; + } + } +} + +@media #{$mobile-only} { + .main-details .right { + font-size: 18px; + } +} + +.other-details { + word-wrap: break-word; +}