├── .eslintrc.cjs
├── .gitignore
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── public
└── vite.svg
├── src
├── App.jsx
├── components
│ ├── Articles
│ │ ├── Articles.jsx
│ │ └── Articles.module.css
│ ├── BookmarkSummary
│ │ ├── BookmarkInformation.jsx
│ │ ├── BookmarkSummary.jsx
│ │ └── BookmarkSummary.module.css
│ ├── Header
│ │ ├── Header.jsx
│ │ └── Header.module.css
│ ├── InfoSidebar
│ │ ├── InfoSidebar.jsx
│ │ └── InfoSidebar.module.css
│ └── News
│ │ └── News.jsx
├── context
│ └── bookmark-context.jsx
├── data
│ └── dummy-articles.js
├── index.css
└── main.jsx
└── vite.config.js
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | "eslint:recommended",
6 | "plugin:react/recommended",
7 | "plugin:react/jsx-runtime",
8 | "plugin:react-hooks/recommended",
9 | ],
10 | ignorePatterns: ["dist", ".eslintrc.cjs"],
11 | parserOptions: { ecmaVersion: "latest", sourceType: "module" },
12 | settings: { react: { version: "18.2" } },
13 | plugins: ["react-refresh"],
14 | rules: {
15 | "react/prop-types": "off",
16 | "react-refresh/only-export-components": [
17 | "warn",
18 | { allowConstantExport: true },
19 | ],
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite --port 3000",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0",
15 | "react-icons": "^4.10.1"
16 | },
17 | "devDependencies": {
18 | "@types/react": "^18.2.15",
19 | "@types/react-dom": "^18.2.7",
20 | "@vitejs/plugin-react": "^4.0.3",
21 | "eslint": "^8.45.0",
22 | "eslint-plugin-react": "^7.32.2",
23 | "eslint-plugin-react-hooks": "^4.6.0",
24 | "eslint-plugin-react-refresh": "^0.4.3",
25 | "vite": "^4.4.5"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import Header from "./components/Header/Header";
2 | import News from "./components/News/News";
3 |
4 | function App() {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 | >
12 | );
13 | }
14 |
15 | export default App;
16 |
--------------------------------------------------------------------------------
/src/components/Articles/Articles.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import { FaBookmark, FaRegBookmark } from "react-icons/fa";
3 |
4 | import dummyArticles from "../../data/dummy-articles";
5 | import classes from "./Articles.module.css";
6 | import { useBookmarkContext } from "../../context/bookmark-context";
7 |
8 | function Articles() {
9 | const { bookmarkedArticles, bookmarkArticle, unbookmarkArticle } =
10 | useBookmarkContext();
11 | return (
12 |
13 | {dummyArticles.map((article) => {
14 | // will be true, if this article item is also included in the bookmarkedArticles array
15 | const isBookmarked = bookmarkedArticles.some(
16 | (bArticle) => bArticle.id === article.id
17 | );
18 |
19 | // default button action => bookmark article, because not bookmarked yet
20 | let buttonAction = () => bookmarkArticle(article);
21 | // default button icon: Empty bookmark icon, because not bookmarked
22 | let buttonIcon = ;
23 |
24 | if (isBookmarked) {
25 | buttonAction = () => unbookmarkArticle(article.id);
26 | buttonIcon = ;
27 | }
28 |
29 | return (
30 | -
31 |
{article.title}
32 | {article.description}
33 |
34 |
35 | );
36 | })}
37 |
38 | );
39 | }
40 |
41 | export default Articles;
42 |
--------------------------------------------------------------------------------
/src/components/Articles/Articles.module.css:
--------------------------------------------------------------------------------
1 | .list {
2 | margin: 3rem 0;
3 | }
4 |
5 | .list li {
6 | position: relative;
7 | margin: 1rem 0;
8 | padding: 1rem;
9 | background-color: #e3e3dd;
10 | border-radius: 8px;
11 | }
12 |
13 | .list li button {
14 | position: absolute;
15 | top: 0.5rem;
16 | right: 0.5rem;
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | width: 3rem;
21 | height: 3rem;
22 | font-size: 1.5rem;
23 | background-color: transparent;
24 | border: none;
25 | cursor: pointer;
26 | color: #3b2605;
27 | z-index: 10;
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/BookmarkSummary/BookmarkInformation.jsx:
--------------------------------------------------------------------------------
1 | import BookmarkSummary from './BookmarkSummary';
2 |
3 | function BookmarkInformation({ bookmarkedArticles }) {
4 | return (
5 |
11 | );
12 | }
13 |
14 | export default BookmarkInformation;
15 |
--------------------------------------------------------------------------------
/src/components/BookmarkSummary/BookmarkSummary.jsx:
--------------------------------------------------------------------------------
1 | import classes from "./BookmarkSummary.module.css";
2 | import { useBookmarkContext } from "../../context/bookmark-context";
3 |
4 | function BookmarkSummary() {
5 | const { bookmarkedArticles } = useBookmarkContext();
6 | const numberOfArticles = bookmarkedArticles.length;
7 |
8 | return (
9 | <>
10 | {numberOfArticles} articles bookmarked
11 |
12 | {bookmarkedArticles.map((article) => (
13 | - {article.title}
14 | ))}
15 |
16 | >
17 | );
18 | }
19 |
20 | export default BookmarkSummary;
21 |
--------------------------------------------------------------------------------
/src/components/BookmarkSummary/BookmarkSummary.module.css:
--------------------------------------------------------------------------------
1 | .summary {
2 | margin: 1rem 0;
3 | font-weight: bold;
4 | }
5 |
6 | .list li {
7 | margin: 0.5rem;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/Header/Header.jsx:
--------------------------------------------------------------------------------
1 | import classes from './Header.module.css';
2 |
3 | function Header() {
4 | return (
5 |
6 | Top React Articles
7 |
8 | );
9 | }
10 |
11 | export default Header;
12 |
--------------------------------------------------------------------------------
/src/components/Header/Header.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | width: 100%;
3 | height: 7rem;
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | background-color: #f3df91;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/InfoSidebar/InfoSidebar.jsx:
--------------------------------------------------------------------------------
1 | import BookmarkInformation from '../BookmarkSummary/BookmarkInformation';
2 | import classes from './InfoSidebar.module.css';
3 |
4 | function InfoSidebar({ bookmarkedArticles }) {
5 | return (
6 |
9 | );
10 | }
11 |
12 | export default InfoSidebar;
13 |
--------------------------------------------------------------------------------
/src/components/InfoSidebar/InfoSidebar.module.css:
--------------------------------------------------------------------------------
1 | .sidebar {
2 | position: fixed;
3 | top: 8rem;
4 | right: 0;
5 | width: 15rem;
6 | height: calc(100vh - 8rem);
7 | padding: 2rem;
8 | }
--------------------------------------------------------------------------------
/src/components/News/News.jsx:
--------------------------------------------------------------------------------
1 | import Articles from "../Articles/Articles";
2 | import InfoSidebar from "../InfoSidebar/InfoSidebar";
3 | import { BookmarkContextProvider } from "../../context/bookmark-context";
4 |
5 | function News() {
6 | return (
7 |
8 |
9 |
10 |
11 | );
12 | }
13 |
14 | export default News;
15 |
--------------------------------------------------------------------------------
/src/context/bookmark-context.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useState } from "react";
2 |
3 | export const BookmarkContext = createContext({
4 | bookmarkedArticles: [],
5 | bookmarkArticle: () => {},
6 | unbookmarkArticle: () => {},
7 | });
8 |
9 | export const BookmarkContextProvider = ({ children }) => {
10 | const [savedArticles, setSavedArticles] = useState([]);
11 |
12 | const addArticle = (article) =>
13 | setSavedArticles((prev) => [...prev, article]);
14 |
15 | const removeArticle = (articleId) => {
16 | return setSavedArticles((prev) =>
17 | prev.filter((article) => article.id !== articleId)
18 | );
19 | };
20 |
21 | return (
22 |
29 | {children}
30 |
31 | );
32 | };
33 |
34 | export const useBookmarkContext = () => {
35 | return useContext(BookmarkContext);
36 | };
37 |
--------------------------------------------------------------------------------
/src/data/dummy-articles.js:
--------------------------------------------------------------------------------
1 | const dummyArticles = [
2 | {
3 | id: "a1",
4 | title: "Learn React - From The Ground Up",
5 | description:
6 | "Learn all the key fundamentals and basics you need to know about React!",
7 | },
8 | {
9 | id: "a2",
10 | title: "JavaScript Foundation",
11 | description:
12 | "No React without JavaScript! This article discusses core fundamentals you must know.",
13 | },
14 | {
15 | id: "a3",
16 | title: "Is that all?",
17 | description: "The 10 best next steps after mastering React fundamentals.",
18 | },
19 | {
20 | id: "a4",
21 | title: "Beyond React",
22 | description:
23 | "The modern web uses React - everywhere! Really everywhere? Time to dive deeper!",
24 | },
25 | ];
26 |
27 | export default dummyArticles;
28 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | color: #242424;
9 | }
10 |
11 | main {
12 | max-width: 50rem;
13 | margin: auto;
14 | padding: 0 10px;
15 | display: grid;
16 | grid-template-columns: 80% 20%;
17 | }
18 |
19 | ul {
20 | list-style: none;
21 | margin: 0;
22 | padding: 0;
23 | }
24 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.jsx'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------