Commit ea3f42c4 authored by Natthaphong Malaphan's avatar Natthaphong Malaphan 😊

del

parent be960032
.git/
dist/
examples/
node_modules/
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false
NODE_ENV=development
SERVER_PORT=4040
JWT_SECRET=0a6b944d-d2fb-46fc-a85e-0295c986cd9f
MONGO_HOST=mongodb://localhost/mean
MEAN_FRONTEND=angular
NODE_ENV=development
SERVER_PORT=4040
JWT_SECRET=0a6b944d-d2fb-46fc-a85e-0295c986cd9f
MONGO_HOST=mongodb://localhost/mean
MEAN_FRONTEND=angular
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/dist-server
/tmp
/out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# e2e
/e2e/*.js
/e2e/*.map
# System Files
.DS_Store
Thumbs.db
FROM node:lts-buster
WORKDIR /usr/src/app
ADD . /usr/src/app
RUN yarn
RUN yarn build
EXPOSE 4040
CMD ["yarn", "serve"]
theme: jekyll-theme-minimal
logo: https://www.linnovate.net/sites/all/themes/linnovate/images/mean-picture.png
<!DOCTYPE html>
<html lang="{{ site.lang | default: "en-US" }}">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% seo %}
<link rel="stylesheet" href="{{ "/assets/css/style.css?v=" | append: site.github.build_revision | relative_url }}">
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]-->
</head>
<body>
<div class="wrapper">
<header>
{% if site.logo %}
<img class="logo" src="{{site.logo | relative_url}}" alt="Logo" />
{% endif %}
<p>{{ site.description | default: site.github.project_tagline }}</p>
{% if site.github.is_project_page %}
<p class="view"><a href="{{ site.github.repository_url }}">View the Project on GitHub <small>{{ site.github.repository_nwo }}</small></a></p>
{% endif %}
<!-- Place this tag where you want the button to render. -->
<a class="github-button" href="https://github.com/linnovate/mean" data-show-count="true" aria-label="Star ntkme/github-buttons on GitHub">Star</a>
{% if site.github.is_user_page %}
<p class="view"><a href="{{ site.github.owner_url }}">View My GitHub Profile</a></p>
{% endif %}
{% if site.show_downloads %}
<ul class="downloads">
<li><a href="{{ site.github.zip_url }}">Download <strong>ZIP File</strong></a></li>
<li><a href="{{ site.github.tar_url }}">Download <strong>TAR Ball</strong></a></li>
<li><a href="{{ site.github.repository_url }}">View On <strong>GitHub</strong></a></li>
</ul>
{% endif %}
<img class="ninja" src="/assets/img/ninja.jpg"/>
</header>
<section>
{{ content }}
{% if site.github.is_project_page %}
<p>This project is maintained by <a href="{{ site.github.owner_url }}">{{ site.github.owner_name }}</a></p>
{% endif %}
</section>
</div>
<script src="{{ "/assets/js/scale.fix.js" | relative_url }}"></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-36499287-4"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
// gtag('config', 'UA-XXXXXX-XX'); // change this to your own UA config
</script>
<!-- Place this tag in your head or just before your close body tag. -->
<script async defer src="https://buttons.github.io/buttons.js"></script>
</body>
</html>
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"mean": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "./tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/assets",
"src/favicon.ico"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "mean:build"
},
"configurations": {
"production": {
"browserTarget": "mean:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "mean:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"karmaConfig": "./karma.conf.js",
"polyfills": "src/polyfills.ts",
"tsConfig": "./tsconfig.spec.json",
"scripts": [],
"styles": [
"src/styles.scss"
],
"assets": [
"src/assets",
"src/favicon.ico"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"./tsconfig.app.json",
"./tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "mean",
"schematics": {
"@schematics/angular:component": {
"prefix": "app",
"styleext": "scss"
},
"@schematics/angular:directive": {
"prefix": "app"
}
},
"cli": {
"analytics": "9ab085ba-bb22-4e92-92f7-a6b2e0ff105f"
}
}
\ No newline at end of file
---
---
@import "{{ site.theme }}";
h1 {
a {
color:#00758f;
text-size:40px;
}
}
header {
img.logo {
margin-left:30px;
display:block;
height: auto;
width: auto;
max-width: 150px;
max-height: 200px;
margin-bottom: 17%;
}
img.ninja {
margin-top: 20px;
height: auto;
width: auto;
max-width: 350px;
max-height: 200px;
margin-bottom: 50px;
}
}
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11
This diff is collapsed.
version: '3'
services:
app:
build: ./
image: mean
ports:
- 80:4040
expose:
- 4040
environment:
NODE_ENV: production
SERVER_PORT: 4040
JWT_SECRET: 0a6b944d-d2fb-46fc-a85e-0295c986cd9f
MONGO_HOST: mongodb://mongo/odmp
restart: always
depends_on:
- mongo
mongo:
image: mongo:3.6
mongo-express:
image: mongo-express
restart: always
depends_on:
- mongo
ports:
- 8082:8082
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client:{
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
},
angularCli: {
environment: 'dev'
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};
This diff is collapsed.
{
"name": "mean",
"version": "2.0.2",
"license": "MIT",
"scripts": {
"ng": "ng",
"serve": "node server",
"start": "concurrently -c \"yellow.bold,green.bold\" -n \"SERVER,BUILD\" \"nodemon server\" \"ng build --watch\"",
"build": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points"
},
"private": true,
"dependencies": {
"@angular/animations": "^9.1.4",
"@angular/cdk": "^9.2.1",
"@angular/common": "^9.1.4",
"@angular/compiler": "^9.1.4",
"@angular/core": "^9.1.4",
"@angular/forms": "^9.1.4",
"@angular/material": "^9.2.1",
"@angular/platform-browser": "^9.1.4",
"@angular/platform-browser-dynamic": "^9.1.4",
"@angular/router": "^9.1.4",
"bcrypt": "^3.0.2",
"body-parser": "^1.18.2",
"compression": "^1.7.2",
"cookie-parser": "^1.4.3",
"cors": "^2.8.4",
"dotenv": "^6.0.0",
"events": "^3.0.0",
"express": "^4.16.3",
"express-async-handler": "^1.1.3",
"express-jwt": "^5.3.1",
"express-validation": "^1.0.2",
"formidable": "^1.2.1",
"helmet": "^3.21.1",
"http-errors": "^1.6.3",
"joi": "^13.3.0",
"jsonwebtoken": "^8.2.1",
"method-override": "^2.3.10",
"mongoose": "^5.7.5",
"morgan": "^1.9.1",
"nodemon": "^1.17.5",
"passport": "^0.4.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"rxjs": "^6.5.5",
"swagger-ui-express": "^3.0.9",
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.901.4",
"@angular/cli": "^9.1.4",
"@angular/compiler-cli": "^9.1.4",
"@angular/language-service": "^9.1.4",
"@types/jasmine": "~2.8.3",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~12.12.14",
"codelyzer": "^5.2.0",
"concurrently": "^3.5.1",
"jasmine-core": "~3.1.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "^3.1.3",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "2.0.1",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "1.1.0",
"ts-node": "~6.1.0",
"tslint": "^5.20.1",
"typescript": "3.8.3"
}
}
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: 'e2e/tsconfig.e2e.json'
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};
const Joi = require('joi');
// require and configure dotenv, will load vars in .env in PROCESS.ENV
require('dotenv').config();
// define validation for all the env vars
const envVarsSchema = Joi.object({
NODE_ENV: Joi.string()
.allow(['development', 'production', 'test', 'provision'])
.default('development'),
SERVER_PORT: Joi.number()
.default(4040),
MONGOOSE_DEBUG: Joi.boolean()
.when('NODE_ENV', {
is: Joi.string().equal('development'),
then: Joi.boolean().default(true),
otherwise: Joi.boolean().default(false)
}),
JWT_SECRET: Joi.string().required()
.description('JWT Secret required to sign'),
MONGO_HOST: Joi.string().required()
.description('Mongo DB host url'),
MONGO_PORT: Joi.number()
.default(27017)
}).unknown()
.required();
const { error, value: envVars } = Joi.validate(process.env, envVarsSchema);
if (error) {
throw new Error(`Config validation error: ${error.message}`);
}
const config = {
env: envVars.NODE_ENV,
port: envVars.SERVER_PORT,
mongooseDebug: envVars.MONGOOSE_DEBUG,
jwtSecret: envVars.JWT_SECRET,
frontend: envVars.MEAN_FRONTEND || 'angular',
mongo: {
host: envVars.MONGO_HOST,
port: envVars.MONGO_PORT
}
};
module.exports = config;
const path = require('path');
const express = require('express');
const httpError = require('http-errors');
const logger = require('morgan');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const compress = require('compression');
const methodOverride = require('method-override');
const cors = require('cors');
const helmet = require('helmet');
const swaggerUi = require('swagger-ui-express');
const swaggerDocument = require('./swagger.json');
const routes = require('../routes/index.route');
const config = require('./config');
const passport = require('./passport')
const app = express();
if (config.env === 'development') {
app.use(logger('dev'));
}
// Choose what fronten framework to serve the dist from
var distDir = '../../dist/';
if (config.frontend == 'react'){
distDir ='../../node_modules/material-dashboard-react/dist'
}else{
distDir ='../../dist/' ;
}
//
app.use(express.static(path.join(__dirname, distDir)))
app.use(/^((?!(api)).)*/, (req, res) => {
res.sendFile(path.join(__dirname, distDir + '/index.html'));
});
console.log(distDir);
//React server
app.use(express.static(path.join(__dirname, '../../node_modules/material-dashboard-react/dist')))
app.use(/^((?!(api)).)*/, (req, res) => {
res.sendFile(path.join(__dirname, '../../dist/index.html'));
});
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(compress());
app.use(methodOverride());
// secure apps by setting various HTTP headers
app.use(helmet());
// enable CORS - Cross Origin Resource Sharing
app.use(cors());
app.use(passport.initialize());
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
// API router
app.use('/api/', routes);
// catch 404 and forward to error handler
app.use((req, res, next) => {
const err = new httpError(404)
return next(err);
});
// error handler, send stacktrace only during development
app.use((err, req, res, next) => {
// customize Joi validation errors
if (err.isJoi) {
err.message = err.details.map(e => e.message).join("; ");
err.status = 400;
}
res.status(err.status || 500).json({
message: err.message
});
next(err);
});
module.exports = app;
const mongoose = require('mongoose');
const util = require('util');
const debug = require('debug')('express-mongoose-es6-rest-api:index');
const config = require('./config');
// connect to mongo db
const mongoUri = config.mongo.host;
mongoose.connect(mongoUri, { keepAlive: 1 });
mongoose.connection.on('error', () => {
throw new Error(`unable to connect to database: ${mongoUri}`);
});
// print mongoose logs in dev env
if (config.MONGOOSE_DEBUG) {
mongoose.set('debug', (collectionName, method, query, doc) => {
debug(`${collectionName}.${method}`, util.inspect(query, false, 20), doc);
});
}
const passport = require('passport');
const LocalStrategy = require('passport-local');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const bcrypt = require('bcrypt');
const User = require('../models/user.model');
const config = require('./config');
const localLogin = new LocalStrategy({
usernameField: 'email'
}, async (email, password, done) => {
let user = await User.findOne({ email });
if (!user || !bcrypt.compareSync(password, user.hashedPassword)) {
return done(null, false, { error: 'Your login details could not be verified. Please try again.' });
}
user = user.toObject();
delete user.hashedPassword;
done(null, user);
});
const jwtLogin = new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.jwtSecret
}, async (payload, done) => {
let user = await User.findById(payload._id);
if (!user) {
return done(null, false);
}
user = user.toObject();
delete user.hashedPassword;
done(null, user);
});
passport.use(jwtLogin);
passport.use(localLogin);
module.exports = passport;
{
"swagger": "2.0",
"info": {
"version": "1.0.0",
"title": "Mean Application API",
"description": "Mean Application API",
"license": {
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
}
},
"host": "localhost:4040",
"basePath": "/api/",
"tags": [
{
"name": "Users",
"description": "API for users in the system"
},
{
"name": "Auth",
"description": "API for auth in the system"
}
],
"schemes": [
"http"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"securityDefinitions": {
"AuthHeader": {
"type": "apiKey",
"in": "header",
"name": "Authorization"
}
},
"paths": {
"/auth/login": {
"post": {
"tags": ["Auth"],
"description": "Login to the system",
"parameters": [{
"name": "auth",
"in": "body",
"description": "User auth details",
"schema": {
"type": "object",
"required": ["email", "password"],
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string"
}
}
}
}],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "User is loggedin",
"schema": {
"$ref": "#/definitions/User"
}
}
}
}
}
},
"definitions": {
"User": {
"required": [
"email",
"fullname"
],
"properties": {
"_id": {
"type": "string",
"uniqueItems": true
},
"email": {
"type": "string",
"uniqueItems": true
},
"fullname": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"roles": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"Users": {
"type": "array",
"$ref": "#/definitions/User"
},
"Auth": {
"type": "object",
"properties": [{
"token": {
"type": "string"
},
"user": {
"$ref": "#/definitions/User"
}
}]
}
}
}
\ No newline at end of file
const jwt = require('jsonwebtoken');
const config = require('../config/config');
module.exports = {
generateToken
}
function generateToken(user) {
const payload = JSON.stringify(user);
return jwt.sign(payload, config.jwtSecret);
}
const bcrypt = require('bcrypt');
const Joi = require('joi');
const User = require('../models/user.model');
const userSchema = Joi.object({
fullname: Joi.string().required(),
email: Joi.string().email(),
mobileNumber: Joi.string().regex(/^[1-9][0-9]{9}$/),
password: Joi.string().required(),
repeatPassword: Joi.string().required().valid(Joi.ref('password'))
})
module.exports = {
insert
}
async function insert(user) {
user = await Joi.validate(user, userSchema, { abortEarly: false });
user.hashedPassword = bcrypt.hashSync(user.password, 10);
delete user.password;
return await new User(user).save();
}
// config should be imported before importing any other file
const config = require('./config/config');
const app = require('./config/express');
require('./config/mongoose');
// module.parent check is required to support mocha watch
// src: https://github.com/mochajs/mocha/issues/1912
if (!module.parent) {
app.listen(config.port, () => {
console.info(`server started on port ${config.port} (${config.env})`);
});
}
module.exports = app;
const httpError = require('http-errors');
const requireAdmin = function (req, res, next) {
if (req.user && req.user.roles.indexOf('admin') > -1)
return next();
const err = new httpError(401);
return next(err);
}
module.exports = requireAdmin;
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
fullname: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true,
// Regexp to validate emails with more strict rules as added in tests/users.js which also conforms mostly with RFC2822 guide lines
match: [/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, 'Please enter a valid email'],
},
hashedPassword: {
type: String,
required: true
},
createdAt: {
type: Date,
default: Date.now
},
roles: [{
type: String,
}]
}, {
versionKey: false
});
module.exports = mongoose.model('User', UserSchema);
const express = require('express');
const asyncHandler = require('express-async-handler')
const passport = require('passport');
const userCtrl = require('../controllers/user.controller');
const authCtrl = require('../controllers/auth.controller');
const config = require('../config/config');
const router = express.Router();
module.exports = router;
router.post('/register', asyncHandler(register), login);
router.post('/login', passport.authenticate('local', { session: false }), login);
router.get('/me', passport.authenticate('jwt', { session: false }), login);
async function register(req, res, next) {
let user = await userCtrl.insert(req.body);
user = user.toObject();
delete user.hashedPassword;
req.user = user;
next()
}
function login(req, res) {
let user = req.user;
let token = authCtrl.generateToken(user);
res.json({ user, token });
}
const express = require('express');
const userRoutes = require('./user.route');
const authRoutes = require('./auth.route');
const router = express.Router(); // eslint-disable-line new-cap
/** GET /health-check - Check service health */
router.get('/health-check', (req, res) =>
res.send('OK')
);
router.use('/auth', authRoutes);
router.use('/user', userRoutes);
module.exports = router;
const express = require('express');
const passport = require('passport');
const asyncHandler = require('express-async-handler');
const userCtrl = require('../controllers/user.controller');
const router = express.Router();
module.exports = router;
router.use(passport.authenticate('jwt', { session: false }))
router.route('/')
.post(asyncHandler(insert));
async function insert(req, res) {
let user = await userCtrl.insert(req.body);
res.json(user);
}
/*-----------------------------------------------
Variables
-----------------------------------------------*/
$linesColor: #dbdbdb;
$categoryTitleColor: #686868;
$categoryEntityColor: #3F3F3F;
\ No newline at end of file
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdminComponent } from './admin.component';
import { OnlyAdminUsersGuard } from './admin-user-guard';
const routes: Routes = [{
path: 'admin',
canActivate: [OnlyAdminUsersGuard],
children: [{
path: '',
component: AdminComponent,
}]
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdminRoutingModule {}
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AuthService } from '@app/shared/services';
@Injectable()
export class OnlyAdminUsersGuard implements CanActivate {
constructor(private authService: AuthService) {}
canActivate(): Observable<boolean> {
return this.authService.getUser().pipe(map(user => !!user?.isAdmin));
}
}
<h4>HELLO FROM ADMIN PAGE</h4>
\ No newline at end of file
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-admin',
templateUrl: './admin.component.html',
})
export class AdminComponent implements OnInit {
constructor() {}
public ngOnInit() {
}
}
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import { AdminRoutingModule } from './admin-routing.module';
import {AdminComponent} from './admin.component';
import {OnlyAdminUsersGuard} from './admin-user-guard';
@NgModule({
declarations: [
AdminComponent
],
imports: [
CommonModule,
AdminRoutingModule,
],
providers: [
OnlyAdminUsersGuard
]})
export class AdminModule {}
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "./shared/guards";
import { HomeComponent } from "./home/home.component";
import { TopicComponent } from "./topic/topic.component";
import { CreateTopicComponent } from "./create-topic/create-topic.component";
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
const routes: Routes = [
// {
// path: "",
// component: HomeComponent,
// canActivate: [AuthGuard],
// },
{
path: "",
component: HomeComponent
},
{
path: "login",
component: LoginComponent
},
{
path: "register",
component: RegisterComponent
},
{
path: "topic",
component: TopicComponent
},
{
path: "create",
component: CreateTopicComponent
},
{
path: "auth",
loadChildren: () => import("./auth/auth.module").then((m) => m.AuthModule),
},
{
path: "admin",
loadChildren: () =>
import("./admin/admin.module").then((m) => m.AdminModule),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
<app-header></app-header>
<!-- have login & register -->
<!-- <app-header [user]="user$ | async"></app-header> -->
<div class="wrapper-app">
<router-outlet></router-outlet>
</div>
<footer></footer>
import { Component } from '@angular/core';
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
import { merge, Observable } from 'rxjs';
import { User } from './shared/interfaces';
import { AuthService } from './shared/services';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
user$: Observable<User | null> = merge(
// Init on startup
this.authService.me(),
// Update after login/register/logout
this.authService.getUser()
);
constructor(
private domSanitizer: DomSanitizer,
private matIconRegistry: MatIconRegistry,
private authService: AuthService
) {
this.registerSvgIcons();
}
registerSvgIcons() {
[
'close',
'add',
'add-blue',
'airplane-front-view',
'air-station',
'balloon',
'boat',
'cargo-ship',
'car',
'catamaran',
'clone',
'convertible',
'delete',
'drone',
'fighter-plane',
'fire-truck',
'horseback-riding',
'motorcycle',
'railcar',
'railroad-train',
'rocket-boot',
'sailing-boat',
'segway',
'shuttle',
'space-shuttle',
'steam-engine',
'suv',
'tour-bus',
'tow-truck',
'transportation',
'trolleybus',
'water-transportation',
].forEach(icon => {
this.matIconRegistry.addSvgIcon(
icon,
this.domSanitizer.bypassSecurityTrustResourceUrl(`assets/icons/${icon}.svg`)
);
});
}
}
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { SharedModule } from './shared/shared.module';
import { AppComponent } from './app.component';
import { AuthHeaderInterceptor } from './interceptors/header.interceptor';
import { CatchErrorInterceptor } from './interceptors/http-error.interceptor';
import { AppRoutingModule } from './app-routing.module';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
import { AuthService } from './shared/services';
import { TopicComponent } from './topic/topic.component';
import { CreateTopicComponent } from './create-topic/create-topic.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
export function appInitializerFactory(authService: AuthService) {
return () => authService.checkTheUserOnTheFirstLoad();
}
@NgModule({
imports: [BrowserAnimationsModule, HttpClientModule, SharedModule, AppRoutingModule],
declarations: [AppComponent, HeaderComponent, HomeComponent, TopicComponent, CreateTopicComponent, LoginComponent, RegisterComponent],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthHeaderInterceptor,
multi: true,
},
{
provide: HTTP_INTERCEPTORS,
useClass: CatchErrorInterceptor,
multi: true,
},
{
provide: APP_INITIALIZER,
useFactory: appInitializerFactory,
multi: true,
deps: [AuthService],
},
],
bootstrap: [AppComponent],
})
export class AppModule {}
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
const routes: Routes = [
{
path: '',
children: [
{
path: '',
redirectTo: '/auth/login',
pathMatch: 'full',
},
{
path: 'login',
component: LoginComponent,
},
{
path: 'register',
component: RegisterComponent,
},
],
},
];
export const AuthRoutingModule = RouterModule.forChild(routes);
.example-icon {
padding: 0 14px;
}
.example-spacer {
flex: 1 1 auto;
}
.example-card {
width: 400px;
margin: 10% auto;
}
.mat-card-title {
font-size: 16px;
}
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { AuthRoutingModule } from './auth-routing.module';
@NgModule({
imports: [SharedModule, AuthRoutingModule],
declarations: [LoginComponent, RegisterComponent],
})
export class AuthModule {}
<mat-card class="example-card">
<mat-card-header>
<mat-card-title>Login</mat-card-title>
</mat-card-header>
<mat-card-content>
<form class="example-form">
<table cellspacing="0">
<tr>
<td>
<mat-form-field>
<input matInput placeholder="Email" [(ngModel)]="email" name="email" required>
</mat-form-field>
</td>
</tr>
<tr>
<td>
<mat-form-field>
<input matInput placeholder="Password" [(ngModel)]="password" type="password" name="password" required>
</mat-form-field>
</td>
</tr>
</table>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button (click)="login()" color="primary">Login</button>
<span>Don't have an account ? <a [routerLink]="['/auth/register']" >register</a> here</span>
</mat-card-actions>
</mat-card>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '@app/shared/services';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['../auth.component.scss'],
})
export class LoginComponent {
email: string | null = null;
password: string | null = null;
constructor(private router: Router, private authService: AuthService) {}
login(): void {
this.authService.login(this.email!, this.password!).subscribe(() => {
this.router.navigateByUrl('/');
});
}
}
<mat-card class="example-card">
<mat-card-header>
<mat-card-title>Register</mat-card-title>
</mat-card-header>
<mat-card-content>
<form class="example-form">
<table cellspacing="0" [formGroup]="userForm">
<tr>
<td>
<mat-form-field>
<input matInput placeholder="Fullname" formControlName="fullname" name="fullname" required>
</mat-form-field>
</td>
</tr>
<tr>
<td>
<mat-form-field>
<input matInput placeholder="Email" formControlName="email" name="email" required>
<mat-error *ngIf="email.invalid && email.hasError('email')">Invalid email address</mat-error>
</mat-form-field>
</td>
</tr>
<tr>
<td>
<mat-form-field>
<input matInput placeholder="Password" formControlName="password" type="password" name="password" required>
</mat-form-field>
</td>
</tr>
<tr>
<td>
<mat-form-field>
<input matInput placeholder="Repeat Password" formControlName="repeatPassword" type="password" name="repeatPassword" required>
<mat-error *ngIf="repeatPassword.invalid && repeatPassword.hasError('passwordMatch')">Password mismatch</mat-error>
</mat-form-field>
</td>
</tr>
</table>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button (click)="register()" color="primary">Register</button>
<span>Allrady have an account ? <a [routerLink]="['/auth/login']">login</a> here</span>
</mat-card-actions>
</mat-card>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RegisterComponent } from './register.component';
describe('RegisterComponent', () => {
let component: RegisterComponent;
let fixture: ComponentFixture<RegisterComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ RegisterComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RegisterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import {
FormGroup,
FormControl,
Validators,
ValidationErrors,
AbstractControl,
} from '@angular/forms';
import { AuthService } from '@app/shared/services';
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['../auth.component.scss'],
})
export class RegisterComponent {
constructor(private router: Router, private authService: AuthService) {}
passwordsMatchValidator(control: FormControl): ValidationErrors | null {
const password = control.root.get('password');
return password && control.value !== password.value
? {
passwordMatch: true,
}
: null;
}
userForm = new FormGroup({
fullname: new FormControl('', [Validators.required]),
email: new FormControl('', [Validators.required, Validators.email]),
password: new FormControl('', [Validators.required]),
repeatPassword: new FormControl('', [Validators.required, this.passwordsMatchValidator]),
});
get fullname(): AbstractControl {
return this.userForm.get('fullname')!;
}
get email(): AbstractControl {
return this.userForm.get('email')!;
}
get password(): AbstractControl {
return this.userForm.get('password')!;
}
get repeatPassword(): AbstractControl {
return this.userForm.get('repeatPassword')!;
}
register(): void {
if (this.userForm.invalid) {
return;
}
const { fullname, email, password, repeatPassword } = this.userForm.getRawValue();
this.authService.register(fullname, email, password, repeatPassword).subscribe(data => {
this.router.navigate(['']);
});
}
}
@import url("https://fonts.googleapis.com/css2?family=Kanit:wght@200;300;600&display=swap");
*{
font-family: 'Kanit', sans-serif;
}
\ No newline at end of file
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CreateTopicComponent } from './create-topic.component';
describe('CreateTopicComponent', () => {
let component: CreateTopicComponent;
let fixture: ComponentFixture<CreateTopicComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CreateTopicComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CreateTopicComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-create-topic',
templateUrl: './create-topic.component.html',
styleUrls: ['./create-topic.component.css']
})
export class CreateTopicComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
<header>
<mat-toolbar color="primary">
<div class="logo">
<img src="../../assets/img/Logo.png" alt="">
</div>
<span class="example-spacer"></span>
<div>
<button class="button-item " routerLink="">หน้าหลัก</button>
<button class="button-item" routerLink="/create">โพสต์</button>
</div>
<div class="card">
<p class="text-profile">น้องโอมไอ่เด็กเหี้ย</p>
<button class="button-logout" routerLink="/login">ออกจากระบบ</button>
</div>
</mat-toolbar>
</header>
<!-- <header>
<mat-toolbar color="primary">
<a routerLink="/" class="logo"></a>
<span class="example-spacer"></span>
<a class="links side" routerLink="/auth/login" *ngIf="!user">Login</a>
<div>
<a class="links side" *ngIf="user" routerLink="/topic">Topic</a>
<a class="links side" *ngIf="user" routerLink="/create">Create topic</a>
<a class="links side" *ngIf="user" [matMenuTriggerFor]="menu">
<mat-icon>account_circle</mat-icon>{{ user.fullname }}
</a>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngIf="user?.isAdmin" routerLink="/admin">admin</button>
<button mat-menu-item (click)="logout()">logout</button>
</mat-menu>
</div>
</mat-toolbar>
</header> -->
@import url("https://fonts.googleapis.com/css2?family=Kanit:wght@200;300;600&display=swap");
* {
font-family: "Kanit", sans-serif;
margin: 0px;
padding: 0px;
box-sizing: border-box;
}
header {
width: 100%;
.logo {
background-image: url("../../assets/logo.png");
width: 100%;
height: 70%;
background-size: contain;
background-repeat: no-repeat;
}
.example-spacer {
flex: 1 1 auto;
}
.button-item {
background-color: Transparent;
background-repeat: no-repeat;
border: none;
cursor: pointer;
overflow: hidden;
outline: none;
padding-left: 20px;
padding-right: 20px;
padding-top: 10px;
padding-bottom: 10px;
margin-right: 30px;
font-size: 20px;
color: white;
letter-spacing: 1px;
border-radius: 10px;
font-weight: bold;
}
.button-item:hover {
background-color: #ebebf5;
color: #2c2c2c;
}
.mat-toolbar {
display: flex;
background: #2c2c2c;
justify-content: center;
align-items: center;
min-height: 8vh;
}
.card {
display: flex;
background-color: #1e1e1e;
transition: 0.3s;
padding: 7px;
border-radius: 6px;
}
.button-logout {
border: none;
cursor: pointer;
overflow: hidden;
outline: none;
background-color: #ff3b30;
color: white;
letter-spacing: 1px;
border-radius: 6px;
font-size: 16px;
padding-left: 20px;
padding-right: 20px;
}
.button-logout:hover {
background-color: #c02b23;
}
.text-profile {
margin-right: 20px;
margin-left: 10px;
letter-spacing: 0.5px;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeaderComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, Input } from '@angular/core';
import { Router } from '@angular/router';
import { User } from '@app/shared/interfaces';
import { AuthService } from '@app/shared/services';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent {
@Input() user: User | null = null;
constructor(private router: Router, private authService: AuthService) {}
logout(): void {
this.authService.signOut();
this.router.navigateByUrl('/auth/login');
}
}
<div class="content-topic-page">
<div class="row">
<div class="card-content">
<div class="row">
<h2 class="title-topic">ล่าสุด</h2>
</div>
<table>
<tr>
<th>
<button class="content-flim">
Review: The Devil All the Time (Antonio Campos, 2020) รีวิวโดย MDC
</button>
</th>
<th>
<button class="content-flim">
ซ่อนเงาผี - Behind You - ไปดูมาแล้ว
</button>
</th>
<th>
<button class="content-flim">
มู่หลาน สงครามย้อนยุค ไปดูๆๆๆๆๆๆๆ
</button>
</th>
</tr>
</table>
</div>
</div>
</div>
<div class="content-topic-page">
<div class="row">
<div class="card-content leftcolumn">
<div class="row">
<h2 class="title-topic" style="padding-bottom: 30px">บทความ</h2>
</div>
<div class="row">
<div class="content-topic">
<div style="padding: 10px" *ngFor="let i of datas">
<a href=""
><label>{{ i.heading }}</label></a
>
<p>{{ i.type }} &nbsp;|&nbsp;&nbsp;{{ i.date }}</p>
</div>
</div>
</div>
</div>
<div class="card-content rightcolumn">
<div class="row">
<h2 class="title-topic" style="padding-bottom: 30px">หมวดหมู</h2>
</div>
<div class="row">
<div class="content-topic">
<div style="padding: 10px" *ngFor="let i of catagorys">
<a href=""
><label>{{ i.type }}</label></a
>
</div>
</div>
</div>
</div>
</div>
</div>
@import url("https://fonts.googleapis.com/css2?family=Kanit:wght@200;300;600&display=swap");
* {
font-family: "Kanit", sans-serif;
}
.content-topic-page {
padding-left: 30px;
padding-right: 30px;
padding-top: 20px;
padding-bottom: 5px;
}
.title-topic {
padding-left: 15px;
padding-bottom: 15px;
}
.card-content {
padding-bottom: 35px;
padding-left: 30px;
padding-right: 30px;
background-color: white;
border-radius: 10px;
opacity: 0.8;
background-color: #3e3e3e;
}
.content-card {
background-color: #262626;
opacity: 1;
}
table {
width: 100%;
}
th {
text-align: center;
}
.content-flim {
background-color: #262626;
opacity: 1px;
width: 400px;
height: 100px;
font-size: 18px;
font-weight: lighter;
padding: 10px;
border-radius: 6px;
border: none;
outline: none;
}
.content-topic {
margin: auto;
background-color: #262626;
opacity: 1px;
font-size: 18px;
width: 95%;
font-weight: lighter;
padding: 10px;
border-radius: 6px;
border: none;
outline: none;
}
.content-catagory {
margin: auto;
background-color: #262626;
width: 55%;
opacity: 1px;
width: 400px;
height: 100px;
font-size: 18px;
font-weight: lighter;
padding: 10px;
border-radius: 6px;
border: none;
outline: none;
}
.leftcolumn {
float: left;
width: 73%;
}
.rightcolumn {
float: right;
width: 25%;
padding-left: 20px;
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HomeComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from "@angular/core";
import { data } from "data";
import { catagory } from "data";
@Component({
selector: "app-home",
templateUrl: "./home.component.html",
styleUrls: ["./home.component.scss"],
})
export class HomeComponent implements OnInit {
datas = data;
catagorys = catagory;
constructor() {}
ngOnInit() {
console.log(this.datas);
}
}
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from '@app/shared/services';
@Injectable()
export class AuthHeaderInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
req = req.clone({
setHeaders: this.authService.getAuthorizationHeaders(),
});
return next.handle(req);
}
}
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpInterceptor,
HttpHandler,
HttpRequest,
HttpErrorResponse,
} from '@angular/common/http';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class CatchErrorInterceptor implements HttpInterceptor {
constructor(private snackBar: MatSnackBar) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError(this.showSnackBar));
}
private showSnackBar = (response: HttpErrorResponse): Observable<never> => {
const text: string | undefined = response.error?.message ?? response.error.statusText;
if (text) {
this.snackBar.open(text, 'Close', {
duration: 2000,
});
}
return throwError(response);
};
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RegisterComponent } from './register.component';
describe('RegisterComponent', () => {
let component: RegisterComponent;
let fixture: ComponentFixture<RegisterComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ RegisterComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RegisterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.css']
})
export class RegisterComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AuthService } from '../services';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private router: Router, private authService: AuthService) {}
canActivate(): Observable<boolean> {
return this.authService.getUser().pipe(
map(user => {
if (user !== null) {
return true;
}
this.router.navigateByUrl('/auth/login');
return false;
})
);
}
}
export * from './user.interface';
export interface User {
_id: string;
fullname: string;
createdAt: string;
roles: string[];
isAdmin: boolean;
}
import { TestBed, inject } from '@angular/core/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AuthService]
});
});
it('should be created', inject([AuthService], (service: AuthService) => {
expect(service).toBeTruthy();
}));
});
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, EMPTY } from 'rxjs';
import { tap, pluck } from 'rxjs/operators';
import { User } from '@app/shared/interfaces';
import { TokenStorage } from './token.storage';
interface AuthResponse {
token: string;
user: User;
}
@Injectable({ providedIn: 'root' })
export class AuthService {
private user$ = new BehaviorSubject<User | null>(null);
constructor(private http: HttpClient, private tokenStorage: TokenStorage) {}
login(email: string, password: string): Observable<User> {
return this.http
.post<AuthResponse>('/api/auth/login', { email, password })
.pipe(
tap(({ token, user }) => {
this.setUser(user);
this.tokenStorage.saveToken(token);
}),
pluck('user')
);
}
register(
fullname: string,
email: string,
password: string,
repeatPassword: string
): Observable<User> {
return this.http
.post<AuthResponse>('/api/auth/register', {
fullname,
email,
password,
repeatPassword,
})
.pipe(
tap(({ token, user }) => {
this.setUser(user);
this.tokenStorage.saveToken(token);
}),
pluck('user')
);
}
setUser(user: User | null): void {
if (user) {
user.isAdmin = user.roles.includes('admin');
}
this.user$.next(user);
window.user = user;
}
getUser(): Observable<User | null> {
return this.user$.asObservable();
}
me(): Observable<User> {
const token: string | null = this.tokenStorage.getToken();
if (token === null) {
return EMPTY;
}
return this.http.get<AuthResponse>('/api/auth/me').pipe(
tap(({ user }) => this.setUser(user)),
pluck('user')
);
}
signOut(): void {
this.tokenStorage.signOut();
this.setUser(null);
delete window.user;
}
getAuthorizationHeaders() {
const token: string | null = this.tokenStorage.getToken() || '';
return { Authorization: `Bearer ${token}` };
}
/**
* Let's try to get user's information if he was logged in previously,
* thus we can ensure that the user is able to access the `/` (home) page.
*/
checkTheUserOnTheFirstLoad(): Promise<User> {
return this.me().toPromise();
}
}
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class TokenStorage {
private tokenKey = 'authToken';
signOut(): void {
localStorage.removeItem(this.tokenKey);
localStorage.clear();
}
saveToken(token?: string): void {
if (!token) return;
localStorage.setItem(this.tokenKey, token);
}
getToken(): string | null {
return localStorage.getItem(this.tokenKey);
}
}
export * from './auth/auth.service';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatMenuModule } from '@angular/material/menu';
import { MatTabsModule } from '@angular/material/tabs';
import { MatCardModule } from '@angular/material/card';
import { MatListModule } from '@angular/material/list';
import { MatIconModule } from '@angular/material/icon';
import { MatTreeModule } from '@angular/material/tree';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatProgressBarModule } from '@angular/material/progress-bar';
@NgModule({
exports: [
FormsModule,
ReactiveFormsModule,
CommonModule,
MatMenuModule,
MatTabsModule,
MatCardModule,
MatListModule,
MatIconModule,
MatTreeModule,
MatInputModule,
MatSelectModule,
MatDialogModule,
MatButtonModule,
MatDividerModule,
MatToolbarModule,
MatSidenavModule,
MatSnackBarModule,
MatExpansionModule,
MatFormFieldModule,
MatProgressBarModule,
],
})
export class SharedModule {}
@import url("https://fonts.googleapis.com/css2?family=Kanit:wght@200;300;600&display=swap");
*{
font-family: 'Kanit', sans-serif;
}
\ No newline at end of file
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TopicComponent } from './topic.component';
describe('TopicComponent', () => {
let component: TopicComponent;
let fixture: ComponentFixture<TopicComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TopicComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TopicComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-topic',
templateUrl: './topic.component.html',
styleUrls: ['./topic.component.css']
})
export class TopicComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 5H9V9H5V11H9V15H11V11H15V9H11V5ZM10 0C4.48 0 0 4.48 0 10C0 15.52 4.48 20 10 20C15.52 20 20 15.52 20 10C20 4.48 15.52 0 10 0ZM10 18C5.59 18 2 14.41 2 10C2 5.59 5.59 2 10 2C14.41 2 18 5.59 18 10C18 14.41 14.41 18 10 18Z" transform="translate(0.97168 0.970551)" fill="#2D9CDB"/>
</svg>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.89626 5.39898H8.09694V8.09847H5.39796V9.89813H8.09694V12.5976H9.89626V9.89813H12.5952V8.09847H9.89626V5.39898ZM8.9966 0C4.04847 0 0 4.04923 0 8.9983C0 13.9474 4.04847 17.9966 8.9966 17.9966C13.9447 17.9966 17.9932 13.9474 17.9932 8.9983C17.9932 4.04923 13.9447 0 8.9966 0ZM8.9966 16.1969C5.0381 16.1969 1.79932 12.9575 1.79932 8.9983C1.79932 5.03905 5.0381 1.79966 8.9966 1.79966C12.9551 1.79966 16.1939 5.03905 16.1939 8.9983C16.1939 12.9575 12.9551 16.1969 8.9966 16.1969Z" transform="translate(-0.000488281 -0.000610352)" fill="black"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 40 40" version="1.1">
<g id="surface1">
<path style=" fill:#B6DCFE;" d="M 24.117188 35.5 L 15.882813 35.5 L 10.5 38.191406 L 10.5 36.234375 L 16.179688 31.5 L 23.820313 31.5 L 29.5 36.234375 L 29.5 38.191406 Z "></path>
<path style=" fill:#4788C7;" d="M 23.636719 32 L 29 36.46875 L 29 37.382813 L 24.234375 35 L 15.765625 35 L 11 37.382813 L 11 36.46875 L 16.363281 32 L 23.636719 32 M 24 31 L 16 31 L 10 36 L 10 39 L 16 36 L 24 36 L 30 39 L 30 36 Z "></path>
<path style=" fill:#B6DCFE;" d="M 24.070313 23.5 L 15.929688 23.5 L 2.5 27.335938 L 2.5 24.289063 L 16.132813 16.5 L 23.867188 16.5 L 37.5 24.289063 L 37.5 27.335938 Z "></path>
<path style=" fill:#4788C7;" d="M 23.734375 17 L 37 24.578125 L 37 26.675781 L 24.273438 23.039063 L 24.140625 23 L 15.859375 23 L 15.726563 23.039063 L 3 26.675781 L 3 24.578125 L 16.265625 17 L 23.734375 17 M 24 16 L 16 16 L 2 24 L 2 28 L 16 24 L 24 24 L 38 28 L 38 24 Z "></path>
<path style=" fill:#DFF0FE;" d="M 16.5 35.5 L 16.5 5 C 16.5 3.070313 18.070313 1.5 20 1.5 C 21.929688 1.5 23.5 3.070313 23.5 5 L 23.5 35.5 Z "></path>
<path style=" fill:#4788C7;" d="M 20 2 C 21.652344 2 23 3.347656 23 5 L 23 35 L 17 35 L 17 5 C 17 3.347656 18.347656 2 20 2 M 20 1 C 17.789063 1 16 2.789063 16 5 L 16 36 L 24 36 L 24 5 C 24 2.789063 22.210938 1 20 1 Z "></path>
</g>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 40 40" version="1.1">
<g id="surface1">
<path style=" fill:#4788C7;" d="M 25.5 14 L 14.5 14 C 14.222656 14 14 13.777344 14 13.5 C 14 13.222656 14.222656 13 14.5 13 L 25.5 13 C 25.777344 13 26 13.222656 26 13.5 C 26 13.777344 25.777344 14 25.5 14 Z "></path>
<path style=" fill:#DFF0FE;" d="M 17.605469 18.5 L 18.96875 11.414063 C 19.046875 10.882813 19.488281 10.5 20 10.5 C 20.511719 10.5 20.953125 10.882813 21.027344 11.390625 L 22.394531 18.5 Z "></path>
<path style=" fill:#4788C7;" d="M 20 11 C 20.265625 11 20.492188 11.199219 20.539063 11.507813 L 21.789063 18 L 18.210938 18 L 19.46875 11.460938 C 19.507813 11.199219 19.734375 11 20 11 M 20 10 C 19.234375 10 18.585938 10.5625 18.476563 11.320313 L 17 19 L 23 19 L 21.523438 11.320313 C 21.414063 10.5625 20.765625 10 20 10 Z "></path>
<path style=" fill:#DFF0FE;" d="M 17.058594 25.5 L 0.5 21.605469 L 0.5 19.5 L 39.5 19.5 L 39.5 21.605469 L 22.941406 25.503906 Z "></path>
<path style=" fill:#4788C7;" d="M 39 20 L 39 21.207031 L 22.882813 25.003906 L 17.117188 25 L 1 21.207031 L 1 20 L 39 20 M 40 19 L 0 19 L 0 22 L 17 26 L 23 26.003906 L 40 22 Z "></path>
<path style=" fill:#B6DCFE;" d="M 20 29.5 C 17.039063 29.5 14.5 26.109375 14.5 23.332031 C 14.5 20.667969 16.96875 18.5 20 18.5 C 23.03125 18.5 25.5 20.667969 25.5 23.332031 C 25.5 26.109375 22.960938 29.5 20 29.5 Z "></path>
<path style=" fill:#4788C7;" d="M 20 19 C 22.757813 19 25 20.945313 25 23.332031 C 25 25.832031 22.644531 29 20 29 C 17.355469 29 15 25.832031 15 23.332031 C 15 20.945313 17.242188 19 20 19 M 20 18 C 16.6875 18 14 20.386719 14 23.332031 C 14 26.277344 16.6875 30 20 30 C 23.3125 30 26 26.277344 26 23.332031 C 26 20.386719 23.3125 18 20 18 Z "></path>
<path style=" fill:#B6DCFE;" d="M 35.5 25.5 C 35.5 27.15625 34.15625 28.5 32.5 28.5 C 30.84375 28.5 29.5 27.15625 29.5 25.5 C 29.5 23.84375 30.84375 22.5 32.5 22.5 C 34.15625 22.5 35.5 23.84375 35.5 25.5 Z "></path>
<path style=" fill:#4788C7;" d="M 32.5 23 C 33.878906 23 35 24.121094 35 25.5 C 35 26.878906 33.878906 28 32.5 28 C 31.121094 28 30 26.878906 30 25.5 C 30 24.121094 31.121094 23 32.5 23 M 32.5 22 C 30.566406 22 29 23.566406 29 25.5 C 29 27.433594 30.566406 29 32.5 29 C 34.433594 29 36 27.433594 36 25.5 C 36 23.566406 34.433594 22 32.5 22 Z "></path>
<path style=" fill:#4788C7;" d="M 21.25 26 C 21.25 26.691406 20.691406 27.25 20 27.25 C 19.308594 27.25 18.75 26.691406 18.75 26 C 18.75 25.308594 19.308594 24.75 20 24.75 C 20.691406 24.75 21.25 25.308594 21.25 26 Z "></path>
<path style=" fill:#B6DCFE;" d="M 10.5 25.5 C 10.5 27.15625 9.15625 28.5 7.5 28.5 C 5.84375 28.5 4.5 27.15625 4.5 25.5 C 4.5 23.84375 5.84375 22.5 7.5 22.5 C 9.15625 22.5 10.5 23.84375 10.5 25.5 Z "></path>
<path style=" fill:#4788C7;" d="M 7.5 23 C 8.878906 23 10 24.121094 10 25.5 C 10 26.878906 8.878906 28 7.5 28 C 6.121094 28 5 26.878906 5 25.5 C 5 24.121094 6.121094 23 7.5 23 M 7.5 22 C 5.566406 22 4 23.566406 4 25.5 C 4 27.433594 5.566406 29 7.5 29 C 9.433594 29 11 27.433594 11 25.5 C 11 23.566406 9.433594 22 7.5 22 Z "></path>
<path style=" fill:#4788C7;" d="M 23 23 L 17 23 L 18 22 L 22 22 Z "></path>
</g>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 40 40" version="1.1">
<g id="surface1">
<path style=" fill:#4788C7;" d="M 23 29.5 L 24 29.5 L 24 36.285156 L 23 36.285156 Z "></path>
<path style=" fill:#4788C7;" d="M 16 29.5 L 17 29.5 L 17 36.285156 L 16 36.285156 Z "></path>
<path style=" fill:#98CCFD;" d="M 16.25 30.5 C 15.375 29.351563 14.421875 28.175781 13.492188 27.035156 C 10.054688 22.808594 6.5 18.433594 6.5 14.570313 C 6.5 7.363281 12.554688 1.5 20 1.5 C 27.445313 1.5 33.5 7.363281 33.5 14.570313 C 33.5 18.433594 29.945313 22.808594 26.507813 27.035156 C 25.578125 28.175781 24.625 29.347656 23.75 30.5 Z "></path>
<path style=" fill:#4788C7;" d="M 20 2 C 27.167969 2 33 7.640625 33 14.570313 C 33 18.257813 29.503906 22.558594 26.117188 26.71875 C 25.242188 27.796875 24.339844 28.90625 23.503906 30 L 16.496094 30 C 15.660156 28.90625 14.757813 27.796875 13.882813 26.71875 C 10.496094 22.558594 7 18.257813 7 14.570313 C 7 7.640625 12.832031 2 20 2 M 20 1 C 12.269531 1 6 7.074219 6 14.570313 C 6 19.753906 11.867188 25.507813 16 31 L 24 31 C 28.132813 25.507813 34 19.753906 34 14.570313 C 34 7.074219 27.730469 1 20 1 Z "></path>
<path style=" fill:#DFF0FE;" d="M 22 30 C 36.5625 1.9375 20 2 20 2 C 20 2 30.59375 4.78125 21.023438 30 Z "></path>
<path style=" fill:#DFF0FE;" d="M 18 30 C 3.4375 1.9375 20 2 20 2 C 20 2 9.40625 4.78125 18.976563 30 Z "></path>
<path style=" fill:#DFF0FE;" d="M 18 38.5 C 17.171875 38.5 16.5 37.828125 16.5 37 L 16.5 33.5 L 23.5 33.5 L 23.5 37 C 23.5 37.828125 22.828125 38.5 22 38.5 Z "></path>
<path style=" fill:#4788C7;" d="M 23 34 L 23 37 C 23 37.550781 22.550781 38 22 38 L 18 38 C 17.449219 38 17 37.550781 17 37 L 17 34 L 23 34 M 24 33 L 16 33 L 16 37 C 16 38.105469 16.894531 39 18 39 L 22 39 C 23.105469 39 24 38.105469 24 37 Z "></path>
</g>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 40 40" version="1.1">
<g id="surface1">
<path style=" fill:#DFF0FE;" d="M 17.738281 8.585938 C 20.917969 8.996094 32.84375 11.085938 35.371094 19.585938 L 22.335938 21.445313 Z "></path>
<path style=" fill:#4788C7;" d="M 18.488281 9.195313 C 22.335938 9.789063 32.125 11.992188 34.710938 19.175781 L 22.667969 20.894531 L 18.488281 9.195313 M 17 8 L 22 22 L 36 20 C 33.382813 9.257813 17 8 17 8 Z "></path>
<path style=" fill:#4788C7;" d="M 12 2 L 13 2 L 13 27 L 12 27 Z "></path>
<path style=" fill:#4788C7;" d="M 12 6 C 16.132813 3.425781 17.769531 5.980469 21.5 4.5 C 20.121094 4.480469 17.003906 2 13.355469 2 C 12.09375 2 12 2 12 2 Z "></path>
<path style=" fill:#DFF0FE;" d="M 12.859375 23.140625 C 16.355469 16.285156 14.050781 9.386719 12.9375 6.8125 C 16.449219 7.980469 27.5625 12.4375 29.414063 22.523438 Z "></path>
<path style=" fill:#4788C7;" d="M 13.835938 7.65625 C 17.953125 9.191406 26.882813 13.457031 28.808594 22.042969 L 13.671875 22.609375 C 16.421875 16.558594 14.984375 10.699219 13.835938 7.65625 M 12 6 C 12 6 17.03125 14.957031 12 23.675781 L 30 23 C 28.109375 10.140625 12 6 12 6 Z "></path>
<path style=" fill:#98CCFD;" d="M 6.25 34.5 C 5.570313 33.578125 2.695313 29.390625 2.511719 24.5 L 8.792969 24.5 L 10.792969 26.5 L 26.207031 26.5 L 28.207031 24.5 L 36.488281 24.5 C 36.191406 30.480469 30.703125 33.996094 29.863281 34.5 Z "></path>
<path style=" fill:#4788C7;" d="M 35.949219 25 C 35.40625 30.191406 30.765625 33.351563 29.722656 34 L 6.503906 34 C 5.695313 32.855469 3.371094 29.246094 3.039063 25 L 8.585938 25 L 10.585938 27 L 26.414063 27 L 28.414063 25 L 35.949219 25 M 37 24 L 28 24 L 26 26 L 11 26 L 9 24 L 2 24 C 2 29.988281 6 35 6 35 L 30 35 C 30 35 37 31.144531 37 24 Z "></path>
<path style=" fill:#B6DCFE;" d="M 1 39 L 39 39 L 39 31.878906 C 37.691406 31.613281 36.148438 31 35 31 C 33.542969 31 31.4375 32 30 32 C 28.5625 32 26.457031 31 25 31 C 23.542969 31 21.4375 32 20 32 C 18.5625 32 16.457031 31 15 31 C 13.542969 31 11.4375 32 10 32 C 8.5625 32 6.457031 31 5 31 C 3.851563 31 2.308594 31.613281 1 31.878906 Z "></path>
<path style="fill:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke:#4788C7;stroke-opacity:1;stroke-miterlimit:10;" d="M 1.5 32.277344 C 1.863281 32.191406 2.238281 32.085938 2.613281 31.980469 C 3.460938 31.746094 4.335938 31.5 5 31.5 C 5.664063 31.5 6.535156 31.746094 7.382813 31.984375 C 8.28125 32.238281 9.214844 32.5 10 32.5 C 10.785156 32.5 11.71875 32.238281 12.617188 31.984375 C 13.464844 31.746094 14.335938 31.5 15 31.5 C 15.664063 31.5 16.535156 31.746094 17.382813 31.984375 C 18.28125 32.238281 19.214844 32.5 20 32.5 C 20.785156 32.5 21.71875 32.238281 22.617188 31.984375 C 23.464844 31.746094 24.335938 31.5 25 31.5 C 25.664063 31.5 26.535156 31.746094 27.382813 31.984375 C 28.28125 32.238281 29.214844 32.5 30 32.5 C 30.785156 32.5 31.71875 32.238281 32.617188 31.984375 C 33.464844 31.746094 34.335938 31.5 35 31.5 C 35.664063 31.5 36.539063 31.746094 37.386719 31.980469 C 37.761719 32.085938 38.136719 32.191406 38.5 32.277344 "></path>
</g>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 40 40" version="1.1">
<g id="surface1">
<path style=" fill:#98CCFD;" d="M 4 28.5 C 2.070313 28.5 0.5 26.929688 0.5 25 L 0.5 19.753906 C 0.5 18.757813 1.175781 17.890625 2.144531 17.652344 L 6.328125 16.421875 L 8.429688 12.875 C 9.296875 11.410156 10.890625 10.5 12.59375 10.5 L 21.992188 10.5 C 23.273438 10.5 24.523438 11.019531 25.425781 11.929688 L 29.730469 16.441406 L 37.914063 18.929688 C 38.851563 19.195313 39.5 20.050781 39.5 21.015625 L 39.5 26.332031 C 39.5 27.527344 38.527344 28.5 37.332031 28.5 Z "></path>
<path style=" fill:#4788C7;" d="M 21.992188 11 C 23.140625 11 24.261719 11.46875 25.058594 12.269531 L 29.277344 16.691406 L 29.457031 16.878906 L 29.707031 16.957031 L 37.785156 19.410156 C 38.503906 19.613281 39 20.273438 39 21.015625 L 39 26.332031 C 39 27.253906 38.253906 28 37.332031 28 L 4 28 C 2.347656 28 1 26.652344 1 25 L 1 19.753906 C 1 18.988281 1.519531 18.320313 2.308594 18.125 L 6.28125 16.960938 L 6.660156 16.847656 L 6.859375 16.511719 L 8.863281 13.128906 C 9.636719 11.816406 11.066406 11 12.59375 11 L 21.992188 11 M 21.992188 10 L 12.59375 10 C 10.707031 10 8.960938 10.996094 8 12.621094 L 6 16 L 2.027344 17.164063 C 0.835938 17.460938 0 18.527344 0 19.753906 L 0 25 C 0 27.210938 1.789063 29 4 29 L 37.332031 29 C 38.804688 29 40 27.804688 40 26.332031 L 40 21.015625 C 40 19.824219 39.210938 18.773438 38.0625 18.449219 L 30 16 L 25.78125 11.578125 C 24.777344 10.570313 23.414063 10 21.992188 10 Z "></path>
<path style=" fill:#DFF0FE;" d="M 35.5 27.5 C 35.5 29.710938 33.710938 31.5 31.5 31.5 C 29.289063 31.5 27.5 29.710938 27.5 27.5 C 27.5 25.289063 29.289063 23.5 31.5 23.5 C 33.710938 23.5 35.5 25.289063 35.5 27.5 Z "></path>
<path style=" fill:#4788C7;" d="M 31.5 24 C 33.429688 24 35 25.570313 35 27.5 C 35 29.429688 33.429688 31 31.5 31 C 29.570313 31 28 29.429688 28 27.5 C 28 25.570313 29.570313 24 31.5 24 M 31.5 23 C 29.015625 23 27 25.015625 27 27.5 C 27 29.984375 29.015625 32 31.5 32 C 33.984375 32 36 29.984375 36 27.5 C 36 25.015625 33.984375 23 31.5 23 Z "></path>
<path style=" fill:#4788C7;" d="M 33 27.5 C 33 28.328125 32.328125 29 31.5 29 C 30.671875 29 30 28.328125 30 27.5 C 30 26.671875 30.671875 26 31.5 26 C 32.328125 26 33 26.671875 33 27.5 Z "></path>
<path style=" fill:#DFF0FE;" d="M 12.5 27.5 C 12.5 29.710938 10.710938 31.5 8.5 31.5 C 6.289063 31.5 4.5 29.710938 4.5 27.5 C 4.5 25.289063 6.289063 23.5 8.5 23.5 C 10.710938 23.5 12.5 25.289063 12.5 27.5 Z "></path>
<path style=" fill:#4788C7;" d="M 8.5 24 C 10.429688 24 12 25.570313 12 27.5 C 12 29.429688 10.429688 31 8.5 31 C 6.570313 31 5 29.429688 5 27.5 C 5 25.570313 6.570313 24 8.5 24 M 8.5 23 C 6.015625 23 4 25.015625 4 27.5 C 4 29.984375 6.015625 32 8.5 32 C 10.984375 32 13 29.984375 13 27.5 C 13 25.015625 10.984375 23 8.5 23 Z "></path>
<path style=" fill:#4788C7;" d="M 10 27.5 C 10 28.328125 9.328125 29 8.5 29 C 7.671875 29 7 28.328125 7 27.5 C 7 26.671875 7.671875 26 8.5 26 C 9.328125 26 10 26.671875 10 27.5 Z "></path>
<path style=" fill:#DFF0FE;" d="M 5.890625 16.5 L 8.410156 12.90625 C 9.296875 11.410156 10.890625 10.5 12.59375 10.5 L 21.992188 10.5 C 23.273438 10.5 24.523438 11.019531 25.425781 11.929688 L 30.0625 16.5 Z "></path>
<path style=" fill:#4788C7;" d="M 21.992188 11 C 23.140625 11 24.261719 11.46875 25.078125 12.292969 L 28.84375 16 L 6.851563 16 L 8.816406 13.195313 L 8.839844 13.164063 L 8.859375 13.128906 C 9.636719 11.816406 11.066406 11 12.59375 11 L 21.992188 11 M 21.992188 10 L 12.59375 10 C 10.707031 10 8.960938 10.996094 8 12.621094 L 5.394531 16.332031 L 3 17 L 33 17 L 30.605469 16.332031 L 25.78125 11.578125 C 24.777344 10.570313 23.414063 10 21.992188 10 Z "></path>
<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#4788C7;stroke-opacity:1;stroke-miterlimit:10;" d="M 16.5 17 L 16.5 10.332031 "></path>
<path style=" fill:#FFFFFF;" d="M 2.5 23 L 1 23 L 1 20 L 2.5 20 C 3.328125 20 4 20.671875 4 21.5 C 4 22.328125 3.328125 23 2.5 23 Z "></path>
<path style=" fill:#FFFFFF;" d="M 38.5 21 C 37.671875 21 37 21.671875 37 22.5 C 37 23.328125 37.671875 24 38.5 24 L 39 24 L 39 21.5 C 39 21.324219 38.964844 21.160156 38.921875 21 Z "></path>
</g>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 40 40" version="1.1">
<g id="surface1">
<path style=" fill:#4788C7;" d="M 5.5 8 C 5.226563 8 5 7.773438 5 7.5 L 5 1.5 C 5 1.226563 5.226563 1 5.5 1 C 5.773438 1 6 1.226563 6 1.5 L 6 7.5 C 6 7.773438 5.773438 8 5.5 8 Z "></path>
<path style=" fill:#B6DCFE;" d="M 16.5 7.5 L 22.5 7.5 L 22.5 13.5 L 16.5 13.5 Z "></path>
<path style=" fill:#4788C7;" d="M 22 8 L 22 13 L 17 13 L 17 8 L 22 8 M 23 7 L 16 7 L 16 14 L 23 14 Z "></path>
<path style=" fill:#B6DCFE;" d="M 22.5 7.5 L 28.5 7.5 L 28.5 13.5 L 22.5 13.5 Z "></path>
<path style=" fill:#4788C7;" d="M 28 8 L 28 13 L 23 13 L 23 8 L 28 8 M 29 7 L 22 7 L 22 14 L 29 14 Z "></path>
<path style=" fill:#B6DCFE;" d="M 28.5 7.5 L 34.5 7.5 L 34.5 13.5 L 28.5 13.5 Z "></path>
<path style=" fill:#4788C7;" d="M 34 8 L 34 13 L 29 13 L 29 8 L 34 8 M 35 7 L 28 7 L 28 14 L 35 14 Z "></path>
<path style=" fill:#B6DCFE;" d="M 16.5 13.5 L 22.5 13.5 L 22.5 19.5 L 16.5 19.5 Z "></path>
<path style=" fill:#4788C7;" d="M 22 14 L 22 19 L 17 19 L 17 14 L 22 14 M 23 13 L 16 13 L 16 20 L 23 20 Z "></path>
<path style=" fill:#B6DCFE;" d="M 22.5 13.5 L 28.5 13.5 L 28.5 19.5 L 22.5 19.5 Z "></path>
<path style=" fill:#4788C7;" d="M 28 14 L 28 19 L 23 19 L 23 14 L 28 14 M 29 13 L 22 13 L 22 20 L 29 20 Z "></path>
<path style=" fill:#B6DCFE;" d="M 28.5 13.5 L 34.5 13.5 L 34.5 19.5 L 28.5 19.5 Z "></path>
<path style=" fill:#4788C7;" d="M 34 14 L 34 19 L 29 19 L 29 14 L 34 14 M 35 13 L 28 13 L 28 20 L 35 20 Z "></path>
<path style=" fill:#DFF0FE;" d="M 1.5 19.5 L 1.5 6.5 L 12.390625 6.5 L 11.511719 10.902344 L 11.5 19.5 Z "></path>
<path style=" fill:#4788C7;" d="M 11.78125 7 L 11.019531 10.804688 L 11 10.902344 L 11 19 L 2 19 L 2 7 L 11.78125 7 M 13 6 L 1 6 L 1 20 L 12 20 L 12 11 Z "></path>
<path style=" fill:#98CCFD;" d="M 4.207031 34.558594 C 4.148438 32.5 3.695313 31.261719 3.175781 29.839844 C 2.449219 27.859375 1.550781 25.410156 1.503906 19.5 C 1.503906 19.5 24.003906 19.5 24.007813 19.5 C 24.671875 19.5 27.613281 19.34375 30.222656 16.5 L 38.496094 16.5 C 38.417969 24.304688 36.503906 27.332031 34.957031 29.777344 C 34.015625 31.269531 33.1875 32.574219 33.082031 34.558594 L 19.207031 36.394531 Z "></path>
<path style=" fill:#4788C7;" d="M 37.988281 17 C 37.835938 24.289063 36.015625 27.171875 34.535156 29.511719 C 33.65625 30.902344 32.820313 32.226563 32.617188 34.117188 L 19.203125 35.890625 L 4.6875 34.113281 C 4.578125 32.21875 4.121094 30.972656 3.644531 29.667969 C 2.914063 27.683594 2.097656 25.453125 2.007813 20 L 23.949219 20 C 23.949219 20 23.992188 20 24.074219 20 C 24.675781 20 27.714844 19.851563 30.4375 17 L 37.988281 17 M 39 16 C 39 16 32.554688 16 30 16 C 27.46875 18.871094 24.574219 19 24.074219 19 C 24.027344 19 24 19 24 19 L 1 19 C 1 29.855469 3.714844 29.59375 3.714844 35 L 19.207031 36.898438 L 33.570313 35 C 33.570313 29.59375 39 29.570313 39 16 Z "></path>
<path style=" fill:#98CCFD;" d="M 5 9 L 7 9 L 7 12 L 5 12 Z "></path>
<path style=" fill:#DFF0FE;" d="M 1.691406 23.5 C 1.5625 22.339844 1.507813 21.199219 1.5 19.5 L 24.300781 19.5 L 24.445313 19.230469 C 24.503906 19.121094 25.921875 16.5 29.5 16.5 L 38.496094 16.5 C 38.472656 19.253906 38.230469 21.503906 37.746094 23.5 Z "></path>
<path style=" fill:#4788C7;" d="M 37.988281 17 C 37.945313 19.316406 37.738281 21.257813 37.347656 23 L 2.140625 23 C 2.058594 22.121094 2.019531 21.207031 2.003906 20 L 24 20 L 24.601563 19.992188 L 24.886719 19.464844 C 24.9375 19.363281 26.242188 17 29.5 17 L 37.988281 17 M 39 16 C 39 16 32.054688 16 29.5 16 C 25.554688 16 24 19 24 19 L 1 19 C 1 21.222656 1.0625 22.546875 1.246094 24 L 38.132813 24 C 38.660156 22 39 19.515625 39 16 Z "></path>
<path style=" fill:#98CCFD;" d="M 30 23 C 30 21.894531 30.894531 21 32 21 L 33 21 C 34.105469 21 35 21.894531 35 23 Z "></path>
<path style=" fill:#B6DCFE;" d="M 1 39 L 39 39 L 39 32.878906 C 37.691406 32.613281 36.148438 32 35 32 C 33.542969 32 31.4375 33 30 33 C 28.5625 33 26.457031 32 25 32 C 23.542969 32 21.4375 33 20 33 C 18.5625 33 16.457031 32 15 32 C 13.542969 32 11.4375 33 10 33 C 8.5625 33 6.457031 32 5 32 C 3.851563 32 2.308594 32.613281 1 32.878906 Z "></path>
<path style="fill:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke:#4788C7;stroke-opacity:1;stroke-miterlimit:10;" d="M 1.5 33.277344 C 1.863281 33.191406 2.238281 33.085938 2.613281 32.980469 C 3.460938 32.746094 4.335938 32.5 5 32.5 C 5.664063 32.5 6.535156 32.746094 7.382813 32.984375 C 8.28125 33.238281 9.214844 33.5 10 33.5 C 10.785156 33.5 11.71875 33.238281 12.617188 32.984375 C 13.464844 32.746094 14.335938 32.5 15 32.5 C 15.664063 32.5 16.535156 32.746094 17.382813 32.984375 C 18.28125 33.238281 19.214844 33.5 20 33.5 C 20.785156 33.5 21.71875 33.238281 22.617188 32.984375 C 23.464844 32.746094 24.335938 32.5 25 32.5 C 25.664063 32.5 26.535156 32.746094 27.382813 32.984375 C 28.28125 33.238281 29.214844 33.5 30 33.5 C 30.785156 33.5 31.71875 33.238281 32.617188 32.984375 C 33.464844 32.746094 34.335938 32.5 35 32.5 C 35.664063 32.5 36.539063 32.746094 37.386719 32.980469 C 37.761719 33.085938 38.136719 33.191406 38.5 33.277344 "></path>
<path style=" fill:#98CCFD;" d="M 9 12 L 11 12 L 11 10.902344 L 11.019531 10.804688 L 11.378906 9 L 9 9 Z "></path>
</g>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 40 40" version="1.1">
<g id="surface1">
<path style="fill:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke:#4788C7;stroke-opacity:1;stroke-miterlimit:10;" d="M 19.539063 1.375 L 25.605469 15.417969 L 20.429688 27.460938 "></path>
<path style=" fill:#DFF0FE;" d="M 2.664063 22.636719 C 7.933594 8.601563 16.949219 3.109375 19.5 1.792969 L 19.5 26.378906 Z "></path>
<path style=" fill:#4788C7;" d="M 19 2.636719 L 19 25.753906 L 3.339844 22.273438 C 8.136719 9.921875 15.871094 4.433594 19 2.636719 M 20 1 C 20 1 8.25 5.78125 1.996094 23 L 20 27 Z "></path>
<path style=" fill:#DFF0FE;" d="M 38.5 13 C 38.5 14.378906 37.378906 15.5 36 15.5 C 34.621094 15.5 33.5 14.378906 33.5 13 C 33.5 11.621094 34.621094 10.5 36 10.5 C 37.378906 10.5 38.5 11.621094 38.5 13 Z "></path>
<path style=" fill:#4788C7;" d="M 36 11 C 37.101563 11 38 11.898438 38 13 C 38 14.101563 37.101563 15 36 15 C 34.898438 15 34 14.101563 34 13 C 34 11.898438 34.898438 11 36 11 M 36 10 C 34.34375 10 33 11.34375 33 13 C 33 14.65625 34.34375 16 36 16 C 37.65625 16 39 14.65625 39 13 C 39 11.34375 37.65625 10 36 10 Z "></path>
<path style=" fill:#B6DCFE;" d="M 23.855469 30.5 C 23.554688 30.5 23.257813 30.394531 23.019531 30.203125 C 22.722656 29.964844 22.546875 29.621094 22.523438 29.242188 C 22.503906 28.859375 22.636719 28.503906 22.90625 28.230469 L 26.769531 24.8125 C 26.933594 24.652344 27.039063 24.578125 27.148438 24.539063 L 27.285156 24.5 L 30.324219 24.5 L 32.113281 20.5 L 29.203125 20.5 C 28.984375 20.5 28.777344 20.429688 28.601563 20.300781 L 25.140625 18.125 C 24.867188 17.910156 24.664063 17.5625 24.613281 17.171875 C 24.566406 16.777344 24.671875 16.390625 24.914063 16.078125 C 25.195313 15.71875 25.617188 15.511719 26.074219 15.511719 C 26.394531 15.511719 26.699219 15.613281 26.957031 15.808594 L 29.863281 17.5 L 35.820313 17.5 C 36.3125 17.5 36.753906 17.734375 37.027344 18.144531 C 37.304688 18.550781 37.355469 19.042969 37.171875 19.496094 C 36.007813 22.382813 34.3125 26.558594 34.3125 26.558594 C 34.191406 26.835938 34.003906 27.078125 33.753906 27.261719 C 33.558594 27.40625 33.304688 27.449219 32.980469 27.496094 L 32.871094 27.519531 C 32.851563 27.515625 31.933594 27.515625 31.65625 27.515625 C 30.457031 27.515625 28.804688 27.507813 28.226563 27.503906 L 27.941406 27.5 L 24.835938 30.078125 C 24.546875 30.359375 24.214844 30.5 23.855469 30.5 Z "></path>
<path style=" fill:#4788C7;" d="M 26.074219 16.011719 C 26.285156 16.011719 26.488281 16.078125 26.65625 16.207031 L 26.703125 16.242188 L 26.753906 16.269531 L 29.496094 17.863281 L 29.730469 18 L 35.820313 18 C 36.144531 18 36.433594 18.152344 36.613281 18.421875 C 36.792969 18.6875 36.828125 19.011719 36.707031 19.308594 C 35.546875 22.1875 33.855469 26.351563 33.855469 26.351563 C 33.765625 26.554688 33.632813 26.726563 33.453125 26.859375 C 33.355469 26.929688 33.144531 26.964844 32.898438 27.003906 L 32.882813 27.003906 C 32.800781 27.007813 32.53125 27.011719 31.648438 27.011719 C 30.371094 27.011719 28.582031 27.003906 28.125 27 L 27.761719 26.996094 L 27.480469 27.230469 L 24.515625 29.695313 L 24.480469 29.722656 L 24.445313 29.757813 C 24.289063 29.914063 24.078125 30 23.855469 30 C 23.722656 30 23.523438 29.96875 23.335938 29.816406 C 23.148438 29.664063 23.039063 29.453125 23.023438 29.214844 C 23.011719 28.984375 23.089844 28.769531 23.242188 28.601563 L 27.101563 25.1875 L 27.125 25.167969 L 27.148438 25.144531 C 27.257813 25.035156 27.300781 25.015625 27.308594 25.011719 L 27.351563 25 L 30.648438 25 L 30.914063 24.40625 L 32.253906 21.40625 L 32.882813 20 L 29.203125 20 C 29.128906 20 29.015625 19.984375 28.902344 19.898438 L 28.871094 19.875 L 28.835938 19.851563 L 25.453125 17.730469 C 25.265625 17.574219 25.144531 17.351563 25.113281 17.105469 C 25.078125 16.847656 25.148438 16.589844 25.3125 16.386719 C 25.496094 16.148438 25.773438 16.011719 26.074219 16.011719 M 26.074219 15.011719 C 25.488281 15.011719 24.910156 15.273438 24.519531 15.769531 C 23.847656 16.636719 24.007813 17.882813 24.875 18.546875 L 28.304688 20.699219 C 28.574219 20.902344 28.890625 21 29.203125 21 C 29.253906 21 29.433594 21 29.472656 21 L 31.339844 21 L 30 24 L 27.285156 24 C 27.210938 24 27.085938 24.011719 26.988281 24.066406 C 26.777344 24.140625 26.609375 24.269531 26.441406 24.441406 L 22.550781 27.878906 C 21.789063 28.652344 21.863281 29.914063 22.707031 30.59375 C 23.042969 30.867188 23.449219 31 23.855469 31 C 24.328125 31 24.796875 30.820313 25.152344 30.464844 L 28.121094 28 C 28.578125 28.003906 30.371094 28.011719 31.652344 28.011719 C 32.390625 28.011719 32.960938 28.011719 33 28 C 33.382813 27.9375 33.742188 27.890625 34.050781 27.660156 C 34.359375 27.433594 34.613281 27.125 34.773438 26.746094 C 34.773438 26.746094 36.472656 22.570313 37.636719 19.683594 C 38.15625 18.398438 37.207031 17 35.820313 17 L 30 17 L 27.257813 15.40625 C 26.902344 15.140625 26.488281 15.011719 26.074219 15.011719 Z "></path>
<path style=" fill:#4788C7;" d="M 11 27 L 29 27 L 29 28 L 11 28 Z "></path>
<path style=" fill:#98CCFD;" d="M 31 34.5 C 29.539063 34.5 27.671875 31.171875 27.511719 27.5 L 34.488281 27.5 C 34.328125 31.171875 32.460938 34.5 31 34.5 Z "></path>
<path style=" fill:#4788C7;" d="M 33.957031 28 C 33.667969 31.292969 32.027344 34 31 34 C 29.972656 34 28.332031 31.292969 28.042969 28 L 33.957031 28 M 35 27 L 27 27 C 27 30.839844 28.953125 35 31 35 C 33.046875 35 35 30.839844 35 27 Z "></path>
<path style=" fill:#98CCFD;" d="M 9 34.5 C 7.539063 34.5 5.671875 31.171875 5.511719 27.5 L 12.488281 27.5 C 12.328125 31.171875 10.460938 34.5 9 34.5 Z "></path>
<path style=" fill:#4788C7;" d="M 11.957031 28 C 11.667969 31.292969 10.027344 34 9 34 C 7.972656 34 6.332031 31.292969 6.042969 28 L 11.957031 28 M 13 27 L 5 27 C 5 30.839844 6.953125 35 9 35 C 11.046875 35 13 30.839844 13 27 Z "></path>
<path style=" fill:#B6DCFE;" d="M 1 38 L 39 38 L 39 32.878906 C 37.691406 32.613281 36.148438 32 35 32 C 33.542969 32 31.4375 33 30 33 C 28.5625 33 26.457031 32 25 32 C 23.542969 32 21.4375 33 20 33 C 18.5625 33 16.457031 32 15 32 C 13.542969 32 11.4375 33 10 33 C 8.5625 33 6.457031 32 5 32 C 3.851563 32 2.308594 32.613281 1 32.878906 Z "></path>
<path style="fill:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke:#4788C7;stroke-opacity:1;stroke-miterlimit:10;" d="M 1.5 33.277344 C 1.863281 33.191406 2.238281 33.085938 2.613281 32.980469 C 3.460938 32.746094 4.335938 32.5 5 32.5 C 5.664063 32.5 6.535156 32.746094 7.382813 32.984375 C 8.28125 33.238281 9.214844 33.5 10 33.5 C 10.785156 33.5 11.71875 33.238281 12.617188 32.984375 C 13.464844 32.746094 14.335938 32.5 15 32.5 C 15.664063 32.5 16.535156 32.746094 17.382813 32.984375 C 18.28125 33.238281 19.214844 33.5 20 33.5 C 20.785156 33.5 21.71875 33.238281 22.617188 32.984375 C 23.464844 32.746094 24.335938 32.5 25 32.5 C 25.664063 32.5 26.535156 32.746094 27.382813 32.984375 C 28.28125 33.238281 29.214844 33.5 30 33.5 C 30.785156 33.5 31.71875 33.238281 32.617188 32.984375 C 33.464844 32.746094 34.335938 32.5 35 32.5 C 35.664063 32.5 36.539063 32.746094 37.386719 32.980469 C 37.761719 33.085938 38.136719 33.191406 38.5 33.277344 "></path>
<path style=" fill:#4788C7;" d="M 34 26 L 35 26 L 35 27 L 34 27 Z "></path>
</g>
</svg>
\ No newline at end of file
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.2 2.4V16.8H2.4V2.4H13.2ZM13.2 0H2.4C1.08 0 0 1.08 0 2.4V16.8C0 18.12 1.08 19.2 2.4 19.2H13.2C14.52 19.2 15.6 18.12 15.6 16.8V2.4C15.6 1.08 14.52 0 13.2 0Z" transform="translate(10.7998 8.80005)" fill="black"/>
<path d="M2.4 18V2.4H14.4V0H2.4C1.08 0 0 1.08 0 2.4V18H2.4Z" transform="translate(6 4)" fill="black"/>
</svg>
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 5H9V9H5V11H9V15H11V11H15V9H11V5ZM10 0C4.48 0 0 4.48 0 10C0 15.52 4.48 20 10 20C15.52 20 20 15.52 20 10C20 4.48 15.52 0 10 0ZM10 18C5.59 18 2 14.41 2 10C2 5.59 5.59 2 10 2C14.41 2 18 5.59 18 10C18 14.41 14.41 18 10 18Z" transform="translate(10.9707 -3.17157) rotate(45)" fill="#BDBDBD"/>
</svg>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment