├── frontend ├── .npmrc ├── public │ ├── _redirects │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── .dockerignore ├── Dockerfile ├── src │ ├── components │ │ ├── Card │ │ │ └── Card.js │ │ ├── Chip │ │ │ └── Chip.js │ │ ├── ResourceSkeleton │ │ │ └── ResourceSkeleton.js │ │ ├── ResourceList │ │ │ └── ResourceList.js │ │ ├── ResourceSkeletonList │ │ │ └── ResourceSkeletonList.js │ │ ├── BaseModal │ │ │ └── BaseModal.js │ │ ├── Buttons │ │ │ ├── ClearBookmarksButton.js │ │ │ ├── GoToTopButton.js │ │ │ ├── BookmarkButton.js │ │ │ ├── RemoveBookmarkButton.js │ │ │ ├── LoadMoreResourcesButton.js │ │ │ └── BookmarkButtonsGroup.js │ │ ├── BookmarkGroupCard │ │ │ └── BookmarkGroupCard.js │ │ ├── Nav │ │ │ └── Nav.js │ │ ├── ConfirmModal │ │ │ └── ConfirmModal.js │ │ ├── NewBookmarkGroupModal │ │ │ └── NewBookmarkGroupModal.js │ │ ├── Resource │ │ │ └── Resource.js │ │ ├── EditBookmarkGroupModal │ │ │ └── EditBookmarkGroupModal.js │ │ ├── BookmarkGroupListModal │ │ │ └── BookmarkGroupListModal.js │ │ ├── Header │ │ │ └── Header.js │ │ ├── BookmarkGroupDetailsHeader │ │ │ └── BookmarkGroupDetailsHeader.js │ │ └── SearchForm │ │ │ └── SearchForm.js │ ├── stories │ │ ├── NotFound.stories.js │ │ ├── SearchForm.stories.js │ │ ├── NewBookmarkGroupModal.stories.js │ │ ├── BookmarkButtonsGroup.stories.js │ │ ├── EditBookmarkGroupModal.stories.js │ │ ├── Modals.stories.js │ │ ├── ConfirmModal.stories.js │ │ ├── ErrorFetchingResources.stories.js │ │ ├── BookmarkGroupCard.stories.js │ │ ├── Card.stories.js │ │ ├── BookmarkGroupListModal.stories.js │ │ └── BookmarkGroupDetailsHeader.stories.js │ ├── assets │ │ └── checkmark.svg │ ├── constants.js │ ├── index.js │ ├── App.js │ ├── pages │ │ ├── NotFound │ │ │ └── NotFound.js │ │ ├── ErrorFetchingResources │ │ │ └── ErrorFetchingResources.js │ │ ├── Bookmarks │ │ │ └── Bookmarks.js │ │ └── Resources │ │ │ └── Resources.js │ ├── AppContext.js │ ├── svgs.js │ └── App.css ├── .storybook │ ├── main.js │ └── preview.js ├── .gitignore ├── package.json ├── README2.md └── STYLEGUIDE.md ├── .dockerignore ├── backend ├── Procfile ├── .dockerignore ├── Dockerfile ├── src │ ├── docs │ │ ├── servers.js │ │ ├── tags.js │ │ ├── index.js │ │ ├── paths │ │ │ ├── index.js │ │ │ ├── topic.js │ │ │ ├── resource.js │ │ │ └── project.js │ │ └── swagger.js │ ├── constants.js │ ├── get-resources-from-database.js │ ├── utils.js │ ├── format-resources.js │ ├── firebase.js │ ├── database-config.js │ ├── server.js │ ├── routes │ │ ├── all.js │ │ ├── topics.js │ │ └── projects.js │ ├── update-resources-database.js │ ├── web-scrape-resources.js │ └── __tests__ │ │ └── getPageData.test.js ├── README.md ├── package.json └── .gitignore ├── .prettierignore ├── .vscode ├── extensions.json ├── settings.json └── coding-resource-finder.code-workspace ├── docs ├── screenshots │ ├── Screenshot (141).png │ ├── Screenshot (143).png │ ├── Screenshot (146).png │ ├── Screenshot (148).png │ ├── Screenshot (150).png │ ├── Screenshot (152).png │ ├── Screenshot (154).png │ └── Screenshot (156).png └── SCREENSHOTS.md ├── .deepsource.toml ├── docker-compose.yml ├── .prettierrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── pull_request_template.md └── workflows │ └── update-database.yml ├── LICENSE ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # node_modules 2 | **/node_modules -------------------------------------------------------------------------------- /backend/Procfile: -------------------------------------------------------------------------------- 1 | web: node ./src/server.js 2 | -------------------------------------------------------------------------------- /frontend/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | # node_modules 2 | node_modules -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | # node_modules 2 | node_modules -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add any files Prettier should not format 2 | 3 | backend 4 | docs 5 | .github -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ngoakor12/coding-resource-finder/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ngoakor12/coding-resource-finder/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ngoakor12/coding-resource-finder/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": ["./backend", "./frontend"], 3 | "editor.formatOnSave": true 4 | } 5 | -------------------------------------------------------------------------------- /docs/screenshots/Screenshot (141).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ngoakor12/coding-resource-finder/HEAD/docs/screenshots/Screenshot (141).png -------------------------------------------------------------------------------- /docs/screenshots/Screenshot (143).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ngoakor12/coding-resource-finder/HEAD/docs/screenshots/Screenshot (143).png -------------------------------------------------------------------------------- /docs/screenshots/Screenshot (146).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ngoakor12/coding-resource-finder/HEAD/docs/screenshots/Screenshot (146).png -------------------------------------------------------------------------------- /docs/screenshots/Screenshot (148).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ngoakor12/coding-resource-finder/HEAD/docs/screenshots/Screenshot (148).png -------------------------------------------------------------------------------- /docs/screenshots/Screenshot (150).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ngoakor12/coding-resource-finder/HEAD/docs/screenshots/Screenshot (150).png -------------------------------------------------------------------------------- /docs/screenshots/Screenshot (152).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ngoakor12/coding-resource-finder/HEAD/docs/screenshots/Screenshot (152).png -------------------------------------------------------------------------------- /docs/screenshots/Screenshot (154).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ngoakor12/coding-resource-finder/HEAD/docs/screenshots/Screenshot (154).png -------------------------------------------------------------------------------- /docs/screenshots/Screenshot (156).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ngoakor12/coding-resource-finder/HEAD/docs/screenshots/Screenshot (156).png -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "javascript" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | plugins = ["react"] -------------------------------------------------------------------------------- /.vscode/coding-resource-finder.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".." 5 | } 6 | ], 7 | "settings": {} 8 | } 9 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | WORKDIR /app/backend 4 | 5 | COPY . . 6 | 7 | RUN npm install 8 | 9 | EXPOSE 2856 10 | 11 | CMD ["npm", "run", "dev"] 12 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | WORKDIR /app/frontend 4 | 5 | COPY . . 6 | 7 | RUN npm install 8 | 9 | RUN npm run lint 10 | 11 | EXPOSE 3000 12 | 13 | CMD ["npm", "run", "start"] 14 | -------------------------------------------------------------------------------- /frontend/src/components/Card/Card.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Card({ children, fullWidth }) { 4 | return ( 5 |
{children}
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/docs/servers.js: -------------------------------------------------------------------------------- 1 | const { PORT } = require('./../constants'); 2 | 3 | module.exports = { 4 | servers: [ 5 | { 6 | url: `http://localhost:${PORT}`, 7 | description: 'Local server', 8 | }, 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /backend/src/docs/tags.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tags: [ 3 | { 4 | name: 'Projects', 5 | }, 6 | { 7 | name: 'Resources', 8 | }, 9 | { 10 | name: 'Topics', 11 | }, 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /frontend/src/components/Chip/Chip.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Chip({ title, isActive, onClick }) { 4 | return ( 5 |

6 | {title} 7 |

8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/constants.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | const ACN_URL = "https://syllabus.africacode.net/"; 4 | const PORT = process.env.PORT || 2856; 5 | const API_BASE_URL = process.env.PROD_BASE_URL || `http://localhost:${PORT}`; 6 | 7 | module.exports = { ACN_URL, PORT, API_BASE_URL }; 8 | -------------------------------------------------------------------------------- /backend/src/docs/index.js: -------------------------------------------------------------------------------- 1 | const servers = require('./servers'); 2 | const tags = require('./tags'); 3 | const swagger = require('./swagger'); 4 | const apis = require('./paths'); 5 | 6 | module.exports = { 7 | ...swagger, 8 | ...servers, 9 | ...tags, 10 | ...apis, 11 | }; 12 | -------------------------------------------------------------------------------- /backend/src/docs/paths/index.js: -------------------------------------------------------------------------------- 1 | const project = require('./project'); 2 | const resource = require('./resource'); 3 | const topic = require('./topic'); 4 | 5 | module.exports = { 6 | paths: { 7 | ...project, 8 | ...resource, 9 | ...topic, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/src/stories/NotFound.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import NotFound from "../pages/NotFound/NotFound"; 4 | 5 | export default { 6 | title: "Pages/NotFound", 7 | component: NotFound, 8 | }; 9 | const Template = (args) => ; 10 | 11 | export const NotFoundPage = Template.bind({}); 12 | -------------------------------------------------------------------------------- /frontend/src/assets/checkmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/stories/SearchForm.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import SearchForm from "../components/SearchForm/SearchForm"; 4 | 5 | export default { 6 | title: "Components/SearchForm", 7 | component: SearchForm, 8 | }; 9 | const Template = (args) => ; 10 | 11 | export const Search = Template.bind({}); 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | backend: 4 | build: ./backend 5 | ports: 6 | - "2856:2856" 7 | container_name: coding-resource-finder-backend 8 | frontend: 9 | depends_on: 10 | - backend 11 | build: ./frontend 12 | ports: 13 | - "3000:3000" 14 | container_name: coding-resource-finder-frontend -------------------------------------------------------------------------------- /frontend/src/components/ResourceSkeleton/ResourceSkeleton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function SkeletonResource() { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/stories/NewBookmarkGroupModal.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NewBookmarkGroupModal from "../components/NewBookmarkGroupModal/NewBookmarkGroupModal"; 3 | 4 | export default { 5 | title: "components/Modals/NewBookmarkGroupModal", 6 | component: NewBookmarkGroupModal, 7 | }; 8 | const Template = (args) => ; 9 | 10 | export const Primary = Template.bind({}); 11 | -------------------------------------------------------------------------------- /frontend/.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 3 | addons: [ 4 | "@storybook/addon-links", 5 | "@storybook/addon-essentials", 6 | "@storybook/addon-interactions", 7 | "@storybook/preset-create-react-app", 8 | ], 9 | framework: "@storybook/react", 10 | core: { 11 | builder: "@storybook/builder-webpack5", 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /frontend/src/components/ResourceList/ResourceList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { nanoid } from "nanoid"; 3 | 4 | import Resource from "../Resource/Resource"; 5 | 6 | export default function ResourceList({ resources }) { 7 | return ( 8 |
9 | {resources.map((resource) => ( 10 | 11 | ))} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/stories/BookmarkButtonsGroup.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BookmarkButtonsGroup from "../components/Buttons/BookmarkButtonsGroup"; 3 | 4 | export default { 5 | title: "components/BookmarkButtonsGroup", 6 | component: BookmarkButtonsGroup, 7 | }; 8 | 9 | const Template = (args) => ; 10 | 11 | export const Primary = Template.bind({}); 12 | 13 | Primary.args = {}; 14 | -------------------------------------------------------------------------------- /frontend/src/stories/EditBookmarkGroupModal.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import EditBookmarkGroupModal from "../components/EditBookmarkGroupModal/EditBookmarkGroupModal"; 3 | 4 | export default { 5 | title: "components/Modals/EditBookmarkGroupModal", 6 | component: EditBookmarkGroupModal, 7 | }; 8 | const Template = (args) => ; 9 | 10 | export const Primary = Template.bind({}); 11 | -------------------------------------------------------------------------------- /frontend/src/stories/Modals.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BaseModal from "../components/BaseModal/BaseModal"; 3 | 4 | export default { 5 | title: "components/Modals/BaseModal", 6 | component: BaseModal, 7 | }; 8 | const Template = (args) => ; 9 | 10 | export const Default = Template.bind({}); 11 | 12 | Default.args = { 13 | children: "Some text inside the body of the modal", 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env -------------------------------------------------------------------------------- /frontend/src/stories/ConfirmModal.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ConfirmModal from "../components/ConfirmModal/ConfirmModal"; 3 | 4 | export default { 5 | title: "components/Modals/ConfirmModal", 6 | component: ConfirmModal, 7 | }; 8 | const Template = (args) => ; 9 | 10 | export const Primary = Template.bind({}); 11 | 12 | Primary.args = { 13 | prompt: "Are you ready to rumble?", 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/src/stories/ErrorFetchingResources.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import ErrorFetchingResources from "../pages/ErrorFetchingResources/ErrorFetchingResources"; 4 | 5 | export default { 6 | title: "Pages/ErrorFetchingResources", 7 | component: ErrorFetchingResources, 8 | }; 9 | const Template = (args) => ; 10 | 11 | export const PageErrorFetchingResources = Template.bind({}); 12 | -------------------------------------------------------------------------------- /frontend/src/components/ResourceSkeletonList/ResourceSkeletonList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { nanoid } from "nanoid"; 3 | 4 | import ResourceSkeleton from "../ResourceSkeleton/ResourceSkeleton"; 5 | 6 | export default function ResourceSkeletonList() { 7 | return ( 8 |
9 | {[...Array(20)].map(() => ( 10 | 11 | ))} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/stories/BookmarkGroupCard.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BookmarkGroupCard from "../components/BookmarkGroupCard/BookmarkGroupCard"; 3 | 4 | export default { 5 | title: "components/BookmarkGroupCard", 6 | component: BookmarkGroupCard, 7 | }; 8 | 9 | export const Default = (args) => ; 10 | 11 | Default.args = { 12 | title: "A Visual History of Nobel Prize Winners", 13 | count: "10", 14 | bookmarkLink: "", 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/src/components/BaseModal/BaseModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function BaseModal({ heading, children }) { 4 | return ( 5 |
6 |
7 |
8 | {heading ? heading : "Heading"} 9 |
10 |
{children}
11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /docs/SCREENSHOTS.md: -------------------------------------------------------------------------------- 1 | ![Screenshot 1](./screenshots/Screenshot%20(141).png) 2 | 3 | ![Screenshot 2](./screenshots/Screenshot%20(143).png) 4 | 5 | ![Screenshot 3](./screenshots/Screenshot%20(146).png) 6 | 7 | ![Screenshot 4](./screenshots/Screenshot%20(148).png) 8 | 9 | ![Screenshot 5](./screenshots/Screenshot%20(150).png) 10 | 11 | ![Screenshot 6](./screenshots/Screenshot%20(152).png) 12 | 13 | ![Screenshot 7](./screenshots/Screenshot%20(154).png) 14 | 15 | ![Screenshot 8](./screenshots/Screenshot%20(156).png) -------------------------------------------------------------------------------- /backend/src/docs/swagger.js: -------------------------------------------------------------------------------- 1 | const paths = require('./paths'); 2 | 3 | module.exports = { 4 | openapi: '3.0.3', 5 | info: { 6 | title: `Coding Resource Finder API Documentation ( count: ${ 7 | Object.keys(paths.paths).length 8 | } )`, 9 | description: 'API Documentation for Coding Resource Finder', 10 | version: '1.0.0', 11 | contact: { 12 | name: 'Ngoako Ramokgopa', 13 | email: 'ngoakor12@gmail.com', 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /frontend/src/components/Buttons/ClearBookmarksButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useContext } from "react"; 3 | 4 | import { Context } from "../../AppContext"; 5 | 6 | export default function ClearBookmarksButton() { 7 | const { setBookmarks } = useContext(Context); 8 | 9 | function handleClick() { 10 | setBookmarks([]); 11 | } 12 | 13 | return ( 14 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/constants.js: -------------------------------------------------------------------------------- 1 | export const PORT = process.env.PORT || 2856; 2 | export const API_BASE_URL = 3 | process.env.REACT_APP_API_PROD_BASE_URL || `http://localhost:${PORT}`; 4 | export const CLIENT_BASE_URL = 5 | process.env.REACT_APP_CLIENT_PROD_BASE_URL || "http://localhost:3000"; 6 | 7 | export const ALL_RESOURCES_URL = `${API_BASE_URL}/all`; 8 | export const FIRST_PAGE_RESOURCES_URL = `${API_BASE_URL}/all/1`; 9 | 10 | export const ERROR = { 11 | FETCH: "Something went wrong. Please try again.", 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { BrowserRouter as Router } from "react-router-dom"; 4 | 5 | import App from "./App"; 6 | import { ContextProvider } from "./AppContext"; 7 | 8 | const container = document.getElementById("root"); 9 | const root = createRoot(container); 10 | root.render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: "always", 3 | bracketSameLine: false, 4 | bracketSpacing: true, 5 | embeddedLanguageFormatting: "auto", 6 | endOfLine: "lf", 7 | filepath: undefined, 8 | htmlWhitespaceSensitivity: "css", 9 | insertPragma: false, 10 | jsxSingleQuote: false, 11 | printWidth: 80, 12 | proseWrap: "preserve", 13 | quoteProps: "as-needed", 14 | rangeEnd: Infinity, 15 | requirePragma: false, 16 | semi: true, 17 | singleQuote: false, 18 | tabWidth: 2, 19 | trailingComma: "es5", 20 | useTabs: false, 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/src/components/Buttons/GoToTopButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { goToTopIcon } from "../../svgs"; 3 | 4 | export default function GoToTopButton() { 5 | function handleClick() { 6 | // can be set to auto if you want it to snap to top 7 | window.scrollTo({ top: 0, left: 0, behavior: "smooth" }); 8 | } 9 | 10 | return ( 11 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /frontend/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { ContextProvider } from "../src/AppContext"; 2 | import "../src/App.css"; 3 | import { BrowserRouter as Router } from "react-router-dom"; 4 | 5 | export const parameters = { 6 | actions: { argTypesRegex: "^on[A-Z].*" }, 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/, 11 | }, 12 | }, 13 | }; 14 | 15 | export const decorators = [ 16 | (Story) => ( 17 | 18 | 19 | 20 | 21 | 22 | ), 23 | ]; 24 | -------------------------------------------------------------------------------- /frontend/src/components/BookmarkGroupCard/BookmarkGroupCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import Card from "../Card/Card"; 5 | 6 | export default function BookmarkGroupCard(bookmarkGroup) { 7 | const { title, count, bookmarkLink } = bookmarkGroup; 8 | return ( 9 | 10 | 11 |

{title}

12 |

{count}

13 |
14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/stories/Card.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Card from "../components/Card/Card"; 3 | 4 | export default { 5 | title: "components/Card", 6 | component: Card, 7 | }; 8 | 9 | export const Default = (args) => ; 10 | 11 | Default.args = { 12 | children: "Dear God, it's me again...", 13 | fullWidth: false, 14 | }; 15 | 16 | export const FullWidthCard = (args) => ; 17 | 18 | FullWidthCard.args = { 19 | children: "Heal the world, make it a better place...", 20 | fullWidth: true, 21 | }; 22 | 23 | console.log(FullWidthCard.args); 24 | -------------------------------------------------------------------------------- /frontend/src/stories/BookmarkGroupListModal.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BookmarkGroupListModal from "../components/BookmarkGroupListModal/BookmarkGroupListModal"; 3 | 4 | export default { 5 | title: "components/Modals/BookmarkGroupListModal", 6 | component: BookmarkGroupListModal, 7 | }; 8 | 9 | const Template = (args) => ; 10 | 11 | export const Primary = Template.bind({}); 12 | 13 | Primary.args = { 14 | bookmarkGroupList: [ 15 | { id: "1", title: "Bookmarks" }, 16 | { id: "2", title: "Frontend revision and other things" }, 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/components/Buttons/BookmarkButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useContext } from "react"; 3 | 4 | import { Context } from "../../AppContext"; 5 | import { bookmarkIcon } from "../../svgs"; 6 | 7 | export default function RemoveBookmarkButton({ resource }) { 8 | const { addBookmark } = useContext(Context); 9 | 10 | function handleClick() { 11 | addBookmark(resource.url); 12 | } 13 | 14 | return ( 15 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/get-resources-from-database.js: -------------------------------------------------------------------------------- 1 | const { getCurrentCollectionName } = require("./utils"); 2 | 3 | async function getResourcesFromDB(database) { 4 | const collectionName = getCurrentCollectionName(); 5 | const resources = []; 6 | await database 7 | .collection(collectionName) 8 | .find({}, { projection: { _id: 0, type: 1, url: 1, title: 1 } }) 9 | .sort({ title: 1 }) 10 | .forEach((resource) => { 11 | resources.push(resource); 12 | }); 13 | const allResourcesData = { 14 | num_of_resources: resources.length, 15 | data: resources, 16 | }; 17 | return allResourcesData; 18 | } 19 | 20 | module.exports = { getResourcesFromDB }; 21 | -------------------------------------------------------------------------------- /frontend/src/components/Buttons/RemoveBookmarkButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useContext } from "react"; 3 | 4 | import { Context } from "../../AppContext"; 5 | import { removeBookmarkIcon } from "../../svgs"; 6 | 7 | export default function RemoveBookmarkButton({ resource }) { 8 | const { removeBookmark } = useContext(Context); 9 | 10 | function handleClick() { 11 | removeBookmark(resource.url); 12 | } 13 | 14 | return ( 15 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Coding resource finder api 2 | Express API for [coding-resource-finder](https://github.com/Ngoakor12/coding-resource-finder) 3 | 4 | ## Swagger api documentation 5 | http://localhost:2856/api/docs 6 | 7 | ## Api Details 8 | 9 | method | endpoint | params | description | return type 10 | --- | --- | --- | --- | --- 11 | GET | /all/projects | - | Get all projects | Object 12 | GET | /all/projects/{page} | Page number | Get a page of projects | Object 13 | GET | /all | - | Get all resources | Object 14 | GET | /all/{page} | Page number | Get a page of resources | Object 15 | GET | /all/topics | - | Get all topics | Object 16 | GET | /all/topics/{page} | Page number | Get a page of topics | Object -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Routes, Route, Navigate } from "react-router-dom"; 3 | 4 | import "./App.css"; 5 | import Bookmarks from "./pages/Bookmarks/Bookmarks"; 6 | import Resources from "./pages/Resources/Resources"; 7 | import NotFound from "./pages/NotFound/NotFound"; 8 | 9 | export default function App() { 10 | return ( 11 | 12 | } 16 | /> 17 | } /> 18 | } /> 19 | } /> 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | 17 | **Describe alternatives you've considered** 18 | 19 | 20 | 21 | **Additional context** 22 | 23 | 24 | -------------------------------------------------------------------------------- /frontend/src/pages/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | 4 | import { alertIcon } from "../../svgs"; 5 | 6 | export default function NotFound() { 7 | const navigate = useNavigate(); 8 | 9 | function navigateToHomepage() { 10 | navigate("/"); 11 | } 12 | return ( 13 |
14 |
15 | {alertIcon} 16 |

Page not found

17 |

18 | Check if the address you entered is correct and try again 19 |

20 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/stories/BookmarkGroupDetailsHeader.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BookmarkGroupDetailsHeader from "../components/BookmarkGroupDetailsHeader/BookmarkGroupDetailsHeader"; 3 | 4 | export default { 5 | title: "components/BookmarkGroupDetailsHeader", 6 | component: BookmarkGroupDetailsHeader, 7 | }; 8 | 9 | export const Default = (args) => ; 10 | 11 | Default.args = { 12 | heading: "All bookmarks", 13 | bookmarkGroups: [ 14 | { 15 | title: "All bookmarks", 16 | count: 10, 17 | link: "", 18 | }, 19 | { 20 | title: "Frontend revision", 21 | count: 4, 22 | link: "", 23 | }, 24 | { 25 | title: "React native", 26 | count: 7, 27 | link: "", 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /frontend/src/components/Nav/Nav.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useContext } from "react"; 3 | import { NavLink } from "react-router-dom"; 4 | 5 | import { Context } from "../../AppContext"; 6 | 7 | export default function Nav() { 8 | const { renderedResources, bookmarks } = useContext(Context); 9 | 10 | function className({ isActive }) { 11 | return isActive ? "nav-item active-nav" : "nav-item"; 12 | } 13 | 14 | return ( 15 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/components/ConfirmModal/ConfirmModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function ConfirmModal({ prompt }) { 4 | return ( 5 |
6 |
7 |
8 |
9 | {prompt ? prompt : "Are you sure?"} 10 |
11 |
12 | 15 | 18 |
19 |
20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/components/Buttons/LoadMoreResourcesButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useContext } from "react"; 3 | 4 | import { Context } from "../../AppContext"; 5 | 6 | export default function LoadMoreResourcesButton() { 7 | const { allResources, renderedResources, setRenderedResources } = 8 | useContext(Context); 9 | 10 | function handleClick() { 11 | setRenderedResources(allResources); 12 | } 13 | 14 | function handleDisableClick() { 15 | return renderedResources.length === allResources.length; 16 | } 17 | 18 | return ( 19 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Function to get the current collection name 3 | * @returns {String} A string including the current year and month (resources_year_month) 4 | */ 5 | 6 | function getCurrentCollectionName() { 7 | // return `resources_${new Date().getFullYear()}_${new Date().getMonth() + 1}`; 8 | return "resources_2024_4"; 9 | } 10 | 11 | /** 12 | * Function to check if an enter page is a number before or after trying to convert it into a number 13 | * @param page a number representing representing a page of resources 14 | * @returns {Boolean} true if page is a number and false if page can be converted into a number 15 | */ 16 | 17 | function isPageNumber(page) { 18 | const pageNumber = Number(page); 19 | if (pageNumber && typeof pageNumber === "number") return true; 20 | return false; 21 | } 22 | 23 | module.exports = { getCurrentCollectionName, isPageNumber }; 24 | -------------------------------------------------------------------------------- /backend/src/format-resources.js: -------------------------------------------------------------------------------- 1 | function getPages(resources) { 2 | const numOfResources = resources.length; 3 | const numOfResourcesPerPage = 20; 4 | const pages = []; 5 | const numOfPages = Math.ceil(numOfResources / numOfResourcesPerPage); 6 | let start = 0, 7 | end = numOfResourcesPerPage; 8 | 9 | for (let i = 1; i <= numOfPages; i++) { 10 | pages.push({ 11 | current_page: i, 12 | num_of_pages: numOfPages, 13 | num_of_resources: resources.slice(start, end).length, 14 | data: resources.slice(start, end), 15 | }); 16 | start += numOfResourcesPerPage; 17 | end += numOfResourcesPerPage; 18 | } 19 | return pages; 20 | } 21 | 22 | function getPageData(resources, page) { 23 | const pages = getPages(resources); 24 | const pageData = pages[Number(page) - 1]; 25 | return pageData; 26 | } 27 | 28 | module.exports = { 29 | getPageData, 30 | }; 31 | -------------------------------------------------------------------------------- /frontend/src/components/Buttons/BookmarkButtonsGroup.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { expandIcon } from "../../svgs"; 3 | import { Context } from "../../AppContext"; 4 | import { bookmarkIcon } from "../../svgs"; 5 | 6 | export default function BookmarkButtonsGroup() { 7 | const { addBookmark } = useContext(Context); 8 | 9 | function handleClick() { 10 | addBookmark(resource.url); 11 | } 12 | return ( 13 |
14 | 22 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/firebase.js: -------------------------------------------------------------------------------- 1 | const { initializeApp } = require("firebase/app"); 2 | const { 3 | getFirestore, 4 | collection, 5 | getDocs, 6 | addDoc, 7 | query, 8 | orderBy, 9 | } = require("firebase/firestore"); 10 | 11 | const { getCurrentCollectionName } = require("./utils"); 12 | 13 | const firebaseConfig = { 14 | apiKey: "AIzaSyC8suQigoEopXl19pXRGrGSUWAAprv9-mg", 15 | authDomain: "coding-resource-finder.firebaseapp.com", 16 | projectId: "coding-resource-finder", 17 | storageBucket: "coding-resource-finder.appspot.com", 18 | messagingSenderId: "186484380448", 19 | appId: "1:186484380448:web:0d8db73fca5005ffea7d45", 20 | }; 21 | 22 | // init firebase 23 | const app = initializeApp(firebaseConfig); 24 | 25 | // init db 26 | const db = getFirestore(app); 27 | 28 | const resourcesRef = collection(db, getCurrentCollectionName()); 29 | 30 | const resourcesQuery = query(resourcesRef, orderBy("title", "asc")); 31 | 32 | module.exports = { db, resourcesRef, getDocs, addDoc, resourcesQuery }; 33 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 🛠️ Fixes issue(s) 4 | 5 | 6 | 7 | 8 | 9 | ## 👨‍💻 Change(s) proposed 10 | 11 | 12 | 13 | ## ✔️ Checklist (Check all the applicable boxes) 14 | 15 | 16 | 20 | 21 | - [ ] My code follows the code style of this project. 22 | - [ ] This PR does not contain plagiarized content. 23 | - [ ] The title of my pull request is a short description of the requested changes. 24 | 25 | ## 📄 Note to reviewers 26 | 27 | 28 | 29 | ## 📷 Screenshots 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **To Reproduce** 14 | 19 | 20 | **Expected behavior** 21 | 22 | 23 | **Screenshots** 24 | 25 | 26 | **Desktop (please complete the following information):** 27 | 30 | 31 | **Smartphone (please complete the following information):** 32 | 36 | 37 | **Additional context** 38 | 39 | -------------------------------------------------------------------------------- /frontend/src/components/NewBookmarkGroupModal/NewBookmarkGroupModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BaseModal from "../BaseModal/BaseModal"; 3 | 4 | export default function NewBookmarkGroupModal() { 5 | function handleSubmit(e) { 6 | e.preventDefault(); 7 | } 8 | return ( 9 | 10 |
11 |
12 |
13 | 14 | 15 |
16 |
17 |
18 | 21 | 24 |
25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/Resource/Resource.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useContext } from "react"; 3 | 4 | import { Context } from "../../AppContext"; 5 | import RemoveBookmarkButton from "../Buttons/RemoveBookmarkButton"; 6 | import BookmarkButton from "../Buttons/BookmarkButton"; 7 | 8 | export default function Resource({ resource }) { 9 | const { bookmarks } = useContext(Context); 10 | const isBookmarked = bookmarks.find((bookmark) => { 11 | return bookmark.url === resource.url; 12 | }); 13 | const icon = isBookmarked ? ( 14 | 15 | ) : ( 16 | 17 | ); 18 | 19 | return ( 20 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/database-config.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { MongoClient, ServerApiVersion } = require("mongodb"); 3 | 4 | // for local development 5 | const localUri = 6 | "mongodb+srv://local:local@cluster0.7khoml9.mongodb.net/coding-resource-finder?retryWrites=true&w=majority"; 7 | 8 | const uri = process.env.MONGO_URI || localUri; 9 | 10 | const client = new MongoClient(uri, { 11 | serverApi: { 12 | version: ServerApiVersion.v1, 13 | strict: true, 14 | deprecationErrors: true, 15 | }, 16 | }); 17 | 18 | const connectToDb = async () => { 19 | console.log("---In connectToDb---"); 20 | try { 21 | const database = client.db(); 22 | return database; 23 | } catch (err) { 24 | console.error("Error connecting to the database:", err); 25 | throw err; // Rethrow the error to handle it in the caller. 26 | } 27 | }; 28 | 29 | const closeDb = (client) => { 30 | console.log("---In closeDb---"); 31 | if (client) { 32 | client.close(); 33 | console.log("Database connection closed."); 34 | } 35 | }; 36 | 37 | module.exports = { connectToDb, closeDb }; 38 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coding-resource-finder", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "nodemon ./src/server.js", 8 | "start": "node ./src/server.js", 9 | "update": "node ./src/update-resources-database.js", 10 | "deploy": "git push heroku master", 11 | "test": "jest" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Ngoakor12/coding-resource-finder.git" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/Ngoakor12/coding-resource-finder/issues" 22 | }, 23 | "homepage": "https://github.com/Ngoakor12/coding-resource-finder#readme", 24 | "dependencies": { 25 | "cheerio": "^1.0.0-rc.12", 26 | "dotenv": "^16.0.0", 27 | "express": "^4.17.1", 28 | "firebase": "^9.6.10", 29 | "mongodb": "^4.12.1", 30 | "swagger-ui-express": "^4.5.0" 31 | }, 32 | "devDependencies": { 33 | "jest": "^29.2.0", 34 | "cors": "^2.8.5", 35 | "nodemon": "^2.0.15" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/components/EditBookmarkGroupModal/EditBookmarkGroupModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BaseModal from "../BaseModal/BaseModal"; 3 | 4 | export default function EditBookmarkGroupModal() { 5 | function handleSubmit(e) { 6 | e.preventDefault(); 7 | } 8 | 9 | return ( 10 | 11 |
12 |
13 |
14 | 15 | 16 |
17 |
18 |
19 | 22 | 25 |
26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/pages/ErrorFetchingResources/ErrorFetchingResources.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { alertIcon } from "../../svgs"; 4 | 5 | export default function ErrorFetchingResources() { 6 | 7 | function navigateToHomepage() { 8 | 9 | window.location.href = "/"; 10 | } 11 | 12 | return ( 13 |
14 | {alertIcon} 15 |

16 | Something went wrong while fetching resources 17 |

18 | 19 |
20 |

If in development mode:

21 | 22 |
    23 |
  • Make sure the development server is running
  • 24 |
  • {"Check that the address you're trying to fetch from exists"}
  • 25 |
26 | 27 |

Otherwise:

28 |
    29 |
  • Try again
  • 30 |
31 |
32 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ngoako Ramokgopa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/src/server.js: -------------------------------------------------------------------------------- 1 | const app = require("express")(); 2 | const cors = require("cors"); 3 | 4 | const { API_BASE_URL, PORT } = require("./constants"); 5 | const allRoutes = require("./routes/all"); 6 | const topicsRoutes = require("./routes/topics"); 7 | const projectsRoutes = require("./routes/projects"); 8 | 9 | app.use( 10 | cors({ 11 | origin: "*", 12 | }) 13 | ); 14 | 15 | app.get("/", (_, res) => { 16 | res.status(200).json({ 17 | resources: `${API_BASE_URL}/all`, 18 | topics: `${API_BASE_URL}/all/topics`, 19 | projects: `${API_BASE_URL}/all/projects`, 20 | resources_page: `${API_BASE_URL}/all/{page}`, 21 | topics_page: `${API_BASE_URL}/all/topics/{page}`, 22 | projects_page: `${API_BASE_URL}/all/projects/{page}`, 23 | }); 24 | }); 25 | 26 | app.use("/all/topics", topicsRoutes); 27 | app.use("/all/projects", projectsRoutes); 28 | app.use("/all", allRoutes); 29 | 30 | // swagger 31 | const swaggerUi = require("swagger-ui-express"); 32 | const docs = require("./docs"); 33 | app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(docs)); 34 | 35 | app.listen(PORT, () => { 36 | console.log(`App running at ${API_BASE_URL}`); 37 | }); 38 | -------------------------------------------------------------------------------- /.github/workflows/update-database.yml: -------------------------------------------------------------------------------- 1 | name: update-database 2 | on: 3 | schedule: 4 | - cron: "0 00 1 * *" 5 | workflow_dispatch: 6 | 7 | jobs: 8 | update-database-local-dev: 9 | runs-on: ubuntu-latest 10 | 11 | defaults: 12 | run: 13 | working-directory: ./backend 14 | 15 | steps: 16 | - name: Check out repo 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: "latest" 23 | 24 | - name: Clean install dependencies 25 | run: npm ci 26 | 27 | - name: Run update script 28 | run: npm run update 29 | 30 | update-database-production: 31 | runs-on: ubuntu-latest 32 | env: 33 | MONGO_URI: ${{secrets.MONGO_URI}} 34 | 35 | defaults: 36 | run: 37 | working-directory: ./backend 38 | 39 | steps: 40 | - name: Check out repo 41 | uses: actions/checkout@v3 42 | 43 | - name: Set up Node 44 | uses: actions/setup-node@v3 45 | with: 46 | node-version: "latest" 47 | 48 | - name: Clean install dependencies 49 | run: npm ci 50 | 51 | - name: Run update script 52 | if: env.MONGO_URI != '' 53 | run: npm run update 54 | 55 | -------------------------------------------------------------------------------- /frontend/src/pages/Bookmarks/Bookmarks.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useContext } from "react"; 2 | 3 | import { Context } from "../../AppContext"; 4 | import ClearBookmarksButton from "../../components/Buttons/ClearBookmarksButton"; 5 | import ResourceList from "../../components/ResourceList/ResourceList"; 6 | import GoToTopButton from "../../components/Buttons/GoToTopButton"; 7 | import Nav from "../../components/Nav/Nav"; 8 | import Header from "../../components/Header/Header"; 9 | 10 | export default function Bookmarks() { 11 | const { bookmarks, setPageTitle } = useContext(Context); 12 | 13 | useEffect(() => { 14 | setPageTitle("Bookmarks | Coding Resource Finder"); 15 | // eslint-disable-next-line 16 | }, []); 17 | 18 | return ( 19 | 20 | 21 |
22 |
23 | 26 |
27 |
28 | {bookmarks.length ? ( 29 | 30 | 31 |
32 | 33 |
34 |
35 | ) : ( 36 |

No bookmarks yet...

37 | )} 38 |
39 |
40 |
41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /backend/src/routes/all.js: -------------------------------------------------------------------------------- 1 | const allRouter = require("express").Router(); 2 | const { getPageData } = require("../format-resources"); 3 | const { getResourcesFromDB } = require("../get-resources-from-database"); 4 | const { connectToDb, closeDb } = require("../database-config"); 5 | 6 | let dbClient; // Global variable to store the MongoDB client. 7 | 8 | // Connect to the database when application/router starts. 9 | (async () => { 10 | try { 11 | dbClient = await connectToDb(); 12 | } catch (error) { 13 | console.error("Error connecting to the database:", error); 14 | } 15 | })(); 16 | 17 | // Middleware to ensure the database client is available in route handlers. 18 | const withDb = (req, res, next) => { 19 | req.dbClient = dbClient; 20 | next(); 21 | }; 22 | 23 | allRouter.get("/", withDb, async (req, res) => { 24 | try { 25 | const db = req.dbClient; 26 | const resources = await getResourcesFromDB(db); 27 | res.status(200).json(resources); 28 | } catch (error) { 29 | res.json(error); 30 | } 31 | }); 32 | 33 | allRouter.get("/:page", withDb, async (req, res) => { 34 | try { 35 | const db = req.dbClient; 36 | const resources = await getResourcesFromDB(db); 37 | const data = getPageData(resources.data, req.params.page); 38 | res.status(200).json(data); 39 | } catch (error) { 40 | res.json(error); 41 | } 42 | }); 43 | 44 | // Close the database connection when application exits. 45 | process.on("exit", () => { 46 | if (dbClient) { 47 | closeDb(dbClient); 48 | } 49 | }); 50 | 51 | module.exports = allRouter; 52 | -------------------------------------------------------------------------------- /frontend/src/components/BookmarkGroupListModal/BookmarkGroupListModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function BookmarkGroupListModal({ bookmarkGroupList }) { 4 | function handleSubmit(e) { 5 | e.preventDefault(); 6 | } 7 | 8 | return ( 9 |
10 |
Bookmark groups
11 |
12 |
13 |
14 | {bookmarkGroupList.map((bookmarkGroup) => { 15 | return ( 16 |
17 | 22 | 25 |
26 | ); 27 | })} 28 |
29 |
30 | 33 | 36 |
37 |
38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { CLIENT_BASE_URL } from "../../constants"; 4 | 5 | export default function Header() { 6 | return ( 7 |
8 |

9 | 10 | Coding Resource Finder 11 | 12 |

13 |
14 |

15 | An easier way to find coding related topics and projects on the{" "} 16 | 22 | ACN syllabus 23 | 24 |

25 |
26 |

27 | Created by{" "} 28 | 34 | Ngoako 35 | 36 | . Source code on{" "} 37 | 43 | Github 44 | 45 | . 46 |

47 |
48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/components/BookmarkGroupDetailsHeader/BookmarkGroupDetailsHeader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { backArrowIcon } from "../../svgs"; 3 | import { Link } from "react-router-dom"; 4 | 5 | export default function BookmarkGroupDetailsHeader({ 6 | heading, 7 | bookmarkGroups, 8 | }) { 9 | return ( 10 |
11 |
12 |
13 | 14 | {backArrowIcon} 15 | 16 |

{heading}

17 |
18 |
19 | 20 | 21 | 24 |
25 |
26 |
27 | {bookmarkGroups.map((group) => { 28 | return ( 29 | 36 | {`${group.title}(${group.count})`} 37 | 38 | ); 39 | })} 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Coding Resource Finder 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /backend/src/routes/topics.js: -------------------------------------------------------------------------------- 1 | const topicsRouter = require("express").Router(); 2 | const { getPageData } = require("../format-resources"); 3 | const { getResourcesFromDB } = require("../get-resources-from-database"); 4 | const { connectToDb, closeDb } = require("../database-config"); 5 | 6 | let dbClient; // Global variable to store the MongoDB client. 7 | 8 | // Connect to the database when application/router starts. 9 | (async () => { 10 | try { 11 | dbClient = await connectToDb(); 12 | } catch (error) { 13 | console.error("Error connecting to the database:", error); 14 | } 15 | })(); 16 | 17 | // Middleware to ensure the database client is available in route handlers. 18 | const withDb = (req, res, next) => { 19 | req.dbClient = dbClient; 20 | next(); 21 | }; 22 | 23 | topicsRouter.get("/", withDb, async (req, res) => { 24 | try { 25 | const db = req.dbClient; 26 | const resources = await getResourcesFromDB(db); 27 | const topics = resources.data.filter( 28 | (resource) => resource.type === "topic" 29 | ); 30 | const topicsData = { num_of_topics: topics.length, data: topics }; 31 | res.status(200).json(topicsData); 32 | } catch (error) { 33 | res.json(error); 34 | } 35 | }); 36 | 37 | topicsRouter.get("/:page", withDb, async (req, res) => { 38 | try { 39 | const db = req.dbClient; 40 | const resources = await getResourcesFromDB(db); 41 | const topics = resources.data.filter( 42 | (resource) => resource.type === "topic" 43 | ); 44 | const topicsData = { num_of_topics: topics.length, data: topics }; 45 | const data = getPageData(topicsData.data, req.params.page); 46 | res.status(200).json(data); 47 | } catch (error) { 48 | res.json(error); 49 | } 50 | }); 51 | 52 | // Close the database connection when application exits. 53 | process.on("exit", () => { 54 | if (dbClient) { 55 | closeDb(dbClient); 56 | } 57 | }); 58 | 59 | module.exports = topicsRouter; 60 | -------------------------------------------------------------------------------- /backend/src/routes/projects.js: -------------------------------------------------------------------------------- 1 | const projectsRouter = require("express").Router(); 2 | const { getPageData } = require("../format-resources"); 3 | const { getResourcesFromDB } = require("../get-resources-from-database"); 4 | const { connectToDb, closeDb } = require("../database-config"); 5 | 6 | let dbClient; // Global variable to store the MongoDB client. 7 | 8 | // Connect to the database when application/router starts. 9 | (async () => { 10 | try { 11 | dbClient = await connectToDb(); 12 | } catch (error) { 13 | console.error("Error connecting to the database:", error); 14 | } 15 | })(); 16 | 17 | // Middleware to ensure the database client is available in route handlers. 18 | const withDb = (req, res, next) => { 19 | req.dbClient = dbClient; 20 | next(); 21 | }; 22 | 23 | projectsRouter.get("/", withDb, async (req, res) => { 24 | try { 25 | const db = req.dbClient; 26 | const resources = await getResourcesFromDB(db); 27 | const projects = resources.data.filter( 28 | (resource) => resource.type === "project" 29 | ); 30 | const projectsData = { num_of_projects: projects.length, data: projects }; 31 | res.status(200).json(projectsData); 32 | } catch (error) { 33 | res.json(error); 34 | } 35 | }); 36 | 37 | projectsRouter.get("/:page", withDb, async (req, res) => { 38 | try { 39 | const db = req.dbClient; 40 | const resources = await getResourcesFromDB(db); 41 | const projects = resources.data.filter( 42 | (resource) => resource.type === "project" 43 | ); 44 | const projectsData = { num_of_projects: projects.length, data: projects }; 45 | const data = getPageData(projectsData.data, req.params.page); 46 | res.status(200).json(data); 47 | } catch (error) { 48 | res.json(error); 49 | } 50 | }); 51 | 52 | // Close the database connection when application exits. 53 | process.on("exit", () => { 54 | if (dbClient) { 55 | closeDb(dbClient); 56 | } 57 | }); 58 | 59 | module.exports = projectsRouter; 60 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /backend/src/update-resources-database.js: -------------------------------------------------------------------------------- 1 | const { connectToDb, closeDb } = require("./database-config"); 2 | const { getCurrentCollectionName } = require("./utils"); 3 | const { getAllResources } = require("./web-scrape-resources"); 4 | 5 | /** 6 | * A function that adds resources to the database 7 | * @param {Object} database representing the database connection/instance 8 | * @param {Array} resources 9 | * @returns {void} 10 | */ 11 | 12 | async function updateResources(database, resources = []) { 13 | console.log("---In updateResources---"); 14 | console.log(`Format all resources. Number of resources: ${resources.length}`); 15 | const formattedResources = resources.map((resource) => ({ 16 | title: resource.title, 17 | url: resource.url, 18 | type: resource.type, 19 | groups: [], 20 | })); 21 | console.log( 22 | `Successfully formatted all resources. Number of resources: ${formattedResources.length}` 23 | ); 24 | console.log("Run getCurrentCollectionName"); 25 | const collectionName = getCurrentCollectionName(); 26 | console.log(`Successfully got collection name: ${collectionName}`); 27 | 28 | try { 29 | console.log("Run insertMany on collection"); 30 | await database.collection(collectionName).insertMany(formattedResources); 31 | console.log("Successfully ran insertMany"); 32 | } catch (error) { 33 | console.log(`Error running insertMany: ${error}`); 34 | } 35 | } 36 | 37 | async function main() { 38 | console.log("---In main---"); 39 | 40 | let client; 41 | 42 | try { 43 | console.log("Connect to db"); 44 | client = await connectToDb(); 45 | const db = client.db(); 46 | console.log("Successfully connected to db"); 47 | 48 | console.log("Run getAllResources"); 49 | const resources = await getAllResources(); 50 | console.log("Successfully ran getAllResources"); 51 | console.log( 52 | `Run updateResources. Number of resources: ${resources.length}` 53 | ); 54 | await updateResources(db, resources); 55 | console.log("Successfully ran updateResources"); 56 | } catch (error) { 57 | console.log(`Error: ${error}`); 58 | } finally { 59 | closeDb(client); 60 | // prevent node from hanging 61 | process.exit(0); 62 | } 63 | } 64 | 65 | main(); 66 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coding-resource-finder", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/preset-env": "^7.19.4", 7 | "esprima": "^4.0.1", 8 | "nanoid": "^4.0.0", 9 | "react": "^18.2.0", 10 | "react-dom": "^18.2.0", 11 | "react-router-dom": "^6.8.0", 12 | "react-scripts": "^5.0.0" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject", 19 | "lint": "eslint '**/*.js' --fix & prettier --write .", 20 | "storybook": "start-storybook -p 6006 -s public", 21 | "build-storybook": "build-storybook -s public" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ], 28 | "overrides": [ 29 | { 30 | "files": [ 31 | "**/*.stories.*" 32 | ], 33 | "rules": { 34 | "import/no-anonymous-default-export": "off" 35 | } 36 | } 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "@babel/core": "^7.19.3", 53 | "@babel/eslint-parser": "^7.19.1", 54 | "@babel/preset-react": "^7.18.6", 55 | "@storybook/addon-actions": "^6.5.12", 56 | "@storybook/addon-essentials": "^6.5.12", 57 | "@storybook/addon-interactions": "^6.5.12", 58 | "@storybook/addon-links": "^6.5.12", 59 | "@storybook/builder-webpack5": "^6.5.12", 60 | "@storybook/manager-webpack5": "^6.5.12", 61 | "@storybook/node-logger": "^6.5.12", 62 | "@storybook/preset-create-react-app": "^4.1.2", 63 | "@storybook/react": "^6.5.12", 64 | "@storybook/testing-library": "^0.0.13", 65 | "babel-eslint": "^10.1.0", 66 | "babel-plugin-named-exports-order": "^0.0.2", 67 | "eslint": "^8.25.0", 68 | "eslint-config-prettier": "^8.5.0", 69 | "eslint-plugin-html": "^7.1.0", 70 | "eslint-plugin-react": "^7.31.10", 71 | "prettier": "2.7.1", 72 | "prop-types": "^15.8.1", 73 | "webpack": "^5.74.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /backend/src/web-scrape-resources.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const cheerio = require("cheerio"); 3 | 4 | const { ACN_URL } = require("./constants"); 5 | 6 | // use node's core http API to reduce overhead 7 | /** 8 | * Fetches HTML document from {ACN_URL} - http://syllabus.africacode.net/ 9 | * 10 | * @returns {Promise} 11 | */ 12 | function getHTML() { 13 | return new Promise((resolve, reject) => { 14 | try { 15 | http.get(ACN_URL, (res) => { 16 | const { statusCode } = res; 17 | 18 | if (statusCode => 400) { 19 | throw new Error( 20 | `Request failed\nReceived status code ${statusCode}\nExpected status code 200` 21 | ); 22 | } 23 | 24 | let html = ""; 25 | res.on("data", (chunk) => (html += chunk)); 26 | res.on("end", () => resolve(html)); 27 | }); 28 | } catch (e) { 29 | console.error(e.message); 30 | reject(new Error(`Failed to fetch HTML document from ${ACN_URL}`)); 31 | } 32 | }); 33 | } 34 | 35 | /** 36 | * @typedef {Object} Resource 37 | * @property {String} title - Resource title 38 | * @property {String} url - Resource URL 39 | * @property {String} type - Resource type 40 | */ 41 | 42 | /** 43 | * Returns all resources categorized by "topic" or "project" 44 | * 45 | * @returns {Resource[]} - Array of resources 46 | */ 47 | async function getAllResources() { 48 | let html = null; 49 | try { 50 | html = await getHTML(); 51 | } catch (e) { 52 | console.error(e.message); 53 | return []; 54 | } 55 | 56 | const $ = cheerio.load(html); 57 | 58 | const resources = []; 59 | const resourceTypes = ["Topics", "Projects"]; 60 | 61 | resourceTypes.forEach((resourceType) => { 62 | const resourceElements = $("#sidebar").find( 63 | `li[title=${resourceType}] ul li a` 64 | ); 65 | resourceElements.each(function () { 66 | resources.push({ 67 | title: $(this).text().replace(/\n/g, "").trim(), 68 | // remove the preceding `/` from `href` 69 | url: `${ACN_URL}${$(this).attr("href").slice(1)}`, 70 | // Assuming the `resourceType` is a singular lowercased version of itself 71 | type: resourceType.toLowerCase().slice(0, -1), 72 | }); 73 | }); 74 | }); 75 | 76 | return resources; 77 | } 78 | 79 | module.exports = { 80 | getAllResources, 81 | }; 82 | -------------------------------------------------------------------------------- /frontend/src/pages/Resources/Resources.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react"; 2 | 3 | import { Context } from "../../AppContext"; 4 | import SearchForm from "../../components/SearchForm/SearchForm"; 5 | import ResourceList from "../../components/ResourceList/ResourceList"; 6 | import ResourceSkeletonList from "../../components/ResourceSkeletonList/ResourceSkeletonList"; 7 | import LoadMoreResourcesButton from "../../components/Buttons/LoadMoreResourcesButton"; 8 | import GoToTopButton from "../../components/Buttons/GoToTopButton"; 9 | import Nav from "../../components/Nav/Nav"; 10 | import Header from "../../components/Header/Header"; 11 | import ErrorFetchingResources from "../ErrorFetchingResources/ErrorFetchingResources"; 12 | 13 | export default function Resources() { 14 | const { 15 | setPageTitle, 16 | renderedResources, 17 | searchTerm, 18 | hasFetchError, 19 | resourceFilter, 20 | } = useContext(Context); 21 | 22 | const filteredResources = 23 | resourceFilter === "all" ? ( 24 | 25 | ) : ( 26 | resource.type === resourceFilter 29 | )} 30 | /> 31 | ); 32 | 33 | useEffect(() => { 34 | setPageTitle("Resources | Coding Resource Finder"); 35 | // eslint-disable-next-line 36 | }, []); 37 | 38 | return ( 39 | 40 | {hasFetchError ? ( 41 | 42 | ) : ( 43 |
44 |
45 |
46 | 49 |
50 |
51 | 52 | {renderedResources && renderedResources.length ? ( 53 |
54 | {filteredResources} 55 | 56 |
57 | ) : searchTerm ? ( 58 |

59 | Resource(s) not found... 60 |

61 | ) : ( 62 | 63 | )} 64 |
65 |
66 |
67 | 68 |
69 | )} 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /frontend/README2.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /frontend/src/AppContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useEffect, useState } from "react"; 2 | 3 | import { ALL_RESOURCES_URL, FIRST_PAGE_RESOURCES_URL } from "./constants"; 4 | 5 | export const Context = createContext(); 6 | 7 | export function ContextProvider({ children }) { 8 | const [allResources, setAllResources] = useState([]); 9 | // eslint-disable-next-line 10 | const [bookmarks, setBookmarks] = useState(() => { 11 | const saved = localStorage.getItem("bookmarks"); 12 | const initialValue = JSON.parse(saved); 13 | return initialValue || []; 14 | }); 15 | const [searchTerm, setSearchTerm] = useState(""); 16 | const [pageTitle, setPageTitle] = useState("Coding Resource Finder"); 17 | const [renderedResources, setRenderedResources] = useState([]); 18 | const [hasFetchError, setHasFetchError] = useState(false); 19 | const [resourceFilter, setResourceFilter] = useState("all"); 20 | 21 | useEffect(() => { 22 | localStorage.setItem("bookmarks", JSON.stringify(bookmarks)); 23 | }, [bookmarks]); 24 | 25 | useEffect(() => { 26 | document.title = pageTitle; 27 | }, [pageTitle]); 28 | 29 | useEffect(() => { 30 | getAndSetInitialResources(); 31 | // eslint-disable-next-line 32 | }, []); 33 | 34 | async function getAndSetInitialResources() { 35 | const responseData = await Promise.all([ 36 | getFirstPageOfResources(FIRST_PAGE_RESOURCES_URL), 37 | getAllResources(ALL_RESOURCES_URL), 38 | ]); 39 | const [firstPageResourcesResponse, allResourcesResponse] = responseData; 40 | setRenderedResources(firstPageResourcesResponse); 41 | setAllResources(allResourcesResponse); 42 | } 43 | 44 | async function getAllResources(url) { 45 | try { 46 | const response = await fetch(url); 47 | const responseData = await response.json(); 48 | const allResources = await responseData.data; 49 | return allResources; 50 | } catch (error) { 51 | setHasFetchError(true); 52 | console.log(error); 53 | } 54 | } 55 | 56 | async function getFirstPageOfResources(url) { 57 | try { 58 | const response = await fetch(url); 59 | const responseData = await response.json(); 60 | const firstPageResources = await responseData.data; 61 | return firstPageResources; 62 | } catch (error) { 63 | setHasFetchError(true); 64 | console.log(error); 65 | } 66 | } 67 | 68 | function addBookmark(resourceUrl) { 69 | const newBookmark = allResources.find( 70 | (resource) => resource.url === resourceUrl 71 | ); 72 | setBookmarks((prevBookmarks) => [...prevBookmarks, newBookmark]); 73 | localStorage.setItem("bookmarks", JSON.stringify(bookmarks)); 74 | } 75 | 76 | function removeBookmark(bookmarkUrl) { 77 | const newBookmarks = bookmarks.filter( 78 | (bookmark) => bookmark.url !== bookmarkUrl 79 | ); 80 | setBookmarks(newBookmarks); 81 | localStorage.setItem("bookmarks", JSON.stringify(bookmarks)); 82 | } 83 | 84 | return ( 85 | 104 | {children} 105 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /frontend/src/svgs.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const removeBookmarkIcon = ( 4 | 16 | ); 17 | export const bookmarkIcon = ( 18 | 30 | ); 31 | 32 | export const expandIcon = ( 33 | 49 | ); 50 | export const clearSearchIcon = ( 51 | 64 | ); 65 | 66 | export const goToTopIcon = ( 67 | 73 | 77 | 78 | ); 79 | 80 | export const alertIcon = ( 81 | 88 | 92 | 93 | ); 94 | 95 | export const backArrowIcon = ( 96 | 102 | 106 | 107 | ); 108 | -------------------------------------------------------------------------------- /frontend/STYLEGUIDE.md: -------------------------------------------------------------------------------- 1 | # Style Guide 2 | 3 | As an open-source codebase grows, different developers with differing approaches to problem-solving, software design and opinions will converge and attempt to push changes. When this happens, a universal guide for programming style will be needed to ensure a pattern is followed for easier-to-understand code. That is the purpose of this style guide covering the front-end code. 4 | 5 | ### Contents 6 | 7 | - [React/JavaScript](#1) 8 | - [Basic Rules](#1.1) 9 | - [Naming](#1.2) 10 | - [Tags](#1.3) 11 | - [Spacing](#1.4) 12 | - [Quotes](#1.5) 13 | - [Editor Setup](#2) 14 | - [Install Prettier & ESLint](#2.1) 15 | - [Set up Prettier](#2.2) 16 | - [Set up ESLint](#2.3) 17 | 18 |

React/JavaScript

19 |

Basic Rules

20 | 21 | - Follow the best practices contained in [Thinking in React](https://beta.reactjs.org/learn/thinking-in-react). 22 | - Always use JSX syntax. 23 | - Avoid classes and use Hooks to the best possible extent. 24 | - All components should be inside a **descriptive** folder. If a component falls under an **existing** category, e.g Buttons, place its .js file in there. If an existing category doesn't exist, but you feel you can create one, create a descriptive category and use it in naming the folder. If it is impossible to create a category, name the folder the same as the component, e.g if you need to build a Tooltip.js component, you can place it in a folder named Tooltips. 25 | - All styles should be defined in `App.css`. 26 | - SVGs are treated as reusable components. All SVGs are & should be placed in `src/svgs.js.` 27 | - Abstract component **logic** into Context as much as possible. 28 | - Use `` syntax instead of `<>` when creating fragments. Doing otherwise might result in an error depending on how your workspace is set up if you use VSCode. We're working on getting rid of this restriction. 29 | - Use ES6 syntax to the best of your ability. 30 | - State the export type when defining the function and not after. 31 | 32 | ```javascript 33 | // don't do this 34 | function LiftWeights(){ 35 | // do stuff 36 | } 37 | export default LiftWeights 38 | 39 | 40 | // do this instead 41 | export default function SleepAllDay(){ 42 | // do stuff 43 | } 44 | ``` 45 | 46 | - Always use camelCase for prop names. 47 | - Omit the value of the prop when it is explicitly true. There's no need for `primary={true}`, simply use `primary` 48 | 49 |

Naming

50 | 51 | - **File extensions**: Use the .js extension for React components and any utility functions. 52 | - **Component file names**: Component file/folder names should follow the same PascalCase format as React components. Component/folder names should be as descriptive as possible. Avoid loose names like "AsyncComponent" that could encompass a variety of components. Instead, use specific names like ResourceDetailsMenu.js. Don't be afraid to use relatively long names within a reasonable limit (~22 chars). 53 | 54 |

Quotes

55 | 56 | Always use double quotes ("). 57 | 58 | ```javascript 59 | // bad 👎🏾 60 | 61 | 62 | // good 👍🏾 63 | 64 | ``` 65 | 66 |

Spacing

67 | 68 | - Always include a **single** space in your self-closing tag. 69 | 70 | ```javascript 71 | // bad 👎🏾 72 | 73 | 74 | // also bad 👎🏾 75 | 76 | 77 | // please don't do this 😑 78 | 80 | 81 | // good 👍🏾 82 | 83 | ``` 84 | 85 | - Do not pad JSX curly braces with spaces. 86 | 87 | ```javascript 88 | // bad 👎🏾 89 | 90 | 91 | // good 👍🏾 92 | 93 | ``` 94 | 95 |

Tags

96 | 97 | Always self-close tags that have no children. 98 | 99 | ```javascript 100 | // bad 👎🏾 101 | 102 | 103 | // good 👍🏾 104 |