Compare commits
7 commits
master
...
gh-pages-v
Author | SHA1 | Date | |
---|---|---|---|
|
1b0452e63e | ||
|
8dcad9625f | ||
|
05aa21bf63 | ||
|
8a99255347 | ||
|
fad7fd6c7c | ||
|
df66c9d4ed | ||
|
d1713a6aa8 |
54 changed files with 14002 additions and 0 deletions
18
page-src/.editorconfig
Normal file
18
page-src/.editorconfig
Normal file
|
@ -0,0 +1,18 @@
|
|||
root = true
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = crlf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
trim_trailing_whitespace = true
|
||||
[*.md]
|
||||
max_line_length = 0
|
||||
trim_trailing_whitespace = false
|
||||
[{Makefile,**.mk}]
|
||||
# Use tabs for indentation (Makefiles require tabs)
|
||||
indent_style = tab
|
||||
[*.scss]
|
||||
indent_size = 2
|
||||
indent_style = space
|
2
page-src/.env
Normal file
2
page-src/.env
Normal file
|
@ -0,0 +1,2 @@
|
|||
REACT_APP_API_URL=1c2ddaab-5ec2-4824-bd14-c22599ee9941.pub.instances.scw.cloud
|
||||
REACT_APP_API_PORT=8081
|
42
page-src/.eslintrc
Normal file
42
page-src/.eslintrc
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"extends": [
|
||||
"react-app",
|
||||
"airbnb",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"plugins": ["prettier"],
|
||||
"rules": {
|
||||
"no-undef": "off",
|
||||
"no-use-before-define": "off",
|
||||
|
||||
"camelcase": "off",
|
||||
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn"],
|
||||
|
||||
"@typescript-eslint/no-explicit-any": "off", // FIXME: remove this rule
|
||||
|
||||
"react/prop-types": [0],
|
||||
"react/jsx-filename-extension": [1, { "extensions": ["ts", "tsx"] }],
|
||||
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"ts": "never",
|
||||
"tsx": "never"
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"extensions": [".js", ".jsx", ".json", ".ts", ".tsx"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
page-src/.gitignore
vendored
Normal file
23
page-src/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
1
page-src/.prettierignore
Normal file
1
page-src/.prettierignore
Normal file
|
@ -0,0 +1 @@
|
|||
build
|
6
page-src/.prettierrc.json
Normal file
6
page-src/.prettierrc.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"printWidth": 100,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true
|
||||
}
|
8
page-src/.vscode/settings.json
vendored
Normal file
8
page-src/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.stylelint": true
|
||||
},
|
||||
"cSpell.words": ["browserslist", "camelcase", "craco", "middlewares", "predeploy"]
|
||||
}
|
44
page-src/README.md
Normal file
44
page-src/README.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `yarn start`
|
||||
|
||||
Runs the app in the development mode.<br />
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br />
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `yarn test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br />
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `yarn build`
|
||||
|
||||
Builds the app for production to the `build` folder.<br />
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br />
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `yarn eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
70
page-src/package.json
Normal file
70
page-src/package.json
Normal file
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"name": "open-source-mac-os-apps",
|
||||
"version": "0.1.0",
|
||||
"homepage": "http://serhii-londar.github.io/open-source-mac-os-apps",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.31",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.0",
|
||||
"@fortawesome/react-fontawesome": "^0.1.11",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"node-sass": "^6.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-redux": "^7.2.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"redux": "^4.0.5",
|
||||
"redux-devtools-extension": "^2.13.8",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-saga": "^1.1.3",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"predeploy": "npm run build",
|
||||
"deploy": "gh-pages -e docs -d build",
|
||||
"eslint": "eslint ./src --ext .js,.ts,.jsx,.tsx"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^16.7.1",
|
||||
"@types/react": "^17.0.19",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-redux": "^7.1.9",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/redux-logger": "^3.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
||||
"@typescript-eslint/parser": "^4.29.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint-config-airbnb": "^18.2.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-react": "^7.21.2",
|
||||
"gh-pages": "^3.1.0",
|
||||
"prettier": "^2.1.2"
|
||||
}
|
||||
}
|
BIN
page-src/public/favicon.png
Normal file
BIN
page-src/public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
40
page-src/public/index.html
Normal file
40
page-src/public/index.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created using create-react-app" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
BIN
page-src/public/logo.png
Normal file
BIN
page-src/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
20
page-src/public/manifest.json
Normal file
20
page-src/public/manifest.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512 192x192"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
page-src/public/robots.txt
Normal file
3
page-src/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
21
page-src/src/app/App.module.scss
Normal file
21
page-src/src/app/App.module.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
@import "./components/shared/css/colors.scss";
|
||||
|
||||
$sidebar-width: 300px;
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.categories-list {
|
||||
width: $sidebar-width;
|
||||
background-color: $charcoal;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: $white;
|
||||
flex: 1;
|
||||
}
|
61
page-src/src/app/App.tsx
Normal file
61
page-src/src/app/App.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import React, { FC, useEffect } from "react";
|
||||
|
||||
import { useDispatch } from "react-redux";
|
||||
import { Redirect, Route, BrowserRouter as Router, Switch } from "react-router-dom";
|
||||
import { fetchAllCategoriesAction } from "./actions/category";
|
||||
import { fetchAllApplicationsAction } from "./actions/application";
|
||||
|
||||
import styles from "./App.module.scss";
|
||||
|
||||
import Application from "./components/Application/Application";
|
||||
import CategoriesList from "./components/CategoriesList/CategoriesList";
|
||||
import Category from "./components/Category/Category";
|
||||
import Home from "./components/Home/Home";
|
||||
import PathBuilder from "./services/PathBuilder";
|
||||
|
||||
enum APP_ROUTES {
|
||||
HOME = "/",
|
||||
CATEGORY = "/:category",
|
||||
APPLICATION = "/:category/:application",
|
||||
}
|
||||
|
||||
/* TODO:
|
||||
- [ ]: setup husky
|
||||
- [ ]: setup stylelint
|
||||
- [ ]: use redux toolkit
|
||||
- [ ]: revisit the API service. use axios
|
||||
- [ ]: prepare config for PATH alias (craco)
|
||||
- [ ]: prepare some model for errors
|
||||
*/
|
||||
|
||||
const App: FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAllCategoriesAction());
|
||||
dispatch(fetchAllApplicationsAction());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<main className={styles.container}>
|
||||
<Router>
|
||||
<aside className={styles["categories-list"]}>
|
||||
<CategoriesList />
|
||||
</aside>
|
||||
<section className={styles.content}>
|
||||
<Switch>
|
||||
<Route exact path={PathBuilder.build(APP_ROUTES.HOME)} component={Home} />
|
||||
<Route exact path={PathBuilder.build(APP_ROUTES.CATEGORY)} component={Category} />
|
||||
<Route exact path={PathBuilder.build(APP_ROUTES.APPLICATION)} component={Application} />
|
||||
|
||||
<Route path="*">
|
||||
<Redirect to={PathBuilder.build(APP_ROUTES.HOME)} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</section>
|
||||
</Router>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
45
page-src/src/app/actions/application.ts
Normal file
45
page-src/src/app/actions/application.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import Application from "../models/application";
|
||||
|
||||
export enum ApplicationActions {
|
||||
FETCH_ALL = "APPLICATIONS_FETCH_ALL",
|
||||
FETCH_ALL_SUCCEED = "APPLICATIONS_FETCH_ALL_SUCCEED",
|
||||
FETCH_ALL_FAILED = "APPLICATIONS_FETCH_ALL_FAILED",
|
||||
}
|
||||
|
||||
export type ApplicationAction<T = any> = {
|
||||
type: ApplicationActions;
|
||||
payload?: T;
|
||||
};
|
||||
|
||||
// ---------------------------------
|
||||
|
||||
export type FetchAllApplicationsAction = ApplicationAction;
|
||||
|
||||
export const fetchAllApplicationsAction = (): FetchAllApplicationsAction => ({
|
||||
type: ApplicationActions.FETCH_ALL,
|
||||
});
|
||||
|
||||
// ---------------------------------
|
||||
|
||||
export type FetchAllApplicationsSucceedPayload = Application[];
|
||||
export type FetchAllApplicationsSucceedAction =
|
||||
ApplicationAction<FetchAllApplicationsSucceedPayload>;
|
||||
|
||||
export const fetchAllApplicationsSucceedAction = (
|
||||
payload: FetchAllApplicationsSucceedPayload,
|
||||
): FetchAllApplicationsSucceedAction => ({
|
||||
type: ApplicationActions.FETCH_ALL_SUCCEED,
|
||||
payload,
|
||||
});
|
||||
|
||||
// ---------------------------------
|
||||
|
||||
export type FetchAllApplicationsFailedPayload = any;
|
||||
export type FetchAllApplicationsFailedAction = ApplicationAction<FetchAllApplicationsFailedPayload>;
|
||||
|
||||
export const fetchAllApplicationsFailedAction = (
|
||||
payload: FetchAllApplicationsFailedPayload,
|
||||
): FetchAllApplicationsFailedAction => ({
|
||||
type: ApplicationActions.FETCH_ALL_FAILED,
|
||||
payload,
|
||||
});
|
44
page-src/src/app/actions/category.ts
Normal file
44
page-src/src/app/actions/category.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import Category from "../models/category";
|
||||
|
||||
export enum CategoryActions {
|
||||
FETCH_ALL = "CATEGORY_FETCH_ALL",
|
||||
FETCH_ALL_SUCCEED = "CATEGORY_FETCH_ALL_SUCCEED",
|
||||
FETCH_ALL_FAILED = "CATEGORY_FETCH_ALL_FAILED",
|
||||
}
|
||||
|
||||
export type CategoryAction<T = any> = {
|
||||
type: CategoryActions;
|
||||
payload?: T;
|
||||
};
|
||||
|
||||
// ---------------------------------
|
||||
|
||||
export type FetchAllCategoriesAction = CategoryAction;
|
||||
|
||||
export const fetchAllCategoriesAction = (): FetchAllCategoriesAction => ({
|
||||
type: CategoryActions.FETCH_ALL,
|
||||
});
|
||||
|
||||
// ---------------------------------
|
||||
|
||||
export type FetchAllCategoriesSucceedPayload = Category[];
|
||||
export type FetchAllCategoriesSucceedAction = CategoryAction<FetchAllCategoriesSucceedPayload>;
|
||||
|
||||
export const fetchAllCategoriesSucceedAction = (
|
||||
payload: FetchAllCategoriesSucceedPayload,
|
||||
): FetchAllCategoriesSucceedAction => ({
|
||||
type: CategoryActions.FETCH_ALL_SUCCEED,
|
||||
payload,
|
||||
});
|
||||
|
||||
// ---------------------------------
|
||||
|
||||
export type FetchAllCategoriesFailedPayload = any;
|
||||
export type FetchAllCategoriesFailedAction = CategoryAction<FetchAllCategoriesFailedPayload>;
|
||||
|
||||
export const fetchCategoryFailedAction = (
|
||||
payload: FetchAllCategoriesFailedPayload,
|
||||
): CategoryAction<FetchAllCategoriesFailedPayload> => ({
|
||||
type: CategoryActions.FETCH_ALL_FAILED,
|
||||
payload,
|
||||
});
|
5
page-src/src/app/components/Application/Application.tsx
Normal file
5
page-src/src/app/components/Application/Application.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React, { FC } from "react";
|
||||
|
||||
const Application: FC = () => <div>Application</div>;
|
||||
|
||||
export default Application;
|
|
@ -0,0 +1,103 @@
|
|||
@import "../shared/css/colors.scss";
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 25px;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
font-size: 25px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.logo {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.top-text {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 70px;
|
||||
}
|
||||
.bottom-text {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 80px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
padding: 0 15px;
|
||||
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: $white;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 10px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: $white;
|
||||
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
padding: 10px 10px 10px 35px;
|
||||
border-radius: 5px;
|
||||
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
flex-grow: 1;
|
||||
overflow: overlay;
|
||||
list-style: none;
|
||||
padding-left: 10px;
|
||||
|
||||
.item {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: darken($white, 10%);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import React, { FC } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import Category from "../../models/category";
|
||||
import { RootState } from "../../reducers";
|
||||
import { CategoriesState } from "../../reducers/categories";
|
||||
|
||||
import CategoryItem from "./CategoryItem/CategoryItem";
|
||||
|
||||
import styles from "./CategoriesList.module.scss";
|
||||
import { ApplicationsState } from "../../reducers/application";
|
||||
import Application from "../../models/application";
|
||||
|
||||
import icon from "../../../assets/app_icon.png";
|
||||
|
||||
const CategoriesList: FC = () => {
|
||||
const categoriesState = useSelector<RootState, CategoriesState>((state) => state.categories);
|
||||
const applicationsState = useSelector<RootState, ApplicationsState>(
|
||||
(state) => state.applications,
|
||||
);
|
||||
|
||||
const appsInCategory = (category: Category): number =>
|
||||
applicationsState.data.reduce(
|
||||
(acc: number, application: Application) =>
|
||||
acc + application.categories.filter((c: Category) => c.id === category.id).length,
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.title}>
|
||||
<img src={icon} alt="Logo" className={styles.logo} />
|
||||
<span className={styles["top-text"]}>Open Source</span>
|
||||
<span className={styles["bottom-text"]}>Applications</span>
|
||||
</div>
|
||||
<div className={styles.search}>
|
||||
<span className={styles["search-wrapper"]}>
|
||||
<FontAwesomeIcon icon={faSearch} className={styles["search-icon"]} />
|
||||
<input type="text" placeholder="Search" className={styles["search-input"]} />
|
||||
</span>
|
||||
</div>
|
||||
<ul className={styles.list}>
|
||||
<li className={styles.item}>
|
||||
<CategoryItem badge={false} path="/" label="Home" className={styles.link} />
|
||||
</li>
|
||||
{categoriesState.data.map((category: Category) => (
|
||||
<li className={styles.item} key={category.id}>
|
||||
<CategoryItem
|
||||
path={`/${category.shortName}`}
|
||||
label={category.name}
|
||||
className={styles.link}
|
||||
items={applicationsState.loading ? "..." : appsInCategory(category)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoriesList;
|
|
@ -0,0 +1,40 @@
|
|||
@import "../../shared/css/colors.scss";
|
||||
|
||||
$top-bottom-padding: 2px;
|
||||
$left-padding: 9px;
|
||||
$right-padding: 15px;
|
||||
|
||||
$height: 28px;
|
||||
|
||||
.container {
|
||||
color: $white;
|
||||
padding: $top-bottom-padding $right-padding $top-bottom-padding $left-padding;
|
||||
height: $height;
|
||||
line-height: calc(#{$height} - #{2 * $top-bottom-padding});
|
||||
box-sizing: border-box;
|
||||
|
||||
border-top-left-radius: 17px;
|
||||
border-bottom-left-radius: 17px;
|
||||
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
background-color: $charcoal;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
padding: 0 4px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import React, { FC } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
|
||||
import PathBuilder from "../../../services/PathBuilder";
|
||||
|
||||
import styles from "./CategoryItem.module.scss";
|
||||
|
||||
type CategoryItemProps = {
|
||||
label: string;
|
||||
path: string;
|
||||
items?: number | string;
|
||||
badge?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const CategoryItem: FC<CategoryItemProps> = ({
|
||||
path,
|
||||
label,
|
||||
items = 0,
|
||||
className = "",
|
||||
badge = true,
|
||||
}) => (
|
||||
<NavLink
|
||||
activeClassName={styles.active}
|
||||
to={PathBuilder.build(path)}
|
||||
className={`${styles.container} ${className}`}
|
||||
exact
|
||||
>
|
||||
<span>{label}</span>
|
||||
{badge && <span className={styles.badge}>{items}</span>}
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
export default CategoryItem;
|
31
page-src/src/app/components/Category/Category.module.scss
Normal file
31
page-src/src/app/components/Category/Category.module.scss
Normal file
|
@ -0,0 +1,31 @@
|
|||
.container {
|
||||
display: grid;
|
||||
grid-template-rows: 250px 1fr;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
.applications {
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 5px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
align-content: start;
|
||||
|
||||
@media (max-width: 1600px) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
@media (max-width: 1300px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
}
|
49
page-src/src/app/components/Category/Category.tsx
Normal file
49
page-src/src/app/components/Category/Category.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import React, { FC, useMemo } from "react";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import ApplicationCard from "../shared/ApplicatioinCard/ApplicationCard";
|
||||
import Application from "../../models/application";
|
||||
import TCategory from "../../models/category";
|
||||
import { RootState } from "../../reducers";
|
||||
import { ApplicationsState } from "../../reducers/application";
|
||||
|
||||
import styles from "./Category.module.scss";
|
||||
|
||||
const Category: FC = () => {
|
||||
const params = useParams<{ category: string }>();
|
||||
|
||||
const applicationsState = useSelector<RootState, ApplicationsState>(
|
||||
(state) => state.applications,
|
||||
);
|
||||
|
||||
const appsInCategory = useMemo<Application[]>(
|
||||
() =>
|
||||
applicationsState.data.reduce(
|
||||
(acc: Application[], application: Application) => [
|
||||
...acc,
|
||||
...(application.categories.some((c: TCategory) => c.shortName === params.category)
|
||||
? [application]
|
||||
: []),
|
||||
],
|
||||
[],
|
||||
),
|
||||
[params, applicationsState],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>Header</div>
|
||||
<div className={styles.applications}>
|
||||
{appsInCategory.map((app: Application) => (
|
||||
<div className={styles.item} key={app.id}>
|
||||
<ApplicationCard application={app} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Category;
|
5
page-src/src/app/components/Home/Home.tsx
Normal file
5
page-src/src/app/components/Home/Home.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React, { FC } from "react";
|
||||
|
||||
const Home: FC = () => <div>Home</div>;
|
||||
|
||||
export default Home;
|
|
@ -0,0 +1,189 @@
|
|||
@import "../css/colors.scss";
|
||||
|
||||
$height: 120px;
|
||||
$height-hover: 200px;
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
height: $height;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.wrapper {
|
||||
z-index: 100;
|
||||
height: $height-hover;
|
||||
transform: scale(1.05);
|
||||
|
||||
.image {
|
||||
border-radius: 0;
|
||||
width: $height-hover;
|
||||
height: $height-hover;
|
||||
}
|
||||
|
||||
.right {
|
||||
grid-template-rows: 30px 1fr 60px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
grid-template-columns: none;
|
||||
height: 60px;
|
||||
justify-content: flex-end;
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.stars,
|
||||
.forks,
|
||||
.watchers {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 70px
|
||||
}
|
||||
|
||||
.forks {
|
||||
padding: 3px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
.wrapper {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.top {
|
||||
.wrapper {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
height: $height;
|
||||
width: 100%;
|
||||
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: $height 1fr;
|
||||
box-shadow: 0px 0px 10px -5px $charcoal;
|
||||
|
||||
border-radius: 5px;
|
||||
|
||||
transform: scale(1);
|
||||
transition: all 0.15s linear;
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 30px 1fr 30px;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
transition: height 0.15s linear, border-radius 0.15s linear;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.description {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background: rgb(255,255,255);
|
||||
background: linear-gradient(0deg, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
font-size: 13px;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.icon {
|
||||
width: 15px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: none;
|
||||
text-align: end;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.stars,
|
||||
.forks,
|
||||
.watchers {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.forks {
|
||||
padding: 0 3px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import React, { FC, useCallback, useState } from "react";
|
||||
import { faCodeBranch, faEye, faStar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import Application from "../../../models/application";
|
||||
|
||||
import styles from "./ApplicationCard.module.scss";
|
||||
|
||||
import icon from "../../../../assets/placeholder_svg.svg";
|
||||
|
||||
type ApplicationCardProps = {
|
||||
application: Application;
|
||||
};
|
||||
|
||||
// TODO: split component
|
||||
|
||||
const ApplicationCard: FC<ApplicationCardProps> = ({ application }) => {
|
||||
const { title } = application;
|
||||
|
||||
const [animationPosition, setAnimationPosition] = useState<string>("");
|
||||
|
||||
const measuredRef = useCallback((node) => {
|
||||
if (node !== null) {
|
||||
const targetRect = node.getBoundingClientRect();
|
||||
const bodyRect = document.querySelector("body")?.getBoundingClientRect();
|
||||
|
||||
setAnimationPosition(
|
||||
bodyRect && bodyRect.height + bodyRect.top - targetRect.top < 200
|
||||
? styles.bottom
|
||||
: styles.top,
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const img = application.screenshots.length ? application.screenshots[0].url : icon;
|
||||
|
||||
return (
|
||||
<div className={`${styles.container} ${animationPosition}`} ref={measuredRef}>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.left}>
|
||||
<div className={styles.image}>
|
||||
<img src={img} alt="img" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.right}>
|
||||
<div className={styles.header}>
|
||||
<span className={styles.title}>{title}</span>
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
<p>{application.description}</p>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<span className={styles.stars}>
|
||||
<span className={styles.label}>Stars</span>
|
||||
<span className={styles.data}>
|
||||
<span className={styles.icon}>
|
||||
<FontAwesomeIcon icon={faStar} color="#ffca28" />
|
||||
</span>
|
||||
{application.stars || "-"}
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.forks}>
|
||||
<span className={styles.label}>Forks</span>
|
||||
<span className={styles.data}>
|
||||
<span className={styles.icon}>
|
||||
<FontAwesomeIcon icon={faCodeBranch} color="#aaa" />
|
||||
</span>
|
||||
{application.forks || "-"}
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.watchers}>
|
||||
<span className={styles.label}>Watchers</span>
|
||||
<span className={styles.data}>
|
||||
<span className={styles.icon}>
|
||||
<FontAwesomeIcon icon={faEye} color="#007eff" />
|
||||
</span>
|
||||
{application.watchers || "-"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApplicationCard;
|
8
page-src/src/app/components/shared/css/colors.scss
Normal file
8
page-src/src/app/components/shared/css/colors.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
$white: #ffffffff;
|
||||
$black: #000000ff;
|
||||
|
||||
$charcoal: #264653ff;
|
||||
$persian-green: #2a9d8fff;
|
||||
$orange-yellow-crayola: #e9c46aff;
|
||||
$sandy-brown: #f4a261ff;
|
||||
$burnt-sienna: #e76f51ff;
|
19
page-src/src/app/models/application.ts
Normal file
19
page-src/src/app/models/application.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import Category from "./category";
|
||||
import Language from "./language";
|
||||
import Screenshot from "./screenshot";
|
||||
|
||||
export type Application = {
|
||||
id: string;
|
||||
forks: number;
|
||||
watchers: number;
|
||||
repo_url: string;
|
||||
stars: number;
|
||||
title: string;
|
||||
description: string;
|
||||
icon_url: string;
|
||||
languages: Language[];
|
||||
categories: Category[];
|
||||
screenshots: Screenshot[];
|
||||
};
|
||||
|
||||
export default Application;
|
7
page-src/src/app/models/category.ts
Normal file
7
page-src/src/app/models/category.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
type Category = {
|
||||
id: string;
|
||||
name: string;
|
||||
shortName: string;
|
||||
};
|
||||
|
||||
export default Category;
|
6
page-src/src/app/models/language.ts
Normal file
6
page-src/src/app/models/language.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export type Language = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export default Language;
|
6
page-src/src/app/models/screenshot.ts
Normal file
6
page-src/src/app/models/screenshot.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export type Screenshot = {
|
||||
id: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default Screenshot;
|
55
page-src/src/app/reducers/application.ts
Normal file
55
page-src/src/app/reducers/application.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import {
|
||||
ApplicationActions,
|
||||
FetchAllApplicationsAction,
|
||||
FetchAllApplicationsFailedAction,
|
||||
FetchAllApplicationsFailedPayload,
|
||||
FetchAllApplicationsSucceedAction,
|
||||
FetchAllApplicationsSucceedPayload,
|
||||
} from "../actions/application";
|
||||
import Application from "../models/application";
|
||||
|
||||
export type ApplicationsState = {
|
||||
loading: boolean;
|
||||
error: unknown;
|
||||
data: Application[];
|
||||
};
|
||||
|
||||
export const InitialApplicationsState: ApplicationsState = {
|
||||
loading: false,
|
||||
error: null,
|
||||
data: [],
|
||||
};
|
||||
|
||||
const FetchAllApplications = (state: ApplicationsState) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
});
|
||||
|
||||
const FetchAllApplicationsSucceed = (
|
||||
state: ApplicationsState,
|
||||
payload: FetchAllApplicationsSucceedPayload,
|
||||
) => ({ ...state, data: payload, error: null, loading: false });
|
||||
|
||||
const FetchAllApplicationsFailed = (
|
||||
state: ApplicationsState,
|
||||
payload: FetchAllApplicationsFailedPayload,
|
||||
) => ({ ...state, data: [], error: payload, loading: false });
|
||||
|
||||
export const applications = (
|
||||
state: ApplicationsState = InitialApplicationsState,
|
||||
action:
|
||||
| FetchAllApplicationsAction
|
||||
| FetchAllApplicationsSucceedAction
|
||||
| FetchAllApplicationsFailedAction,
|
||||
): ApplicationsState => {
|
||||
switch (action.type) {
|
||||
case ApplicationActions.FETCH_ALL:
|
||||
return FetchAllApplications(state);
|
||||
case ApplicationActions.FETCH_ALL_SUCCEED:
|
||||
return FetchAllApplicationsSucceed(state, action.payload);
|
||||
case ApplicationActions.FETCH_ALL_FAILED:
|
||||
return FetchAllApplicationsFailed(state, action.payload);
|
||||
default:
|
||||
return { ...state };
|
||||
}
|
||||
};
|
61
page-src/src/app/reducers/categories.ts
Normal file
61
page-src/src/app/reducers/categories.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import {
|
||||
CategoryActions,
|
||||
FetchAllCategoriesAction,
|
||||
FetchAllCategoriesFailedPayload,
|
||||
FetchAllCategoriesSucceedAction,
|
||||
FetchAllCategoriesSucceedPayload,
|
||||
FetchAllCategoriesFailedAction,
|
||||
} from "../actions/category";
|
||||
import Category from "../models/category";
|
||||
|
||||
export type CategoriesState = {
|
||||
loading: boolean;
|
||||
error: any;
|
||||
data: Category[];
|
||||
};
|
||||
|
||||
export const InitialCategoriesState: CategoriesState = {
|
||||
loading: false,
|
||||
error: null,
|
||||
data: [],
|
||||
};
|
||||
|
||||
const FetchAllCategories = (state: CategoriesState) => ({ ...state, loading: true });
|
||||
|
||||
const FetchAllCategoriesSucceed = (
|
||||
state: CategoriesState,
|
||||
payload: FetchAllCategoriesSucceedPayload,
|
||||
) => {
|
||||
const categories = payload;
|
||||
|
||||
return { ...state, data: categories, error: null, loading: false };
|
||||
};
|
||||
|
||||
const FetchAllCategoriesFailed = (
|
||||
state: CategoriesState,
|
||||
payload: FetchAllCategoriesFailedPayload,
|
||||
) => ({
|
||||
...state,
|
||||
data: [],
|
||||
error: payload,
|
||||
loading: false,
|
||||
});
|
||||
|
||||
export const categories = (
|
||||
state: CategoriesState = InitialCategoriesState,
|
||||
action:
|
||||
| FetchAllCategoriesAction
|
||||
| FetchAllCategoriesSucceedAction
|
||||
| FetchAllCategoriesFailedAction,
|
||||
): CategoriesState => {
|
||||
switch (action.type) {
|
||||
case CategoryActions.FETCH_ALL:
|
||||
return FetchAllCategories(state);
|
||||
case CategoryActions.FETCH_ALL_SUCCEED:
|
||||
return FetchAllCategoriesSucceed(state, action.payload);
|
||||
case CategoryActions.FETCH_ALL_FAILED:
|
||||
return FetchAllCategoriesFailed(state, action.payload);
|
||||
default:
|
||||
return { ...state };
|
||||
}
|
||||
};
|
13
page-src/src/app/reducers/index.ts
Normal file
13
page-src/src/app/reducers/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { combineReducers } from "redux";
|
||||
|
||||
import { applications } from "./application";
|
||||
import { categories } from "./categories";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
applications,
|
||||
categories,
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
|
||||
export default rootReducer;
|
23
page-src/src/app/sagas/application/fetchAll.ts
Normal file
23
page-src/src/app/sagas/application/fetchAll.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { takeLatest, call, put } from "redux-saga/effects";
|
||||
|
||||
import {
|
||||
ApplicationActions,
|
||||
fetchAllApplicationsFailedAction,
|
||||
fetchAllApplicationsSucceedAction,
|
||||
} from "../../actions/application";
|
||||
import Application from "../../models/application";
|
||||
|
||||
import { FetchAllApplications } from "../../services/Api";
|
||||
|
||||
function* fetchAll() {
|
||||
try {
|
||||
const applications: Application[] = yield call(FetchAllApplications);
|
||||
yield put(fetchAllApplicationsSucceedAction(applications));
|
||||
} catch (error) {
|
||||
yield put(fetchAllApplicationsFailedAction(error));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* applicationWatchers(): Generator {
|
||||
yield takeLatest(ApplicationActions.FETCH_ALL, fetchAll);
|
||||
}
|
5
page-src/src/app/sagas/application/index.ts
Normal file
5
page-src/src/app/sagas/application/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import applicationFetchAllWatcher from "./fetchAll";
|
||||
|
||||
const applicationWatchers = [applicationFetchAllWatcher()];
|
||||
|
||||
export default applicationWatchers;
|
23
page-src/src/app/sagas/category/fetchAll.ts
Normal file
23
page-src/src/app/sagas/category/fetchAll.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { takeLatest, call, put } from "redux-saga/effects";
|
||||
|
||||
import {
|
||||
CategoryActions,
|
||||
fetchAllCategoriesSucceedAction,
|
||||
fetchCategoryFailedAction,
|
||||
} from "../../actions/category";
|
||||
import Category from "../../models/category";
|
||||
|
||||
import { FetchAllCategories } from "../../services/Api";
|
||||
|
||||
function* fetchAll() {
|
||||
try {
|
||||
const categories: Category[] = yield call(FetchAllCategories);
|
||||
yield put(fetchAllCategoriesSucceedAction(categories));
|
||||
} catch (error) {
|
||||
yield put(fetchCategoryFailedAction(error));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* categoryFetchAllWatcher(): Generator {
|
||||
yield takeLatest(CategoryActions.FETCH_ALL, fetchAll);
|
||||
}
|
5
page-src/src/app/sagas/category/index.ts
Normal file
5
page-src/src/app/sagas/category/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import categoryFetchAllWatcher from "./fetchAll";
|
||||
|
||||
const categoryWatchers = [categoryFetchAllWatcher()];
|
||||
|
||||
export default categoryWatchers;
|
7
page-src/src/app/sagas/index.ts
Normal file
7
page-src/src/app/sagas/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { all } from "redux-saga/effects";
|
||||
import categoryWatchers from "./category";
|
||||
import applicationWatchers from "./application";
|
||||
|
||||
export default function* rootSaga(): Generator {
|
||||
yield all([...categoryWatchers, ...applicationWatchers]);
|
||||
}
|
11
page-src/src/app/services/Api/index.ts
Normal file
11
page-src/src/app/services/Api/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import config from "../../../config";
|
||||
|
||||
export const FetchAllCategories = (): Promise<any> =>
|
||||
fetch(`http://${config.api.url}:${config.api.port}/categories`).then((response: Response) =>
|
||||
response.json(),
|
||||
);
|
||||
|
||||
export const FetchAllApplications = (): Promise<any> =>
|
||||
fetch(`http://${config.api.url}:${config.api.port}/apps`).then((response: Response) =>
|
||||
response.json(),
|
||||
);
|
9
page-src/src/app/services/PathBuilder.ts
Normal file
9
page-src/src/app/services/PathBuilder.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import config from "../../config";
|
||||
|
||||
class PathBuilder {
|
||||
static build(path = "/"): string {
|
||||
return `/${config.repository_name}${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default PathBuilder;
|
BIN
page-src/src/assets/app_icon.png
Normal file
BIN
page-src/src/assets/app_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
13
page-src/src/assets/placeholder_svg.svg
Normal file
13
page-src/src/assets/placeholder_svg.svg
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
|
||||
<g>
|
||||
<path fill="#B3E5FC" d="M34.4,43H13.6C8.9,43,5,39.1,5,34.4V13.6C5,8.9,8.9,5,13.6,5h20.7c4.8,0,8.6,3.9,8.6,8.6v20.7
|
||||
C43,39.1,39.1,43,34.4,43z"/>
|
||||
</g>
|
||||
<path fill="#03A9F4" d="M43,16h-2v6h2V16z M43,26h-2v6h2V26z M7,16H5v6h2V16z M7,26H5v6h2V26z M16,7h6V5h-6V7z M26,7h6V5h-6V7z
|
||||
M16,43h6v-2h-6V43z M27,43h6v-2h-6V43z M12,42.8c-3.5-0.7-6.2-3.4-6.8-6.8h2.1c0.6,2.4,2.4,4.2,4.8,4.8V42.8z M12,5.2v2.1
|
||||
C9.6,7.8,7.8,9.6,7.2,12H5.2C5.8,8.5,8.5,5.8,12,5.2z M36,7.2V5.2c3.5,0.7,6.2,3.4,6.8,6.8h-2.1C40.2,9.6,38.4,7.8,36,7.2z M40.8,36
|
||||
h2.1c-0.7,3.5-3.4,6.2-6.8,6.8v-2.1C38.4,40.2,40.2,38.4,40.8,36z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 930 B |
7
page-src/src/config.ts
Normal file
7
page-src/src/config.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
repository_name: "test-gh-page",
|
||||
api: {
|
||||
url: process.env.REACT_APP_API_URL,
|
||||
port: process.env.REACT_APP_API_PORT,
|
||||
},
|
||||
};
|
23
page-src/src/index.scss
Normal file
23
page-src/src/index.scss
Normal file
|
@ -0,0 +1,23 @@
|
|||
@import url("https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap");
|
||||
|
||||
html {
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
display: grid;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
||||
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-family: "Quicksand", sans-serif;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 1100px;
|
||||
}
|
24
page-src/src/index.tsx
Normal file
24
page-src/src/index.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import { Provider } from "react-redux";
|
||||
import store from "./setupStore";
|
||||
|
||||
import "./index.scss";
|
||||
|
||||
import App from "./app/App";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root"),
|
||||
);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorker.unregister();
|
1
page-src/src/react-app-env.d.ts
vendored
Normal file
1
page-src/src/react-app-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="react-scripts" />
|
144
page-src/src/serviceWorker.ts
Normal file
144
page-src/src/serviceWorker.ts
Normal file
|
@ -0,0 +1,144 @@
|
|||
/* eslint-disable */
|
||||
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === "localhost" ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === "[::1]" ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/),
|
||||
);
|
||||
|
||||
type Config = {
|
||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||
};
|
||||
|
||||
export function register(config?: Config) {
|
||||
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
"This web app is being served cache-first by a service " +
|
||||
"worker. To learn more, visit https://bit.ly/CRA-PWA",
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl: string, config?: Config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === "installed") {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
"New content is available and will be used when all " +
|
||||
"tabs for this page are closed. See https://bit.ly/CRA-PWA.",
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log("Content is cached for offline use.");
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error during service worker registration:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { "Service-Worker": "script" },
|
||||
})
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf("javascript") === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log("No internet connection found. App is running in offline mode.");
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) => {
|
||||
registration.unregister();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
}
|
22
page-src/src/setupStore.ts
Normal file
22
page-src/src/setupStore.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { createStore, applyMiddleware, compose } from "redux";
|
||||
|
||||
import createSagaMiddleware from "redux-saga";
|
||||
|
||||
import { composeWithDevTools } from "redux-devtools-extension";
|
||||
import logger from "redux-logger";
|
||||
|
||||
import reducers from "./app/reducers";
|
||||
import rootSaga from "./app/sagas";
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
|
||||
const middlewares = [logger, sagaMiddleware];
|
||||
const middlewareEnhancer = composeWithDevTools(applyMiddleware(...middlewares));
|
||||
|
||||
const enhancers = [middlewareEnhancer];
|
||||
|
||||
const store = createStore(reducers, compose(...enhancers));
|
||||
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
export default store;
|
5
page-src/src/setupTests.ts
Normal file
5
page-src/src/setupTests.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import "@testing-library/jest-dom/extend-expect";
|
26
page-src/tsconfig.json
Normal file
26
page-src/tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
12424
page-src/yarn.lock
Normal file
12424
page-src/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue