├── babel.config.js
├── public
├── favicon.ico
├── manifest.json
└── index.html
├── src
├── react-app-env.d.ts
├── components
│ ├── NavigationItem
│ │ ├── NavigationItem.css
│ │ ├── NavigationItem.tsx
│ │ └── SubMenu
│ │ │ └── SubMenu.js
│ ├── Post
│ │ ├── Post.css
│ │ ├── Post.test.tsx
│ │ ├── Post.tsx
│ │ └── SinglePost.tsx
│ ├── WPPage
│ │ └── index.tsx
│ └── Auth
│ │ ├── Signout.js
│ │ ├── SignIn.js
│ │ └── SignUp.js
├── setupTests.js
├── axios-wp.ts
├── store
│ ├── actions
│ │ ├── index.actions.ts
│ │ ├── pages.actions.ts
│ │ ├── users.actions.ts
│ │ └── posts.actions.ts
│ ├── store.ts
│ └── slices
│ │ ├── pages.slice.ts
│ │ ├── posts.slice.ts
│ │ └── users.slice.ts
├── index.css
├── App.css
├── commons
│ └── constants.ts
├── types
│ ├── pages.types.ts
│ ├── posts.types.ts
│ ├── users.types.ts
│ └── wptypes.ts
├── index.tsx
├── containers
│ ├── Navigation
│ │ └── Navigation.tsx
│ ├── Dashboard
│ │ └── Dashboard.js
│ ├── Blog
│ │ └── Blog.tsx
│ └── Auth
│ │ └── Auth.js
├── hooks
│ ├── useWPPages
│ │ └── index.tsx
│ └── useNavMenu
│ │ └── index.tsx
├── logo.svg
└── App.tsx
├── .github
└── workflows
│ └── test.yaml
├── .gitignore
├── test-preprocessor.js
├── craco.config.js
├── README.md
├── tsconfig.json
├── CHANGELOG.md
├── .eslintrc.js
├── .snyk
└── package.json
/babel.config.js:
--------------------------------------------------------------------------------
1 | {
2 | presets: ["@babel/preset-env", "@babel/preset-react"];
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kvnam/reactpress/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare module "*";
--------------------------------------------------------------------------------
/src/components/NavigationItem/NavigationItem.css:
--------------------------------------------------------------------------------
1 | .nav-item:not(:last-child) {
2 | margin-right: 2rem;
3 | }
4 |
5 | .submenu {
6 | flex-direction: row-reverse;
7 | }
8 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | const Enzyme = require("enzyme");
2 | const Adapter = require("enzyme-adapter-react-16");
3 |
4 | Enzyme.configure({
5 | adapter: new Adapter(),
6 | });
7 |
--------------------------------------------------------------------------------
/src/axios-wp.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const axiosInstance = axios.create({
4 | baseURL: process.env.REACT_APP_WORDPRESS_URL,
5 | });
6 |
7 | export default axiosInstance;
8 |
--------------------------------------------------------------------------------
/src/components/Post/Post.css:
--------------------------------------------------------------------------------
1 | .excerpt-img {
2 | width: 100%;
3 | }
4 |
5 | .post-img {
6 | width: 100%;
7 | margin: 1rem 0;
8 | }
9 |
10 | .cat-tags {
11 | display: inline;
12 | }
13 | .cat-tags:not(:first-child) {
14 | margin-left: 1rem;
15 | }
16 |
--------------------------------------------------------------------------------
/src/store/actions/index.actions.ts:
--------------------------------------------------------------------------------
1 | export { loadAllPosts, loadSinglePost, searchAllPosts } from "./posts.actions";
2 |
3 | export { userSignin, userSignup, validateToken, userSignout } from "./users.actions";
4 |
5 | export { getAllPages } from "./pages.actions";
6 |
--------------------------------------------------------------------------------
/src/store/store.ts:
--------------------------------------------------------------------------------
1 | import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
2 | import { AppDispatch, RootState } from "@/index";
3 |
4 | export const useRPDispatch = () => useDispatch();
5 | export const useRPSelector: TypedUseSelectorHook = useSelector;
6 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: test-reactpress
2 | on: [pull_request]
3 |
4 | jobs:
5 | test:
6 | name: Test ReactPress
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v2
11 | - uses: actions/setup-node@v2
12 | - run: yarn install --frozen-lockfile
13 | - run: yarn test
14 |
--------------------------------------------------------------------------------
/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 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
24 | reactpress.todo
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
5 | "Droid Sans", "Helvetica Neue", sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/NavigationItem/NavigationItem.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NavLink } from "react-router-dom";
3 | import { NavItem } from "reactstrap";
4 |
5 | type NavItemProps = {
6 | link: string;
7 | linkName: string;
8 | };
9 |
10 | const navigationItem = (props: NavItemProps) => {
11 | const { link, linkName } = props;
12 | return (
13 |
14 | {linkName}
15 |
16 | );
17 | };
18 |
19 | export default navigationItem;
20 |
--------------------------------------------------------------------------------
/test-preprocessor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Transpiles TypeScript to JavaScript code.
3 | *
4 | * @link https://github.com/facebook/jest/blob/master/examples/typescript/preprocessor.js
5 | * @copyright 2004-present Facebook. All Rights Reserved.
6 | */
7 | const tsc = require("typescript");
8 | const tsConfig = require("./tsconfig.json");
9 |
10 | module.exports = {
11 | process(src, path) {
12 | if (path.endsWith(".ts") || path.endsWith(".tsx")) {
13 | return tsc.transpile(src, tsConfig.compilerOptions, path, []);
14 | }
15 | return src;
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: left;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 40vmin;
8 | }
9 |
10 | .App-header {
11 | background-color: #282c34;
12 | min-height: 100vh;
13 | display: flex;
14 | flex-direction: column;
15 | align-items: center;
16 | justify-content: center;
17 | font-size: calc(10px + 2vmin);
18 | color: white;
19 | }
20 |
21 | .App-link {
22 | color: #61dafb;
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from {
27 | transform: rotate(0deg);
28 | }
29 |
30 | to {
31 | transform: rotate(360deg);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/NavigationItem/SubMenu/SubMenu.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NavLink } from "react-router-dom";
3 | import { Nav, NavItem } from "reactstrap";
4 |
5 | const sidebar = (props) => {
6 |
7 | return(
8 |
19 | );
20 | };
21 |
22 | export default sidebar;
--------------------------------------------------------------------------------
/src/commons/constants.ts:
--------------------------------------------------------------------------------
1 | import { NavItemType } from "../types/pages.types";
2 |
3 | export const baseNavItems: NavItemType[] = [
4 | {
5 | link: "/",
6 | linkName: "BLOG",
7 | isVisible: "all",
8 | component: "Blog",
9 | },
10 | {
11 | link: "/auth/signin",
12 | linkName: "LOG IN",
13 | isVisible: "noauth",
14 | },
15 | {
16 | link: "/auth/signup",
17 | linkName: "SIGN UP",
18 | isVisible: "noauth",
19 | },
20 | {
21 | link: "/post",
22 | linkName: "Post",
23 | isVisible: "all",
24 | component: "SinglePost",
25 | },
26 | ];
27 |
28 | export const APP_NAME = "ReatPress";
29 |
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | typescript: {
5 | enableTypeChecking: true,
6 | },
7 | babel: {
8 | plugins: ["babel-plugin-styled-components"],
9 | },
10 | webpack: {
11 | alias: {
12 | "@": path.resolve(__dirname, "src/"),
13 | "@components": path.resolve(__dirname, "src/components"),
14 | "@containers": path.resolve(__dirname, "src/containers"),
15 | "@commons": path.resolve(__dirname, "src/commons"),
16 | "@hooks": path.resolve(__dirname, "src/hooks"),
17 | "@store": path.resolve(__dirname, "src/store"),
18 | "@rptypes": path.resolve(__dirname, "src/types"),
19 | },
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/Post/Post.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow, ShallowWrapper } from "enzyme";
3 | import { Col } from "reactstrap";
4 |
5 | import Post from "./Post";
6 |
7 | describe("", () => {
8 | test("Render 4 Cols and title - Test Post", () => {
9 | const wrapper = shallow(
10 | {}} />,
11 | );
12 | const colElements: ShallowWrapper = wrapper.find(Col);
13 | const titleCol = colElements.first().props().children[0];
14 | expect(colElements).toHaveLength(4);
15 | expect(titleCol).toEqual("Test Post");
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/components/WPPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | // TODO: Add classname and other props from WP meta tags
4 | // Set up each page
5 |
6 | type WPPageProps = {
7 | children?: React.ReactNode;
8 | pageClassName?: string;
9 | };
10 |
11 | const WPPage: React.FC = (props: WPPageProps) => {
12 | const { children, pageClassName } = props;
13 | return {children}
;
14 | };
15 |
16 | WPPage.defaultProps = {
17 | children: null,
18 | pageClassName: "",
19 | };
20 |
21 | WPPage.propTypes = {
22 | children: PropTypes.node,
23 | pageClassName: PropTypes.string,
24 | };
25 |
26 | export default WPPage;
27 |
--------------------------------------------------------------------------------
/src/types/pages.types.ts:
--------------------------------------------------------------------------------
1 | import type { WPPage } from "./wptypes";
2 |
3 | export const GET_ALL_PAGES = "GET_ALL_PAGES";
4 |
5 | export type NavItemType = {
6 | link: string;
7 | linkName: string;
8 | isVisible: string;
9 | component?: string;
10 | };
11 |
12 | export type AllPagesType = {
13 | pagesLoading: boolean;
14 | pages?: WPPage[] | any | null;
15 | error?: Error | any | null;
16 | };
17 |
18 | export type RPPage = {
19 | [key: string]: WPPage;
20 | };
21 |
22 | export type RPPagesHookType = {
23 | pagesLoading?: boolean;
24 | pages: RPPage;
25 | };
26 |
27 | export type GETALLPAGESACTION = {
28 | pagesLoading?: boolean;
29 | pages?: WPPage[] | any | null;
30 | error?: Error | any | null;
31 | };
32 |
--------------------------------------------------------------------------------
/src/store/actions/pages.actions.ts:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk } from "@reduxjs/toolkit";
2 | import axios from "@/axios-wp";
3 |
4 | import type { WPPage } from "@rptypes/wptypes";
5 |
6 | export const getAllPages = createAsyncThunk("pages/getAllPages", async () => {
7 | const pagesRes = await axios.get("wp/v2/pages");
8 | if (pagesRes) {
9 | const finalPages: WPPage[] = [];
10 | pagesRes.data.forEach((page: WPPage) => {
11 | // TODO: Add processing of page
12 | finalPages.push(page);
13 | });
14 | return { pages: finalPages, pagesLoading: false };
15 | }
16 | return { pages: [], pagesLoading: false };
17 | });
18 |
19 | export const getPage = () => {
20 | return { type: "GET_PAGE", page: {}, pageLoading: false };
21 | };
22 |
--------------------------------------------------------------------------------
/src/store/slices/pages.slice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | import type { RootState } from "@/index";
4 | import { GETALLPAGESACTION } from "@rptypes/pages.types";
5 | import { getAllPages } from "@store/actions/index.actions";
6 |
7 | const initialState = {
8 | pages: null,
9 | pagesLoading: false,
10 | };
11 |
12 | export const pagesSlice = createSlice({
13 | name: "pages",
14 | initialState,
15 | reducers: {},
16 | extraReducers: (builder) => {
17 | builder.addCase(getAllPages.fulfilled, (state, action: PayloadAction) => {
18 | state.pages = action.payload.pages;
19 | state.pagesLoading = false;
20 | });
21 | },
22 | });
23 |
24 | export default pagesSlice.reducer;
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ReactPress
2 |
3 | ## Under development
4 |
5 | [](https://www.codacy.com/gh/kvnam/reactpress/dashboard?utm_source=github.com&utm_medium=referral&utm_content=kvnam/reactpress&utm_campaign=Badge_Grade)
6 |
7 | Wordpress API delivered through a React application.
8 |
9 | The app has the following objectives
10 |
11 | **All Users**
12 |
13 | - List all posts
14 | - Search for specific posts
15 | - Filter posts by
16 | - Categories
17 | - Tags
18 | - Date
19 | - Display pages from Wordpress
20 |
21 | **Registered Users**
22 |
23 | - Sign Up / Sign In - Authentication added with JWT-Authentication plugin
24 | - Create a post
25 | - Retrieve self-authored posts
26 | - Delete self-authored posts
27 |
28 | License
29 | This project is licensed under the MIT License - see the LICENSE.md file for details
30 |
--------------------------------------------------------------------------------
/src/types/posts.types.ts:
--------------------------------------------------------------------------------
1 | import { WPPost, WPCategory } from "./wptypes";
2 |
3 | export const LOADING_ALL_POSTS = "LOADING_ALL_POSTS";
4 | export const GET_ALL_POSTS = "GET_ALL_POSTS";
5 | export const SINGLE_POST_ACTION = "SINGLE_POST_ACTION";
6 | export const SEARCH_POSTS_ACTION = "SEARCH_POSTS_ACTION";
7 |
8 | export interface RPPost extends WPPost {
9 | medialink?: string;
10 | categoryTags?: WPCategory[];
11 | }
12 |
13 | export type LoadingPostsAction = {
14 | type: typeof LOADING_ALL_POSTS;
15 | postsLoading: boolean;
16 | };
17 |
18 | export type GetAllPostsAction = {
19 | posts: [WPPost] | any | null;
20 | postsLoading?: boolean;
21 | error?: Error | any | null;
22 | };
23 |
24 | export type GetSinglePostAction = {
25 | post?: RPPost | any | null;
26 | error?: Error | any | null;
27 | postsLoading?: boolean;
28 | };
29 |
30 | export type SearchPostsAction = {
31 | posts?: [WPPost] | any | null;
32 | postsLoading?: boolean;
33 | error?: Error | any | null;
34 | };
35 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react-jsx",
21 | "noFallthroughCasesInSwitch": true,
22 | "baseUrl": "./src",
23 | "paths": {
24 | "@/*": ["./*"],
25 | "@components/*": ["components/*"],
26 | "@containers/*": ["containers/*"],
27 | "@commons/*": ["commons/*"],
28 | "@hook/*s": ["hooks/*"],
29 | "@store/*": ["store/*"],
30 | "@rptypes/*": ["types/*"]
31 | }
32 | },
33 | "exclude": [
34 | "src/**/*.js"
35 | ],
36 | "include": [ "src" ]
37 | }
38 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "bootstrap/dist/css/bootstrap.min.css";
4 |
5 | import { configureStore } from "@reduxjs/toolkit";
6 | import { Provider } from "react-redux";
7 | import { BrowserRouter } from "react-router-dom";
8 | import UserReducer from "./store/slices/users.slice";
9 | import PostReducer from "./store/slices/posts.slice";
10 | import PagesReducer from "./store/slices/pages.slice";
11 | import "./index.css";
12 | import App from "./App";
13 |
14 | const appStore = configureStore({
15 | reducer: {
16 | // users: UserReducer,
17 | posts: PostReducer,
18 | pages: PagesReducer,
19 | },
20 | });
21 | export type RootState = ReturnType;
22 | export type AppDispatch = typeof appStore.dispatch;
23 |
24 | const app = (
25 |
26 |
27 |
28 |
29 |
30 | );
31 |
32 | ReactDOM.render(app, document.getElementById("root"));
33 |
--------------------------------------------------------------------------------
/src/containers/Navigation/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import { Navbar, NavbarBrand, Nav } from "reactstrap";
3 |
4 | import NavigationItem from "../../components/NavigationItem/NavigationItem";
5 | import useNavMenu from "../../hooks/useNavMenu/index";
6 |
7 | import "../../components/NavigationItem/NavigationItem.css";
8 |
9 | // TODO: Add authentication
10 |
11 | const Navigation: React.FC = () => {
12 | const navMenuList = useNavMenu();
13 |
14 | const navList = useMemo(() => {
15 | return navMenuList
16 | .map((item) => {
17 | return ;
18 | })
19 | .filter((v) => !!v);
20 | }, [navMenuList]);
21 |
22 | return (
23 | <>
24 |
25 | REACTPRESS
26 |
27 |
28 | >
29 | );
30 | };
31 |
32 | export default Navigation;
33 |
--------------------------------------------------------------------------------
/src/components/Auth/Signout.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { connect } from "react-redux";
3 | //import { Redirect } from "react-router-dom";
4 | import * as actionTypes from "../../store/actions/index.actions";
5 |
6 | class Signout extends Component{
7 | componentDidMount(){
8 | if(this.props.token){
9 | //Dispatch User Signout action
10 | this.props.logoutUser(this.props.token);
11 | }
12 | }
13 |
14 | render(){
15 | let authRedirect = null;
16 | if(!this.props.token || this.props.token === ""){
17 | // authRedirect = ;
18 | }
19 | return(
20 |
21 | {authRedirect}
22 |
23 | );
24 | }
25 | }
26 |
27 | const mapStateToProps = (state) => {
28 | return {
29 | token: state.usersRed.token
30 | };
31 | };
32 |
33 | const mapDispatchStateToProps = (dispatch) => {
34 | return {
35 | logoutUser: (token) => {dispatch(actionTypes.userSignout(token))}
36 | };
37 | };
38 |
39 | export default connect(mapStateToProps, mapDispatchStateToProps)(Signout);
--------------------------------------------------------------------------------
/src/containers/Dashboard/Dashboard.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Row, Col } from "reactstrap";
3 | import { Switch, Route } from "react-router-dom";
4 | import { connect } from "react-redux";
5 |
6 | class Dashboard extends Component{
7 |
8 | render(){
9 | let authRedirect = null;
10 | if(this.props.token === null || this.props.token === ""){
11 | // authRedirect =
12 | }
13 | return(
14 |
15 | {authRedirect}
16 |
17 |
18 | CREATE POST CONTENT
}/>
19 | DASHBOARD CONTENT
}/>
20 |
21 |
22 |
23 | );
24 | }
25 | }
26 |
27 | const mapStateToProps = (state) => {
28 | return {
29 | token: state.usersRed.token
30 | };
31 | }
32 |
33 | export default connect(mapStateToProps)(Dashboard);
--------------------------------------------------------------------------------
/src/components/Post/Post.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Row, Col, Button } from "reactstrap";
3 | import ReactHtmlParser from "react-html-parser";
4 |
5 | import "./Post.css";
6 |
7 | type PostProps = {
8 | title: string;
9 | excerpt: string;
10 | medialink: string;
11 | postId: number;
12 | onReadMore: (postId: number) => void;
13 | };
14 |
15 | const Post: React.FC = (props: PostProps) => {
16 | const { title, excerpt, medialink, postId, onReadMore } = props;
17 | const titleHtml = ReactHtmlParser(title);
18 | const excerptHtml = ReactHtmlParser(excerpt);
19 |
20 | return (
21 |
22 |
23 | {titleHtml}
24 |
25 |
26 |
27 |
28 |
29 | {excerptHtml}
30 |
31 |
32 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default Post;
41 |
--------------------------------------------------------------------------------
/src/types/users.types.ts:
--------------------------------------------------------------------------------
1 | export const VALIDATE_TOKEN_ACTION = "VALIDATE_TOKEN_ACTION";
2 | export const USER_SIGNUP_ACTION = "USER_SIGNUP_ACTION";
3 | export const USER_SIGNIN_ACTION = "USER_SIGNIN_ACTION";
4 | export const USER_SIGNOUT = "USER_SIGNOUT";
5 | export const USER_SIGNOUT_ACTION = "USER_SIGNOUT_ACTION";
6 | export const USER_LOADING_ACTION = "USER_LOADING_ACTION";
7 |
8 | export type User = {
9 | username?: string;
10 | email?: string;
11 | displayname?: string;
12 | };
13 |
14 | export type LoadingUsersAction = {
15 | type: typeof USER_LOADING_ACTION;
16 | userLoading: boolean;
17 | };
18 |
19 | export type UserSignUpAction = {
20 | userLoading: boolean;
21 | user?: User;
22 | redirectURL?: string;
23 | error?: Error | any | null;
24 | };
25 |
26 | export type UserSignInAction = {
27 | userLoading: boolean;
28 | user?: User;
29 | token?: string;
30 | error?: Error | any | null;
31 | };
32 |
33 | export type ValidateUserAction = {
34 | token?: string;
35 | email?: string;
36 | redirectTo?: string;
37 | error?: Error | any | null;
38 | };
39 |
40 | export type UserSignoutAction = {
41 | status: boolean;
42 | error?: Error | any | null;
43 | };
44 |
--------------------------------------------------------------------------------
/src/hooks/useWPPages/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useRPDispatch, useRPSelector } from "@store/store";
3 |
4 | import { WPPage } from "@rptypes/wptypes";
5 | import { AllPagesType } from "@rptypes/pages.types";
6 | import { getAllPages } from "@store/actions/pages.actions";
7 | import { RootState } from "@/index";
8 |
9 | type RPPage = {
10 | [key: string]: WPPage;
11 | };
12 |
13 | const useWPPages = () => {
14 | const pagesState: AllPagesType = useRPSelector((state: RootState) => state.pages);
15 | const [pages, setPages] = useState({});
16 |
17 | const dispatch = useRPDispatch();
18 |
19 | useEffect(() => {
20 | dispatch(getAllPages());
21 | }, []);
22 |
23 | useEffect(() => {
24 | if (!pagesState?.pages?.length || (pagesState?.pages?.length && Object.keys(pages).length)) {
25 | return;
26 | }
27 | const { pages: pagesData = [] } = pagesState;
28 | const updatedPages: RPPage = {};
29 |
30 | pagesData.forEach((page: WPPage) => {
31 | updatedPages[page.slug] = { ...page };
32 | });
33 | setPages(updatedPages);
34 | }, [pagesState]);
35 |
36 | return {
37 | pagesLoading: pagesState.pagesLoading,
38 | pages,
39 | };
40 | };
41 |
42 | export default useWPPages;
43 |
--------------------------------------------------------------------------------
/src/components/Auth/SignIn.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Col, Form, InputGroup, FormGroup, Input, Button, InputGroupAddon, InputGroupText } from "reactstrap";
3 |
4 | const signIn = (props) => {
5 | return (
6 |
7 | Sign In!
8 |
25 |
26 | )
27 | };
28 |
29 | export default signIn;
--------------------------------------------------------------------------------
/src/store/slices/posts.slice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | import type { RootState } from "@/index";
4 | import type {
5 | GetAllPostsAction,
6 | LoadingPostsAction,
7 | GetSinglePostAction,
8 | SearchPostsAction,
9 | } from "@rptypes/posts.types";
10 | import { loadAllPosts, searchAllPosts, loadSinglePost } from "@store/actions/index.actions";
11 |
12 | const initialState = {
13 | posts: null,
14 | error: null,
15 | post: null,
16 | totalPages: 0,
17 | totalPosts: 0,
18 | perPage: 5,
19 | postsLoading: false,
20 | };
21 |
22 | export const postsSlice = createSlice({
23 | name: "posts",
24 | initialState,
25 | reducers: {},
26 | extraReducers: (builder) => {
27 | builder.addCase(loadAllPosts.fulfilled, (state, action: PayloadAction) => {
28 | state.posts = action.payload.posts;
29 | state.postsLoading = false;
30 | });
31 | builder.addCase(searchAllPosts.fulfilled, (state, action: PayloadAction) => {
32 | state.posts = action.payload.posts;
33 | state.postsLoading = false;
34 | });
35 | builder.addCase(loadSinglePost.fulfilled, (state, action: PayloadAction) => {
36 | state.post = action.payload.post;
37 | state.postsLoading = false;
38 | });
39 | },
40 | });
41 |
42 | export default postsSlice.reducer;
43 |
--------------------------------------------------------------------------------
/src/hooks/useNavMenu/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | import useWPPages from "../useWPPages";
4 | import { AllPagesType, NavItemType } from "../../types/pages.types";
5 | import { WPPage } from "../../types/wptypes";
6 | import { baseNavItems } from "../../commons/constants";
7 |
8 | const useNavMenu = () => {
9 | const wpPages: AllPagesType = useWPPages();
10 | const [navItems, setNavItems] = useState([...baseNavItems]);
11 |
12 | useEffect(() => {
13 | if (!wpPages || navItems.length === baseNavItems.length + Object.keys(wpPages?.pages || {}).length) {
14 | return;
15 | }
16 | const { pages = {} } = wpPages;
17 | const pagesSlugs = Object.keys(pages);
18 | const updatedNavItems: Array = [...navItems];
19 | const slugsInNavItems: Array = updatedNavItems.map((item) => item.link);
20 | pagesSlugs.forEach((pageSlug: string) => {
21 | const page = pages[pageSlug];
22 | if (!slugsInNavItems.includes(`/${page.slug}`)) {
23 | updatedNavItems.push({
24 | link: `/${page.slug}`,
25 | linkName: page.title.rendered,
26 | isVisible: "noauth",
27 | });
28 | slugsInNavItems.push(`/${page.slug}`);
29 | }
30 | });
31 | setNavItems(updatedNavItems);
32 | }, [wpPages]);
33 |
34 | return navItems;
35 | };
36 |
37 | export default useNavMenu;
38 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # ChangeLog
2 |
3 | Date: 18/08/21 (Version 2.0.0)
4 |
5 | - Moved to Redux toolkit
6 | - Feature slices added for User, Post, Pages
7 | - Version upgrades to the following packages
8 |
9 | - Dependencies (Major version upgrades)
10 | - `@craco/craco` - "^5.6.4" to "^6.0.0"
11 | - `bootstrap` - "^4.5.0" to "^5.0.0"
12 | - `react`: "^16.13.0" to "^17.0.0"
13 | - `react-dom`: "^16.13.0" to "^17.0.0"
14 | - `typescript`: "^3.9.6" to "^4.0.0"
15 |
16 | - Dev Dependencies
17 | - `eslint-config-airbnb-typescript`
18 | - `eslint-config-prettier`
19 | - `@types/jest`
20 | - `@types/node`
21 | - `@types/react`
22 | - `@types/react-dom`
23 | - `@types/react-router-dom`
24 | - `@typescript-eslint/eslint-plugin`
25 | - `@typescript-eslint/parser`
26 |
27 | - Added aliases
28 |
29 | Date: 04/08/2021
30 |
31 | - Project under active development, please pull code and run at your own risk.
32 | - Started move to Redux hooks
33 | - Added Feature to pull pages from Wordpress API and display in ReactPress (pending)
34 |
35 | Date: 09/2020
36 |
37 | - Working on moving to Typescript, adding tests, upgrading UI to allow more customization
38 |
39 | Date: 27/11/2018
40 |
41 | - Changed to [simple-jwt-authentication by Jonathan](https://github.com/jonathan-dejong/simple-jwt-authentication/wiki/Documentation)
42 | - Added Login and Log out for existing user
43 |
44 | Date: 25/11/2018
45 |
46 | - Added login with [jwt-authentication-plugin by Tmeister](https://github.com/Tmeister/wp-api-jwt-auth)
47 |
48 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
49 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["airbnb-typescript", "plugin:@typescript-eslint/recommended", "prettier", "plugin:prettier/recommended"],
3 | env: {
4 | browser: true,
5 | node: true,
6 | es6: true,
7 | },
8 | rules: {
9 | "prettier/prettier": [
10 | "warn",
11 | {
12 | trailingComma: "all",
13 | tabWidth: 2,
14 | semi: true,
15 | singleQuote: false,
16 | bracketSpacing: true,
17 | eslintIntegration: true,
18 | printWidth: 120,
19 | endOfLine: "auto",
20 | },
21 | ],
22 | "no-underscore-dangle": "off",
23 | "spaced-comment": "warn",
24 | "prefer-const": "warn",
25 | "prefer-template": "warn",
26 | "one-var": "warn",
27 | "@typescript-eslint/camelcase": "off",
28 | "@typescript-eslint/no-use-before-define": "off",
29 | camelcase: "off",
30 | "import/extensions": [
31 | "error",
32 | "ignorePackages",
33 | {
34 | js: "never",
35 | jsx: "never",
36 | ts: "never",
37 | tsx: "never",
38 | },
39 | ],
40 | "@typescript-eslint/ban-types": [
41 | "error",
42 | {
43 | types: {
44 | String: false,
45 | Boolean: false,
46 | Number: false,
47 | Symbol: false,
48 | "{}": false,
49 | Object: false,
50 | object: false,
51 | Function: false,
52 | },
53 | extendDefaults: true,
54 | },
55 | ],
56 | "no-param-reassign": [
57 | "error",
58 | {
59 | props: true,
60 | ignorePropertyModificationsFor: ["state"],
61 | },
62 | ],
63 | },
64 | overrides: [
65 | {
66 | files: ["*.ts", "*.tsx"],
67 | parserOptions: {
68 | project: ["./tsconfig.json"],
69 | },
70 | },
71 | ],
72 | parser: "@typescript-eslint/parser",
73 | ignorePatterns: [".eslintrc.js"],
74 | };
75 |
--------------------------------------------------------------------------------
/.snyk:
--------------------------------------------------------------------------------
1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
2 | version: v1.19.0
3 | ignore: {}
4 | # patches apply the minimum changes required to fix a vulnerability
5 | patch:
6 | SNYK-JS-LODASH-567746:
7 | - snyk > graphlib > lodash:
8 | patched: '2021-05-24T03:59:25.372Z'
9 | - snyk > @snyk/dep-graph > graphlib > lodash:
10 | patched: '2021-05-24T03:59:25.372Z'
11 | - snyk > snyk-nodejs-lockfile-parser > graphlib > lodash:
12 | patched: '2021-05-24T03:59:25.372Z'
13 | - snyk > snyk-go-plugin > graphlib > lodash:
14 | patched: '2021-05-24T03:59:25.372Z'
15 | - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > graphlib > lodash:
16 | patched: '2021-05-24T03:59:25.372Z'
17 | - snyk > snyk-cpp-plugin > @snyk/dep-graph > graphlib > lodash:
18 | patched: '2021-05-24T03:59:25.372Z'
19 | - snyk > snyk-docker-plugin > @snyk/dep-graph > graphlib > lodash:
20 | patched: '2021-05-24T03:59:25.372Z'
21 | - snyk > snyk-go-plugin > @snyk/dep-graph > graphlib > lodash:
22 | patched: '2021-05-24T03:59:25.372Z'
23 | - snyk > snyk-gradle-plugin > @snyk/java-call-graph-builder > graphlib > lodash:
24 | patched: '2021-05-24T03:59:25.372Z'
25 | - snyk > snyk-docker-plugin > snyk-nodejs-lockfile-parser > graphlib > lodash:
26 | patched: '2021-05-24T03:59:25.372Z'
27 | - snyk > snyk-mvn-plugin > @snyk/java-call-graph-builder > graphlib > lodash:
28 | patched: '2021-05-24T03:59:25.372Z'
29 | - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash:
30 | patched: '2021-05-24T03:59:25.372Z'
31 | - snyk > snyk-gradle-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash:
32 | patched: '2021-05-24T03:59:25.372Z'
33 | - snyk > snyk-mvn-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash:
34 | patched: '2021-05-24T03:59:25.372Z'
35 | - snyk > snyk-python-plugin > snyk-poetry-lockfile-parser > @snyk/dep-graph > graphlib > lodash:
36 | patched: '2021-05-24T03:59:25.372Z'
37 |
--------------------------------------------------------------------------------
/src/components/Auth/SignUp.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Col, Form, FormGroup, InputGroup, InputGroupAddon, InputGroupText, Input, Button } from "reactstrap";
3 |
4 | const signUp = (props) => {
5 | return (
6 |
7 | Sign up here!
8 |
46 |
47 | );
48 | };
49 |
50 | export default signUp;
--------------------------------------------------------------------------------
/src/store/slices/users.slice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | import { userSignup, userSignin, validateToken, userSignout } from "@store/actions/users.actions";
4 | import { UserSignInAction, ValidateUserAction, UserSignoutAction, UserSignUpAction } from "@rptypes/users.types";
5 |
6 | const initialState = {
7 | username: "",
8 | email: "",
9 | displayname: "",
10 | token: "",
11 | error: null,
12 | isLoading: false,
13 | redirectURL: "/",
14 | userLoading: false,
15 | };
16 |
17 | export const usersSlice = createSlice({
18 | initialState,
19 | name: "users",
20 | reducers: {},
21 | extraReducers: (builder) => {
22 | builder.addCase(userSignup.fulfilled, (state, action: PayloadAction) => {
23 | const { user = {} } = action.payload;
24 | state.userLoading = action.payload.userLoading;
25 | state.username = user.username || "";
26 | state.email = user.email || "";
27 | state.displayname = user.displayname || "";
28 | state.redirectURL = "/signin?user=new";
29 | state.error = action.payload.error;
30 | });
31 |
32 | builder.addCase(userSignin.fulfilled, (state, action: PayloadAction) => {
33 | const { user = {} } = action.payload;
34 | state.userLoading = false;
35 | state.username = user.username || "";
36 | state.email = user.email || "";
37 | state.displayname = user.displayname || "";
38 | state.token = action.payload.token || "";
39 | state.error = action.payload.error;
40 | });
41 |
42 | builder.addCase(validateToken.fulfilled, (state, action: PayloadAction) => {
43 | state.token = action.payload.token || "";
44 | state.email = action.payload.email || "";
45 | state.redirectURL = action.payload.redirectTo || "";
46 | state.error = action.payload.error;
47 | });
48 |
49 | builder.addCase(userSignout.fulfilled, (state, action: PayloadAction) => {
50 | if (action.payload.status) {
51 | state.token = "";
52 | state.email = "";
53 | state.redirectURL = "";
54 | } else {
55 | state.error = action.payload.error;
56 | }
57 | });
58 | },
59 | });
60 |
61 | export default usersSlice.reducer;
62 |
--------------------------------------------------------------------------------
/src/components/Post/SinglePost.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactElement, useEffect, useState } from "react";
2 | import { Container, Row, Col, Badge } from "reactstrap";
3 | import reactHtmlParser from "react-html-parser";
4 | import { useLocation } from "react-router-dom";
5 |
6 | import { RootState } from "@/index";
7 | import { useRPSelector, useRPDispatch } from "@store/store";
8 | import * as actionMethods from "@store/actions/index.actions";
9 | import type { RPPost } from "@rptypes/posts.types";
10 |
11 | const SinglePost = () => {
12 | const postsState = useRPSelector((state: RootState) => state.posts);
13 | const location = useLocation();
14 | const dispatch = useRPDispatch();
15 | const [post, setPost] = useState(null);
16 | const [content, setContent] = useState(null);
17 |
18 | useEffect(() => {
19 | const params = new URLSearchParams(location.search);
20 | const pID = params.get("id");
21 | if (pID) {
22 | dispatch(actionMethods.loadSinglePost(parseInt(pID, 10)));
23 | }
24 | }, []);
25 |
26 | useEffect(() => {
27 | if (!postsState?.post) {
28 | return;
29 | }
30 | setPost(postsState.post);
31 | }, [postsState]);
32 |
33 | useEffect(() => {
34 | if (!post) {
35 | return;
36 | }
37 | let contentVal = null;
38 | let catTags = null;
39 | catTags = (post.categoryTags || []).map((cats) => {
40 | return (
41 |
42 | {cats?.name || ""}
43 |
44 | );
45 | });
46 | contentVal = (
47 | <>
48 |
49 | {reactHtmlParser(post.title.rendered)}
50 |
51 |
52 | {catTags}
53 |
54 |
55 |
56 |
57 |
58 | {reactHtmlParser(post.content.rendered)}
59 |
60 | >
61 | );
62 | setContent(contentVal);
63 | }, [post]);
64 |
65 | if (!post) {
66 | return Post loading...
;
67 | }
68 |
69 | return (
70 |
71 | {content}
72 |
73 | );
74 | };
75 |
76 | export default SinglePost;
77 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/store/actions/users.actions.ts:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk } from "@reduxjs/toolkit";
2 | import axios from "@/axios-wp";
3 |
4 | import { User } from "@rptypes/users.types";
5 | import { WPUserType } from "@rptypes/wptypes";
6 |
7 | export const userSignup = createAsyncThunk("users/userSignup", async (user: User | WPUserType, { rejectWithValue }) => {
8 | const newUser = await axios.post("/wp/v2/users", user);
9 | if (newUser?.data) {
10 | const newUserData = newUser.data;
11 | return { user: newUserData, userLoading: false };
12 | }
13 | return rejectWithValue({ error: "User sign up failed", userLoading: false });
14 | });
15 |
16 | export const userSignin = createAsyncThunk("users/userSignin", async (user: User | WPUserType, { rejectWithValue }) => {
17 | const userResponse = await axios.post("/simple-jwt-authentication/v1/token", user);
18 | if (userResponse?.data) {
19 | const userDets = {
20 | username: userResponse.data.user_nicename,
21 | email: userResponse.data.user_email,
22 | displayname: userResponse.data.user_display_name,
23 | };
24 | // Set token and email in local storage in case Redux data is lost
25 | localStorage.setItem("token", userResponse.data.token);
26 | localStorage.setItem("email", userResponse.data.user_email);
27 | return { user: userDets, token: userResponse.data.token, userLoading: false };
28 | }
29 | return rejectWithValue({ error: "User sign in failed", userLoading: false });
30 | });
31 |
32 | export const userSignout = createAsyncThunk("users/userSignout", async (token: string, { rejectWithValue }) => {
33 | const userResponse = await axios.post(
34 | "/simple-jwt-authentication/v1/token/revoke",
35 | {},
36 | { headers: { Authorization: `Bearer ${token}` } },
37 | );
38 |
39 | if (userResponse) {
40 | // Clear local storage
41 | localStorage.removeItem("token");
42 | localStorage.removeItem("email");
43 | return { status: true };
44 | }
45 | return rejectWithValue({ status: true, error: "Error signing user out" });
46 | });
47 |
48 | export const validateToken = createAsyncThunk("users/validateToken", async (url: string, { rejectWithValue }) => {
49 | let tokenVal: string | null = null;
50 | let emailVal: string | null = null;
51 | if (localStorage.getItem("token")) {
52 | tokenVal = localStorage.getItem("token");
53 | emailVal = localStorage.getItem("email");
54 | }
55 | if (tokenVal && emailVal) {
56 | const tokenResponse = await axios.post(
57 | "/simple-jwt-authentication/v1/token/validate",
58 | {},
59 | { headers: { Authorization: `Bearer ${tokenVal}` } },
60 | );
61 | if (tokenResponse.data.data.status === 200) {
62 | return { token: tokenVal || "", email: emailVal || "", redirectTo: url };
63 | }
64 | }
65 | return rejectWithValue({ error: "Token validation failed" });
66 | });
67 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, ReactElement } from "react";
2 | import DOMPurify from "dompurify";
3 | import { AnimatedSwitch } from "react-router-transition";
4 | import { Route } from "react-router-dom";
5 |
6 | import Navigation from "@containers/Navigation/Navigation";
7 | import Blog from "@containers/Blog/Blog";
8 | import SinglePost from "@components/Post/SinglePost";
9 | // import Auth from "./containers/Auth/Auth";
10 |
11 | import useWPPages from "@hooks/useWPPages";
12 | import WPPage from "@components/WPPage";
13 | import type { RPPagesHookType } from "@rptypes/pages.types";
14 | import { baseNavItems } from "@commons/constants";
15 | import "./App.css";
16 |
17 | // TODO: Find a better solution
18 | const getStaticComponent = (linkSlug: string): ReactElement => {
19 | let finalComp = ;
20 | switch (linkSlug) {
21 | // case "/auth/signup": // TODO: Add auth
22 | // case "/auth/signin":
23 | // finalComp = ;
24 | // break;
25 | case "/post":
26 | finalComp = ;
27 | break;
28 | default:
29 | break;
30 | }
31 |
32 | return finalComp;
33 | };
34 |
35 | function App() {
36 | const wpPages: RPPagesHookType = useWPPages();
37 | const [wpRoutesList, setWPRoutesList] = useState[] | null>(null); // Separate state for Wordpress routes
38 | const [routes, setRoutes] = useState[]>([]); // Final React Router routes
39 |
40 | useEffect(() => {
41 | if (wpRoutesList?.length || wpPages?.pagesLoading || !Object.keys(wpPages?.pages).length) {
42 | return;
43 | }
44 | const { pages } = wpPages || {};
45 | const pageSlugs = Object.keys(pages);
46 | const newRoutesList: ReactElement[] = pageSlugs.map((slug) => {
47 | const pageDetails = pages[slug];
48 | return (
49 | (
53 |
54 |
55 |
56 | )}
57 | />
58 | );
59 | });
60 | setWPRoutesList(newRoutesList);
61 | }, [wpPages]);
62 |
63 | useEffect(() => {
64 | if (!wpRoutesList?.length) {
65 | return;
66 | }
67 | const staticRoutes: ReactElement[] = baseNavItems.map((item) => (
68 | getStaticComponent(item.link)} />
69 | ));
70 | const finalRoutesList = [...staticRoutes, ...wpRoutesList];
71 | setRoutes(finalRoutesList);
72 | }, [wpRoutesList]);
73 |
74 | if (!routes?.length || wpPages?.pagesLoading) {
75 | return Loading...
;
76 | }
77 |
78 | return (
79 |
80 |
81 |
82 |
83 |
89 | {routes}
90 |
91 |
92 | );
93 | }
94 |
95 | export default App;
96 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reactpress",
3 | "version": "2.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "@craco/craco": "^6.0.0",
7 | "@reduxjs/toolkit": "^1.6.1",
8 | "@types/react-redux": "^7.1.18",
9 | "antd": "^4.4.1",
10 | "axios": "^0.21.1",
11 | "bootstrap": "^5.0.0",
12 | "dompurify": "^2.3.0",
13 | "prop-types": "^15.7.2",
14 | "react": "^17.0.0",
15 | "react-dom": "^17.0.0",
16 | "react-html-parser": "^2.0.2",
17 | "react-redux": "^7.2.4",
18 | "react-router-dom": "^5.2.0",
19 | "react-router-transition": "^2.0.0",
20 | "react-scripts": "^4.0.0",
21 | "reactstrap": "^8.4.0",
22 | "snyk": "^1.606.0",
23 | "styled-components": "^5.3.0",
24 | "typescript": "^4.0.0"
25 | },
26 | "scripts": {
27 | "start": "craco start",
28 | "build": "react-scripts build",
29 | "test": "jest",
30 | "rs-test": "react-scripts test",
31 | "eject": "react-scripts eject",
32 | "test:watch": "jest --watch"
33 | },
34 | "eslintConfig": {
35 | "extends": "react-app"
36 | },
37 | "browserslist": [
38 | ">0.2%",
39 | "not dead",
40 | "not ie <= 11",
41 | "not op_mini all"
42 | ],
43 | "jest": {
44 | "setupFiles": [
45 | "./src/setupTests.js"
46 | ],
47 | "transform": {
48 | "\\.(ts|tsx)$": "ts-jest"
49 | },
50 | "transformIgnorePatterns": [
51 | "node_modules"
52 | ],
53 | "moduleFileExtensions": [
54 | "js",
55 | "jsx",
56 | "ts",
57 | "tsx"
58 | ],
59 | "moduleDirectories": [
60 | "node_modules",
61 | "src"
62 | ],
63 | "moduleNameMapper": {
64 | "\\.(css|less|scss)$": "identity-obj-proxy"
65 | },
66 | "snapshotSerializers": [
67 | "enzyme-to-json"
68 | ]
69 | },
70 | "devDependencies": {
71 | "@babel/preset-env": "^7.16.4",
72 | "@babel/preset-react": "^7.16.0",
73 | "@types/enzyme": "^3.10.5",
74 | "@types/jest": "^26.0.23",
75 | "@types/node": "^16.0.0",
76 | "@types/react": "^17.0.0",
77 | "@types/react-dom": "^17.0.0",
78 | "@types/react-router-dom": "^5.1.5",
79 | "@typescript-eslint/eslint-plugin": "^4.29.0",
80 | "@typescript-eslint/parser": "^4.29.0",
81 | "babel-plugin-styled-components": "^1.13.2",
82 | "enzyme": "^3.11.0",
83 | "enzyme-adapter-react-16": "^1.15.0",
84 | "enzyme-to-json": "^3.5.0",
85 | "eslint-config-airbnb-typescript": "^12.0.2",
86 | "eslint-config-prettier": "^8.0.0",
87 | "eslint-plugin-import": "^2.20.1",
88 | "eslint-plugin-jsx-a11y": "^6.2.3",
89 | "eslint-plugin-prettier": "^3.1.4",
90 | "eslint-plugin-react": "^7.19.0",
91 | "eslint-plugin-react-hooks": "^2.5.0",
92 | "husky": "^7.0.1",
93 | "jest-styled-components": "^7.0.5",
94 | "prettier": "^2.0.5",
95 | "pretty-quick": "^3.1.1",
96 | "react-test-renderer": "^16.13.0",
97 | "redux-mock-store": "^1.5.4",
98 | "ts-jest":"^26.0.1",
99 | "typescript-plugin-styled-components": "^2.0.0"
100 | },
101 | "prettier": {
102 | "semi": true,
103 | "printWidth": 120,
104 | "singleQuote": false,
105 | "bracketSpacing": true,
106 | "trailingComma": "all"
107 | },
108 | "husky": {
109 | "hooks": {
110 | "pre-commit": "pretty-quick --staged --pattern '**/*.*(js|jsx|ts|tsx)'"
111 | }
112 | },
113 | "snyk": true
114 | }
115 |
--------------------------------------------------------------------------------
/src/containers/Blog/Blog.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, SyntheticEvent, useEffect } from "react";
2 | import { Container, Row, Col, InputGroup, InputGroupAddon, Button, Alert } from "reactstrap";
3 | import { Spin } from "antd";
4 | import { useHistory } from "react-router-dom";
5 |
6 | import { RootState } from "@/index";
7 | import { useRPSelector, useRPDispatch } from "store/store";
8 | import { WPPost } from "@rptypes/wptypes";
9 | import * as actionMethods from "@store/actions/index.actions";
10 | import Post from "@components/Post/Post";
11 |
12 | type PostsList = JSX.Element[] | [];
13 |
14 | const Blog = () => {
15 | const [searchTerm, setSearchTerm] = useState("");
16 | const [onAlert, setOnAlert] = useState(false);
17 | const [internalError, setInternalError] = useState("");
18 | const [postsList, setPostsList] = useState([]);
19 |
20 | const history = useHistory();
21 |
22 | const postsState = useRPSelector((state: RootState) => state.posts);
23 |
24 | const dispatch = useRPDispatch();
25 |
26 | const { postsLoading, posts = [], error } = postsState;
27 |
28 | const resetError = () => {
29 | setInternalError("");
30 | };
31 |
32 | const toggleAlertShow = () => {
33 | if (internalError && onAlert) {
34 | resetError();
35 | }
36 | setOnAlert(!onAlert);
37 | };
38 |
39 | const readMoreHandler = (id: number) => {
40 | // Send selected post to Post component
41 | history.push(`/post?id=${id}`);
42 | };
43 |
44 | const onSearchHandler = () => {
45 | if (searchTerm !== "") {
46 | dispatch(actionMethods.searchAllPosts(searchTerm));
47 | } else {
48 | setInternalError("Search Term cannot be empty!");
49 | setOnAlert(true);
50 | }
51 | };
52 |
53 | const searchInputChanged = (event: SyntheticEvent) => {
54 | const element = event.target as HTMLInputElement;
55 | setSearchTerm(element.value);
56 | };
57 |
58 | useEffect(() => {
59 | dispatch(actionMethods.loadAllPosts(5));
60 | }, []);
61 |
62 | useEffect(() => {
63 | if (postsLoading || !posts?.length || postsList.length) {
64 | return;
65 | }
66 | const updatedPosts = posts.map((post: WPPost) => {
67 | return (
68 |
76 | );
77 | });
78 | setPostsList(updatedPosts);
79 | }, [posts, postsLoading]);
80 |
81 | return (
82 |
83 | {error ? (
84 |
85 | {error}
86 |
87 | ) : null}
88 | {internalError ? (
89 |
90 | {internalError}
91 |
92 | ) : null}
93 |
94 |
95 | All Posts
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | {postsLoading ? : null}
107 | {!postsLoading && postsList?.length ? postsList : null}
108 |
109 | );
110 | };
111 |
112 | export default Blog;
113 |
--------------------------------------------------------------------------------
/src/store/actions/posts.actions.ts:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk } from "@reduxjs/toolkit";
2 | import axiosOrig from "axios";
3 | import axios from "@/axios-wp";
4 |
5 | import { SINGLE_POST_ACTION, SEARCH_POSTS_ACTION, GET_ALL_POSTS, RPPost } from "@rptypes/posts.types";
6 | import { WPMedia, WPPost, WPCategory } from "@rptypes/wptypes";
7 |
8 | type MediaLinkType = {
9 | media_link?: string;
10 | };
11 |
12 | export const loadAllPosts = createAsyncThunk("posts/loadAllPosts", async (perpage: number, { rejectWithValue }) => {
13 | const postsResponse = await axios.get(`/wp/v2/posts?per_page=${perpage}`);
14 | if (postsResponse) {
15 | const mediaIds: [number?] = [];
16 | postsResponse.data.forEach((post: WPPost) => {
17 | mediaIds.push(post.featured_media);
18 | });
19 | // Retrieve media from WP DB
20 | const mediaRes = await axios.get(`/wp/v2/media?include=${mediaIds.join(",")}`);
21 | if (mediaRes) {
22 | let updatedPosts = null;
23 | updatedPosts = postsResponse.data.map((postObj: WPPost) => {
24 | const mediaLinkObj: MediaLinkType = {};
25 | mediaRes.data.forEach((media: WPMedia) => {
26 | if (media.id === postObj.featured_media) {
27 | mediaLinkObj.media_link = media.guid.rendered;
28 | }
29 | });
30 | return {
31 | ...postObj,
32 | ...mediaLinkObj,
33 | };
34 | });
35 | return { type: GET_ALL_POSTS, posts: updatedPosts, postsLoading: false };
36 | }
37 | }
38 | return rejectWithValue({ type: GET_ALL_POSTS, posts: [], postsLoading: false, error: "Error loading posts" });
39 | });
40 |
41 | export const loadSinglePost = createAsyncThunk("posts/loadSinglePost", async (pid: number, { rejectWithValue }) => {
42 | const postResponse = await axios.get(`/wp/v2/posts/${pid}`);
43 | if (postResponse) {
44 | const post: WPPost = postResponse.data;
45 | const postObject: RPPost = {
46 | ...post,
47 | };
48 | // Retrieve featured image
49 | const mediaId = post.featured_media;
50 | if (mediaId) {
51 | const mediaRes = await axios.get(`/wp/v2/media/${mediaId}`);
52 | postObject.medialink = mediaRes.data.guid.rendered;
53 | }
54 | if (post.categories?.length) {
55 | const categoriesResp = await axios.get(`wp/v2/categories?include=${post.categories.join(",")}`);
56 | postObject.categoryTags = [...categoriesResp.data];
57 | }
58 |
59 | return { type: SINGLE_POST_ACTION, post: postObject, postsLoading: false };
60 | }
61 | return rejectWithValue({ type: SINGLE_POST_ACTION, post: null, postsLoading: false, error: "Error loading post" });
62 | });
63 |
64 | export const searchAllPosts = createAsyncThunk("posts/searchAllPosts", async (term: string, { rejectWithValue }) => {
65 | const postResults = await axios.get(`/wp/v2/posts?search=${term}`);
66 | if (postResults?.data) {
67 | const mediaIds: [number?] = [];
68 | postResults.data.forEach((post: WPPost) => {
69 | mediaIds.push(post.featured_media);
70 | });
71 | if (mediaIds.length !== 0) {
72 | const mediaResults = await axios.get(`/wp/v2/media?include${mediaIds.join(",")}`);
73 | if (mediaResults?.data) {
74 | let updatedPosts = null;
75 | updatedPosts = postResults.data.map((post: WPPost) => {
76 | const mediaLinkObj: MediaLinkType = {};
77 | mediaResults.data.forEach((media: WPMedia) => {
78 | if (media.id === post.featured_media) {
79 | mediaLinkObj.media_link = media.guid.rendered;
80 | }
81 | });
82 | return {
83 | ...post,
84 | ...mediaLinkObj,
85 | };
86 | });
87 |
88 | return { type: SEARCH_POSTS_ACTION, posts: updatedPosts, postsLoading: false };
89 | }
90 | }
91 | }
92 | return rejectWithValue({ type: SEARCH_POSTS_ACTION, posts: [], postsLoading: false, error: "No posts found" });
93 | });
94 |
--------------------------------------------------------------------------------
/src/containers/Auth/Auth.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Switch, Route } from "react-router-dom";
3 | import { connect } from "react-redux";
4 |
5 | import * as actionMethods from "../../store/actions/index.actions";
6 | import SignUp from "../../components/Auth/SignUp";
7 | import SignIn from "../../components/Auth/SignIn";
8 | import SignOut from "../../components/Auth/Signout";
9 |
10 | class Auth extends Component {
11 | state = {
12 | user: {
13 | firstName: "",
14 | lastName: "",
15 | email: "",
16 | password: "",
17 | username: "",
18 | locale: "en_US",
19 | description: "",
20 | name: "",
21 | },
22 | isNewUser: false,
23 | alertText: null,
24 | };
25 |
26 | componentDidMount() {
27 | const params = new URLSearchParams(this.props.location.search);
28 | if (params.get("user")) {
29 | this.setState({ isNewUser: true });
30 | }
31 | }
32 | /* Validate inputs for the User fields for Signin and Sign Up forms
33 | * 1. Check for empty input ~
34 | * 2. Check for password strength (OWASP regex) X
35 | * 3. Check email validity X
36 | * 4. Check for blacklisted words x
37 | * 5. Trigger error texts x
38 | */
39 |
40 | validateInputs(forForm) {
41 | // Empty or false inputs
42 | if (this.state.user.username === "" || !this.state.user.username) {
43 | return false;
44 | }
45 | if (this.state.user.password === "" || !this.state.user.password) {
46 | return false;
47 | }
48 | if (forForm === "signup") {
49 | if (this.state.user.email === "" || !this.state.user.email) {
50 | return false;
51 | }
52 | if (this.state.user.firstName === "" || this.state.user.lastName === "") {
53 | return false;
54 | }
55 | }
56 | return true;
57 | }
58 |
59 | onUserSignUp = () => {
60 | const tempUser = {
61 | ...this.state.user,
62 | };
63 | tempUser.username = tempUser.email;
64 | tempUser.name = tempUser.firstName + " " + tempUser.lastName;
65 | this.setState({ user: tempUser }, () => {
66 | // TODO: Validate inputs
67 | let isValid = this.validateInputs("signup");
68 | if (isValid) {
69 | // Assign email to username
70 | this.props.onUserSignUpSubmit(this.state.user);
71 | }
72 | });
73 | };
74 |
75 | onUserSignin = () => {
76 | // TODO: Validate inputs
77 | if (this.state.user.username === "" || this.state.user.password === "") {
78 | // TODO: ADD ERROR DIALOG
79 | } else {
80 | // Submit the form
81 | const userDets = {
82 | username: this.state.user.username,
83 | password: this.state.user.password,
84 | };
85 | this.props.onUserSigninSubmit(userDets);
86 | }
87 | };
88 |
89 | onInputClicked = (event, forField, forForm) => {
90 | const tempUser = this.state.user;
91 | tempUser[forField] = event.target.value;
92 | this.setState({ user: tempUser });
93 | };
94 |
95 | render() {
96 | return (
97 |
98 | {/* this.props.token ? : null */}
99 |
100 | (
103 |
109 | )}
110 | />
111 | (
114 |
115 | )}
116 | />
117 | } />
118 |
119 |
120 | );
121 | }
122 | }
123 |
124 | const mapStateToProps = (state) => {
125 | return {
126 | userInfo: state.usersRed.userInfo,
127 | token: state.usersRed.token,
128 | };
129 | };
130 |
131 | const mapDispatchToProps = (dispatch) => {
132 | return {
133 | onUserSigninSubmit: (user) => {
134 | dispatch(actionMethods.userSignin(user));
135 | },
136 | onUserSignUpSubmit: (user) => {
137 | dispatch(actionMethods.userSignup(user));
138 | },
139 | };
140 | };
141 |
142 | export default connect(mapStateToProps, mapDispatchToProps)(Auth);
143 |
--------------------------------------------------------------------------------
/src/types/wptypes.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/camelcase */
2 |
3 | export type WPPost = {
4 | id: number;
5 | date: Date;
6 | date_gmt: Date;
7 | guid: {
8 | rendered: string;
9 | };
10 | modified: Date;
11 | modified_gmt: Date;
12 | slug: string;
13 | status: string;
14 | type: string;
15 | link: string;
16 | title: {
17 | rendered: string;
18 | };
19 | content: {
20 | rendered: string;
21 | protected: boolean;
22 | };
23 | excerpt: {
24 | rendered: string;
25 | protected: boolean;
26 | };
27 | author: number;
28 | featured_media: number;
29 | comment_status: string;
30 | ping_status: string;
31 | sticky: boolean;
32 | template: string;
33 | format: string;
34 | meta: {
35 | hide_page_title: string;
36 | _coblocks_attr: string;
37 | _coblocks_dimensions: string;
38 | _coblocks_responsive_height: string;
39 | _coblocks_accordion_ie_support: string;
40 | };
41 | categories: [number];
42 | tags: [string];
43 | _links: {
44 | self: [object];
45 | collection: [object];
46 | about: [object];
47 | author: [object];
48 | replies: [object];
49 | "version-history": [object];
50 | "wp:featuredmedia": [object];
51 | "wp:attachment": [object];
52 | "wp:term": [object];
53 | curies: [object];
54 | };
55 | media_link: "string";
56 | };
57 |
58 | export type WPCategory = {
59 | id: number;
60 | name: string;
61 | };
62 |
63 | type MediaSizesType = {
64 | file: string;
65 | width: number;
66 | height: number;
67 | mime_type: string;
68 | source_url: string;
69 | };
70 |
71 | export type WPMedia = {
72 | id: number;
73 | date: Date;
74 | date_gmt: Date;
75 | guid: {
76 | rendered: string;
77 | };
78 | modified: Date;
79 | modified_gmt: Date;
80 | slug: string;
81 | status: string;
82 | type: string;
83 | link: string;
84 | title: {
85 | rendered: string;
86 | };
87 | author: number;
88 | comment_status: string;
89 | ping_status: string;
90 | template: string;
91 | meta: {
92 | hide_page_title: string;
93 | _coblocks_attr: string;
94 | _coblocks_dimensions: string;
95 | _coblocks_responsive_height: string;
96 | _coblocks_accordion_ie_support: string;
97 | };
98 | description: {
99 | rendered: string;
100 | };
101 | caption: {
102 | rendered: string;
103 | };
104 | alt_text: string;
105 | media_type: string;
106 | mime_type: string;
107 | media_details: {
108 | width: number;
109 | height: number;
110 | file: string;
111 | sizes: {
112 | medium: MediaSizesType;
113 | large: MediaSizesType;
114 | thumbnail: MediaSizesType;
115 | medium_large: MediaSizesType;
116 | full: MediaSizesType;
117 | };
118 | image_meta: {
119 | aperture: string;
120 | credit: string;
121 | camera: string;
122 | caption: string;
123 | created_timestamp: string;
124 | copyright: string;
125 | focal_length: string;
126 | iso: string;
127 | shutter_speed: string;
128 | title: string;
129 | orientation: string;
130 | keywords: [string];
131 | };
132 | post: null;
133 | source_url: string;
134 | _links: {
135 | self: [object];
136 | collection: [object];
137 | about: [object];
138 | author: [object];
139 | replies: [object];
140 | };
141 | };
142 | };
143 |
144 | export type WPUserType = {
145 | user_nicename?: string;
146 | user_email?: string;
147 | user_display_name?: string;
148 | };
149 |
150 | export type WPPage = {
151 | id: number;
152 | date: Date;
153 | date_gmt: Date;
154 | guid: {
155 | rendered: string;
156 | };
157 | modified: Date;
158 | modified_gmt: Date;
159 | slug: string;
160 | status: string;
161 | type: string;
162 | link: string;
163 | title: {
164 | rendered: string;
165 | };
166 | content: {
167 | rendered: string;
168 | protected: boolean;
169 | };
170 | excerpt: {
171 | rendered: string;
172 | protected: boolean;
173 | };
174 | author: number;
175 | featured_media: number;
176 | parent: number;
177 | menu_order: number;
178 | comment_status: string;
179 | ping_status: string;
180 | template: string;
181 | meta: Array;
182 | _links: {
183 | self: [object];
184 | collection: [object];
185 | about: [object];
186 | author: [object];
187 | replies: [object];
188 | "version-history": [object];
189 | "wp:featuredmedia": [object];
190 | "wp:attachment": [object];
191 | curies: [object];
192 | };
193 | };
194 |
--------------------------------------------------------------------------------