Compare commits

...

7 commits

Author SHA1 Message Date
Taras Kerod
1b0452e63e Merge branch 'master' into gh-pages-v1 2021-08-24 22:00:43 +03:00
Taras Kerod
8dcad9625f - packages updated
- models refactored
- updated the eslint config
- updated the project folders structure
- prepared small TODO's list for project improving
- removed unnecessary packages
- implemented small components updates
2021-08-24 21:59:33 +03:00
Taras Kerod
05aa21bf63 added icons (stars, forks, watchers) and some new style for apps tiles 2021-01-17 17:42:58 +02:00
Taras Kerod
8a99255347 app tile UI is changed 2021-01-17 16:37:18 +02:00
Taras Kerod
fad7fd6c7c application card
added initial "application card" ui
2020-10-15 22:04:28 +03:00
Taras Kerod
df66c9d4ed initial
some initial project structure
2020-09-30 21:58:52 +03:00
Taras Kerod
d1713a6aa8 gh-pages initial changes 2020-09-27 18:18:24 +03:00
54 changed files with 14002 additions and 0 deletions

18
page-src/.editorconfig Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
build

View file

@ -0,0 +1,6 @@
{
"printWidth": 100,
"trailingComma": "all",
"tabWidth": 2,
"semi": true
}

8
page-src/.vscode/settings.json vendored Normal file
View 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
View 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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View 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"
}

View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View 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
View 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;

View 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,
});

View 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,
});

View file

@ -0,0 +1,5 @@
import React, { FC } from "react";
const Application: FC = () => <div>Application</div>;
export default Application;

View file

@ -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%);
}
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;

View 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;
}

View 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;

View file

@ -0,0 +1,5 @@
import React, { FC } from "react";
const Home: FC = () => <div>Home</div>;
export default Home;

View file

@ -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;
}
}

View file

@ -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;

View file

@ -0,0 +1,8 @@
$white: #ffffffff;
$black: #000000ff;
$charcoal: #264653ff;
$persian-green: #2a9d8fff;
$orange-yellow-crayola: #e9c46aff;
$sandy-brown: #f4a261ff;
$burnt-sienna: #e76f51ff;

View 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;

View file

@ -0,0 +1,7 @@
type Category = {
id: string;
name: string;
shortName: string;
};
export default Category;

View file

@ -0,0 +1,6 @@
export type Language = {
id: string;
name: string;
};
export default Language;

View file

@ -0,0 +1,6 @@
export type Screenshot = {
id: string;
url: string;
};
export default Screenshot;

View 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 };
}
};

View 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 };
}
};

View 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;

View 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);
}

View file

@ -0,0 +1,5 @@
import applicationFetchAllWatcher from "./fetchAll";
const applicationWatchers = [applicationFetchAllWatcher()];
export default applicationWatchers;

View 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);
}

View file

@ -0,0 +1,5 @@
import categoryFetchAllWatcher from "./fetchAll";
const categoryWatchers = [categoryFetchAllWatcher()];
export default categoryWatchers;

View 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]);
}

View 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(),
);

View file

@ -0,0 +1,9 @@
import config from "../../config";
class PathBuilder {
static build(path = "/"): string {
return `/${config.repository_name}${path}`;
}
}
export default PathBuilder;

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View 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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View 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);
});
}
}

View 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;

View 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
View 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

File diff suppressed because it is too large Load diff