Skip to content
This repository has been archived by the owner on Dec 18, 2024. It is now read-only.

Feature/resource page #49

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {
About,
Account,
Authenticated,
BookmarksPage,
Drafts,
Home,
NotFound,
Resource,
Suggest,
} from "./pages";

Expand All @@ -24,9 +26,13 @@ const App = () => (
<Route path="/drafts" element={<Authenticated adminOnly />}>
<Route index element={<Drafts />} />
</Route>
<Route path="/resource/:id" element={<Resource />} />
<Route path="/suggest" element={<Authenticated />}>
<Route index element={<Suggest />} />
</Route>
<Route path="/bookmarks" element={<Authenticated />}>
<Route index element={<BookmarksPage />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</article>
Expand Down
2 changes: 1 addition & 1 deletion client/src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
main > article {
padding: calc(#{layout.$baseline} * 4);

> h2 {
> h2 :last-child {
border-bottom: 2px solid palette.$red;
display: inline-block;
padding-bottom: calc(#{layout.$baseline} / 2);
Expand Down
43 changes: 43 additions & 0 deletions client/src/components/BookmarkFlag/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import PropTypes from "prop-types";

const BookmarkFlag = ({ color, stroke, onClick }) => {
return (
// Use a button for accessibility
<button
onClick={onClick}
style={{
background: "none",
border: "none",
padding: 0,
cursor: "pointer",
}}
// Add keyboard accessibility
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
onClick();
}
}}
// Ensure the button is focusable
tabIndex={0}
aria-label="Bookmark"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 30 30"
width="30px"
height="30px"
style={{ fill: color, stroke }}
>
<path d="M23,27l-8-7l-8,7V5c0-1.105,0.895-2,2-2h12c1.105,0,2,0.895,2,2V27z" />
</svg>
</button>
);
};

BookmarkFlag.propTypes = {
color: PropTypes.string,
stroke: PropTypes.string,
onClick: PropTypes.func.isRequired,
};

export default BookmarkFlag;
1 change: 1 addition & 0 deletions client/src/components/Header/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default function Header() {
<nav aria-label="site navigation">
<ul>
<li>{principal && <NavLink to="/suggest">Suggest</NavLink>}</li>
<li>{principal && <NavLink to="/bookmarks">Bookmarks</NavLink>}</li>
<li>
{principal?.is_admin && <NavLink to="/drafts">Drafts</NavLink>}
</li>
Expand Down
69 changes: 66 additions & 3 deletions client/src/components/ResourceList/index.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
import PropTypes from "prop-types";
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";

import { BookmarkService, useService } from "../../services";
import BookmarkFlag from "../BookmarkFlag";

import "./ResourceList.scss";

export default function ResourceList({ publish, resources }) {
export default function ResourceList({
publish,
resources,
bookmarkedResources,
setBookmarkedResources,
onBookmarkToggle,
}) {
const [bookmarkedResourceIds, setBookmarkedResourceIds] = useState({});
const bookmarkService = useService(BookmarkService);

useEffect(() => {
const ids = {};
bookmarkedResources.forEach((bookmark) => {

Check failure on line 22 in client/src/components/ResourceList/index.jsx

View workflow job for this annotation

GitHub Actions / build

src/components/ResourceList/ResourceList.test.jsx > ResourceList > shows a message if no resources are available

TypeError: Cannot read properties of undefined (reading 'forEach') ❯ src/components/ResourceList/index.jsx:22:23 ❯ commitHookEffectListMount ../node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber ../node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete ../node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin ../node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects ../node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl ../node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects ../node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ ../node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue ../node_modules/react/cjs/react.development.js:2667:24

Check failure on line 22 in client/src/components/ResourceList/index.jsx

View workflow job for this annotation

GitHub Actions / build

src/pages/Drafts/Drafts.test.jsx > Drafts > shows draft resources

TypeError: Cannot read properties of undefined (reading 'forEach') ❯ src/components/ResourceList/index.jsx:22:23 ❯ commitHookEffectListMount ../node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber ../node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete ../node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin ../node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects ../node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl ../node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects ../node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ ../node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue ../node_modules/react/cjs/react.development.js:2667:24

Check failure on line 22 in client/src/components/ResourceList/index.jsx

View workflow job for this annotation

GitHub Actions / build

src/pages/Drafts/Drafts.test.jsx > Drafts > lets those resources be published

TypeError: Cannot read properties of undefined (reading 'forEach') ❯ src/components/ResourceList/index.jsx:22:23 ❯ commitHookEffectListMount ../node_modules/react-dom/cjs/react-dom.development.js:23189:26 ❯ commitPassiveMountOnFiber ../node_modules/react-dom/cjs/react-dom.development.js:24970:11 ❯ commitPassiveMountEffects_complete ../node_modules/react-dom/cjs/react-dom.development.js:24930:9 ❯ commitPassiveMountEffects_begin ../node_modules/react-dom/cjs/react-dom.development.js:24917:7 ❯ commitPassiveMountEffects ../node_modules/react-dom/cjs/react-dom.development.js:24905:3 ❯ flushPassiveEffectsImpl ../node_modules/react-dom/cjs/react-dom.development.js:27078:3 ❯ flushPassiveEffects ../node_modules/react-dom/cjs/react-dom.development.js:27023:14 ❯ ../node_modules/react-dom/cjs/react-dom.development.js:26808:9 ❯ flushActQueue ../node_modules/react/cjs/react.development.js:2667:24
ids[bookmark.resource_id] = true;
});
setBookmarkedResourceIds(ids);
}, [bookmarkedResources]);

const handleToggleBookmark = async (resourceId) => {
try {
if (bookmarkedResourceIds[resourceId]) {
await bookmarkService.removeBookmark(resourceId);
setBookmarkedResourceIds((prev) => ({ ...prev, [resourceId]: false }));
setBookmarkedResources((prev) =>
prev.filter((bookmark) => bookmark.resource_id !== resourceId)
);
onBookmarkToggle(resourceId);
} else {
const newBookmark = await bookmarkService.addBookmark(resourceId);
setBookmarkedResourceIds((prev) => ({ ...prev, [resourceId]: true }));
setBookmarkedResources((prev) => [...prev, newBookmark]);
}
} catch (error) {
throw error("Error toggling bookmark:", error);
}
};

return (
<ul className="resource-list">
{resources.length === 0 && (
Expand All @@ -11,15 +52,30 @@
</li>
)}
{resources.map(({ description, id, title, topic_name, url }) => (
<li key={id}>
<li
key={id}
style={{
backgroundColor: bookmarkedResourceIds[id] ? "#E1D7C6" : "white",
border: "1px solid #333",
borderRadius: "4px",
padding: "16px",
}}
>
<div>
<h3>{title}</h3>
<h3>
<Link to={`/resource/${id}`}>{title}</Link>
</h3>
{topic_name && <span className="topic">{topic_name}</span>}
</div>
{description && <p className="resource-description">{description}</p>}
<div>
<a href={url}>{formatUrl(url)}</a>
{publish && <button onClick={() => publish(id)}>Publish</button>}
<BookmarkFlag
color={bookmarkedResourceIds[id] ? "black" : "white"}
stroke="black"
onClick={() => handleToggleBookmark(id)}
/>
</div>
</li>
))}
Expand All @@ -38,6 +94,13 @@
url: PropTypes.string.isRequired,
})
).isRequired,
bookmarkedResources: PropTypes.arrayOf(
PropTypes.shape({
resource_id: PropTypes.string.isRequired,
})
).isRequired,
setBookmarkedResources: PropTypes.func.isRequired,
onBookmarkToggle: PropTypes.func.isRequired,
};

function formatUrl(url) {
Expand Down
1 change: 1 addition & 0 deletions client/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { default as Form, FormControls } from "./Form";
export { default as Header } from "./Header";
export { default as Pagination } from "./Pagination";
export { default as ResourceList } from "./ResourceList";
export { default as BookmarkFlag } from "./BookmarkFlag";
4 changes: 3 additions & 1 deletion client/src/pages/About.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const About = () => (
<>
<h2>About</h2>
<h2>
<span>About</span>
</h2>
<p>Demonstration project for CYF Tech Products.</p>
<p>
Check out the code{" "}
Expand Down
4 changes: 3 additions & 1 deletion client/src/pages/Account/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export default function Account() {
}
return (
<>
<h2>Account</h2>
<h2>
<span>Account</span>
</h2>
<table>
<tbody>
<tr>
Expand Down
57 changes: 57 additions & 0 deletions client/src/pages/BookmarksPage/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useEffect, useState } from "react";

import { ResourceList } from "../../components";
import { useService } from "../../services";
import { BookmarkService, ResourceService } from "../../services";

const BookmarksPage = () => {
const [bookmarkedResourcesList, setBookmarkedResourcesList] = useState([]);
const [allBookmarks, setAllBookmarks] = useState([]);
const bookmarkService = useService(BookmarkService);
const resourceService = useService(ResourceService);

useEffect(() => {
const fetchData = async () => {
try {
const allResourcesEnvelope = await resourceService.getPublished();
const allResources = allResourcesEnvelope.resources;

const bookmarks = await bookmarkService.getBookmarks();
setAllBookmarks(bookmarks);

const bookmarkedResources = allResources.filter((resource) =>
bookmarks.some((bookmark) => bookmark.resource_id === resource.id)
);
setBookmarkedResourcesList(bookmarkedResources);
} catch (error) {
throw ("Error fetching bookmarks or resources:", error);
}
};

fetchData();
}, [bookmarkService, resourceService]);

const handleBookmarkToggle = (resourceId) => {
setBookmarkedResourcesList((prev) =>
prev.filter((resource) => resource.id !== resourceId)
);
};

return (
<section>
<h2>Your Bookmarked Resources</h2>
{bookmarkedResourcesList.length > 0 ? (
<ResourceList
resources={bookmarkedResourcesList}
bookmarkedResources={allBookmarks}
setBookmarkedResources={setAllBookmarks}
onBookmarkToggle={handleBookmarkToggle}
/>
) : (
<p>You haven&apos;t bookmarked any resources yet.</p>
)}
</section>
);
};

export default BookmarksPage;
27 changes: 23 additions & 4 deletions client/src/pages/Home/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,39 @@ import { useEffect, useState } from "react";

import { Pagination, ResourceList } from "../../components";
import { useSearchParams } from "../../hooks";
import { ResourceService, useService } from "../../services";
import { ResourceService, BookmarkService, useService } from "../../services";

export function Home() {
const resourceService = useService(ResourceService);
const bookmarkService = useService(BookmarkService);
const searchParams = useSearchParams();
const [{ lastPage, resources } = {}, setEnvelope] = useState();
const [bookmarkedResources, setBookmarkedResources] = useState([]);

useEffect(() => {
resourceService.getPublished(searchParams).then(setEnvelope);
}, [resourceService, searchParams]);
const fetchData = async () => {
try {
const publishedResources =
await resourceService.getPublished(searchParams);
setEnvelope(publishedResources);

const bookmarks = await bookmarkService.getBookmarks();
setBookmarkedResources(bookmarks);
} catch (error) {
return error;
}
};

fetchData();
}, [resourceService, bookmarkService, searchParams]);

return (
<section>
<ResourceList resources={resources ?? []} />
<ResourceList
resources={resources ?? []}
bookmarkedResources={bookmarkedResources ?? []}
setBookmarkedResources={setBookmarkedResources}
/>
<Pagination lastPage={lastPage ?? 1} />
</section>
);
Expand Down
4 changes: 3 additions & 1 deletion client/src/pages/NotFound.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export default function NotFound() {
return (
<>
<h2>Not Found</h2>
<h2>
<span>Not Found</span>
</h2>
<p>Sorry, we couldn&apos;t find what you&apos;re looking for.</p>
</>
);
Expand Down
14 changes: 14 additions & 0 deletions client/src/pages/Resource/Resource.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@use "../../styles/layout";

table.resource-details {
width: 100%;

tbody th {
text-align: end;

&:after {
content: ":";
margin-right: layout.$baseline;
}
}
}
Loading
Loading