init
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
node_modules/
|
||||
build/
|
||||
tmp/
|
||||
temp/
|
||||
dist/
|
||||
log/
|
||||
7
README.md
Normal file
7
README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Awesome Project Build with TypeORM
|
||||
|
||||
Steps to run this project:
|
||||
|
||||
1. Run `npm i` command
|
||||
2. Setup database settings inside `ormconfig.json` file
|
||||
3. Run `npm start` command
|
||||
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
version: "3"
|
||||
services:
|
||||
jokedb:
|
||||
image: registry.dennisgunia.de/jokedb
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: dockerfile
|
||||
restart: always
|
||||
hostname: backend
|
||||
networks:
|
||||
- backend
|
||||
ports:
|
||||
- "8080:3000"
|
||||
networks:
|
||||
backend:
|
||||
driver: overlay
|
||||
21
dockerfile
Normal file
21
dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM node:12-alpine
|
||||
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
|
||||
RUN mkdir -p /home/node/app/node_modules/log && chown -R node:node /home/node/app
|
||||
RUN mkdir -p /home/node/app/node_modules/dist && chown -R node:node /home/node/app
|
||||
RUN mkdir -p /home/node/app/node_modules/static && chown -R node:node /home/node/app
|
||||
|
||||
COPY ./dist /home/node/app/dist
|
||||
COPY ./static /home/node/app/static
|
||||
COPY ./*.json /home/node/app/
|
||||
|
||||
WORKDIR /home/node/app
|
||||
|
||||
RUN chmod -R 777 /home/node/app
|
||||
|
||||
USER node
|
||||
|
||||
RUN npm install
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD [ "node", "dist/index.js"]
|
||||
24
ormconfig.json
Normal file
24
ormconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"type": "mysql",
|
||||
"host": "sql1.dennisgunia.de",
|
||||
"port": 7574,
|
||||
"username": "joke",
|
||||
"password": "FivXcBxrUB6Fm5qN",
|
||||
"database": "joke",
|
||||
"synchronize": true,
|
||||
"logging": false,
|
||||
"entities": [
|
||||
"dist/entity/**/*.js"
|
||||
],
|
||||
"migrations": [
|
||||
"dist/migration/**/*.js"
|
||||
],
|
||||
"subscribers": [
|
||||
"dist/subscriber/**/*.js"
|
||||
],
|
||||
"cli": {
|
||||
"entitiesDir": "dist/entity",
|
||||
"migrationsDir": "dist/migration",
|
||||
"subscribersDir": "dist/subscriber"
|
||||
}
|
||||
}
|
||||
1455
package-lock.json
generated
Normal file
1455
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
package.json
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "flachwitze",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "ts-node src/index.ts"
|
||||
},
|
||||
"author": "Dennis Gunia",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.8",
|
||||
"@types/morgan": "^1.9.1",
|
||||
"@types/node": "^8.10.63",
|
||||
"ts-node": "3.3.0",
|
||||
"typeorm": "^0.2.26",
|
||||
"typescript": "3.3.3333"
|
||||
},
|
||||
"dependencies": {
|
||||
"@overnightjs/core": "^1.7.5",
|
||||
"@overnightjs/logger": "^1.2.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"morgan": "^1.10.0",
|
||||
"mysql": "^2.18.1",
|
||||
"reflect-metadata": "^0.1.10",
|
||||
"rotating-file-stream": "^2.1.3",
|
||||
"typeorm": "0.2.26"
|
||||
}
|
||||
}
|
||||
51
src/api/express.ts
Normal file
51
src/api/express.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import express from 'express';
|
||||
import * as bodyParser from 'body-parser';
|
||||
import { logInfo, logError } from "../util/logging";
|
||||
import { ApiController } from '../types/ApiController';
|
||||
import { responseJson } from '../util/response';
|
||||
import * as rfs from 'rotating-file-stream'
|
||||
import morgan from 'morgan'
|
||||
|
||||
export class WebAPI {
|
||||
public app: express.Application;
|
||||
public port: number;
|
||||
public accessLogStream: rfs.RotatingFileStream;
|
||||
|
||||
constructor(port: number) {
|
||||
this.app = express();
|
||||
this.port = port;
|
||||
this.accessLogStream = rfs.createStream('access.log', {
|
||||
interval: '1d', // rotate daily
|
||||
path: './log'
|
||||
});
|
||||
this.initializeMiddlewares();
|
||||
}
|
||||
|
||||
private initializeMiddlewares() {
|
||||
this.app.use(morgan('combined', {
|
||||
stream: this.accessLogStream
|
||||
}))
|
||||
this.app.use(bodyParser.json());
|
||||
}
|
||||
|
||||
public initializeControllers(controllers: ApiController[]) {
|
||||
controllers.forEach(el => {
|
||||
this.app.use(el.path, el.router);
|
||||
})
|
||||
// JSON parse error
|
||||
this.app.use((err: Error, req: express.Request, res: express.Response, next: any) => {
|
||||
if (err instanceof SyntaxError) {
|
||||
responseJson(req,res,400,{},"JSON invalid");
|
||||
} else {
|
||||
logError(`Express: ${err.message}`);
|
||||
responseJson(req,res,500,{},"Internal error");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public listen() {
|
||||
this.app.listen(this.port, () => {
|
||||
logInfo(`App listening on the port ${this.port}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
88
src/api/root/joke/joke-controller.ts
Normal file
88
src/api/root/joke/joke-controller.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import * as express from 'express';
|
||||
import { ApiController } from '../../../types/ApiController';
|
||||
import { responseJson } from '../../../util/response';
|
||||
import { JokeService } from '../../../services/JokeService';
|
||||
import { logError } from '../../../util/logging';
|
||||
|
||||
export class JokeController implements ApiController{
|
||||
public path: string= '/joke/';
|
||||
public router: express.Router = express.Router();
|
||||
public childComponents: ApiController[] = [];
|
||||
constructor() {
|
||||
this.intializeRoutes();
|
||||
}
|
||||
|
||||
public intializeRoutes() {
|
||||
this.router.get(`/:id`,this.getSingle)
|
||||
this.router.get(`/:id/upvote`,this.upvote)
|
||||
this.router.get(`/:id/downvote`,this.upvote)
|
||||
this.childComponents.forEach( el => {
|
||||
this.router.use(el.path, el.router)
|
||||
})
|
||||
// 404 Error Response
|
||||
this.router.use((req, res, next) => {
|
||||
responseJson(req,res,404,{},"api request not found");
|
||||
});
|
||||
}
|
||||
|
||||
public upvote(req: express.Request, res: express.Response){
|
||||
const js = new JokeService();
|
||||
const id = req.params.id as unknown as string;
|
||||
if (!(id.match(/^[0-9]*$/))){
|
||||
responseJson(req,res,400,{},"invalid id");
|
||||
}else{
|
||||
js.upvote(Number(id));
|
||||
js.getRandomJoke().then( resp => {
|
||||
responseJson(req,res,200, resp);
|
||||
}).catch( err => {
|
||||
if (err.message === "No Joke found"){
|
||||
responseJson(req,res,404,{},"Not found");
|
||||
return
|
||||
}
|
||||
logError(err)
|
||||
responseJson(req,res,500,{},"internal error");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public downvote(req: express.Request, res: express.Response){
|
||||
const js = new JokeService();
|
||||
const id = req.params.id as unknown as string;
|
||||
if (!(id.match(/^[0-9]*$/))){
|
||||
responseJson(req,res,400,{},"invalid id");
|
||||
}else{
|
||||
js.downvote(Number(id));
|
||||
js.getRandomJoke().then( resp => {
|
||||
responseJson(req,res,200, resp);
|
||||
}).catch( err => {
|
||||
if (err.message === "No Joke found"){
|
||||
responseJson(req,res,404,{},"Not found");
|
||||
return
|
||||
}
|
||||
logError(err)
|
||||
responseJson(req,res,500,{},"internal error");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public getSingle(req: express.Request, res: express.Response){
|
||||
const js = new JokeService();
|
||||
const id = req.params.id as unknown as string;
|
||||
if (!(id.match(/^[0-9]*$/))){
|
||||
responseJson(req,res,400,{},"invalid id");
|
||||
}else{
|
||||
js.getJoke(Number(id)).then( resp => {
|
||||
responseJson(req,res,200, resp);
|
||||
}).catch( err => {
|
||||
if (err.message === "No Joke found"){
|
||||
responseJson(req,res,404,{},"Not found");
|
||||
return
|
||||
}
|
||||
logError(err)
|
||||
responseJson(req,res,500,{},"internal error");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
39
src/api/root/joke/jokes-controller.ts
Normal file
39
src/api/root/joke/jokes-controller.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import * as express from 'express';
|
||||
import { ApiController } from '../../../types/ApiController';
|
||||
import { responseJson } from '../../../util/response';
|
||||
import { JokeService } from '../../../services/JokeService';
|
||||
import { logError } from '../../../util/logging';
|
||||
|
||||
export class JokesController implements ApiController{
|
||||
public path: string= '/jokes/';
|
||||
public router: express.Router = express.Router();
|
||||
public childComponents: ApiController[] = [];
|
||||
public jokesrv: JokeService;
|
||||
constructor() {
|
||||
this.intializeRoutes();
|
||||
this.jokesrv = new JokeService();
|
||||
}
|
||||
|
||||
public intializeRoutes() {
|
||||
this.router.get(`/getRand`,this.getRandJoke)
|
||||
this.childComponents.forEach( el => {
|
||||
this.router.use(el.path, el.router)
|
||||
})
|
||||
// 404 Error Response
|
||||
this.router.use((req, res, next) => {
|
||||
responseJson(req,res,404,{},"api request not found");
|
||||
});
|
||||
}
|
||||
|
||||
public getRandJoke(req: express.Request, res: express.Response){
|
||||
const jokesrv = new JokeService();
|
||||
jokesrv.getRandomJoke().then( resp => {
|
||||
responseJson(req,res,200, resp);
|
||||
}).catch( err => {
|
||||
logError(err)
|
||||
responseJson(req,res,500,{},"internal error");
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
30
src/api/root/root-controller.ts
Normal file
30
src/api/root/root-controller.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as express from 'express';
|
||||
import { ApiController } from '../../types/ApiController';
|
||||
import { responseJson } from '../../util/response';
|
||||
import { JokeController } from './joke/joke-controller';
|
||||
import { JokesController } from './joke/jokes-controller';
|
||||
|
||||
export class RootController implements ApiController{
|
||||
public path: string= '/';
|
||||
public router: express.Router = express.Router();
|
||||
public childComponents: ApiController[] = [
|
||||
new JokeController(),
|
||||
new JokesController(),
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.intializeRoutes();
|
||||
}
|
||||
|
||||
public intializeRoutes() {
|
||||
this.router.use(express.static('./static'));
|
||||
this.childComponents.forEach( el => {
|
||||
this.router.use(el.path, el.router)
|
||||
})
|
||||
// 404 Error Response
|
||||
this.router.use((req, res, next) => {
|
||||
responseJson(req,res,404,{},"api request not found");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
16
src/entity/Category.ts
Normal file
16
src/entity/Category.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { userInfo } from "os";
|
||||
import {Entity, PrimaryGeneratedColumn, Column, OneToMany} from "typeorm";
|
||||
import { Joke } from "./Joke";
|
||||
|
||||
@Entity()
|
||||
export class Category {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column()
|
||||
category!: string;
|
||||
|
||||
@OneToMany(type => Joke, jokes => jokes.category)
|
||||
jokes!: Joke;
|
||||
}
|
||||
36
src/entity/Joke.ts
Normal file
36
src/entity/Joke.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn} from "typeorm";
|
||||
import { Category } from "./Category";
|
||||
import { User } from "./User";
|
||||
|
||||
@Entity()
|
||||
export class Joke {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column()
|
||||
header!: string;
|
||||
|
||||
@Column({ type: "longtext"})
|
||||
joke!: string;
|
||||
|
||||
@Column({nullable: true})
|
||||
source!: string;
|
||||
|
||||
@Column({default: 0, type: "int"})
|
||||
upvotes!: number;
|
||||
|
||||
@Column({default: 0, type: "int"})
|
||||
downvotes!: number;
|
||||
|
||||
@Column({default: 0, type: "int"})
|
||||
views!: number;
|
||||
|
||||
@ManyToOne(type => User, user => user.jokes)
|
||||
@JoinColumn()
|
||||
user!: User;
|
||||
|
||||
@ManyToOne(type => Category, user => user.jokes)
|
||||
@JoinColumn()
|
||||
category!: Category;
|
||||
}
|
||||
28
src/entity/User.ts
Normal file
28
src/entity/User.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { userInfo } from "os";
|
||||
import {Entity, PrimaryGeneratedColumn, Column, OneToMany} from "typeorm";
|
||||
import { Joke } from "./Joke";
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column()
|
||||
firstName!: string;
|
||||
|
||||
@Column()
|
||||
lastName!: string;
|
||||
|
||||
@Column()
|
||||
displayname!: string;
|
||||
|
||||
@Column()
|
||||
age!: number;
|
||||
|
||||
@Column()
|
||||
mail!: string;
|
||||
|
||||
@OneToMany(type => Joke, jokes => jokes.user)
|
||||
jokes!: Joke;
|
||||
}
|
||||
22
src/index.ts
Normal file
22
src/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import "reflect-metadata";
|
||||
import {createConnection} from "typeorm";
|
||||
import { WebAPI } from "./api/express";
|
||||
import { RootController } from "./api/root/root-controller";
|
||||
import { ApiController } from "./types/ApiController";
|
||||
import { logError, logInfo } from "./util/logging";
|
||||
|
||||
// initialize Database Connection
|
||||
createConnection().then(async connection => {
|
||||
logInfo("Database Connected");
|
||||
}).catch(error => {
|
||||
logError(error);
|
||||
process.exit(10);
|
||||
});
|
||||
|
||||
// initialize Web API
|
||||
const app: WebAPI = new WebAPI(3000);
|
||||
const controllers: ApiController[] = [
|
||||
new RootController()
|
||||
];
|
||||
app.initializeControllers(controllers);
|
||||
app.listen();
|
||||
88
src/services/JokeService.ts
Normal file
88
src/services/JokeService.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { getManager, Repository } from "typeorm";
|
||||
import { Category } from "../entity/Category";
|
||||
import { Joke } from "../entity/Joke";
|
||||
import { User } from "../entity/User";
|
||||
|
||||
export interface JokeResponse {
|
||||
id: number,
|
||||
author: string,
|
||||
text: string,
|
||||
upvotes: number,
|
||||
downvotes: number,
|
||||
views: number,
|
||||
category: string,
|
||||
categoryId: number,
|
||||
}
|
||||
|
||||
export class JokeService {
|
||||
constructor(){
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
public async getRandomJoke(): Promise<JokeResponse> {
|
||||
const JokeRepo: Repository<Joke> = getManager().getRepository(Joke);
|
||||
const joke = await (JokeRepo.createQueryBuilder('joke')
|
||||
.leftJoinAndSelect('joke.user', 'user')
|
||||
.leftJoinAndSelect('joke.category', 'category')
|
||||
.orderBy("RAND()")
|
||||
.limit(1)
|
||||
.getOne()
|
||||
);
|
||||
if (joke === undefined){
|
||||
throw new Error("No Joke found");
|
||||
}
|
||||
JokeRepo.increment({ id: joke.id }, "views", 1);
|
||||
return {
|
||||
id: joke.id,
|
||||
author: joke.user.displayname,
|
||||
text: joke.joke,
|
||||
views: joke.views,
|
||||
upvotes: joke.upvotes,
|
||||
downvotes: joke.downvotes,
|
||||
category: joke.category.category,
|
||||
categoryId: joke.category.id
|
||||
};
|
||||
}
|
||||
|
||||
public async upvote(jid: number): Promise<boolean> {
|
||||
const JokeRepo: Repository<Joke> = getManager().getRepository(Joke);
|
||||
try {
|
||||
await JokeRepo.increment({ id: jid }, "upvotes", 1);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async downvote(jid: number): Promise<boolean> {
|
||||
const JokeRepo: Repository<Joke> = getManager().getRepository(Joke);
|
||||
try {
|
||||
await JokeRepo.increment({ id: jid }, "downvotes", 1);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async getJoke(jid: number): Promise<JokeResponse> {
|
||||
const JokeRepo: Repository<Joke> = getManager().getRepository(Joke);
|
||||
const UserRepo: Repository<User> = getManager().getRepository(User);
|
||||
const CatRepo: Repository<Category> = getManager().getRepository(Category);
|
||||
|
||||
const joke = await (JokeRepo.findOne({id: jid},{relations: ['user','category']}));
|
||||
if (joke === undefined){
|
||||
throw new Error("No Joke found");
|
||||
}
|
||||
JokeRepo.increment({ id: joke.id }, "views", 1);
|
||||
return {
|
||||
id: joke.id,
|
||||
author: joke.user.displayname,
|
||||
text: joke.joke,
|
||||
views: joke.views,
|
||||
upvotes: joke.upvotes,
|
||||
downvotes: joke.downvotes,
|
||||
category: joke.category.category,
|
||||
categoryId: joke.category.id
|
||||
};
|
||||
}
|
||||
}
|
||||
9
src/types/ApiController.ts
Normal file
9
src/types/ApiController.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as express from 'express';
|
||||
|
||||
export interface ApiController {
|
||||
path: string;
|
||||
router: express.Router
|
||||
childComponents: ApiController[]
|
||||
intializeRoutes():void
|
||||
}
|
||||
|
||||
5
src/types/global.d.ts
vendored
Normal file
5
src/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as rfs from 'rotating-file-stream'
|
||||
|
||||
export interface Global {
|
||||
ls: rfs.RotatingFileStream;
|
||||
}
|
||||
35
src/util/logging.ts
Normal file
35
src/util/logging.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export function logInfo(text: string) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.log(`\x1b[32m[Info] \x1b[0m ${text}`)
|
||||
}
|
||||
|
||||
export function logWarning(text: string) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.log(`\x1b[33m[Warn] \x1b[0m ${text}`)
|
||||
}
|
||||
|
||||
export function logError(text: string) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.log(`\x1b[31m[Error] \x1b[0m ${text}`)
|
||||
}
|
||||
|
||||
export function logCritical(text: string) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.log(`\x1b[4m\x1b[31m[Critical]\x1b[0m \x1b[4m ${text}\x1b[0m`)
|
||||
}
|
||||
|
||||
export function logNotice (text: string) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.log(`\x1b[36m[Notice] \x1b[0m ${text}`)
|
||||
}
|
||||
|
||||
export function logDebug(text: string) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.log(`\x1b[36m[DEBUG] \x1b[0m ${text}`)
|
||||
}
|
||||
|
||||
export function logChat (text: string) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.log(`\x1b[35m[CHAT] \x1b[0m ${text}`)
|
||||
}
|
||||
|
||||
16
src/util/response.ts
Normal file
16
src/util/response.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as express from 'express';
|
||||
|
||||
export function responseJson(req: express.Request, res: express.Response,code: number, data: any, err?: string): void{
|
||||
const respObj: any = {
|
||||
rescource: req.originalUrl.toString(),
|
||||
success: err ? false : true,
|
||||
data,
|
||||
debug: {
|
||||
host: req.get('host'),
|
||||
proto: req.protocol
|
||||
},
|
||||
error: err ? err : undefined
|
||||
}
|
||||
res.status(code);
|
||||
res.json(respObj);
|
||||
}
|
||||
BIN
static/go-next-8.png
Normal file
BIN
static/go-next-8.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
59
static/index.html
Normal file
59
static/index.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="site.css" >
|
||||
<link href="https://fonts.googleapis.com/css?family=Bowlby+One+SC&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="background">
|
||||
<div id="bg-content"></div>
|
||||
</div>
|
||||
<div class="header-bar">
|
||||
<div class="row" style="width:100%;">
|
||||
<div class="col">flachwitze.dennisgunia.de</div>
|
||||
<div class="col text-right">Time Wasted <p id="wasted"></p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-bar">
|
||||
<div id="score-content">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="gradient-fill" id="gradient-fill"></div>
|
||||
<div class="vertical-center">
|
||||
<div class="container container-card">
|
||||
<div class="card" style="width: 100%" id="joke-card">
|
||||
<div class="card-body">
|
||||
<div id=content>
|
||||
<h5 class="card-title" id="joke-title">Card title</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted" id="joke-source">Card subtitle</h6>
|
||||
<p class="card-text" id="joke-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
|
||||
</div>
|
||||
<br />
|
||||
<a href="#" class="card-link cmd-like">
|
||||
<svg width="1em" height="1em" class="heart" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z"/>
|
||||
<path stroke-linejoin="round" stroke="currentColor" fill="none" stroke-width="2" d="M8 2.748l-.717-.737C5.6.281 2.514.878 1.4 3.053c-.523 1.023-.641 2.5.314 4.385.92 1.815 2.834 3.989 6.286 6.357 3.452-2.368 5.365-4.542 6.286-6.357.955-1.886.838-3.362.314-4.385C13.486.878 10.4.28 8.717 2.01L8 2.748z"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a href="#" class="card-link cmd-more">Mehr Davon!</a>
|
||||
<a href="#" class="card-link cmd-next">Einen anderen Witz bitte ...</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.js" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.js" crossorigin="anonymous"></script>
|
||||
<script src="site.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
232
static/site.css
Normal file
232
static/site.css
Normal file
@@ -0,0 +1,232 @@
|
||||
body {
|
||||
background-color: #b0b0b0;
|
||||
}
|
||||
|
||||
.vertical-center {
|
||||
z-index:-1;
|
||||
min-height: 100%; /* Fallback for browsers do NOT support vh unit */
|
||||
min-height: 100vh; /* These two lines are counted as one :-) */
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/*background-color: rgba(0, 0, 0,0.5);*/
|
||||
}
|
||||
|
||||
.gradient-fill {
|
||||
opacity: 50%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.bg-fill-s1 {
|
||||
background: rgb(2,0,36);
|
||||
background: linear-gradient(30deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 33%, rgba(0,212,255,1) 100%);;
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.bg-fill-s2 {
|
||||
background: rgb(131,58,180);
|
||||
background: linear-gradient(127deg, rgba(131,58,180,1) 0%, rgba(253,29,29,1) 50%, rgba(252,176,69,1) 100%);
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.bg-fill-s3 {
|
||||
background: rgb(34,193,195);
|
||||
background: linear-gradient(320deg, rgba(34,193,195,1) 0%, rgba(253,187,45,1) 100%);
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.background {
|
||||
z-index:0;
|
||||
font-family: 'Bowlby One SC', cursive;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
position : absolute;
|
||||
z-index:20;
|
||||
font-family: 'Bowlby One SC', cursive;
|
||||
top: 0px;
|
||||
width:100%;
|
||||
/*background-color: rgba(0, 0, 0,0.5);*/
|
||||
color: rgb(255,255,255);
|
||||
padding:1rem;
|
||||
background-image: linear-gradient(rgba(0,0,0,0.5), rgba(0, 0, 0,0));
|
||||
}
|
||||
|
||||
.score-bar {
|
||||
display: flex;
|
||||
position : absolute;
|
||||
z-index:19;
|
||||
font-family: 'Bowlby One SC', cursive;
|
||||
bottom: 0px;
|
||||
width:100%;
|
||||
/*background-color: rgba(0, 0, 0,0.5);*/
|
||||
color: rgb(255,255,255);
|
||||
padding:1rem;
|
||||
background-image: linear-gradient(rgba(0,0,0,0), rgba(0, 0, 0,0.5));
|
||||
}
|
||||
|
||||
#bg-content {
|
||||
|
||||
position : absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
-webkit-transform: translate(-50%, -50%) rotate(-45deg);
|
||||
-moz-transform: translate(-50%, -50%) rotate(-45deg);
|
||||
-ms-transform: translate(-50%, -50%) rotate(-45deg);
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
|
||||
position: absolute;
|
||||
transform-origin: 50% 50%;
|
||||
|
||||
}
|
||||
|
||||
.scr {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.single-line {
|
||||
|
||||
background: url("go-next-8.png") repeat-x;
|
||||
white-space: nowrap;
|
||||
overflow:hidden;
|
||||
color:rgb(153, 136, 136);
|
||||
padding-bottom:48px;
|
||||
width: 5000px;
|
||||
height: 32px;
|
||||
|
||||
|
||||
|
||||
background-size: auto;
|
||||
}
|
||||
|
||||
|
||||
@keyframes slide {
|
||||
from {
|
||||
background-position-x: 0;
|
||||
}
|
||||
to {
|
||||
background-position-x: -653px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide_out {
|
||||
from {
|
||||
position-x: 0;
|
||||
}
|
||||
to {
|
||||
position-x: -100%;
|
||||
}
|
||||
}
|
||||
@keyframes slide_in {
|
||||
from {
|
||||
position-x: 100%;
|
||||
}
|
||||
to {
|
||||
position-x: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.heart > path{
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
|
||||
.heart-en > svg > path {
|
||||
animation: heartClickEn 0.3s linear;
|
||||
color: red;
|
||||
fill: lightcoral;
|
||||
}
|
||||
.heart-en > svg {
|
||||
animation: heartPopIn 0.3s linear;
|
||||
color: red;
|
||||
}
|
||||
|
||||
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: 200;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heartClickEn {
|
||||
0% {
|
||||
color: #007bff ;
|
||||
|
||||
}
|
||||
50% {
|
||||
fill: transparent;
|
||||
|
||||
}
|
||||
100% {
|
||||
color: red ;
|
||||
fill: lightcoral;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heartPopIn {
|
||||
0% {
|
||||
transform: scale(1.0);
|
||||
|
||||
}
|
||||
50% {
|
||||
transform: scale(2);
|
||||
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heartPopOut {
|
||||
0% {
|
||||
transform: scale(1.0);
|
||||
|
||||
}
|
||||
50% {
|
||||
transform: scale(2);
|
||||
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
.heart-da > svg > path {
|
||||
animation: heartClickDa 0.3s linear;
|
||||
fill: transparent;
|
||||
}
|
||||
.heart-da > svg {
|
||||
animation: heartPopOut 0.3s linear;
|
||||
color: 007bff;
|
||||
}
|
||||
|
||||
@keyframes heartClickDa {
|
||||
0% {
|
||||
color: red ;
|
||||
fill: lightcoral;
|
||||
|
||||
}
|
||||
50% {
|
||||
fill: transparent;
|
||||
|
||||
}
|
||||
100% {
|
||||
color: #007bff ;
|
||||
|
||||
}
|
||||
}
|
||||
214
static/site.js
Normal file
214
static/site.js
Normal file
@@ -0,0 +1,214 @@
|
||||
$.urlParam = function(name){
|
||||
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
|
||||
if (results==null) {
|
||||
return null;
|
||||
}
|
||||
return decodeURI(results[1]) || 0;
|
||||
}
|
||||
|
||||
theme = 0;
|
||||
humor = [];
|
||||
liked = false;
|
||||
likelist = [];
|
||||
function modHumor (catId, catDef, amount = 0) {
|
||||
console.log(catId, catDef, amount)
|
||||
let cf = humor.filter( el => el.catId === catId); // category found
|
||||
if (cf.length == 1) {
|
||||
cf[0].amount += amount;
|
||||
}else{
|
||||
humor.push({
|
||||
catId: catId,
|
||||
catDef: catDef,
|
||||
amount: amount
|
||||
})
|
||||
}
|
||||
}
|
||||
function printHumor(){
|
||||
$("#score-content").hide("fast",function(){
|
||||
let htmlContent = "";
|
||||
for (let i = 0 ; i < humor.length; i++){
|
||||
|
||||
htmlContent += "<button type=\"button\" class=\"btn scr btn-primary\"> "+humor[i].catDef+" <span class=\"badge badge-light\">"+humor[i].amount+"</span></button>";
|
||||
}
|
||||
$("#score-content").html(htmlContent);
|
||||
$("#score-content").show("fast");
|
||||
});
|
||||
}
|
||||
|
||||
let jokeClass = {
|
||||
cur_joke: {},
|
||||
get: (id) => {
|
||||
return new Promise((resolve,reject) => {
|
||||
$.getJSON(`/joke/${id}`).done(function ( data ) {
|
||||
resolve(data.data)
|
||||
}).fail(function ( err ) {
|
||||
reject(err);
|
||||
})
|
||||
})
|
||||
},
|
||||
getRnd: () => {
|
||||
return new Promise((resolve,reject) => {
|
||||
$.getJSON(`/jokes/getRand/`).done(function ( data ) {
|
||||
resolve(data.data)
|
||||
}).fail(function ( err ) {
|
||||
reject(err);
|
||||
})
|
||||
})
|
||||
},
|
||||
up: (id) => {
|
||||
return new Promise((resolve,reject) => {
|
||||
$.getJSON(`/joke/${id}/upvote`).done(function ( data ) {
|
||||
resolve(data.data)
|
||||
}).fail(function ( err ) {
|
||||
reject(err);
|
||||
})
|
||||
})
|
||||
},
|
||||
down: (id) => {
|
||||
return new Promise((resolve,reject) => {
|
||||
$.getJSON(`/joke/${id}/downvote`).done(function ( data ) {
|
||||
resolve(data.data)
|
||||
}).fail(function ( err ) {
|
||||
reject(err);
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
function setLikeButton(state){
|
||||
console.log(state)
|
||||
if (state){
|
||||
$('.cmd-like').removeClass('heart-da').addClass('heart-en');
|
||||
}else{
|
||||
$('.cmd-like').removeClass('heart-en').addClass('heart-da');
|
||||
}
|
||||
}
|
||||
|
||||
async function getWitz(action = 0,id = -1){
|
||||
switch(action){
|
||||
case 0:
|
||||
jokeClass.cur_joke = await jokeClass.getRnd(); break;
|
||||
case 1:
|
||||
jokeClass.cur_joke = await jokeClass.up(id); break;
|
||||
case 2:
|
||||
jokeClass.cur_joke = await jokeClass.down(id); break;
|
||||
case 3:
|
||||
jokeClass.cur_joke = await jokeClass.get(id); break;
|
||||
}
|
||||
liked = (likelist.includes(jokeClass.cur_joke.id))
|
||||
setLikeButton(liked);
|
||||
$("#content").hide("fast", function () {
|
||||
$("#joke-title").text(jokeClass.cur_joke.category);
|
||||
$("#joke-source").text(jokeClass.cur_joke.author);
|
||||
$("#joke-text").html(jokeClass.cur_joke.text);
|
||||
$("#content").show("fast");
|
||||
});
|
||||
}
|
||||
|
||||
$('.cmd-next').click(function() {
|
||||
modHumor(jokeClass.cur_joke.categoryId,jokeClass.cur_joke.category,-50);
|
||||
getWitz(2,jokeClass.cur_joke.id);
|
||||
printHumor();
|
||||
|
||||
});
|
||||
|
||||
$('.cmd-more').click(function() {
|
||||
modHumor(jokeClass.cur_joke.categoryId,jokeClass.cur_joke.category,50);
|
||||
getWitz(1,jokeClass.cur_joke.id);
|
||||
printHumor();
|
||||
});
|
||||
|
||||
$('.cmd-like').click(function() {
|
||||
liked = !liked;
|
||||
setLikeButton(liked);
|
||||
if (liked && !likelist.includes(jokeClass.cur_joke.id)){
|
||||
likelist.push(jokeClass.cur_joke.id);
|
||||
}else{
|
||||
likelist = likelist.filter(el => el !== jokeClass.cur_joke.id);
|
||||
}
|
||||
console.log(likelist)
|
||||
});
|
||||
|
||||
|
||||
|
||||
// ANiMATION
|
||||
|
||||
let anim_rows = 50;
|
||||
$( document ).ready(function() {
|
||||
// randomize theme
|
||||
theme = Math.round(Math.random()*2);
|
||||
$("#gradient-fill").addClass(`bg-fill-s${theme + 1}`);
|
||||
|
||||
anim_rows= (window.innerWidth / 68 ) + (window.innerHeight / 68);
|
||||
generateBackground(anim_rows);
|
||||
try {
|
||||
const tmp = JSON.parse($.cookie("score"));
|
||||
humor = tmp.humor;
|
||||
likelist = tmp.likes;
|
||||
}catch{ }
|
||||
printHumor();
|
||||
|
||||
const reqJoke = $.urlParam('wid');
|
||||
console.log(reqJoke)
|
||||
if (reqJoke){
|
||||
getWitz(3,reqJoke);
|
||||
}else{
|
||||
getWitz();
|
||||
}
|
||||
$('#wasted').text(Math.floor((new Date - start) / 1000) + " Sekunden");
|
||||
});
|
||||
|
||||
|
||||
|
||||
$(window).on('resize', function(){
|
||||
var win = $(this); //this = window
|
||||
anim_rows= (win.width() / 68) + (win.height() / 68);
|
||||
generateBackground(anim_rows);
|
||||
});
|
||||
|
||||
$(window).on("unload", function(e) {
|
||||
$.cookie("score", JSON.stringify({
|
||||
humor: humor,
|
||||
likes: likelist
|
||||
}));
|
||||
});
|
||||
|
||||
const start = new Date;
|
||||
|
||||
setInterval(function() {
|
||||
$('#wasted').text(Math.floor((new Date - start) / 1000) + " Sekunden");
|
||||
}, 1000);
|
||||
|
||||
function generateBackground(rows){
|
||||
|
||||
let fullhmtl = ""
|
||||
for (let i = 0; i < rows;i++){
|
||||
const speed = Math.floor(Math.random() * 20 ) +20
|
||||
const css = "animation: " + speed + "s linear infinite slide;"
|
||||
fullhmtl += "<div class='single-line' style='" + css + "'></div>"
|
||||
}
|
||||
|
||||
$("#bg-content").html(fullhmtl);
|
||||
|
||||
}
|
||||
|
||||
function calculateMapping(){
|
||||
let min = 1000000000;
|
||||
let max = -1000000000;
|
||||
|
||||
for (let i = 0 ; i < humor.length; i++){
|
||||
if(humor[i].scr < min){
|
||||
min = humor[i].scr
|
||||
}
|
||||
}
|
||||
|
||||
const offset = (min * -1);
|
||||
let sum = 0;
|
||||
for (let i = 0 ; i < humor.length; i++){
|
||||
sum += (humor[i].scr + offset);
|
||||
}
|
||||
for (let i = 0 ; i < humor.length; i++){
|
||||
const percentage = ((humor[i].scr + offset) / sum);
|
||||
console.log(percentage);
|
||||
}
|
||||
}
|
||||
73
tsconfig.json
Normal file
73
tsconfig.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||
"lib": [
|
||||
"es5",
|
||||
"es6",
|
||||
"DOM"
|
||||
], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./dist/", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
"typeRoots": [ "./src/types/" ], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
|
||||
/* Advanced Options */
|
||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
}
|
||||
}
|
||||
13
tslint.json
Normal file
13
tslint.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended"
|
||||
],
|
||||
"jsRules": {},
|
||||
"rules": {
|
||||
"no-console": true
|
||||
},
|
||||
"rulesDirectory": [
|
||||
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user