removed notification + new react structure

This commit is contained in:
2022-11-23 23:36:30 +01:00
parent 303a5b69b5
commit b1c4c9e976
76 changed files with 10221 additions and 616 deletions

View File

@@ -1,159 +0,0 @@
<?php
namespace Core\API {
use Core\Objects\Context;
abstract class NewsAPI extends Request {
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
parent::__construct($context, $externalCall, $params);
$this->loginRequired = true;
}
}
}
namespace Core\API\News {
use Core\API\NewsAPI;
use Core\API\Parameter\Parameter;
use Core\API\Parameter\StringType;
use Core\Driver\SQL\Condition\Compare;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Group;
use Core\Objects\DatabaseEntity\News;
class Get extends NewsAPI {
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
"since" => new Parameter("since", Parameter::TYPE_DATE_TIME, true, null),
"limit" => new Parameter("limit", Parameter::TYPE_INT, true, 10)
]);
$this->loginRequired = false;
}
public function _execute(): bool {
$since = $this->getParam("since");
$limit = $this->getParam("limit");
if ($limit < 1 || $limit > 30) {
return $this->createError("Limit must be in range 1-30");
}
$sql = $this->context->getSQL();
$newsQuery = News::createBuilder($sql, false)
->limit($limit)
->orderBy("published_at")
->descending()
->fetchEntities();
if ($since) {
$newsQuery->where(new Compare("published_at", $since, ">="));
}
$newsArray = News::findBy($newsQuery);
$this->success = $newsArray !== null;
$this->lastError = $sql->getLastError();
if ($this->success) {
$this->result["news"] = [];
foreach ($newsArray as $news) {
$newsId = $news->getId();
$this->result["news"][$newsId] = $news->jsonSerialize();
}
}
return $this->success;
}
}
class Publish extends NewsAPI {
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
"title" => new StringType("title", 128),
"text" => new StringType("text", 1024)
]);
$this->loginRequired = true;
}
public function _execute(): bool {
$news = new News();
$news->text = $this->getParam("text");
$news->title = $this->getParam("title");
$news->publishedBy = $this->context->getUser();
$sql = $this->context->getSQL();
$this->success = $news->save($sql);
$this->lastError = $sql->getLastError();
if ($this->success) {
$this->result["newsId"] = $news->getId();
}
return $this->success;
}
}
class Delete extends NewsAPI {
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
"id" => new Parameter("id", Parameter::TYPE_INT)
]);
$this->loginRequired = true;
}
public function _execute(): bool {
$sql = $this->context->getSQL();
$currentUser = $this->context->getUser();
$news = News::find($sql, $this->getParam("id"));
$this->success = ($news !== false);
$this->lastError = $sql->getLastError();
if (!$this->success) {
return false;
} else if ($news === null) {
return $this->createError("News Post not found");
} else if ($news->publishedBy->getId() !== $currentUser->getId() && !$currentUser->hasGroup(Group::ADMIN)) {
return $this->createError("You do not have permissions to delete news post of other users.");
}
$this->success = $news->delete($sql);
$this->lastError = $sql->getLastError();
return $this->success;
}
}
class Edit extends NewsAPI {
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
"id" => new Parameter("id", Parameter::TYPE_INT),
"title" => new StringType("title", 128),
"text" => new StringType("text", 1024)
]);
$this->loginRequired = true;
}
public function _execute(): bool {
$sql = $this->context->getSQL();
$currentUser = $this->context->getUser();
$news = News::find($sql, $this->getParam("id"));
$this->success = ($news !== false);
$this->lastError = $sql->getLastError();
if (!$this->success) {
return false;
} else if ($news === null) {
return $this->createError("News Post not found");
} else if ($news->publishedBy->getId() !== $currentUser->getId() && !$currentUser->hasGroup(Group::ADMIN)) {
return $this->createError("You do not have permissions to edit news post of other users.");
}
$news->text = $this->getParam("text");
$news->title = $this->getParam("title");
$this->success = $news->save($sql);
$this->lastError = $sql->getLastError();
return $this->success;
}
}
}

View File

@@ -5,7 +5,6 @@ namespace Core\API;
use Core\API\Parameter\StringType;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Group;
use Core\Objects\DatabaseEntity\User;
class Swagger extends Request {
@@ -26,7 +25,7 @@ class Swagger extends Request {
$classes = [];
$apiDirs = ["Core", "Site"];
foreach ($apiDirs as $apiDir) {
$basePath = realpath(WEBROOT . "/$apiDir/Api/");
$basePath = realpath(WEBROOT . "/$apiDir/API/");
if (!$basePath) {
continue;
}
@@ -36,7 +35,7 @@ class Swagger extends Request {
if (is_file($fullPath) && endsWith($fileName, ".class.php")) {
require_once $fullPath;
$apiName = explode(".", $fileName)[0];
$className = "\\API\\$apiName";
$className = "\\$apiDir\\API\\$apiName";
if (!class_exists($className)) {
var_dump("Class not exist: $className");
continue;
@@ -108,6 +107,7 @@ class Swagger extends Request {
$settings = $this->context->getSettings();
$siteName = $settings->getSiteName();
$domain = parse_url($settings->getBaseUrl(), PHP_URL_HOST);
$protocol = getProtocol();
$permissions = $this->fetchPermissions();
@@ -194,7 +194,7 @@ class Swagger extends Request {
],
"host" => $domain,
"basePath" => "/api",
"schemes" => ["https"],
"schemes" => ["$protocol"],
"paths" => $paths,
"definitions" => $definitions
];

View File

@@ -328,37 +328,13 @@ namespace Core\API\User {
} else {
$queriedUser = $user->jsonSerialize();
// either we are querying own info or we are support / admin
$currentUser = $this->context->getUser();
$canView = ($userId === $currentUser->getId() ||
$currentUser->hasGroup(Group::ADMIN) ||
$currentUser->hasGroup(Group::SUPPORT));
// full info only when we have administrative privileges, or we are querying ourselves
$fullInfo = ($userId === $currentUser->getId() ||
$currentUser->hasGroup(Group::ADMIN) ||
$currentUser->hasGroup(Group::SUPPORT));
if (!$canView) {
// check if user posted something publicly
$res = $sql->select(new JsonArrayAgg(new Column("publishedBy"), "publisherIds"))
->from("News")
->execute();
$this->success = ($res !== false);
$this->lastError = $sql->getLastError();
if (!$this->success) {
return false;
} else {
$canView = in_array($userId, json_decode($res[0]["publisherIds"], true));
}
}
if (!$canView) {
return $this->createError("No permissions to access this user");
}
if (!$fullInfo) {
if (!$queriedUser["confirmed"]) {
return $this->createError("No permissions to access this user");

View File

@@ -21,9 +21,9 @@ class CreateDatabase extends DatabaseScript {
]);
$queries[] = Group::getHandler($sql)->getInsertQuery([
new Group(Group::ADMIN, Group::GROUPS[Group::ADMIN], "#007bff"),
new Group(Group::ADMIN, Group::GROUPS[Group::ADMIN], "#dc3545"),
new Group(Group::MODERATOR, Group::GROUPS[Group::MODERATOR], "#28a745"),
new Group(Group::SUPPORT, Group::GROUPS[Group::SUPPORT], "#dc3545"),
new Group(Group::SUPPORT, Group::GROUPS[Group::SUPPORT], "#007bff"),
]);
$queries[] = $sql->createTable("Visitor")
@@ -84,6 +84,7 @@ class CreateDatabase extends DatabaseScript {
->addRow("Mail/Sync", array(Group::SUPPORT, Group::ADMIN), "Allows users to synchronize mails with the database")
->addRow("Settings/get", array(Group::ADMIN), "Allows users to fetch server settings")
->addRow("Settings/set", array(Group::ADMIN), "Allows users create, delete or modify server settings")
->addRow("Settings/generateJWT", array(Group::ADMIN), "Allows users generate a new jwt key")
->addRow("Stats", array(Group::ADMIN, Group::SUPPORT), "Allows users to fetch server stats")
->addRow("User/create", array(Group::ADMIN), "Allows users to create a new user, email address does not need to be confirmed")
->addRow("User/fetch", array(Group::ADMIN, Group::SUPPORT), "Allows users to list all registered users")

View File

@@ -47,15 +47,18 @@ class Settings {
public function getJwtPublicKey(bool $allowPrivate = true): ?\Firebase\JWT\Key {
if (empty($this->jwtPublicKey)) {
// we might have a symmetric key, should we instead return the private key?
return $allowPrivate ? new \Firebase\JWT\Key($this->jwtSecretKey, $this->jwtAlgorithm) : null;
if ($allowPrivate && $this->jwtSecretKey) {
// we might have a symmetric key, should we instead return the private key?
return new \Firebase\JWT\Key($this->jwtSecretKey, $this->jwtAlgorithm);
}
return null;
} else {
return new \Firebase\JWT\Key($this->jwtPublicKey, $this->jwtAlgorithm);
}
}
public function getJwtSecretKey(): \Firebase\JWT\Key {
return new \Firebase\JWT\Key($this->jwtSecretKey, $this->jwtAlgorithm);
public function getJwtSecretKey(): ?\Firebase\JWT\Key {
return $this->jwtSecretKey ? new \Firebase\JWT\Key($this->jwtSecretKey, $this->jwtAlgorithm) : null;
}
public function isInstalled(): bool {

View File

@@ -198,15 +198,6 @@ namespace Documents\Install {
$success = $req->execute(array("settings" => array("installation_completed" => "1")));
if (!$success) {
$this->errorString = $req->getLastError();
} else {
$req = new \Core\API\Notifications\Create($context);
$req->execute(array(
"title" => "Welcome",
"message" => "Your Web-base was successfully installed. Check out the admin dashboard. Have fun!",
"groupId" => Group::ADMIN
)
);
$this->errorString = $req->getLastError();
}
}
}

View File

@@ -103,8 +103,9 @@ class Context {
try {
$token = $_COOKIE['session'];
$settings = $this->configuration->getSettings();
$decoded = (array)JWT::decode($token, $settings->getJwtSecretKey());
if (!is_null($decoded)) {
$jwtKey = $settings->getJwtSecretKey();
if ($jwtKey) {
$decoded = (array)JWT::decode($token, $jwtKey);
$userId = ($decoded['userId'] ?? NULL);
$sessionId = ($decoded['sessionId'] ?? NULL);
if (!is_null($userId) && !is_null($sessionId)) {

View File

@@ -1,32 +0,0 @@
<?php
namespace Core\Objects\DatabaseEntity;
use Core\API\Parameter\Parameter;
use Core\Driver\SQL\Expression\CurrentTimeStamp;
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
class News extends DatabaseEntity {
public User $publishedBy;
#[DefaultValue(CurrentTimeStamp::class)] private \DateTime $publishedAt;
#[MaxLength(128)] public string $title;
#[MaxLength(1024)] public string $text;
public function __construct(?int $id = null) {
parent::__construct($id);
$this->publishedAt = new \DateTime();
}
public function jsonSerialize(): array {
return [
"id" => $this->getId(),
"publishedBy" => $this->publishedBy->jsonSerialize(),
"publishedAt" => $this->publishedAt->format(Parameter::DATE_TIME_FORMAT),
"title" => $this->title,
"text" => $this->text
];
}
}

View File

@@ -1,4 +1,4 @@
{% extends "account.twig" %}
{% extends "account/account_base.twig" %}
{% set view_title = 'Invitation' %}
{% set view_icon = 'user-check' %}

View File

@@ -1,4 +1,4 @@
{% extends "account.twig" %}
{% extends "account/account_base.twig" %}
{% set view_title = 'Confirm Email' %}
{% set view_icon = 'user-check' %}

View File

@@ -1,4 +1,4 @@
{% extends "account.twig" %}
{% extends "account/account_base.twig" %}
{% set view_title = 'Sign In' %}
{% set view_icon = 'user-lock' %}

View File

@@ -1,4 +1,4 @@
{% extends "account.twig" %}
{% extends "account/account_base.twig" %}
{% set view_title = 'Registration' %}
{% set view_icon = 'user-plus' %}

View File

@@ -1,4 +1,4 @@
{% extends "account.twig" %}
{% extends "account/account_base.twig" %}
{% set view_title = 'Resend Confirm Email' %}
{% set view_icon = 'envelope' %}

View File

@@ -1,4 +1,4 @@
{% extends "account.twig" %}
{% extends "account/account_base.twig" %}
{% set view_title = 'Reset Password' %}
{% set view_icon = 'user-lock' %}

View File

@@ -7,6 +7,6 @@
{% block body %}
<noscript>You need Javascript enabled to run this app</noscript>
<div class="wrapper" id="root"></div>
<script src="/js/admin.min.js" nonce="{{ site.csp.nonce }}"></script>
<div class="wrapper" type="module" id="admin-panel"></div>
<script src="/js/admin-panel/index.js" nonce="{{ site.csp.nonce }}"></script>
{% endblock %}

View File

@@ -10,7 +10,7 @@ if (is_file($autoLoad)) {
require_once $autoLoad;
}
define("WEBBASE_VERSION", "2.3.0");
const WEBBASE_VERSION = "2.3.0";
spl_autoload_extensions(".php");
spl_autoload_register(function ($class) {
@@ -81,7 +81,7 @@ function generateRandomString($length, $type = "ascii"): string {
return $randomString;
}
function base64url_decode($data) {
function base64url_decode($data): bool|string {
$base64 = strtr($data, '-_', '+/');
return base64_decode($base64);
}
@@ -247,7 +247,7 @@ function getClassName($class, bool $short = true): string {
}
}
function createError($msg) {
function createError($msg): array {
return ["success" => false, "msg" => $msg];
}

View File

@@ -1,3 +0,0 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}

View File

@@ -24,7 +24,7 @@ server {
}
# deny access to specific directories
location ~ ^/(files/uploaded|adminPanel|docker|Site|Core|test)/.*$ {
location ~ ^/(files/uploaded|react|docker|Site|Core|test)/.*$ {
rewrite ^(.*)$ /index.php?site=$1;
}

2
js/admin-panel/index.css Normal file
View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

32
js/admin-panel/index.js Normal file
View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

341
js/admin.min.js vendored
View File

File diff suppressed because one or more lines are too long

157
react/.gitignore vendored Normal file
View File

@@ -0,0 +1,157 @@
# Created by https://www.toptal.com/developers/gitignore/api/node,react
# Edit at https://www.toptal.com/developers/gitignore?templates=node,react
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### react ###
.DS_*
**/*.backup.*
**/*.back.*
node_modules
*.sublime*
psd
thumb
sketch
# End of https://www.toptal.com/developers/gitignore/api/node,react

View File

View File

@@ -0,0 +1,15 @@
const fs = require('fs');
const path = require('path');
const {
override,
removeModuleScopePlugin,
babelInclude,
} = require('customize-cra');
module.exports = override(
removeModuleScopePlugin(),
babelInclude([
path.resolve(path.join(__dirname, 'src')),
fs.realpathSync(path.join(__dirname, '../shared')),
]),
);

View File

@@ -0,0 +1,26 @@
{
"name": "admin-panel",
"version": "1.0.0",
"dependencies": {
"shared": "link:../shared"
},
"scripts": {
"debug": "react-app-rewired start"
},
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@@ -0,0 +1 @@
../../../css/

View File

@@ -0,0 +1 @@
../../../fonts

View File

@@ -0,0 +1,9 @@
<html>
<head>
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/fontawesome.min.css">
</head>
<body>
<div id="admin-panel"></div>
</body>
</html>

View File

@@ -0,0 +1,93 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './res/adminlte.min.css';
import './res/index.css';
import API from "shared/api";
import Icon from "shared/elements/icon";
class AdminDashboard extends React.Component {
constructor(props) {
super(props);
this.api = new API();
this.state = {
loaded: false,
dialog: { onClose: () => this.hideDialog() },
notifications: [ ],
contactRequests: [ ]
};
}
onUpdate() {
}
showDialog(message, title, options=["Close"], onOption = null) {
const props = { show: true, message: message, title: title, options: options, onOption: onOption };
this.setState({ ...this.state, dialog: { ...this.state.dialog, ...props } });
}
hideDialog() {
this.setState({ ...this.state, dialog: { ...this.state.dialog, show: false } });
}
componentDidMount() {
this.api.fetchUser().then(Success => {
if (!Success) {
document.location = "/admin";
} else {
this.fetchNotifications();
this.fetchContactRequests();
setInterval(this.onUpdate.bind(this), 60*1000);
this.setState({...this.state, loaded: true});
}
});
}
render() {
if (!this.state.loaded) {
return <b>Loading <Icon icon={"spinner"}/></b>
}
this.controlObj = {
showDialog: this.showDialog.bind(this),
api: this.api
};
return <b>test</b>
/*return <Router>
<Header {...this.controlObj} notifications={this.state.notifications} />
<Sidebar {...this.controlObj} notifications={this.state.notifications} contactRequests={this.state.contactRequests}/>
<div className={"content-wrapper p-2"}>
<section className={"content"}>
<Switch>
<Route path={"/admin/dashboard"}><Overview {...this.controlObj} notifications={this.state.notifications} /></Route>
<Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route>
<Route path={"/admin/user/add"}><CreateUser {...this.controlObj} /></Route>
<Route path={"/admin/user/edit/:userId"} render={(props) => {
let newProps = {...props, ...this.controlObj};
return <EditUser {...newProps} />
}}/>
<Route path={"/admin/user/permissions"}><PermissionSettings {...this.controlObj}/></Route>
<Route path={"/admin/group/add"}><CreateGroup {...this.controlObj} /></Route>
<Route exact={true} path={"/admin/contact/"}><ContactRequestOverview {...this.controlObj} /></Route>
<Route path={"/admin/visitors"}><Visitors {...this.controlObj} /></Route>
<Route path={"/admin/logs"}><Logs {...this.controlObj} notifications={this.state.notifications} /></Route>
<Route path={"/admin/settings"}><Settings {...this.controlObj} /></Route>
<Route path={"/admin/pages"}><PageOverview {...this.controlObj} /></Route>
<Route path={"/admin/help"}><HelpPage {...this.controlObj} /></Route>
<Route path={"*"}><View404 /></Route>
</Switch>
<Dialog {...this.state.dialog}/>
</section>
</div>
<Footer />
</Router>
}*/
}
}
ReactDOM.render(
<AdminDashboard />,
document.getElementById('admin-panel')
);

View File

@@ -0,0 +1,3 @@
{
"presets": ["react"],
}

View File

View File

@@ -0,0 +1 @@
DENY FROM ALL

View File

View File

@@ -25,9 +25,6 @@
"build": "webpack --mode production && mv dist/main.js ../js/admin.min.js",
"debug": "react-scripts start"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
.page-link { color: #222629; }
.page-link:hover { color: black; }
.ReactCollapse--collapse {
transition: height 500ms;
}

View File

File diff suppressed because one or more lines are too long

View File

39
react/package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "web-base-frontend",
"version": "1.0.0",
"description": "",
"private": true,
"targets": {
"admin-panel": {
"source": "./admin-panel/src/index.jsx",
"distDir": "./dist/admin-panel"
}
},
"workspaces": [
"admin-panel"
],
"scripts": {
"build": "npx parcel build",
"deploy": "cp -r dist/* ../js/",
"clean": "rm -rfd .parcel-cache dist/*"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.20.2",
"customize-cra": "^1.0.0",
"parcel": "^2.8.0",
"react-app-rewired": "^2.2.1",
"react-scripts": "^5.0.1"
},
"@parcel/bundler-default": {
"minBundles": 1,
"minBundleSize": 3000,
"maxParallelRequests": 1
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.3"
}
}

View File

@@ -1,4 +1,4 @@
import 'babel-polyfill';
// import 'babel-polyfill';
export default class API {
constructor() {
@@ -43,14 +43,6 @@ export default class API {
return this.apiCall("user/logout");
}
async getNotifications(onlyNew = true) {
return this.apiCall("notifications/fetch", { new: onlyNew });
}
async markNotificationsSeen() {
return this.apiCall("notifications/seen");
}
async getUser(id) {
return this.apiCall("user/get", { id: id });
}
@@ -118,16 +110,4 @@ export default class API {
async getVisitors(type, date) {
return this.apiCall("visitors/stats", { type: type, date: date });
}
async fetchContactRequests() {
return this.apiCall('contact/fetch');
}
async getContactMessages(id) {
return this.apiCall('contact/get', { requestId: id });
}
async sendContactMessage(id, message) {
return this.apiCall('contact/respond', { requestId: id, message: message });
}
};

View File

@@ -0,0 +1,24 @@
import React from 'react';
export default function Icon(props) {
let classes = props.className || [];
classes = Array.isArray(classes) ? classes : classes.toString().split(" ");
let type = props.type || "fas";
let icon = props.icon;
classes.push(type);
classes.push("fa-" + icon);
if (icon === "spinner" || icon === "circle-notch") {
classes.push("fa-spin");
}
let newProps = {...props, className: classes.join(" ") };
delete newProps["type"];
delete newProps["icon"];
return (
<i {...newProps} />
);
}

View File

@@ -0,0 +1,8 @@
{
"name": "shared",
"version": "1.0.0",
"devDependencies": { },
"author": "",
"license": "ISC",
"description": ""
}

9759
react/yarn.lock Normal file
View File

File diff suppressed because it is too large Load Diff