Add basic React navbar
This commit is contained in:
parent
1808f9887e
commit
8f297dbbc8
14 changed files with 6882 additions and 103 deletions
105
.gitignore
vendored
105
.gitignore
vendored
|
@ -1,104 +1,3 @@
|
||||||
# Logs
|
build
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-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/
|
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
|
||||||
typings/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# 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 variables file
|
|
||||||
.env
|
|
||||||
.env.test
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
.next
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
.nuxt
|
|
||||||
dist
|
dist
|
||||||
|
node_modules
|
||||||
# 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
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# TernJS port file
|
|
||||||
.tern-port
|
|
||||||
|
|
6351
package-lock.json
generated
Normal file
6351
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
50
package.json
Normal file
50
package.json
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"name": "portfolio",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "build/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack & tsc",
|
||||||
|
"watch": "npm-watch build"
|
||||||
|
},
|
||||||
|
"watch": {
|
||||||
|
"build": {
|
||||||
|
"patterns": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"quiet": true,
|
||||||
|
"silent": true,
|
||||||
|
"extensions": "js,jsx,ts,tsx,html,css"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/mongoose": "^5.7.36",
|
||||||
|
"@types/node": "^14.17.32",
|
||||||
|
"@types/react-dom": "^16.9.14",
|
||||||
|
"@types/ws": "^7.4.7",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"react": "^16.14.0",
|
||||||
|
"react-dom": "^16.14.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.15.8",
|
||||||
|
"@babel/preset-env": "^7.15.8",
|
||||||
|
"@babel/preset-react": "^7.14.5",
|
||||||
|
"@types/react": "^16.14.20",
|
||||||
|
"babel-loader": "^8.2.3",
|
||||||
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
|
"css-loader": "^5.2.7",
|
||||||
|
"html-webpack-plugin": "^4.5.2",
|
||||||
|
"license-webpack-plugin": "^2.3.21",
|
||||||
|
"npm-watch": "^0.7.0",
|
||||||
|
"style-loader": "^2.0.0",
|
||||||
|
"ts-loader": "^8.3.0",
|
||||||
|
"typescript": "^4.4.4",
|
||||||
|
"webpack": "^5.60.0",
|
||||||
|
"webpack-cli": "^4.9.1"
|
||||||
|
}
|
||||||
|
}
|
208
src/Database.ts
Normal file
208
src/Database.ts
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
import {Schema, Document, LeanDocument} from "mongoose";
|
||||||
|
import * as mongoose from "mongoose";
|
||||||
|
import {promisify} from "util";
|
||||||
|
import {randomBytes, pbkdf2} from "crypto";
|
||||||
|
|
||||||
|
const pbkdf = promisify(pbkdf2);
|
||||||
|
|
||||||
|
const User = mongoose.model("User", new Schema({
|
||||||
|
display: String,
|
||||||
|
email: {
|
||||||
|
public: Boolean,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
picture: String,
|
||||||
|
password: {
|
||||||
|
hash: Buffer,
|
||||||
|
salt: Buffer
|
||||||
|
},
|
||||||
|
bio: {type: String, default: ""},
|
||||||
|
isOfficer: {type: Boolean, default: false},
|
||||||
|
title: {type: String, default: "Member"},
|
||||||
|
uuid: String
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface DBUser extends Document {
|
||||||
|
display: string;
|
||||||
|
email: string;
|
||||||
|
picture: string;
|
||||||
|
password: HashSalt
|
||||||
|
isOfficer: boolean;
|
||||||
|
title: string;
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
interface HashSalt {
|
||||||
|
hash: Buffer;
|
||||||
|
salt: Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class UserDatabase {
|
||||||
|
|
||||||
|
// How many iterations pbkdf2 will iterate the password hashing algorithm
|
||||||
|
private static PW_ITERATIONS = 100000;
|
||||||
|
// The length of the salt that should be used with every password hash
|
||||||
|
private static SALT_LENGTH = 64;
|
||||||
|
// How many bytes will be used to generate a UUID of length 2 times this variable
|
||||||
|
private static UUID_LENGTH = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a new UserDatabase
|
||||||
|
* @param url the mongodb database url to connect to
|
||||||
|
*/
|
||||||
|
constructor(url: string) {
|
||||||
|
mongoose.connect(url, {useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false});
|
||||||
|
mongoose.set("returnOriginal", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Functions to edit current users
|
||||||
|
/**
|
||||||
|
* Changes a user's display name on the website
|
||||||
|
* @param uuid the UUID of the user
|
||||||
|
* @param newName the user's new name
|
||||||
|
*/
|
||||||
|
public async setName(uuid: string, newName: string) {
|
||||||
|
await User.findOneAndUpdate({uuid: uuid}, {name: newName}).exec();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the display name of a user with a given UUID
|
||||||
|
* @param uuid the UUID of the user
|
||||||
|
* @returns the user's display name
|
||||||
|
*/
|
||||||
|
public async getName(uuid: string): Promise<string> {
|
||||||
|
return (await User.findOne({uuid: uuid}).lean().exec() as DBUser).display;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a user's display email
|
||||||
|
* @param uuid the UUID of the user
|
||||||
|
* @param newEmail the user's new display email
|
||||||
|
*/
|
||||||
|
public async setEmail(uuid: string, newEmail: string) {
|
||||||
|
await User.findOneAndUpdate({uuid: uuid}, {email: newEmail}).exec();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the email of a user with a given UUID
|
||||||
|
* @param uuid the UUID of the user
|
||||||
|
* @returns the user's email
|
||||||
|
*/
|
||||||
|
public async getEmail(uuid: string): Promise<string> {
|
||||||
|
return (await User.findOne({uuid: uuid}).lean().exec() as DBUser).email;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a user's password
|
||||||
|
* @param uuid the UUID of the user
|
||||||
|
* @param newPassword the user's new password
|
||||||
|
*/
|
||||||
|
public async setPassword(uuid: string, newPassword: string) {
|
||||||
|
let hashsalt = await UserDatabase.hash(newPassword);
|
||||||
|
await User.findOneAndUpdate({uuid: uuid}, {password: hashsalt}).exec();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the password of a user with a given UUID
|
||||||
|
* @param uuid the UUID of the user
|
||||||
|
* @returns the user's password
|
||||||
|
*/
|
||||||
|
public async getPasswordHash(uuid: string): Promise<Buffer> {
|
||||||
|
return (await User.findOne({uuid: uuid}).lean().exec() as DBUser).password.hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a member's position
|
||||||
|
* @param uuid the UUID of the user
|
||||||
|
* @param newTitle the user's new officer title/position
|
||||||
|
*/
|
||||||
|
public async setRank(uuid: string, newTitle: string, isOfficer: boolean) {
|
||||||
|
await User.findOneAndUpdate({uuid: uuid}, {title: newTitle, isOfficer: isOfficer}).exec();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the title of a user with a given UUID
|
||||||
|
* @param uuid the UUID of the user
|
||||||
|
* @returns the user's title
|
||||||
|
*/
|
||||||
|
public async getRank(uuid: string): Promise<string> {
|
||||||
|
return (await User.findOne({uuid: uuid}).lean().exec() as DBUser).title;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets whether of a user with a given UUID is an officer
|
||||||
|
* @param uuid the UUID of the user
|
||||||
|
* @returns true if the user is an officer
|
||||||
|
*/
|
||||||
|
public async isOfficer(uuid: string): Promise<boolean> {
|
||||||
|
return (await User.findOne({uuid: uuid}).lean().exec() as DBUser).isOfficer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the user's new profile picture
|
||||||
|
* @param uuid the UUID of the user
|
||||||
|
* @param newPic the link to the new URL of the profile picture
|
||||||
|
*/
|
||||||
|
public async setPicture(uuid: string, newPic: string) {
|
||||||
|
await User.findOneAndUpdate({uuid: uuid}, {picture: newPic}).exec();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the profile picture of a user with a given UUID
|
||||||
|
* @param uuid the UUID of the user
|
||||||
|
* @returns the user's profile picture
|
||||||
|
*/
|
||||||
|
public async getPicture(uuid: string): Promise<string> {
|
||||||
|
return (await User.findOne({uuid: uuid}).lean().exec() as DBUser).picture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getUser(uuid: string): Promise<DBUser> {
|
||||||
|
return User.findOne({uuid: uuid}).exec() as Promise<DBUser>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User creation and deletion functions
|
||||||
|
/**
|
||||||
|
* Deletes a user with the given UUID
|
||||||
|
* @param uuid the UUID of the user
|
||||||
|
*/
|
||||||
|
public async deleteUser(uuid: string) {
|
||||||
|
await User.findOneAndDelete({uuid: uuid}).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a new user and adds it to the database
|
||||||
|
* @param display the user's display name
|
||||||
|
* @param email the user's display email
|
||||||
|
* @param picture the user's picture
|
||||||
|
* @param password the user's plaintext password
|
||||||
|
*/
|
||||||
|
public async makeNewUser(display: string, email: string, picture: string, password: string, title?: string, isOfficer?: boolean): Promise<void> {
|
||||||
|
let encrypted = await UserDatabase.hash(password);
|
||||||
|
await (new User({
|
||||||
|
display: display,
|
||||||
|
email: email,
|
||||||
|
picture: picture,
|
||||||
|
password: encrypted,
|
||||||
|
uuid: UserDatabase.genUUID(),
|
||||||
|
isOfficer: !!isOfficer,
|
||||||
|
title: title ? title : "Member"
|
||||||
|
})).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
/**
|
||||||
|
* Hashes a password using a salt of 64 random bytes and pbkdf2 with 100000 iterations
|
||||||
|
* @param password the password as a string that should be hashed
|
||||||
|
* @returns the hashed password
|
||||||
|
*/
|
||||||
|
public static async hash(password: string): Promise<HashSalt> {
|
||||||
|
let salt = randomBytes(UserDatabase.SALT_LENGTH);
|
||||||
|
let pass = Buffer.from(String.prototype.normalize(password));
|
||||||
|
return {
|
||||||
|
hash: await pbkdf(pass, salt, UserDatabase.PW_ITERATIONS, 512, "sha512"),
|
||||||
|
salt: salt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a UUID for a user or anything else
|
||||||
|
* @returns a random hex string of length 2 * UUID_LENGTH
|
||||||
|
*/
|
||||||
|
public static genUUID(): string {
|
||||||
|
return Array.from(randomBytes(UserDatabase.UUID_LENGTH))
|
||||||
|
.map(val=>(val.toString(16).padStart(2,"0"))).join("");
|
||||||
|
}
|
||||||
|
}
|
67
src/index.ts
Normal file
67
src/index.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import express from "express";
|
||||||
|
import {Request, Response} from "express";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import UserDatabase from "./Database";
|
||||||
|
|
||||||
|
interface Website {
|
||||||
|
[key: string]: string;
|
||||||
|
sitename: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
jsfile: string;
|
||||||
|
cssfile: string;
|
||||||
|
themecolor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const template = fs.readFileSync(path.join(__dirname, "public/template.html")).toString();
|
||||||
|
const websites = [
|
||||||
|
{
|
||||||
|
sitename: "index",
|
||||||
|
title: "IEEE at UCSD",
|
||||||
|
description: "",
|
||||||
|
jsfile: "js/index.js",
|
||||||
|
cssfile: "css/styles.css",
|
||||||
|
themecolor: ""
|
||||||
|
}
|
||||||
|
] as Website[];
|
||||||
|
|
||||||
|
const PORT = 8080;
|
||||||
|
|
||||||
|
// Make the public directory traversible to people online
|
||||||
|
app.use(express.static(path.join(__dirname, "public")));
|
||||||
|
// Put the cookies as a variable in the request
|
||||||
|
app.use((req: Request, res: Response, next: any)=>{
|
||||||
|
req.cookies = req.headers.cookie;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
// Receive json post requests and urlencoded requests
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({extended: true}));
|
||||||
|
|
||||||
|
// Send main page
|
||||||
|
app.get("/", (req: Request, res: Response) => {
|
||||||
|
respond(res, "index");
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for above methods
|
||||||
|
*/
|
||||||
|
function respond(res: any, filename: string) {
|
||||||
|
res.set({
|
||||||
|
"Content-Type": "text/html"
|
||||||
|
});
|
||||||
|
res.send(generatePage(filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePage(name: string): string {
|
||||||
|
let site = websites.find(e=>e.sitename===name);
|
||||||
|
let html = template;
|
||||||
|
for (let key of Object.keys(site)) {
|
||||||
|
html = html.replace(new RegExp("\\$" + key.toUpperCase()), site[key]);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.listen(PORT, "127.0.0.1");
|
20
src/public/components/NavLink.tsx
Normal file
20
src/public/components/NavLink.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface NavLinkProps {
|
||||||
|
url: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
interface NavLinkState {}
|
||||||
|
|
||||||
|
export default class NavLink extends React.Component<NavLinkProps, NavLinkState> {
|
||||||
|
constructor(props: NavLinkProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return <div className="nav-link">
|
||||||
|
<a href={this.props.url}>{this.props.text}</a>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
27
src/public/components/TopNav.tsx
Normal file
27
src/public/components/TopNav.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import React from "react";
|
||||||
|
import NavLink from "./NavLink";
|
||||||
|
|
||||||
|
interface TopNavProps {
|
||||||
|
image: string;
|
||||||
|
links: string[];
|
||||||
|
alt: string;
|
||||||
|
}
|
||||||
|
interface TopNavState {}
|
||||||
|
|
||||||
|
export default class TopNav extends React.Component<TopNavProps, TopNavState> {
|
||||||
|
constructor(props: TopNavProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
let navLinks = this.props.links.map(name=>{
|
||||||
|
return <NavLink text={name} url={"/"+name.toLowerCase()} key={name}></NavLink>;
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className="top-nav">
|
||||||
|
<img className="logo" src={this.props.image} alt={this.props.alt}></img>
|
||||||
|
{navLinks}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
32
src/public/css/styles.css
Normal file
32
src/public/css/styles.css
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-button,
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #ececec;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #e2e2e2;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:active {
|
||||||
|
background: #b8b8b8;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #383838;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color vars should go here */
|
||||||
|
:root {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive queries go here */
|
||||||
|
@media screen and (max-width: 760px) {
|
||||||
|
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 430px) {
|
||||||
|
|
||||||
|
}
|
0
src/public/events.tsx
Normal file
0
src/public/events.tsx
Normal file
23
src/public/index.tsx
Normal file
23
src/public/index.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import * as ReactDom from "react-dom";
|
||||||
|
import * as React from "react";
|
||||||
|
import TopNav from "./components/TopNav";
|
||||||
|
|
||||||
|
interface MainProps {}
|
||||||
|
interface MainState {}
|
||||||
|
|
||||||
|
class Main extends React.Component<MainProps, MainState> {
|
||||||
|
constructor(props: MainProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
public render() {
|
||||||
|
return <>
|
||||||
|
<TopNav links={["Events","Officers","Projects","Resources","Sponsors"]}
|
||||||
|
image="img/logo.png" alt="IEEE at UCSD Logo"></TopNav>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDom.render(<Main></Main>, document.getElementById("root"));
|
||||||
|
|
||||||
|
export default {};
|
2
src/public/robots.txt
Normal file
2
src/public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
15
src/public/template.html
Normal file
15
src/public/template.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>$TITLE</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="$CSSFILE" media="screen">
|
||||||
|
<link rel="icon" href="img/favicon.png" type="image/png">
|
||||||
|
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1">
|
||||||
|
<meta name="description" content="$DESCRIPTION">
|
||||||
|
<meta name="theme-color" content="$THEMECOLOR">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="text/javascript" src="$JSFILE"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./build/",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es6",
|
||||||
|
"jsx": "react",
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"include": ["./src"],
|
||||||
|
"exclude": ["./src/public"]
|
||||||
|
}
|
73
webpack.config.js
Normal file
73
webpack.config.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
const path = require("path");
|
||||||
|
const CopyPlugin = require("copy-webpack-plugin");
|
||||||
|
const LicensePlugin = require("license-webpack-plugin").LicenseWebpackPlugin;
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: loadEntries("./src/public"),
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, "build/public"),
|
||||||
|
filename: "./js/[name].js"
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [{
|
||||||
|
test: /\.(tsx|ts)$/,
|
||||||
|
use: "ts-loader"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(js)$/,
|
||||||
|
use: "babel-loader"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ["style-loader", "css-loader"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: [".tsx", ".ts", ".js"],
|
||||||
|
},
|
||||||
|
mode: "production",
|
||||||
|
plugins: [
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [{
|
||||||
|
from: "./src/public",
|
||||||
|
to: ".",
|
||||||
|
globOptions: {
|
||||||
|
ignore: ["**/*.tsx", "**/*.ts"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
new LicensePlugin({
|
||||||
|
outputFilename: 'third-party-notice.txt'
|
||||||
|
})
|
||||||
|
],
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin({
|
||||||
|
terserOptions: {
|
||||||
|
output: {
|
||||||
|
comments: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extractComments: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
//devtool: "source-map"
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadEntries(dir) {
|
||||||
|
let files = fs.readdirSync(path.join(__dirname, dir));
|
||||||
|
let entries = {};
|
||||||
|
files.forEach(file => {
|
||||||
|
let name = file.match(/^(.*)\.tsx$/);
|
||||||
|
if (name) {
|
||||||
|
entries[name[1]] = path.join(__dirname, dir, file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return entries;
|
||||||
|
}
|
Loading…
Reference in a new issue