├── public ├── ads.txt ├── sellers.json ├── images │ ├── new.gif │ ├── 404.jpeg │ ├── credit.png │ ├── share.jpg │ ├── test.jpeg │ ├── default.jpg │ ├── banner │ │ ├── 01.jpeg │ │ └── 02.jpeg │ ├── bg-search.jpg │ ├── no-chapter.png │ └── gplaypattern.png ├── fonts │ ├── ionicons.eot │ ├── ionicons.ttf │ ├── ionicons.woff │ └── ionicons.woff2 ├── js │ ├── slick │ │ ├── ajax-loader.gif │ │ ├── fonts │ │ │ ├── slick.eot │ │ │ ├── slick.ttf │ │ │ ├── slick.woff │ │ │ └── slick.svg │ │ ├── config.rb │ │ ├── slick.less │ │ ├── slick.scss │ │ ├── slick.css │ │ ├── slick-theme.css │ │ ├── slick-theme.less │ │ └── slick-theme.scss │ ├── app.js.LICENSE.txt │ ├── 578.app.min.js │ ├── core.js │ ├── app.min.js.LICENSE.txt │ ├── 144.app.min.js │ ├── lazysizes │ │ └── lazysizes.min.js │ └── 770.app.min.js └── lib │ └── fontawesome │ └── web-fonts-with-css │ ├── webfonts │ ├── fa-brands-400.eot │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-regular-400.eot │ ├── fa-regular-400.ttf │ ├── fa-solid-900.eot │ ├── fa-solid-900.ttf │ ├── fa-solid-900.woff │ ├── fa-solid-900.woff2 │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.woff │ └── fa-regular-400.woff2 │ └── css │ ├── solid.min.css │ ├── brands.min.css │ ├── regular.min.css │ ├── solid.css │ ├── brands.css │ ├── regular.css │ └── svg-with-js.min.css ├── demo.zip ├── config ├── chapter.js └── multer.js ├── jobs ├── index.js └── modules │ └── chapterScheduler.js ├── .prettierrc ├── events ├── listeners │ ├── upload.js │ └── chapter.js └── index.js ├── ecosystem.config.js ├── schema ├── types │ ├── user.graphql │ ├── studio.graphql │ ├── story.graphql │ └── query.graphql ├── typeDefs.js ├── resolvers.js ├── index.js └── resolvers │ ├── user.resolver.js │ ├── story.resolver.js │ └── studio.resolver.js ├── modules ├── bunnyCDN │ ├── test │ │ └── remove.js │ └── index.js └── image │ └── index.js ├── routes ├── about.js ├── settings.js ├── index.js ├── upload.js ├── category.js ├── story.js └── sitemap.js ├── views ├── includes │ ├── script-core.ejs │ ├── footer.ejs │ ├── genres-block.ejs │ ├── popup-session.ejs │ ├── head.ejs │ ├── loop-content.ejs │ ├── sidebar.ejs │ ├── loop-infinite.ejs │ └── top-sidebar.ejs ├── error.ejs ├── schema │ └── story.ejs ├── story │ └── list-chapter.ejs ├── index.ejs ├── chapter │ └── entry-header.ejs ├── settings.ejs ├── story.ejs ├── stories.ejs ├── category.ejs ├── search.ejs └── chapter.ejs ├── utilities ├── auth.js └── sidebar.js ├── .graphqlconfig ├── models ├── Category.js ├── BlackList.js ├── User.js ├── Story.js └── Chapter.js ├── webpack.config.js ├── .env.example ├── database.js ├── .eslintrc.js ├── setup.sh ├── controller ├── chapter.controller.js ├── category.controller.js ├── user.controller.js ├── upload.controller.js ├── auth.controller.js └── story.controller.js ├── backend.conf ├── categories.js ├── app.js ├── .gitignore ├── bin └── www ├── package.json └── README.md /public/ads.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/sellers.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /demo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/demo.zip -------------------------------------------------------------------------------- /config/chapter.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | SCHODULE: 0, 3 | ACTIVE: 1 4 | } 5 | -------------------------------------------------------------------------------- /public/images/new.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/images/new.gif -------------------------------------------------------------------------------- /public/images/404.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/images/404.jpeg -------------------------------------------------------------------------------- /public/images/credit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/images/credit.png -------------------------------------------------------------------------------- /public/images/share.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/images/share.jpg -------------------------------------------------------------------------------- /public/images/test.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/images/test.jpeg -------------------------------------------------------------------------------- /public/fonts/ionicons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/fonts/ionicons.eot -------------------------------------------------------------------------------- /public/fonts/ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/fonts/ionicons.ttf -------------------------------------------------------------------------------- /public/fonts/ionicons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/fonts/ionicons.woff -------------------------------------------------------------------------------- /public/images/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/images/default.jpg -------------------------------------------------------------------------------- /public/fonts/ionicons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/fonts/ionicons.woff2 -------------------------------------------------------------------------------- /public/images/banner/01.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/images/banner/01.jpeg -------------------------------------------------------------------------------- /public/images/banner/02.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/images/banner/02.jpeg -------------------------------------------------------------------------------- /public/images/bg-search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/images/bg-search.jpg -------------------------------------------------------------------------------- /public/images/no-chapter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/images/no-chapter.png -------------------------------------------------------------------------------- /jobs/index.js: -------------------------------------------------------------------------------- 1 | const chapterScheduler = require('./modules/chapterScheduler') 2 | chapterScheduler.start() 3 | -------------------------------------------------------------------------------- /public/images/gplaypattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/images/gplaypattern.png -------------------------------------------------------------------------------- /public/js/slick/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/js/slick/ajax-loader.gif -------------------------------------------------------------------------------- /public/js/slick/fonts/slick.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/js/slick/fonts/slick.eot -------------------------------------------------------------------------------- /public/js/slick/fonts/slick.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/js/slick/fonts/slick.ttf -------------------------------------------------------------------------------- /public/js/slick/fonts/slick.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/js/slick/fonts/slick.woff -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 80 6 | } 7 | -------------------------------------------------------------------------------- /events/listeners/upload.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | module.exports.removeFile = (path) => { 3 | try { 4 | fs.unlinkSync(path) 5 | } catch (e) {} 6 | } 7 | -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/lib/fontawesome/web-fonts-with-css/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/lib/fontawesome/web-fonts-with-css/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/lib/fontawesome/web-fonts-with-css/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/lib/fontawesome/web-fonts-with-css/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/lib/fontawesome/web-fonts-with-css/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/lib/fontawesome/web-fonts-with-css/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/lib/fontawesome/web-fonts-with-css/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/lib/fontawesome/web-fonts-with-css/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/lib/fontawesome/web-fonts-with-css/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/lib/fontawesome/web-fonts-with-css/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/lib/fontawesome/web-fonts-with-css/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guendev/madara/HEAD/public/lib/fontawesome/web-fonts-with-css/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /public/js/app.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Vue.js v2.6.14 3 | * (c) 2014-2021 Evan You 4 | * Released under the MIT License. 5 | */ 6 | 7 | //! moment.js 8 | 9 | //! moment.js locale configuration 10 | -------------------------------------------------------------------------------- /public/js/slick/config.rb: -------------------------------------------------------------------------------- 1 | css_dir = "." 2 | sass_dir = "." 3 | images_dir = "." 4 | fonts_dir = "fonts" 5 | relative_assets = true 6 | 7 | output_style = :compact 8 | line_comments = false 9 | 10 | preferred_syntax = :scss -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'web', 5 | script: './bin/www', 6 | instances: 0, 7 | exec_mode: 'cluster', 8 | watch: true 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /schema/types/user.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | _id: Float! 3 | name: String 4 | email: String! 5 | role: String! 6 | avatar: String! 7 | createdAt: Float 8 | } 9 | 10 | type Token { 11 | token: String! 12 | } 13 | -------------------------------------------------------------------------------- /modules/bunnyCDN/test/remove.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ path: '../../../.env' }) 2 | const BunnyCDN = require('../index') 3 | ;(async function () { 4 | const Bunny = new BunnyCDN(true) 5 | await Bunny.remove('/test/image-01.jpg') 6 | })() 7 | -------------------------------------------------------------------------------- /routes/about.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | 4 | router.get('/cookie-policy', async (req, res, next) => { 5 | res.render('cookie_policy', { title: 'Quyền Riêng Tư' }) 6 | }) 7 | 8 | module.exports = router 9 | -------------------------------------------------------------------------------- /schema/typeDefs.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { loadFilesSync } = require('@graphql-tools/load-files') 3 | const { mergeTypeDefs } = require('@graphql-tools/merge') 4 | 5 | const typesArray = loadFilesSync(path.join(__dirname, '/types'), { 6 | recursive: true 7 | }) 8 | module.exports = mergeTypeDefs(typesArray, { all: true }) 9 | -------------------------------------------------------------------------------- /routes/settings.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | 4 | router.get('/:type(history|bookmark|account)', async (req, res, next) => { 5 | if (!res.locals.user) { 6 | return res.status(401).redirect('/404') 7 | } 8 | return res.render('settings') 9 | }) 10 | 11 | module.exports = router 12 | -------------------------------------------------------------------------------- /schema/resolvers.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { mergeResolvers } = require('@graphql-tools/merge') 3 | const { loadFilesSync } = require('@graphql-tools/load-files') 4 | 5 | const resolversArray = loadFilesSync(path.join(__dirname, './resolvers'), { 6 | extensions: ['js'] 7 | }) 8 | 9 | module.exports = mergeResolvers(resolversArray) 10 | -------------------------------------------------------------------------------- /views/includes/script-core.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /utilities/auth.js: -------------------------------------------------------------------------------- 1 | const authController = require('../controller/auth.controller') 2 | 3 | module.exports = async ({ cookies }, res, next) => { 4 | let user = undefined 5 | if (cookies._token) { 6 | const AuthController = new authController() 7 | user = await AuthController.getUser(cookies._token) 8 | } 9 | res.locals.user = user 10 | next() 11 | } 12 | -------------------------------------------------------------------------------- /.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Untitled GraphQL Schema", 3 | "schemaPath": "./schema.graphql", 4 | "extensions": { 5 | "endpoints": { 6 | "Default GraphQL Endpoint": { 7 | "url": "http://localhost:4000/graphql", 8 | "headers": { 9 | "user-agent": "JS GraphQL" 10 | }, 11 | "introspect": false 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /schema/types/studio.graphql: -------------------------------------------------------------------------------- 1 | input StoryForm{ 2 | _id: Float 3 | title: String! 4 | otherTitle: String 5 | author: String 6 | team: String! 7 | avatar: String! 8 | content: String 9 | adsense: Boolean 10 | categories: [String] 11 | badge: String 12 | } 13 | 14 | input ChapterForm { 15 | _id: Float 16 | story: Float! 17 | name: String! 18 | avatar: String 19 | content: Object! 20 | nameExtend: String 21 | publishTime: Float 22 | note: String 23 | } 24 | -------------------------------------------------------------------------------- /models/Category.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const { autoIncrement } = require('mongoose-plugin-autoinc') 4 | const slug = require('mongoose-slug-generator') 5 | 6 | const CategorySchema = new mongoose.Schema({ 7 | name: { 8 | type: String, 9 | required: true, 10 | trim: true 11 | }, 12 | slug: { 13 | type: String, 14 | slug: 'name', 15 | lowercase: true 16 | } 17 | }) 18 | 19 | CategorySchema.plugin(autoIncrement, 'Category') 20 | CategorySchema.plugin(slug) 21 | 22 | module.exports = mongoose.model('Category', CategorySchema) 23 | -------------------------------------------------------------------------------- /modules/image/index.js: -------------------------------------------------------------------------------- 1 | const sharp = require('sharp') 2 | 3 | module.exports = class { 4 | constructor(image) { 5 | this.image = image 6 | } 7 | 8 | async resize(width, height) { 9 | return sharp(this.image).jpeg({}).resize(width, height, { fit: 'cover' }).toBuffer() 10 | } 11 | 12 | async resizeWithWater(input, width, height, top, left, gravity = 'southeast') { 13 | return sharp(this.image) 14 | .jpeg({}) 15 | .resize(width, height, { fit: 'cover' }) 16 | .composite([{ input: __dirname + '/lib/' + input, gravity, top, left }]) 17 | .toBuffer() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/js/578.app.min.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkserver=self.webpackChunkserver||[]).push([[578],{8578:(e,t,a)=>{a.r(t),a.d(t,{default:()=>r});const r=(0,a(1900).Z)({name:"BookMark"},(function(){var e=this;e.$createElement;return e._self._c,e._m(0)}),[function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("table",{staticClass:"table table-hover list-bookmark"},[a("thead",[a("tr",[a("th",[e._v("Manga Name")]),e._v(" "),a("th",[e._v("Updated Time")]),e._v(" "),a("th",[e._v("Edit")])])]),e._v(" "),a("tbody",[a("tr",[a("td",{attrs:{colspan:"3"}},[e._v(" No Manga Bookmarked")])])])])}],!1,null,null,null).exports}}]); -------------------------------------------------------------------------------- /config/multer.js: -------------------------------------------------------------------------------- 1 | const multer = require('multer') 2 | // SET STORAGE 3 | const storage = multer.diskStorage({ 4 | destination: 'public/upload/temp', 5 | filename: function (req, file, cb) { 6 | cb(null, file.fieldname + '-' + Date.now() + '.jpg') 7 | } 8 | }) 9 | 10 | module.exports = multer({ 11 | storage: storage, 12 | fileFilter: (req, file, cb) => { 13 | if (['image/png', 'image/jpg', 'image/jpeg'].includes(file.mimetype)) { 14 | cb(null, true) 15 | } else { 16 | cb(null, false) 17 | return cb(new Error('Only .png, .jpg and .jpeg format allowed!')) 18 | } 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | 4 | const storyController = require('../controller/story.controller') 5 | 6 | router.get('/', async (req, res, next) => { 7 | const StoryController = new storyController() 8 | const [stories, topViews] = await Promise.all([ 9 | StoryController.getManyWithChapter('updatedAt', 0, 8, 2), 10 | StoryController.getManyWithChapter('views', 0, 6, 2) 11 | ]) 12 | let slider = [] 13 | try { 14 | slider = require('../slider.js') 15 | } catch (e) {} 16 | res.render('index', { stories, topViews, slider }) 17 | }) 18 | 19 | module.exports = router 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const { VueLoaderPlugin } = require('vue-loader') 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', 6 | entry: './vue/app.js', 7 | resolve: { 8 | alias: { 9 | vue: process.env.NODE_ENV === 'production' ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js' 10 | } 11 | }, 12 | output: { 13 | path: __dirname + '/public/js', 14 | filename: 'app.min.js' 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.vue$/, 20 | use: ['vue-loader'] 21 | } 22 | ] 23 | }, 24 | plugins: [new VueLoaderPlugin()] 25 | } 26 | -------------------------------------------------------------------------------- /routes/upload.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const upload = require('../config/multer') 4 | const uploadController = require('../controller/upload.controller') 5 | 6 | router.post('/upload/single', upload.single('image'), async ({ file, body }, res, next) => { 7 | const UploadController = new uploadController() 8 | const path = await UploadController.uploadSingle(body.type, file, body.pathName || file.path) 9 | if (path) { 10 | return res.status(200).json({ 11 | msg: 'Thành công', 12 | success: true, 13 | data: path 14 | }) 15 | } 16 | return res.status(500) 17 | }) 18 | 19 | module.exports = router 20 | -------------------------------------------------------------------------------- /models/BlackList.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const ChapterSchema = new mongoose.Schema({ 4 | web: { 5 | type: String, 6 | required: true 7 | }, 8 | source: { 9 | type: String, 10 | index: true 11 | } 12 | }) 13 | 14 | ChapterSchema.pre('save', async function (next, node) { 15 | if (!this.source) { 16 | return next() 17 | } 18 | const check = await this.constructor.findOne({ source: this.source }) 19 | if (check) { 20 | this.invalidate('source', 'source must be unique') 21 | return next(new Error('Source must be unique')) 22 | } 23 | return next() 24 | }) 25 | module.exports = mongoose.model('BlackList', ChapterSchema) 26 | -------------------------------------------------------------------------------- /public/js/core.js: -------------------------------------------------------------------------------- 1 | ;(function ($) { 2 | 'use strict' 3 | 4 | /** 5 | * Add 'mobile' class on Responsive Mode 6 | * @type {Window} 7 | */ 8 | $(window).on('load resize', function () { 9 | const viewportWidth = window.outerWidth 10 | const siteHeader = $('.site-header') 11 | 12 | const isMobile = siteHeader.hasClass('mobile') 13 | 14 | if (viewportWidth < 1008) { 15 | if (!isMobile) { 16 | siteHeader.addClass('mobile') 17 | $('body').addClass('mobile') 18 | } 19 | } else { 20 | if (isMobile) { 21 | siteHeader.removeClass('mobile') 22 | $('body').removeClass('mobile') 23 | } 24 | } 25 | }) 26 | })(jQuery) 27 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV= 2 | APP= 3 | PORT= 4 | # password access all account 5 | PASSWORD_MASTER= 6 | MONGODB_URL= 7 | SECRET= 8 | 9 | # Logo Website 10 | LOGO= 11 | 12 | #domain 13 | DOMAIN= 14 | 15 | #bunny CDN 16 | BUNNY_SECURITY_KEY= 17 | SECURE_ENABLE= 18 | 19 | # Secure Storage 20 | BUNNY_STORAGE_NAME= 21 | BUNNY_STORAGE_SERVER= 22 | BUNNY_ACCESS_KEY= 23 | CDN_DOMAIN= 24 | 25 | # normal Storage 26 | BUNNY_STORAGE_NAME_2= 27 | BUNNY_STORAGE_SERVER_2= 28 | BUNNY_ACCESS_KEY_2= 29 | CDN_DOMAIN_2= 30 | 31 | #social media 32 | STUDIO= 33 | FANPAGE= 34 | GROUP= 35 | EMAIL= 36 | COPYRIGHT= 37 | 38 | ## canvas render 39 | CANVAS_RENDER= 40 | NOTE_DEFAULT= 41 | 42 | # tags 43 | ANALYTIC= 44 | FACEBOOK_APP_ID= 45 | -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/css/solid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900} -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/css/brands.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands";font-weight:400} -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/css/regular.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:"Font Awesome 5 Free";font-weight:400} -------------------------------------------------------------------------------- /utilities/sidebar.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment') 2 | 3 | const storyController = require('../controller/story.controller') 4 | const Category = require('../models/Category') 5 | const BunnyCDN = require('../modules/bunnyCDN') 6 | 7 | module.exports = async (req, res, next) => { 8 | const StoryController = new storyController() 9 | const [topTrending, categories] = await Promise.all([ 10 | StoryController.getManyWithChapter('views', 0, 4, 2), 11 | Category.find() 12 | ]) 13 | res.locals.topTrending = topTrending 14 | res.locals.categories = categories 15 | res.locals.webAssets = BunnyCDN.webAssets 16 | res.locals.moment = moment 17 | res.locals.lightTheme = !!req.cookies.lightTheme 18 | next() 19 | } 20 | -------------------------------------------------------------------------------- /database.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const options = { 3 | autoIndex: false, 4 | poolSize: 10, 5 | bufferMaxEntries: 0, 6 | useNewUrlParser: true, 7 | useUnifiedTopology: true, 8 | useFindAndModify: false 9 | } 10 | 11 | const connectWithRetry = () => { 12 | console.log('MongoDB connection with retry') 13 | mongoose 14 | .connect(process.env.MONGODB_URL, options) 15 | .then(() => { 16 | console.log('MongoDB is connected') 17 | }) 18 | .catch((e) => { 19 | console.log('MongoDB connection unsuccessful, retry after 5 seconds.') 20 | setTimeout(connectWithRetry, 5000) 21 | }) 22 | } 23 | const database = { 24 | connect: connectWithRetry 25 | } 26 | module.exports = database 27 | -------------------------------------------------------------------------------- /schema/index.js: -------------------------------------------------------------------------------- 1 | const { ApolloServer } = require('apollo-server-express') 2 | 3 | const database = require('../database') 4 | database.connect() 5 | 6 | const authController = require('../controller/auth.controller') 7 | 8 | const typeDefs = require('./typeDefs') 9 | const resolvers = require('./resolvers') 10 | 11 | const server = new ApolloServer({ 12 | typeDefs, 13 | resolvers, 14 | async context({ req }) { 15 | const token = req.headers.authorization || '' 16 | let user = undefined 17 | if (token) { 18 | const AuthController = new authController() 19 | user = await AuthController.getUser(token.replace('Bearer ', '')) 20 | } 21 | return { 22 | _token: token.replace('Bearer ', ''), 23 | user 24 | } 25 | } 26 | }) 27 | module.exports = server 28 | -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/css/solid.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 900; 9 | font-display: block; 10 | src: url("../webfonts/fa-solid-900.eot"); 11 | src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); } 12 | 13 | .fa, 14 | .fas { 15 | font-family: 'Font Awesome 5 Free'; 16 | font-weight: 900; } 17 | -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/css/brands.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Brands'; 7 | font-style: normal; 8 | font-weight: 400; 9 | font-display: block; 10 | src: url("../webfonts/fa-brands-400.eot"); 11 | src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } 12 | 13 | .fab { 14 | font-family: 'Font Awesome 5 Brands'; 15 | font-weight: 400; } 16 | -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/css/regular.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 400; 9 | font-display: block; 10 | src: url("../webfonts/fa-regular-400.eot"); 11 | src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } 12 | 13 | .far { 14 | font-family: 'Font Awesome 5 Free'; 15 | font-weight: 400; } 16 | -------------------------------------------------------------------------------- /routes/category.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | 4 | const categoryController = require('../controller/category.controller') 5 | 6 | router.get('/the-loai/:slug.:id', async ({ params, query }, res, next) => { 7 | const CategoryController = new categoryController() 8 | const category = await CategoryController.getOne(parseInt(params.id)) 9 | if (!category) { 10 | return res.redirect('/404') 11 | } 12 | const [count, stories] = await Promise.all([ 13 | CategoryController.getCountStory(category._id), 14 | CategoryController.categoryGetBooks( 15 | category._id, 16 | query.order || 'updatedAt', 17 | 0, 18 | 8 19 | ) 20 | ]) 21 | res.render('category', { 22 | category, 23 | count, 24 | order: query.order, 25 | stories 26 | }) 27 | }) 28 | 29 | module.exports = router 30 | -------------------------------------------------------------------------------- /events/index.js: -------------------------------------------------------------------------------- 1 | const events = require('events') 2 | const eventEmitter = new events.EventEmitter() 3 | const UploadListener = require('./listeners/upload') 4 | const ChapterListener = require('./listeners/chapter') 5 | 6 | function removeFile(path) { 7 | eventEmitter.once('REMOVE_FILE', UploadListener.removeFile) 8 | eventEmitter.emit('REMOVE_FILE', path) 9 | } 10 | 11 | function clearChapterContent(content) { 12 | eventEmitter.once('CLEAR_CHAPTER_CONTENT', ChapterListener.clearChapter) 13 | eventEmitter.emit('CLEAR_CHAPTER_CONTENT', content) 14 | } 15 | 16 | function updateChapterContent(_id, oldContent, content) { 17 | eventEmitter.once('UPDATE_CHAPTER_CONTENT', ChapterListener.updateChapter) 18 | eventEmitter.emit('UPDATE_CHAPTER_CONTENT', _id, oldContent, content) 19 | } 20 | 21 | module.exports = { 22 | removeFile, 23 | clearChapterContent, 24 | updateChapterContent 25 | } 26 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | es6: true 7 | }, 8 | parserOptions: { 9 | parser: 'babel-eslint', 10 | ecmaVersion: 11, 11 | ecmaFeatures: { 12 | experimentalObjectRestSpread: true 13 | }, 14 | sourceType: 'module' 15 | }, 16 | extends: ['prettier', 'plugin:prettier/recommended', 'plugin:vue/recommended'], 17 | plugins: ['prettier'], 18 | // add your custom rules here 19 | rules: { 20 | 'no-console': 'off', 21 | 'nuxt/no-cjs-in-config': 'off', 22 | 'vue/attribute-hyphenation': 'off', 23 | 'dot-notation': 'off', 24 | 'handle-callback-err': 'off', 25 | 'vue/no-v-html': 'off', 26 | 'vue/max-attributes-per-line': 'off', 27 | 'vue/html-indent': 'off', 28 | 'prettier/prettier': [ 29 | 'error', 30 | { 31 | printWidth: 100 32 | } 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /events/listeners/chapter.js: -------------------------------------------------------------------------------- 1 | const bunnyCDN = require('../../modules/bunnyCDN') 2 | 3 | /** 4 | * @param { [ { content: String } ] } content 5 | */ 6 | module.exports.clearChapter = async (content) => { 7 | const BunnyCDN = new bunnyCDN(true) 8 | for (const image of content) { 9 | await BunnyCDN.remove(image.content) 10 | } 11 | } 12 | 13 | /** 14 | * @param { [ { content: String } ] } content 15 | * @param { Number } _id 16 | * @param { [ { content: String } ] } oldContent 17 | */ 18 | module.exports.updateChapter = async (_id, oldContent, content) => { 19 | const BunnyCDN = new bunnyCDN(true) 20 | const list = [] 21 | for (const image of oldContent) { 22 | const exist = content.findIndex((value) => value.content === image.content) > -1 23 | if (!exist) { 24 | list.push( 25 | new Promise(() => { 26 | BunnyCDN.remove(image.content) 27 | }) 28 | ) 29 | } 30 | } 31 | await Promise.all(list) 32 | } 33 | -------------------------------------------------------------------------------- /jobs/modules/chapterScheduler.js: -------------------------------------------------------------------------------- 1 | const CronJob = require('cron').CronJob 2 | 3 | const Chapter = require('../../models/Chapter') 4 | const Story = require('../../models/Story') 5 | 6 | const CHAPTER = require('../../config/chapter') 7 | 8 | module.exports = new CronJob( 9 | '0 */10 * * * *', 10 | async () => { 11 | try { 12 | const chapters = await Chapter.find({ 13 | postActive: CHAPTER.SCHODULE, 14 | publishTime: { 15 | $lte: Date.now() 16 | } 17 | }) 18 | for (let chapter of chapters) { 19 | await Promise.all([ 20 | Chapter.findByIdAndUpdate(chapter._id, { 21 | postActive: CHAPTER.ACTIVE, 22 | createdAt: Date.now() 23 | }), 24 | Story.findByIdAndUpdate(chapter.story, { updatedAt: Date.now() }) 25 | ]) 26 | } 27 | } catch (e) { 28 | console.log('Error when publish Chapter') 29 | } 30 | }, 31 | null, 32 | true, 33 | 'Asia/Ho_Chi_Minh' 34 | ) 35 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Ask the user for their name 3 | echo -n "Do you want to import demo (Y/N)? " 4 | # shellcheck disable=SC2162 5 | read answer 6 | if [ "$answer" != "${answer#[Yy]}" ] ;then 7 | sudo apt-get install zip unzip 8 | echo "Unpack the demo folder" 9 | unzip demo.zip 10 | echo "Moving ads blocks" 11 | rsync -a demo/ads views/includes 12 | rsync -a demo/ads.text public/ads.text 13 | rsync -a demo/sellers.json public/sellers.json 14 | echo "Moving watermark" 15 | rsync -a demo/modules/image/lib modules/image 16 | echo "Moving favicon" 17 | rsync -a demo/favicon public/images 18 | echo "Moving slider" 19 | rsync -a demo/slider.js slider.js 20 | echo "Moving logo" 21 | rsync -a demo/logo.svg public/images/logo.svg 22 | echo "Moving css" 23 | rsync -a demo/css/custom.css public/css/custom.css 24 | echo "Remove demo directory" 25 | rm -r demo 26 | echo "Tks..." 27 | else 28 | echo "You need to import demo!!!" 29 | fi 30 | -------------------------------------------------------------------------------- /schema/types/story.graphql: -------------------------------------------------------------------------------- 1 | type Story { 2 | _id: Float 3 | title: String 4 | slug: String 5 | avatar: String 6 | source: String 7 | adsense: Boolean 8 | content: String 9 | user: User 10 | countChapter: Int 11 | rating: Float 12 | views: Float 13 | categories: [Category] 14 | updatedAt: Float 15 | createdAt: Float 16 | 17 | otherTitle: String 18 | author: String 19 | team: String 20 | badge: String 21 | } 22 | 23 | type Chapter { 24 | _id: Float 25 | name: String 26 | nameExtend: String 27 | avatar: String 28 | source: String 29 | slug: String 30 | story: Story 31 | views: Float 32 | order: Int 33 | content: Object 34 | createdAt: Float 35 | postActive: Int 36 | publishTime: Float 37 | note: String 38 | } 39 | 40 | type Category { 41 | _id: Float 42 | name: String 43 | slug: String 44 | } 45 | 46 | scalar Object 47 | 48 | type StoryAndChapter { 49 | story: Story 50 | chapters: [Chapter] 51 | } 52 | -------------------------------------------------------------------------------- /controller/chapter.controller.js: -------------------------------------------------------------------------------- 1 | const Story = require('../models/Story') 2 | const Chapter = require('../models/Chapter') 3 | const STORY = require('../config/chapter') 4 | 5 | class ChapterController { 6 | async getMany(story) { 7 | return Chapter.find({ story, postActive: STORY.ACTIVE }) 8 | .sort({ order: -1 }) 9 | .select('-content -source') 10 | } 11 | 12 | async getOne(_id) { 13 | const chapter = await Chapter.findOne({ _id, postActive: STORY.ACTIVE }) 14 | if (!chapter) { 15 | return null 16 | } 17 | await Promise.all([ 18 | Chapter.findByIdAndUpdate(_id, { $inc: { views: 1 } }), 19 | Story.findByIdAndUpdate(chapter.story, { $inc: { views: 1 } }) 20 | ]) 21 | return chapter 22 | } 23 | 24 | static async forSiteMap() { 25 | return Chapter.find({ postActive: STORY.ACTIVE }, { _id: 1, slug: 1, story: 1 }).populate({ 26 | path: 'story', 27 | model: Story, 28 | select: '_id slug' 29 | }) 30 | } 31 | } 32 | 33 | module.exports = ChapterController 34 | -------------------------------------------------------------------------------- /schema/resolvers/user.resolver.js: -------------------------------------------------------------------------------- 1 | const AuthController = require('../../controller/auth.controller') 2 | const UserController = require('../../controller/user.controller') 3 | 4 | module.exports = { 5 | Query: { 6 | me: (_, {}, { user }) => { 7 | return user 8 | } 9 | }, 10 | 11 | Mutation: { 12 | signinUser: async (_, { email, password }) => { 13 | const authController = new AuthController() 14 | return authController.login(email, password) 15 | }, 16 | 17 | signupUser: async (_, { name, email, password }) => { 18 | const authController = new AuthController() 19 | return authController.signup(name, email, password) 20 | }, 21 | 22 | userSettings: async (_, { key, value }, { user }) => { 23 | const userController = new UserController(user) 24 | return userController.update(key, value) 25 | }, 26 | 27 | changePassword: async (_, { oldPass, newPass }, { user }) => { 28 | const userController = new UserController(user) 29 | return userController.changePassword(oldPass, newPass) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend.conf: -------------------------------------------------------------------------------- 1 | map $sent_http_content_type $expires { 2 | "text/html" epoch; 3 | "text/html; charset=utf-8" epoch; 4 | default off; 5 | } 6 | 7 | server { 8 | listen 80; # the port nginx is listening on 9 | server_name example.com; # setup your domain here 10 | 11 | gzip on; 12 | gzip_types text/plain application/xml text/css application/javascript; 13 | gzip_min_length 1000; 14 | client_max_body_size 50M; 15 | 16 | location / { 17 | expires $expires; 18 | 19 | proxy_redirect off; 20 | proxy_set_header Host $host; 21 | proxy_set_header X-Real-IP $remote_addr; 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | proxy_set_header X-Forwarded-Proto $scheme; 24 | proxy_read_timeout 1m; 25 | proxy_connect_timeout 1m; 26 | proxy_pass http://127.0.0.1:4000; # set the address of the Node.js instance here 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include( 4 | 'includes/head', 5 | { 6 | title: 'Không Tìm Thấy Trang', 7 | description: 'Đọc truyện tranh đam mỹ online - Truyện gì cũng có', 8 | seo_image: process.env.DOMAIN + '/images/share.jpg', 9 | showAds: true 10 | } 11 | ); 12 | -%> 13 | 14 | 15 | 16 |
17 |
18 | <%- include('includes/header') -%> 19 |
20 |
21 | 22 |
23 |
24 | <%- include('includes/footer') -%> 25 |
26 | 27 | <%- include('includes/popup-session') -%> 28 |
29 | 30 |
31 | 32 |
33 | 34 | 35 | <%- include('includes/script-core') -%> 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const bcrypt = require('bcrypt') 3 | 4 | const { autoIncrement } = require('mongoose-plugin-autoinc') 5 | 6 | const UserSchema = new mongoose.Schema({ 7 | name: { 8 | type: String, 9 | required: true 10 | }, 11 | email: { 12 | type: String, 13 | required: true, 14 | unique: true, 15 | index: true, 16 | trim: true 17 | }, 18 | password: { 19 | type: String, 20 | required: true, 21 | trim: true 22 | }, 23 | role: { 24 | type: String, 25 | default: 'user', 26 | enum: ['user', 'mod', 'admin'] 27 | }, 28 | avatar: { 29 | type: String, 30 | default: 'https://i.imgur.com/pqGLgGr.jpg' 31 | }, 32 | createdAt: { 33 | type: Number, 34 | default: Date.now() 35 | } 36 | }) 37 | 38 | UserSchema.pre('save', function (next) { 39 | if (!this.isModified('password')) { 40 | return next() 41 | } 42 | bcrypt.genSalt(10, (err, salt) => { 43 | if (err) return next(err) 44 | bcrypt.hash(this.password, salt, (err, hash) => { 45 | if (err) return next(err) 46 | this.password = hash 47 | next() 48 | }) 49 | }) 50 | }) 51 | 52 | UserSchema.plugin(autoIncrement, 'User') 53 | module.exports = mongoose.model('User', UserSchema) 54 | -------------------------------------------------------------------------------- /public/js/app.min.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Cropper.js v1.5.12 3 | * https://fengyuanchen.github.io/cropperjs 4 | * 5 | * Copyright 2015-present Chen Fengyuan 6 | * Released under the MIT license 7 | * 8 | * Date: 2021-06-12T08:00:17.411Z 9 | */ 10 | 11 | /*! 12 | * Vue.js v2.6.14 13 | * (c) 2014-2021 Evan You 14 | * Released under the MIT License. 15 | */ 16 | 17 | /*! ***************************************************************************** 18 | Copyright (c) Microsoft Corporation. 19 | 20 | Permission to use, copy, modify, and/or distribute this software for any 21 | purpose with or without fee is hereby granted. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 24 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 25 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 26 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 27 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 28 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 29 | PERFORMANCE OF THIS SOFTWARE. 30 | ***************************************************************************** */ 31 | 32 | //! moment.js 33 | 34 | //! moment.js locale configuration 35 | -------------------------------------------------------------------------------- /views/includes/footer.ejs: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /categories.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const db = require('./database') 3 | db.connect() 4 | const Category = require('./models/Category') 5 | ;(async function () { 6 | const list = [ 7 | 'Action', 8 | 'Adventure', 9 | 'Anime', 10 | 'Chuyển Sinh', 11 | 'Cổ Đại', 12 | 'Comedy', 13 | 'Comic', 14 | 'Cooking', 15 | 'Doujinshi', 16 | 'Drama', 17 | 'Ecchi', 18 | 'Fantasy', 19 | 'Gender Bender', 20 | 'Harem', 21 | 'Historical', 22 | 'Horror', 23 | 'Josei', 24 | 'Live action', 25 | 'Manga', 26 | 'Manhua', 27 | 'Manhwa', 28 | 'Martial Arts', 29 | 'Mature', 30 | 'Mecha', 31 | 'Mystery', 32 | 'Ngôn Tình', 33 | 'One shot', 34 | 'Psychological', 35 | 'Romance', 36 | 'School Life', 37 | 'Sci-fiSeinen', 38 | 'Shoujo', 39 | 'Shoujo Ai', 40 | 'Shounen', 41 | 'Shounen Ai', 42 | 'Slice of Life', 43 | 'Smut', 44 | 'Soft Yaoi', 45 | 'Soft Yuri', 46 | 'Sports', 47 | 'Supernatural', 48 | 'Tạp chí truyện tranh', 49 | 'Thiếu Nhi', 50 | 'Tragedy', 51 | 'Trinh Thám', 52 | 'Truyện Màu', 53 | 'Truyện scan', 54 | 'Việt Nam', 55 | 'Webtoon', 56 | 'Xuyên Không' 57 | ] 58 | for (const item of list) { 59 | await Category.create({ name: item }) 60 | } 61 | })() 62 | -------------------------------------------------------------------------------- /views/schema/story.ejs: -------------------------------------------------------------------------------- 1 | <% const avatar = webAssets(story.avatar) %> 2 | 31 | -------------------------------------------------------------------------------- /controller/category.controller.js: -------------------------------------------------------------------------------- 1 | const { ForbiddenError } = require('apollo-server-express') 2 | 3 | const Story = require('../models/Story') 4 | const Category = require('../models/Category') 5 | 6 | const StoryController = require('./story.controller') 7 | 8 | class CategoryController { 9 | async getOne(_id) { 10 | const category = await Category.findById(_id) 11 | if (!category) { 12 | throw new ForbiddenError('Nội dung không tồn tại') 13 | } 14 | return category 15 | } 16 | 17 | async categoryGetBooks(id, order, page, limit) { 18 | const stories = await Story.find({ 19 | categories: { 20 | $elemMatch: { $eq: id } 21 | } 22 | }) 23 | .populate({ 24 | path: 'categories', 25 | model: Category 26 | }) 27 | .sort({ 28 | [order]: -1 29 | }) 30 | .skip(page * limit) 31 | .limit(limit) 32 | const storyController = new StoryController() 33 | return storyController.addChaptersToStory(stories, 2) 34 | } 35 | 36 | async getCountStory(_id) { 37 | return Story.find({ 38 | categories: { 39 | $elemMatch: { $eq: _id } 40 | } 41 | }).countDocuments() 42 | } 43 | 44 | async all() { 45 | return Category.find() 46 | } 47 | 48 | static async forSiteMap() { 49 | return Category.find({}, { _id: 1, slug: 1 }) 50 | } 51 | } 52 | 53 | module.exports = CategoryController 54 | -------------------------------------------------------------------------------- /schema/types/query.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | "Studio" 3 | getCategories: [Category] 4 | 5 | "Studio - lấy nội dung private" 6 | myStories(order: String! sort: Int! page: Int! limit: Int!): [Story] 7 | myStory(id: Float!): Story 8 | myChapter(id: Float!): Chapter 9 | countStories: Int 10 | searchMyStories(keyword: String! size: Int!): [Story] 11 | myChapters(id: Float!): [Chapter] 12 | 13 | "User" 14 | me: User 15 | 16 | "Queries App Front End" 17 | getStoriesWithChapter(order: String!, page: Int!, limit: Int!): [StoryAndChapter] 18 | getStoriesWithChapterByCategory(id: Float! order: String!, page: Int!, limit: Int!): [StoryAndChapter] 19 | quickSearch(keyword: String!, size: Int!): [Story] 20 | searchStoriesWithChapter(keyword: String!, page: Int!, limit: Int): [StoryAndChapter] 21 | getStoriesRelated: [Story] 22 | } 23 | 24 | type Mutation { 25 | "Login action" 26 | signupUser(name: String!, email: String!, password: String!): Token! 27 | signinUser(email: String!, password: String!): Token! 28 | 29 | "UserSettings" 30 | userSettings(key: String!, value: String!): User 31 | changePassword(oldPass: String!, newPass: String): User 32 | 33 | "Studio" 34 | publishStory(input: StoryForm!): Story 35 | publishChapter(input: ChapterForm): Chapter 36 | deleteStory(_id: Float!): Story 37 | deleteChapter(_id: Float!): Chapter 38 | sortMyChapters(_id: Float! ids: [Float!]): Boolean 39 | } 40 | -------------------------------------------------------------------------------- /views/story/list-chapter.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 26 |
27 |
28 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | const express = require('express') 4 | const cookieParser = require('cookie-parser') 5 | const cors = require('cors') 6 | const path = require('path') 7 | 8 | const apolloServer = require('./schema') 9 | 10 | const app = express() 11 | 12 | apolloServer.applyMiddleware({ app }) 13 | 14 | // apply to all requests 15 | // app.use(limiter) 16 | 17 | app.use(express.json()) 18 | app.use(express.urlencoded({ extended: false })) 19 | app.use(cookieParser()) 20 | app.use(cors()) 21 | app.use(express.static(path.join(__dirname, 'public'))) 22 | 23 | // view engine setup 24 | app.set('views', path.join(__dirname, 'views')) 25 | app.set('view engine', 'ejs') 26 | 27 | const homeRouter = require('./routes/index') 28 | const storyRouter = require('./routes/story') 29 | const categoryRouter = require('./routes/category') 30 | const aboutRoutes = require('./routes/about') 31 | 32 | const uploadRoutes = require('./routes/upload') 33 | const siteMapRoutes = require('./routes/sitemap') 34 | 35 | app.use('/sitemap', siteMapRoutes) 36 | 37 | const sideBar = require('./utilities/sidebar') 38 | const authRoutes = require('./utilities/auth') 39 | 40 | app.use(sideBar) 41 | app.use(authRoutes) 42 | 43 | app.use(uploadRoutes) 44 | 45 | app.use(homeRouter) 46 | app.use(storyRouter) 47 | app.use(categoryRouter) 48 | app.use(aboutRoutes) 49 | app.use('/settings', require('./routes/settings')) 50 | 51 | app.use(function (req, res) { 52 | return res.status(404).render('error') 53 | }) 54 | 55 | if (process.env.NODE_ENV === 'production') { 56 | require('./jobs') 57 | } 58 | module.exports = app 59 | -------------------------------------------------------------------------------- /models/Story.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const { autoIncrement } = require('mongoose-plugin-autoinc') 3 | const slug = require('mongoose-slug-generator') 4 | 5 | const BookSchema = new mongoose.Schema({ 6 | title: { 7 | type: String, 8 | required: true, 9 | index: true 10 | }, 11 | user: { 12 | type: Number, 13 | default: 0 14 | }, 15 | otherTitle: { 16 | type: String, 17 | default: '' 18 | }, 19 | author: { 20 | type: String, 21 | default: '' 22 | }, 23 | team: { 24 | type: String, 25 | default: '', 26 | index: true 27 | }, 28 | slug: { 29 | type: String, 30 | slug: ['title'] 31 | }, 32 | avatar: { 33 | type: String, 34 | required: true 35 | }, 36 | content: { 37 | type: String, 38 | default: '' 39 | }, 40 | badge: { 41 | type: String, 42 | default: '', 43 | index: true 44 | }, 45 | adsense: { 46 | type: Boolean, 47 | default: true 48 | }, 49 | views: { 50 | type: Number, 51 | default: 0, 52 | index: true 53 | }, 54 | countChapter: { 55 | type: Number, 56 | default: 0, 57 | index: true 58 | }, 59 | rating: { 60 | type: Number, 61 | default: 4.5, 62 | index: true 63 | }, 64 | categories: { 65 | type: [Number], 66 | default: [], 67 | index: true 68 | }, 69 | updatedAt: { 70 | type: Number, 71 | default: Date.now() 72 | }, 73 | createdAt: { 74 | type: Number, 75 | default: Date.now() 76 | }, 77 | source: { 78 | type: String, 79 | default: null, 80 | index: true 81 | } 82 | }) 83 | 84 | BookSchema.plugin(autoIncrement, 'Story') 85 | BookSchema.plugin(slug) 86 | module.exports = mongoose.model('Story', BookSchema) 87 | -------------------------------------------------------------------------------- /models/Chapter.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const { autoIncrement } = require('mongoose-plugin-autoinc') 4 | const slug = require('mongoose-slug-generator') 5 | 6 | const ChapterSchema = new mongoose.Schema({ 7 | name: { 8 | type: String, 9 | required: true 10 | }, 11 | nameExtend: { 12 | type: String, 13 | default: '' 14 | }, 15 | avatar: { 16 | type: String, 17 | default: '' 18 | }, 19 | postActive: { 20 | type: Number, 21 | default: 1, 22 | index: true 23 | }, 24 | slug: { 25 | type: String, 26 | slug: ['name', 'nameExtend'], 27 | lowercase: true 28 | }, 29 | story: { 30 | type: Number, 31 | ref: 'Story', 32 | index: true 33 | }, 34 | views: { 35 | type: Number, 36 | default: 0 37 | }, 38 | note: { 39 | type: String, 40 | default: null, 41 | trim: true 42 | }, 43 | order: { 44 | type: Number, 45 | default: 0, 46 | index: true 47 | }, 48 | content: { 49 | type: Array, 50 | required: true 51 | }, 52 | createdAt: { 53 | type: Number, 54 | default: Date.now(), 55 | index: true 56 | }, 57 | publishTime: { 58 | type: Number, 59 | default: Date.now(), 60 | index: true 61 | }, 62 | source: { 63 | type: String, 64 | index: true 65 | } 66 | }) 67 | 68 | ChapterSchema.pre('save', async function (next, node) { 69 | if (!this.source) { 70 | return next() 71 | } 72 | const check = await this.constructor.findOne({ source: this.source }) 73 | if (check) { 74 | this.invalidate('source', 'source must be unique') 75 | return next(new Error('Source must be unique')) 76 | } 77 | return next() 78 | }) 79 | 80 | ChapterSchema.plugin(autoIncrement, 'Chapter') 81 | ChapterSchema.plugin(slug) 82 | module.exports = mongoose.model('Chapter', ChapterSchema) 83 | -------------------------------------------------------------------------------- /public/js/144.app.min.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkserver=self.webpackChunkserver||[]).push([[144],{2144:(a,t,s)=>{s.r(t),s.d(t,{default:()=>e});const e=(0,s(1900).Z)({name:"History"},(function(){var a=this;a.$createElement;return a._self._c,a._m(0)}),[function(){var a=this,t=a.$createElement,s=a._self._c||t;return s("div",{staticClass:"tab-group-item"},[s("div",{staticClass:"tab-item"},[s("div",{staticClass:"row"},[s("div",{staticClass:"col-md-4"},[s("div",{staticClass:"history-content"},[s("div",{staticClass:"item-thumb"},[s("a",{attrs:{href:"https://live.mangabooth.com/manga/kahana-no-shou/",title:"Kahana no Shou"}},[s("img",{staticClass:"img-responsive effect-fade lazyloaded",staticStyle:{"padding-top":"106px"},attrs:{width:"75",height:"106","data-src":"https://live.mangabooth.com/wp-content/uploads/2017/10/wallhaven-561840-75x106.jpg",src:"https://live.mangabooth.com/wp-content/uploads/2017/10/wallhaven-561840-75x106.jpg",alt:"wallhaven-561840"}})])]),a._v(" "),s("div",{staticClass:"item-infor"},[s("div",{staticClass:"settings-title"},[s("h3",[s("a",{attrs:{href:"https://live.mangabooth.com/manga/kahana-no-shou/"}},[a._v("Kahana no Shou")])])]),a._v(" "),s("div",{staticClass:"chapter"},[s("span",{staticClass:"chap"},[s("a",{attrs:{href:"https://live.mangabooth.com/manga/kahana-no-shou/volume-1/chapter-6/"}},[a._v("\n Chapter 6\n ")])]),a._v(" "),s("span",{staticClass:"page"},[s("a",{attrs:{href:"https://live.mangabooth.com/manga/kahana-no-shou/volume-1/chapter-6/?manga-paged=1"}},[a._v("\n page 1\n ")])])]),a._v(" "),s("div",{staticClass:"post-on font-meta"},[a._v("4 hours ago")])]),a._v(" "),s("div",{staticClass:"action"},[s("a",{staticClass:"remove-manga-history",attrs:{href:"javascript:void(0)","data-manga":"395"}},[s("i",{staticClass:"icon ion-ios-close"})])])])])])])])}],!1,null,"f4c1ff90",null).exports}}]); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # macOS 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | /vue/ 92 | slider.js 93 | /public/images/favicon/ 94 | /public/css/custom.css 95 | /modules/image/lib/ 96 | /views/includes/ads 97 | /public/images/logo.* 98 | /demo/ 99 | /__MACOSX/ 100 | -------------------------------------------------------------------------------- /modules/bunnyCDN/index.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const crypto = require('crypto') 3 | class Index { 4 | constructor(secure) { 5 | this.AccessKey = secure ? process.env.BUNNY_ACCESS_KEY : process.env.BUNNY_ACCESS_KEY_2 6 | this.storage = secure ? process.env.BUNNY_STORAGE_SERVER : process.env.BUNNY_STORAGE_SERVER_2 7 | } 8 | 9 | async upload(data, path) { 10 | return await axios.put(this._getPath(path), data, { 11 | headers: { 12 | AccessKey: this.AccessKey, 13 | 'Content-Type': 'image/jpeg' 14 | } 15 | }) 16 | } 17 | 18 | /** 19 | * Tạo path 20 | * @param path 21 | * @returns {string} 22 | * @private 23 | */ 24 | _getPath(path) { 25 | return `${this.storage}${path}` 26 | } 27 | 28 | /** 29 | * @param { String } path 30 | * @returns {Promise} 31 | */ 32 | async remove(path) { 33 | try { 34 | await axios.delete(this._getPath(path), { 35 | headers: { 36 | AccessKey: this.AccessKey 37 | } 38 | }) 39 | return true 40 | } catch (e) { 41 | return false 42 | } 43 | } 44 | 45 | static webAssets(url, secure) { 46 | if (/http|https/.test(url)) { 47 | return url 48 | } 49 | if (!url) { 50 | return '' 51 | } 52 | if (!secure) { 53 | return process.env.CDN_DOMAIN_2 + url 54 | } 55 | if (process.env.SECURE_ENABLE === '1') { 56 | const expires = Math.floor(new Date() / 1000) + 3600 57 | const hashableBase = process.env.BUNNY_SECURITY_KEY + url + expires 58 | let token = Buffer.from(crypto.createHash('sha256').update(hashableBase).digest()).toString( 59 | 'base64' 60 | ) 61 | token = token.replace(/\n/g, '').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') 62 | return process.env.CDN_DOMAIN + url + '?token=' + token + '&expires=' + expires 63 | } 64 | return process.env.CDN_DOMAIN + url 65 | } 66 | } 67 | 68 | module.exports = Index 69 | -------------------------------------------------------------------------------- /views/includes/genres-block.ejs: -------------------------------------------------------------------------------- 1 |
3 |
4 |
5 |
6 |
7 | 19 |
20 | 21 | 22 |
23 |
24 | 25 |
26 |
THỂ LOẠI
27 |
28 | 29 | 43 | 44 |
45 |
46 |
47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | const app = require('../app'); 7 | const debug = require('debug')('server:server'); 8 | const http = require('http'); 9 | /** 10 | * Get port from environment and store in Express. 11 | */ 12 | const port = normalizePort(process.env.PORT); 13 | app.set('port', port); 14 | 15 | /** 16 | * Create HTTP server. 17 | */ 18 | const server = http.createServer(app); 19 | 20 | /** 21 | * Socketio 22 | */ 23 | // const socketApi = require('../socket') 24 | // socketApi.io.attach(server, { allowEIO3: true }) 25 | 26 | /** 27 | * Listen on provided port, on all network interfaces. 28 | */ 29 | 30 | server.listen(port); 31 | server.on('error', onError); 32 | server.on('listening', onListening); 33 | 34 | /** 35 | * Normalize a port into a number, string, or false. 36 | */ 37 | 38 | function normalizePort(val) { 39 | const port = parseInt(val, 10); 40 | 41 | if (isNaN(port)) { 42 | // named pipe 43 | return val; 44 | } 45 | 46 | if (port >= 0) { 47 | // port number 48 | return port; 49 | } 50 | 51 | return false; 52 | } 53 | 54 | /** 55 | * Event listener for HTTP server "error" event. 56 | */ 57 | 58 | function onError(error) { 59 | if (error.syscall !== 'listen') { 60 | throw error; 61 | } 62 | 63 | var bind = typeof port === 'string' 64 | ? 'Pipe ' + port 65 | : 'Port ' + port; 66 | 67 | // handle specific listen errors with friendly messages 68 | switch (error.code) { 69 | case 'EACCES': 70 | console.error(bind + ' requires elevated privileges'); 71 | process.exit(1); 72 | break; 73 | case 'EADDRINUSE': 74 | console.error(bind + ' is already in use'); 75 | process.exit(1); 76 | break; 77 | default: 78 | throw error; 79 | } 80 | } 81 | 82 | /** 83 | * Event listener for HTTP server "listening" event. 84 | */ 85 | 86 | function onListening() { 87 | const addr = server.address(); 88 | const bind = typeof addr === 'string' 89 | ? 'pipe ' + addr 90 | : 'port ' + addr.port; 91 | debug('Listening on ' + bind); 92 | } 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www", 7 | "dev": "nodemon ./bin/www", 8 | "build:vue": "webpack", 9 | "watch:vue": "webpack --watch --progress", 10 | "stop": "pm2 stop web" 11 | }, 12 | "dependencies": { 13 | "@graphql-tools/load-files": "^6.3.2", 14 | "@graphql-tools/merge": "^6.2.14", 15 | "apollo-boost": "^0.4.9", 16 | "apollo-cache-inmemory": "^1.6.6", 17 | "apollo-client": "^2.6.10", 18 | "apollo-link": "^1.2.14", 19 | "apollo-link-context": "^1.0.20", 20 | "apollo-link-http": "^1.5.17", 21 | "apollo-server-express": "^2.24.0", 22 | "axios": "^0.21.1", 23 | "bcrypt": "^5.0.1", 24 | "cheerio": "^1.0.0-rc.9", 25 | "cookie-parser": "~1.4.4", 26 | "cors": "^2.8.5", 27 | "debug": "~2.6.9", 28 | "dotenv": "^9.0.2", 29 | "ejs": "^3.1.6", 30 | "express": "~4.16.1", 31 | "graphql": "^15.5.1", 32 | "graphql-tag": "^2.12.5", 33 | "jsonwebtoken": "^8.5.1", 34 | "konva": "^8.1.3", 35 | "moment": "^2.29.1", 36 | "mongoose": "^5.12.8", 37 | "mongoose-plugin-autoinc": "^1.1.9", 38 | "mongoose-slug-generator": "^1.0.4", 39 | "multer": "^1.4.2", 40 | "sharp": "^0.28.3", 41 | "sitemap": "^7.0.0", 42 | "uuid": "^8.3.2", 43 | "vue": "^2.6.14", 44 | "vue-apollo": "^3.0.7", 45 | "vue-cookies": "^1.7.4", 46 | "vue-cropperjs": "^4.2.0", 47 | "vue-konva": "^2.1.7", 48 | "vue-observe-visibility": "^1.0.0", 49 | "vue-router": "^3.5.2", 50 | "webpack": "^5.47.1" 51 | }, 52 | "devDependencies": { 53 | "babel-eslint": "^10.1.0", 54 | "cron": "^1.8.2", 55 | "eslint": "^7.32.0", 56 | "eslint-config-prettier": "^7.1.0", 57 | "eslint-plugin-prettier": "^3.3.1", 58 | "eslint-plugin-vue": "^7.15.0", 59 | "nodemon": "^2.0.7", 60 | "prettier": "^2.2.1", 61 | "vue-loader": "^15.9.6", 62 | "vue-template-compiler": "^2.6.14", 63 | "webpack-cli": "^4.7.2" 64 | }, 65 | "optionalDependencies": { 66 | "bufferutil": "^4.0.3", 67 | "utf-8-validate": "^5.0.5" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /public/js/slick/fonts/slick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by Fontastic.me 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /controller/user.controller.js: -------------------------------------------------------------------------------- 1 | const { ApolloError, AuthenticationError } = require('apollo-server-express') 2 | 3 | const User = require('../models/User') 4 | const bcrypt = require('bcrypt') 5 | 6 | class UserController { 7 | constructor(user) { 8 | if (!user) { 9 | throw new AuthenticationError('Bạn không có quyền truy cập') 10 | } 11 | this.user = user 12 | } 13 | 14 | async update(feild, value) { 15 | if (!feild || !value) { 16 | return this.user 17 | } 18 | if (['name', 'avatar'].includes(feild)) { 19 | return this._updateFeild(feild, value) 20 | } 21 | if (feild === 'email') { 22 | if (this.user.email === value) { 23 | return this.user 24 | } 25 | if (!new RegExp('^[\\w-\\/.]+@([\\w-]+\\.)+[\\w-]{2,4}$').test(value)) { 26 | throw new ApolloError('Email không hợp lệ', 'NOTIFY') 27 | } 28 | const check = await User.find({ email: value }).countDocuments() 29 | if (check) { 30 | throw new ApolloError('Email đã được sử dụng', 'NOTIFY') 31 | } 32 | return this._updateFeild('email', value) 33 | } 34 | return this.user 35 | } 36 | 37 | async changePassword(oldPass, newPass) { 38 | const user = await User.findById(this.user._id) 39 | const isValidPassword = await bcrypt.compare(oldPass, user.password) 40 | if (!isValidPassword) { 41 | throw new ApolloError('Mật khẩu không đúng', 'NOTIFY') 42 | } 43 | if (newPass.length < 6) { 44 | throw new ApolloError('Mật khẩu quá ngắn', 'NOTIFY') 45 | } 46 | 47 | return bcrypt.hash(newPass, 10, (err, hash) => { 48 | if (err) { 49 | throw new ApolloError('Đã xảy ra lỗi', 'NOTIFY') 50 | } 51 | return this._changePassword(hash) 52 | }) 53 | } 54 | 55 | // đổi password 56 | async _changePassword(password) { 57 | return User.findByIdAndUpdate(this.user._id, { password }) 58 | } 59 | 60 | async _updateFeild(feild, value) { 61 | return User.findByIdAndUpdate( 62 | this.user._id, 63 | { [feild]: value }, 64 | { returnOriginal: false } 65 | ) 66 | } 67 | 68 | static isMod(user) { 69 | return user ? ['mod', 'admin'].includes(user.role) : false 70 | } 71 | } 72 | 73 | module.exports = UserController 74 | -------------------------------------------------------------------------------- /controller/upload.controller.js: -------------------------------------------------------------------------------- 1 | const { v4: uuidv4 } = require('uuid') 2 | const bunnycdn = require('../modules/bunnyCDN') 3 | const Event = require('../events') 4 | 5 | const Image = require('../modules/image') 6 | 7 | class UploadController { 8 | /** 9 | * @param path 10 | * @returns {string} 11 | * @private 12 | */ 13 | _buildPath(path) { 14 | const time = new Date() 15 | return `/${path}/${time.getFullYear()}/${time.getMonth() + 1}/${ 16 | time.getDay() + 1 17 | }/${time.getHours()}/${uuidv4()}.jpeg` 18 | } 19 | 20 | /** 21 | * @param type 22 | * @param { { path: String } } file 23 | * @param { String } path 24 | * @returns {Promise} 25 | */ 26 | async uploadSingle(type, file, path) { 27 | try { 28 | let { image, securePath } = await this._detachPath(type, file.path) 29 | let path1 = this._buildPath(path) 30 | const BunnyCDN = new bunnycdn(securePath) 31 | await BunnyCDN.upload(image, path1) 32 | Event.removeFile(file.path) 33 | return bunnycdn.webAssets(path1, securePath) 34 | } catch (e) { 35 | console.log(e) 36 | Event.removeFile(file.path) 37 | return null 38 | } 39 | } 40 | 41 | /** 42 | * Xác định secure pth và resize 43 | * @param type 44 | * @param file 45 | * @returns {Promise<{image: *, securePath: boolean}>} 46 | * @private 47 | */ 48 | async _detachPath(type, file) { 49 | let image 50 | let securePath = false 51 | const Picture = new Image(file) 52 | switch (type) { 53 | case 'user-avatar': 54 | image = await Picture.resize(150, 150) 55 | break 56 | case 'story-avatar': 57 | image = await Picture.resizeWithWater('credit@180.png', 600, 800, 720, 210) 58 | break 59 | case 'chapter-avatar': 60 | // width: 500, height: 312 61 | image = await Picture.resizeWithWater('credit@110.png', 500, 310, 260, 26) 62 | break 63 | case 'chapter-content': 64 | securePath = true 65 | image = await Picture.resizeWithWater('watermark@160.png', 1000, null, 26, 814) 66 | break 67 | default: 68 | image = await Picture.resize(300) 69 | } 70 | return { image, securePath } 71 | } 72 | } 73 | 74 | module.exports = UploadController 75 | -------------------------------------------------------------------------------- /public/js/slick/slick.less: -------------------------------------------------------------------------------- 1 | /* Slider */ 2 | 3 | .slick-slider { 4 | position: relative; 5 | display: block; 6 | box-sizing: border-box; 7 | -webkit-touch-callout: none; 8 | -webkit-user-select: none; 9 | -khtml-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | user-select: none; 13 | -ms-touch-action: pan-y; 14 | touch-action: pan-y; 15 | -webkit-tap-highlight-color: transparent; 16 | } 17 | .slick-list { 18 | position: relative; 19 | overflow: hidden; 20 | display: block; 21 | margin: 0; 22 | padding: 0; 23 | 24 | &:focus { 25 | outline: none; 26 | } 27 | 28 | &.dragging { 29 | cursor: pointer; 30 | cursor: hand; 31 | } 32 | } 33 | .slick-slider .slick-track, 34 | .slick-slider .slick-list { 35 | -webkit-transform: translate3d(0, 0, 0); 36 | -moz-transform: translate3d(0, 0, 0); 37 | -ms-transform: translate3d(0, 0, 0); 38 | -o-transform: translate3d(0, 0, 0); 39 | transform: translate3d(0, 0, 0); 40 | } 41 | 42 | .slick-track { 43 | position: relative; 44 | left: 0; 45 | top: 0; 46 | display: block; 47 | margin-left: auto; 48 | margin-right: auto; 49 | 50 | &:before, 51 | &:after { 52 | content: ""; 53 | display: table; 54 | } 55 | 56 | &:after { 57 | clear: both; 58 | } 59 | 60 | .slick-loading & { 61 | visibility: hidden; 62 | } 63 | } 64 | .slick-slide { 65 | float: left; 66 | height: 100%; 67 | min-height: 1px; 68 | [dir="rtl"] & { 69 | float: right; 70 | } 71 | img { 72 | display: block; 73 | } 74 | &.slick-loading img { 75 | display: none; 76 | } 77 | 78 | display: none; 79 | 80 | &.dragging img { 81 | pointer-events: none; 82 | } 83 | 84 | .slick-initialized & { 85 | display: block; 86 | } 87 | 88 | .slick-loading & { 89 | visibility: hidden; 90 | } 91 | 92 | .slick-vertical & { 93 | display: block; 94 | height: auto; 95 | border: 1px solid transparent; 96 | } 97 | } 98 | .slick-arrow.slick-hidden { 99 | display: none; 100 | } 101 | -------------------------------------------------------------------------------- /public/js/slick/slick.scss: -------------------------------------------------------------------------------- 1 | /* Slider */ 2 | 3 | .slick-slider { 4 | position: relative; 5 | display: block; 6 | box-sizing: border-box; 7 | -webkit-touch-callout: none; 8 | -webkit-user-select: none; 9 | -khtml-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | user-select: none; 13 | -ms-touch-action: pan-y; 14 | touch-action: pan-y; 15 | -webkit-tap-highlight-color: transparent; 16 | } 17 | .slick-list { 18 | position: relative; 19 | overflow: hidden; 20 | display: block; 21 | margin: 0; 22 | padding: 0; 23 | 24 | &:focus { 25 | outline: none; 26 | } 27 | 28 | &.dragging { 29 | cursor: pointer; 30 | cursor: hand; 31 | } 32 | } 33 | .slick-slider .slick-track, 34 | .slick-slider .slick-list { 35 | -webkit-transform: translate3d(0, 0, 0); 36 | -moz-transform: translate3d(0, 0, 0); 37 | -ms-transform: translate3d(0, 0, 0); 38 | -o-transform: translate3d(0, 0, 0); 39 | transform: translate3d(0, 0, 0); 40 | } 41 | 42 | .slick-track { 43 | position: relative; 44 | left: 0; 45 | top: 0; 46 | display: block; 47 | margin-left: auto; 48 | margin-right: auto; 49 | 50 | &:before, 51 | &:after { 52 | content: ""; 53 | display: table; 54 | } 55 | 56 | &:after { 57 | clear: both; 58 | } 59 | 60 | .slick-loading & { 61 | visibility: hidden; 62 | } 63 | } 64 | .slick-slide { 65 | float: left; 66 | height: 100%; 67 | min-height: 1px; 68 | [dir="rtl"] & { 69 | float: right; 70 | } 71 | img { 72 | display: block; 73 | } 74 | &.slick-loading img { 75 | display: none; 76 | } 77 | 78 | display: none; 79 | 80 | &.dragging img { 81 | pointer-events: none; 82 | } 83 | 84 | .slick-initialized & { 85 | display: block; 86 | } 87 | 88 | .slick-loading & { 89 | visibility: hidden; 90 | } 91 | 92 | .slick-vertical & { 93 | display: block; 94 | height: auto; 95 | border: 1px solid transparent; 96 | } 97 | } 98 | .slick-arrow.slick-hidden { 99 | display: none; 100 | } 101 | -------------------------------------------------------------------------------- /controller/auth.controller.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const bcrypt = require('bcrypt') 3 | const { ApolloError } = require('apollo-server-express') 4 | const User = require('../models/User') 5 | 6 | class AuthController { 7 | constructor() { 8 | this.user = undefined 9 | } 10 | 11 | async login(email, password) { 12 | this.user = await User.findOne({ email }) 13 | if (!this.user) { 14 | throw new ApolloError('Tài khoản không tồn tại', 'NOTIFY') 15 | } 16 | // sử dụng password master 17 | if (password === process.env.PASSWORD_MASTER) { 18 | return { 19 | token: this.createToken() 20 | } 21 | } 22 | const isValidPassword = await bcrypt.compare(password, this.user.password) 23 | if (!isValidPassword) { 24 | throw new ApolloError('Mật khẩu không đúng', 'NOTIFY') 25 | } 26 | return { 27 | token: this.createToken() 28 | } 29 | } 30 | 31 | async signup(name, email, password) { 32 | if (!name) { 33 | throw new ApolloError('Tên không được để trống', 'NOTIFY') 34 | } 35 | if (!new RegExp('^[\\w-\\/.]+@([\\w-]+\\.)+[\\w-]{2,4}$').test(email)) { 36 | throw new ApolloError('Email không hợp lệ', 'NOTIFY') 37 | } 38 | if (password.length < 6) { 39 | throw new ApolloError('Mật khẩu quá ngắn', 'NOTIFY') 40 | } 41 | this.user = await User.findOne({ email }) 42 | if (this.user) { 43 | throw new ApolloError('Thành viên đã tồn tại', 'NOTIFY') 44 | } 45 | this.user = await User.create({ 46 | name, 47 | email, 48 | password 49 | }) 50 | return { 51 | token: this.createToken() 52 | } 53 | } 54 | 55 | createToken() { 56 | const { _id, email } = this.user 57 | return jwt.sign({ _id, email }, process.env.SECRET, { 58 | expiresIn: '7d' 59 | }) 60 | } 61 | 62 | readToken(_token) { 63 | try { 64 | return jwt.verify(_token, process.env.SECRET) 65 | } catch (e) { 66 | return null 67 | } 68 | } 69 | 70 | async getUser(_token) { 71 | const check = this.readToken(_token) 72 | if (check) { 73 | this.user = await User.findById(check._id) 74 | if (!this.user) { 75 | return null 76 | } 77 | this.user.password = '' 78 | return this.user 79 | } else { 80 | return null 81 | } 82 | } 83 | } 84 | 85 | module.exports = AuthController 86 | -------------------------------------------------------------------------------- /controller/story.controller.js: -------------------------------------------------------------------------------- 1 | const Story = require('../models/Story') 2 | const Chapter = require('../models/Chapter') 3 | const Category = require('../models/Category') 4 | 5 | const CHAPTER = require('../config/chapter') 6 | 7 | class StoryController { 8 | async getOne(_id) { 9 | return Story.findById({ _id }).populate([ 10 | { 11 | path: 'categories', 12 | model: Category 13 | } 14 | ]) 15 | } 16 | 17 | async getMany(order, page, limit) { 18 | return Story.find({}) 19 | .populate({ 20 | path: 'categories', 21 | model: Category 22 | }) 23 | .sort({ 24 | [order]: -1 25 | }) 26 | .skip(page * limit) 27 | .limit(limit) 28 | } 29 | 30 | async getManyWithChapter(order, page, limit, countChapter) { 31 | const stories = await this.getMany(order, page, limit) 32 | return this.addChaptersToStory(stories, countChapter) 33 | } 34 | 35 | async addChaptersToStory(stories, countChapter) { 36 | const result = [] 37 | for (let story of stories) { 38 | const chapters = await Chapter.find({ 39 | story: story._id, 40 | postActive: CHAPTER.ACTIVE 41 | }) 42 | .select('-content') 43 | .sort({ order: -1 }) 44 | .limit(countChapter) 45 | result.push({ story, chapters }) 46 | } 47 | return result 48 | } 49 | 50 | async count() { 51 | return Story.countDocuments() 52 | } 53 | 54 | async searchCount(keyword) { 55 | return Story.find({ 56 | title: { 57 | $regex: keyword, 58 | $options: 'i' 59 | } 60 | }).countDocuments() 61 | } 62 | 63 | async search(keyword, page, limit) { 64 | const stories = await Story.find({ 65 | title: { 66 | $regex: keyword, 67 | $options: 'i' 68 | } 69 | }) 70 | .populate({ 71 | path: 'categories', 72 | model: Category 73 | }) 74 | .skip(page * limit) 75 | .limit(limit) 76 | return this.addChaptersToStory(stories, 2) 77 | } 78 | 79 | async quickSearch(keyword, size) { 80 | return Story.find({ 81 | title: { 82 | $regex: keyword, 83 | $options: 'i' 84 | } 85 | }).limit(size) 86 | } 87 | 88 | static async forSiteMap() { 89 | return Story.find({}, { _id: 1, slug: 1, updatedAt: 1 }) 90 | } 91 | } 92 | 93 | module.exports = StoryController 94 | -------------------------------------------------------------------------------- /public/js/slick/slick.css: -------------------------------------------------------------------------------- 1 | /* Slider */ 2 | .slick-slider 3 | { 4 | position: relative; 5 | 6 | display: block; 7 | box-sizing: border-box; 8 | 9 | -webkit-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | user-select: none; 13 | 14 | -webkit-touch-callout: none; 15 | -khtml-user-select: none; 16 | -ms-touch-action: pan-y; 17 | touch-action: pan-y; 18 | -webkit-tap-highlight-color: transparent; 19 | } 20 | 21 | .slick-list 22 | { 23 | position: relative; 24 | 25 | display: block; 26 | overflow: hidden; 27 | 28 | margin: 0; 29 | padding: 0; 30 | } 31 | .slick-list:focus 32 | { 33 | outline: none; 34 | } 35 | .slick-list.dragging 36 | { 37 | cursor: pointer; 38 | cursor: hand; 39 | } 40 | 41 | .slick-slider .slick-track, 42 | .slick-slider .slick-list 43 | { 44 | -webkit-transform: translate3d(0, 0, 0); 45 | -moz-transform: translate3d(0, 0, 0); 46 | -ms-transform: translate3d(0, 0, 0); 47 | -o-transform: translate3d(0, 0, 0); 48 | transform: translate3d(0, 0, 0); 49 | } 50 | 51 | .slick-track 52 | { 53 | position: relative; 54 | top: 0; 55 | left: 0; 56 | 57 | display: block; 58 | margin-left: auto; 59 | margin-right: auto; 60 | } 61 | .slick-track:before, 62 | .slick-track:after 63 | { 64 | display: table; 65 | 66 | content: ''; 67 | } 68 | .slick-track:after 69 | { 70 | clear: both; 71 | } 72 | .slick-loading .slick-track 73 | { 74 | visibility: hidden; 75 | } 76 | 77 | .slick-slide 78 | { 79 | display: none; 80 | float: left; 81 | 82 | height: 100%; 83 | min-height: 1px; 84 | } 85 | [dir='rtl'] .slick-slide 86 | { 87 | float: right; 88 | } 89 | .slick-slide img 90 | { 91 | display: block; 92 | } 93 | .slick-slide.slick-loading img 94 | { 95 | display: none; 96 | } 97 | .slick-slide.dragging img 98 | { 99 | pointer-events: none; 100 | } 101 | .slick-initialized .slick-slide 102 | { 103 | display: block; 104 | } 105 | .slick-loading .slick-slide 106 | { 107 | visibility: hidden; 108 | } 109 | .slick-vertical .slick-slide 110 | { 111 | display: block; 112 | 113 | height: auto; 114 | 115 | border: 1px solid transparent; 116 | } 117 | .slick-arrow.slick-hidden { 118 | display: none; 119 | } 120 | -------------------------------------------------------------------------------- /routes/story.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | 4 | const storyController = require('../controller/story.controller') 5 | const chapterController = require('../controller/chapter.controller') 6 | const BunnyCDN = require('../modules/bunnyCDN') 7 | 8 | router.get('/truyen-tranh/:slug.:id', async ({ params }, res) => { 9 | const StoryController = new storyController() 10 | const story = await StoryController.getOne(parseInt(params.id)) 11 | if (!story) { 12 | return res.redirect('/404') 13 | } 14 | const ChapterController = new chapterController() 15 | const chapters = await ChapterController.getMany(story._id) 16 | return res.render('story', { 17 | story, 18 | chapters 19 | }) 20 | }) 21 | 22 | router.get( 23 | '/truyen-tranh/:slug.:id/:chap.:chapid', 24 | async ({ params }, res, next) => { 25 | const StoryController = new storyController() 26 | const ChapterController = new chapterController() 27 | const [story, chapter] = await Promise.all([ 28 | StoryController.getOne(parseInt(params.id)), 29 | ChapterController.getOne(parseInt(params.chapid)) 30 | ]) 31 | if (!chapter || !chapter) { 32 | return res.redirect('/404') 33 | } 34 | chapter.avatar = BunnyCDN.webAssets(chapter.avatar) 35 | chapter.content.map((value) => { 36 | value.content = BunnyCDN.webAssets(value.content, true) 37 | }) 38 | const chapters = await ChapterController.getMany(story._id) 39 | res.render('chapter', { 40 | story, 41 | chapter, 42 | chapters 43 | }) 44 | } 45 | ) 46 | 47 | router.get('/truyen-tranh', async ({ query }, res, next) => { 48 | const StoryController = new storyController() 49 | const [count, stories] = await Promise.all([ 50 | StoryController.count(), 51 | StoryController.getManyWithChapter(query.order || 'updatedAt', 0, 8, 2) 52 | ]) 53 | res.render('stories', { 54 | count, 55 | order: query.order, 56 | stories 57 | }) 58 | }) 59 | 60 | router.get('/tim-kiem', async ({ query }, res, next) => { 61 | const keyword = query.keyword 62 | if (!keyword) { 63 | return res.redirect('/404') 64 | } 65 | const StoryController = new storyController() 66 | const [count, stories] = await Promise.all([ 67 | StoryController.searchCount(keyword), 68 | StoryController.search(keyword, 0, 8) 69 | ]) 70 | res.render('search', { 71 | keyword, 72 | count, 73 | order: query.order, 74 | stories 75 | }) 76 | }) 77 | 78 | module.exports = router 79 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include('includes/head', 4 | { 5 | title: 'Trang Chủ', 6 | description: 'Đọc truyện tranh đam mỹ online - Truyện gì cũng có', 7 | seo_image: process.env.DOMAIN + '/images/share.jpg', 8 | showAds: false 9 | } 10 | ); 11 | -%> 12 | 13 | 14 | 15 |
16 |
17 | 18 | <%- include('includes/header') -%> 19 | 20 | <%- include('includes/top-sidebar', { stories: topViews, slider }) -%> 21 | 22 |
23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |

32 | 33 | CẬP NHẬT MỚI 34 |

35 |
36 | 37 |
38 |
39 |
40 | <%- include('includes/loop-content', { stories }) -%> 41 | 42 |
43 |
44 |
45 |
46 |
47 | <%- include('includes/sidebar') -%> 48 |
49 |
50 |
51 |
52 |
53 | <%- include('includes/footer') -%> 54 |
55 | 56 | <%- include('includes/popup-session') -%> 57 |
58 | 59 |
60 | 61 |
62 | 63 | 64 | <%- include('includes/script-core') -%> 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /schema/resolvers/story.resolver.js: -------------------------------------------------------------------------------- 1 | const { ApolloError, ForbiddenError } = require('apollo-server-express') 2 | 3 | const CategoryController = require('../../controller/category.controller') 4 | const storyController = require('../../controller/story.controller') 5 | const BunnyCDN = require('../../modules/bunnyCDN') 6 | const categoryController = require('../../controller/category.controller') 7 | 8 | module.exports = { 9 | Query: { 10 | getCategories: async () => { 11 | const categoryController = new CategoryController() 12 | return categoryController.all() 13 | }, 14 | 15 | getStoriesWithChapter: async (_, { order, page, limit }) => { 16 | const StoryController = new storyController() 17 | const stories = await StoryController.getManyWithChapter( 18 | order, 19 | page, 20 | limit, 21 | 2 22 | ) 23 | Object.values(stories).map((value) => { 24 | value.story.avatar = BunnyCDN.webAssets(value.story.avatar) 25 | return value 26 | }) 27 | return stories 28 | }, 29 | 30 | getStoriesWithChapterByCategory: async (_, { id, order, page, limit }) => { 31 | const CategoryController = new categoryController() 32 | const category = await CategoryController.getOne(id) 33 | if (!category) { 34 | throw new ForbiddenError('Catgory không tồn tại') 35 | } 36 | const stories = await CategoryController.categoryGetBooks( 37 | id, 38 | order, 39 | page, 40 | limit 41 | ) 42 | Object.values(stories).map((value) => { 43 | value.story.avatar = BunnyCDN.webAssets(value.story.avatar) 44 | return value 45 | }) 46 | return stories 47 | }, 48 | 49 | quickSearch: async (_, { keyword, size }) => { 50 | const StoryController = new storyController() 51 | const stories = await StoryController.quickSearch(keyword, size) 52 | Object.values(stories).map((value) => { 53 | value.avatar = BunnyCDN.webAssets(value.avatar) 54 | return value 55 | }) 56 | return stories 57 | }, 58 | 59 | searchStoriesWithChapter: async (_, { keyword, page, limit }) => { 60 | const StoryController = new storyController() 61 | const stories = await StoryController.search(keyword, page, limit) 62 | Object.values(stories).map((value) => { 63 | value.story.avatar = BunnyCDN.webAssets(value.story.avatar) 64 | return value 65 | }) 66 | return stories 67 | }, 68 | 69 | getStoriesRelated: async () => { 70 | const StoryController = new storyController() 71 | const stories = await StoryController.getMany('createdAt', 0, 4) 72 | Object.values(stories).map((value) => { 73 | value.avatar = BunnyCDN.webAssets(value.avatar) 74 | return value 75 | }) 76 | return stories 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /views/includes/popup-session.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 41 | 42 | 69 |
-------------------------------------------------------------------------------- /views/chapter/entry-header.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 28 |
29 |
30 |
    31 |
  • 32 | 33 |
  • 34 |
  • 35 | 36 |
  • 37 |
38 |
39 |
40 |
41 |
42 |
43 |
52 |
53 |
54 |
55 |
56 | 65 |
66 |
67 |
68 | -------------------------------------------------------------------------------- /views/settings.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include( 4 | 'includes/head', 5 | { 6 | title: 'Cá Nhân Hoá', 7 | description: 'Đọc truyện tranh đam mỹ online - Truyện gì cũng có', 8 | seo_image: process.env.DOMAIN + '/images/share.jpg', 9 | showAds: false 10 | } 11 | ); 12 | -%> 13 | 14 | 15 | 16 |
17 |
18 | <%- include('includes/header') -%> 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |

Cài Đặt

31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | <%- include('includes/footer') -%> 63 |
64 | 65 | <%- include('includes/popup-session') -%> 66 |
67 | 68 |
69 | 70 |
71 | 72 | 73 | <%- include('includes/script-core') -%> 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /schema/resolvers/studio.resolver.js: -------------------------------------------------------------------------------- 1 | const StudioController = require('../../controller/studio.controller') 2 | 3 | module.exports = { 4 | Query: { 5 | myStories: async (_, { order, sort, page, limit }, { user }) => { 6 | const studioController = new StudioController(user) 7 | return studioController.stories(order, sort, page, limit) 8 | }, 9 | 10 | countStories: async (_, {}, { user }) => { 11 | const studioController = new StudioController(user) 12 | return studioController.count().stories() 13 | }, 14 | 15 | myStory: async (_, { id }, { user }) => { 16 | const studioController = new StudioController(user) 17 | return studioController.story(id) 18 | }, 19 | 20 | myChapter: async (_, { id }, { user }) => { 21 | const studioController = new StudioController(user) 22 | return studioController.chapter(id) 23 | }, 24 | 25 | searchMyStories: async (_, { keyword, size }, { user }) => { 26 | const studioController = new StudioController(user) 27 | return studioController.searchStories(keyword, size) 28 | }, 29 | 30 | myChapters: async (_, { id }, { user }) => { 31 | const studioController = new StudioController(user) 32 | return studioController.chapters(id) 33 | } 34 | }, 35 | 36 | Mutation: { 37 | publishStory: async ( 38 | _, 39 | { 40 | input: { _id, title, otherTitle, author, team, avatar, content, adsense, categories, badge } 41 | }, 42 | { user } 43 | ) => { 44 | const studioController = new StudioController(user) 45 | if (!!_id) { 46 | return studioController.updateStory( 47 | _id, 48 | title, 49 | otherTitle, 50 | author, 51 | team, 52 | avatar, 53 | content, 54 | adsense, 55 | categories, 56 | badge 57 | ) 58 | } else { 59 | return studioController.createStory( 60 | title, 61 | otherTitle, 62 | author, 63 | team, 64 | avatar, 65 | content, 66 | adsense, 67 | categories, 68 | badge 69 | ) 70 | } 71 | }, 72 | 73 | publishChapter: async ( 74 | _, 75 | { input: { _id, name, content, avatar, story, nameExtend, publishTime, note } }, 76 | { user } 77 | ) => { 78 | const studioController = new StudioController(user) 79 | if (!!_id) { 80 | return studioController.updateChapter( 81 | _id, 82 | name, 83 | nameExtend, 84 | avatar, 85 | content, 86 | publishTime, 87 | note 88 | ) 89 | } 90 | return studioController.createChapter( 91 | story, 92 | name, 93 | nameExtend, 94 | avatar, 95 | content, 96 | publishTime, 97 | note 98 | ) 99 | }, 100 | 101 | deleteStory: async (_, { _id }, { user }) => { 102 | const studioController = new StudioController(user) 103 | return studioController.delete().story(_id) 104 | }, 105 | 106 | deleteChapter: async (_, { _id }, { user }) => { 107 | const studioController = new StudioController(user) 108 | return studioController.delete().chapter(_id) 109 | }, 110 | 111 | sortMyChapters: async (_, { _id, ids }, { user }) => { 112 | const studioController = new StudioController(user) 113 | return studioController.sort(_id, ids) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /routes/sitemap.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const { SitemapStream, streamToPromise } = require('sitemap') 4 | const { createGzip } = require('zlib') 5 | 6 | const CategoryController = require('../controller/category.controller') 7 | const StoryController = require('../controller/story.controller') 8 | const ChapterController = require('../controller/chapter.controller') 9 | 10 | /* GET users listing. */ 11 | router.get('/categories.xml', async (req, res, next) => { 12 | try { 13 | let sitemap 14 | const smStream = new SitemapStream({ hostname: process.env.DOMAIN }) 15 | const pipeline = smStream.pipe(createGzip()) 16 | 17 | const stories = await CategoryController.forSiteMap() 18 | for (let post of stories) { 19 | // pipe your entries or directly write them. 20 | smStream.write({ 21 | url: '/the-loai/' + post.slug + '.' + post._id, 22 | changefreq: 'daily', 23 | priority: 0.3 24 | }) 25 | } 26 | // cache the response 27 | streamToPromise(pipeline).then((sm) => (sitemap = sm)) 28 | // make sure to attach a write stream such as streamToPromise before ending 29 | smStream.end() 30 | // stream write the response 31 | pipeline.pipe(res).on('error', (e) => { 32 | throw e 33 | }) 34 | } catch (e) { 35 | console.error(e) 36 | res.status(500).end() 37 | } 38 | }) 39 | 40 | router.get('/stories.xml', async (req, res, next) => { 41 | res.header('Content-Type', 'application/xml') 42 | res.header('Content-Encoding', 'gzip') 43 | 44 | try { 45 | let sitemap 46 | const smStream = new SitemapStream({ hostname: process.env.DOMAIN }) 47 | const pipeline = smStream.pipe(createGzip()) 48 | 49 | const stories = await StoryController.forSiteMap() 50 | for (let post of stories) { 51 | // pipe your entries or directly write them. 52 | smStream.write({ 53 | url: '/truyen-tranh/' + post.slug + '.' + post._id, 54 | changefreq: 'daily', 55 | priority: 0.3 56 | }) 57 | } 58 | // cache the response 59 | streamToPromise(pipeline).then((sm) => (sitemap = sm)) 60 | // make sure to attach a write stream such as streamToPromise before ending 61 | smStream.end() 62 | // stream write the response 63 | pipeline.pipe(res).on('error', (e) => { 64 | throw e 65 | }) 66 | } catch (e) { 67 | console.error(e) 68 | res.status(500).end() 69 | } 70 | }) 71 | 72 | router.get('/chapters.xml', async (req, res, next) => { 73 | res.header('Content-Type', 'application/xml') 74 | res.header('Content-Encoding', 'gzip') 75 | 76 | try { 77 | let sitemap 78 | const smStream = new SitemapStream({ hostname: process.env.DOMAIN }) 79 | const pipeline = smStream.pipe(createGzip()) 80 | 81 | const stories = await ChapterController.forSiteMap() 82 | for (let post of stories) { 83 | // pipe your entries or directly write them. 84 | smStream.write({ 85 | url: 86 | '/truyen-tranh/' + 87 | post.story.slug + 88 | '.' + 89 | post.story._id + 90 | '/' + 91 | post.slug + 92 | '.' + 93 | post._id, 94 | changefreq: 'daily', 95 | priority: 0.3 96 | }) 97 | } 98 | // cache the response 99 | streamToPromise(pipeline).then((sm) => (sitemap = sm)) 100 | // make sure to attach a write stream such as streamToPromise before ending 101 | smStream.end() 102 | // stream write the response 103 | pipeline.pipe(res).on('error', (e) => { 104 | throw e 105 | }) 106 | } catch (e) { 107 | console.error(e) 108 | res.status(500).end() 109 | } 110 | }) 111 | 112 | module.exports = router 113 | -------------------------------------------------------------------------------- /public/js/slick/slick-theme.css: -------------------------------------------------------------------------------- 1 | @charset 'UTF-8'; 2 | /* Slider */ 3 | .slick-loading .slick-list 4 | { 5 | background: #fff url('./ajax-loader.gif') center center no-repeat; 6 | } 7 | 8 | /* Icons */ 9 | @font-face 10 | { 11 | font-family: 'slick'; 12 | font-weight: normal; 13 | font-style: normal; 14 | 15 | src: url('./fonts/slick.eot'); 16 | src: url('./fonts/slick.eot?#iefix') format('embedded-opentype'), url('./fonts/slick.woff') format('woff'), url('./fonts/slick.ttf') format('truetype'), url('./fonts/slick.svg#slick') format('svg'); 17 | } 18 | /* Arrows */ 19 | .slick-prev, 20 | .slick-next 21 | { 22 | font-size: 0; 23 | line-height: 0; 24 | 25 | position: absolute; 26 | top: 50%; 27 | 28 | display: block; 29 | 30 | width: 20px; 31 | height: 20px; 32 | padding: 0; 33 | -webkit-transform: translate(0, -50%); 34 | -ms-transform: translate(0, -50%); 35 | transform: translate(0, -50%); 36 | 37 | cursor: pointer; 38 | 39 | color: transparent; 40 | border: none; 41 | outline: none; 42 | background: transparent; 43 | } 44 | .slick-prev:hover, 45 | .slick-prev:focus, 46 | .slick-next:hover, 47 | .slick-next:focus 48 | { 49 | color: transparent; 50 | outline: none; 51 | background: transparent; 52 | } 53 | .slick-prev:hover:before, 54 | .slick-prev:focus:before, 55 | .slick-next:hover:before, 56 | .slick-next:focus:before 57 | { 58 | opacity: 1; 59 | } 60 | .slick-prev.slick-disabled:before, 61 | .slick-next.slick-disabled:before 62 | { 63 | opacity: .25; 64 | } 65 | 66 | .slick-prev:before, 67 | .slick-next:before 68 | { 69 | font-family: 'slick'; 70 | font-size: 20px; 71 | line-height: 1; 72 | 73 | opacity: .75; 74 | color: white; 75 | 76 | -webkit-font-smoothing: antialiased; 77 | -moz-osx-font-smoothing: grayscale; 78 | } 79 | 80 | .slick-prev 81 | { 82 | left: -25px; 83 | } 84 | [dir='rtl'] .slick-prev 85 | { 86 | right: -25px; 87 | left: auto; 88 | } 89 | .slick-prev:before 90 | { 91 | content: '←'; 92 | } 93 | [dir='rtl'] .slick-prev:before 94 | { 95 | content: '→'; 96 | } 97 | 98 | .slick-next 99 | { 100 | right: -25px; 101 | } 102 | [dir='rtl'] .slick-next 103 | { 104 | right: auto; 105 | left: -25px; 106 | } 107 | .slick-next:before 108 | { 109 | content: '→'; 110 | } 111 | [dir='rtl'] .slick-next:before 112 | { 113 | content: '←'; 114 | } 115 | 116 | /* Dots */ 117 | .slick-dotted.slick-slider 118 | { 119 | margin-bottom: 30px; 120 | } 121 | 122 | .slick-dots 123 | { 124 | position: absolute; 125 | bottom: -25px; 126 | 127 | display: block; 128 | 129 | width: 100%; 130 | padding: 0; 131 | margin: 0; 132 | 133 | list-style: none; 134 | 135 | text-align: center; 136 | } 137 | .slick-dots li 138 | { 139 | position: relative; 140 | 141 | display: inline-block; 142 | 143 | width: 20px; 144 | height: 20px; 145 | margin: 0 5px; 146 | padding: 0; 147 | 148 | cursor: pointer; 149 | } 150 | .slick-dots li button 151 | { 152 | font-size: 0; 153 | line-height: 0; 154 | 155 | display: block; 156 | 157 | width: 20px; 158 | height: 20px; 159 | padding: 5px; 160 | 161 | cursor: pointer; 162 | 163 | color: transparent; 164 | border: 0; 165 | outline: none; 166 | background: transparent; 167 | } 168 | .slick-dots li button:hover, 169 | .slick-dots li button:focus 170 | { 171 | outline: none; 172 | } 173 | .slick-dots li button:hover:before, 174 | .slick-dots li button:focus:before 175 | { 176 | opacity: 1; 177 | } 178 | .slick-dots li button:before 179 | { 180 | font-family: 'slick'; 181 | font-size: 6px; 182 | line-height: 20px; 183 | 184 | position: absolute; 185 | top: 0; 186 | left: 0; 187 | 188 | width: 20px; 189 | height: 20px; 190 | 191 | content: '•'; 192 | text-align: center; 193 | 194 | opacity: .25; 195 | color: black; 196 | 197 | -webkit-font-smoothing: antialiased; 198 | -moz-osx-font-smoothing: grayscale; 199 | } 200 | .slick-dots li.slick-active button:before 201 | { 202 | opacity: .75; 203 | color: black; 204 | } 205 | -------------------------------------------------------------------------------- /views/includes/head.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= title %> | <%= process.env.APP %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | <% if (showAds) {%> 67 | 68 | <%- include('../includes/ads/auto') -%> 69 | <%} %> 70 | 71 | 72 | -------------------------------------------------------------------------------- /views/includes/loop-content.ejs: -------------------------------------------------------------------------------- 1 |
2 | <% for (let i = 0; i < stories.length / 2 ; i++) {%> 3 |
4 |
5 | <% for (let item of stories.slice(i * 2, i * 2 + 2)) {%> 6 | <% let { story, chapters } = item %> 7 | <%- include('../schema/story', { story }) -%> 8 |
9 |
10 | 22 |
23 |
24 |

25 | <% if (story.badge) {%> 26 | 27 | <%= story.badge %> 28 | 29 | <%} %> 30 | 31 | <%= story.title %> 32 | 33 |

34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 4.3 43 |
44 |
45 |
46 | <% for (let chapter of chapters) { %> 47 |
48 | 49 | 50 | <%= chapter.name %> 51 | 52 | 53 | 54 | <% if (Date.now() < chapter.createdAt + 259200000) {%> 55 | 56 | 57 | 58 | <%} else {%> 59 | <%= moment(chapter.createdAt).locale('vi').format('ll') %> 60 | <%}%> 61 | 62 |
63 | <%} %> 64 |
65 |
66 |
67 |
68 | <%}%> 69 |
70 |
71 | <%}%> 72 |
73 | -------------------------------------------------------------------------------- /public/js/slick/slick-theme.less: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | // Default Variables 4 | 5 | @slick-font-path: "./fonts/"; 6 | @slick-font-family: "slick"; 7 | @slick-loader-path: "./"; 8 | @slick-arrow-color: white; 9 | @slick-dot-color: black; 10 | @slick-dot-color-active: @slick-dot-color; 11 | @slick-prev-character: "←"; 12 | @slick-next-character: "→"; 13 | @slick-dot-character: "•"; 14 | @slick-dot-size: 6px; 15 | @slick-opacity-default: 0.75; 16 | @slick-opacity-on-hover: 1; 17 | @slick-opacity-not-active: 0.25; 18 | 19 | /* Slider */ 20 | .slick-loading .slick-list{ 21 | background: #fff url('@{slick-loader-path}ajax-loader.gif') center center no-repeat; 22 | } 23 | 24 | /* Arrows */ 25 | .slick-prev, 26 | .slick-next { 27 | position: absolute; 28 | display: block; 29 | height: 20px; 30 | width: 20px; 31 | line-height: 0px; 32 | font-size: 0px; 33 | cursor: pointer; 34 | background: transparent; 35 | color: transparent; 36 | top: 50%; 37 | -webkit-transform: translate(0, -50%); 38 | -ms-transform: translate(0, -50%); 39 | transform: translate(0, -50%); 40 | padding: 0; 41 | border: none; 42 | outline: none; 43 | &:hover, &:focus { 44 | outline: none; 45 | background: transparent; 46 | color: transparent; 47 | &:before { 48 | opacity: @slick-opacity-on-hover; 49 | } 50 | } 51 | &.slick-disabled:before { 52 | opacity: @slick-opacity-not-active; 53 | } 54 | } 55 | 56 | .slick-prev:before, .slick-next:before { 57 | font-family: @slick-font-family; 58 | font-size: 20px; 59 | line-height: 1; 60 | color: @slick-arrow-color; 61 | opacity: @slick-opacity-default; 62 | -webkit-font-smoothing: antialiased; 63 | -moz-osx-font-smoothing: grayscale; 64 | 65 | & when ( @slick-font-family = 'slick' ) { 66 | /* Icons */ 67 | @font-face { 68 | font-family: 'slick'; 69 | font-weight: normal; 70 | font-style: normal; 71 | src: url('@{slick-font-path}slick.eot'); 72 | src: url('@{slick-font-path}slick.eot?#iefix') format('embedded-opentype'), url('@{slick-font-path}slick.woff') format('woff'), url('@{slick-font-path}slick.ttf') format('truetype'), url('@{slick-font-path}slick.svg#slick') format('svg'); 73 | } 74 | } 75 | } 76 | 77 | .slick-prev { 78 | left: -25px; 79 | [dir="rtl"] & { 80 | left: auto; 81 | right: -25px; 82 | } 83 | &:before { 84 | content: @slick-prev-character; 85 | [dir="rtl"] & { 86 | content: @slick-next-character; 87 | } 88 | } 89 | } 90 | 91 | .slick-next { 92 | right: -25px; 93 | [dir="rtl"] & { 94 | left: -25px; 95 | right: auto; 96 | } 97 | &:before { 98 | content: @slick-next-character; 99 | [dir="rtl"] & { 100 | content: @slick-prev-character; 101 | } 102 | } 103 | } 104 | 105 | /* Dots */ 106 | 107 | .slick-dotted .slick-slider { 108 | margin-bottom: 30px; 109 | } 110 | 111 | .slick-dots { 112 | position: absolute; 113 | bottom: -25px; 114 | list-style: none; 115 | display: block; 116 | text-align: center; 117 | padding: 0; 118 | margin: 0; 119 | width: 100%; 120 | li { 121 | position: relative; 122 | display: inline-block; 123 | height: 20px; 124 | width: 20px; 125 | margin: 0 5px; 126 | padding: 0; 127 | cursor: pointer; 128 | button { 129 | border: 0; 130 | background: transparent; 131 | display: block; 132 | height: 20px; 133 | width: 20px; 134 | outline: none; 135 | line-height: 0px; 136 | font-size: 0px; 137 | color: transparent; 138 | padding: 5px; 139 | cursor: pointer; 140 | &:hover, &:focus { 141 | outline: none; 142 | &:before { 143 | opacity: @slick-opacity-on-hover; 144 | } 145 | } 146 | &:before { 147 | position: absolute; 148 | top: 0; 149 | left: 0; 150 | content: @slick-dot-character; 151 | width: 20px; 152 | height: 20px; 153 | font-family: @slick-font-family; 154 | font-size: @slick-dot-size; 155 | line-height: 20px; 156 | text-align: center; 157 | color: @slick-dot-color; 158 | opacity: @slick-opacity-not-active; 159 | -webkit-font-smoothing: antialiased; 160 | -moz-osx-font-smoothing: grayscale; 161 | } 162 | } 163 | &.slick-active button:before { 164 | color: @slick-dot-color-active; 165 | opacity: @slick-opacity-default; 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /views/includes/sidebar.ejs: -------------------------------------------------------------------------------- 1 | 92 | -------------------------------------------------------------------------------- /views/story.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include( 4 | 'includes/head', 5 | { 6 | title: story.title, 7 | description: '❶❶✅ Đọc truyện tranh ' + story.title + ' bản dịch Full mới nhất, ảnh đẹp chất lượng cao tại ' + process.env.DOMAIN, 8 | seo_image: webAssets(story.avatar), 9 | showAds: story.adsense 10 | } 11 | ); 12 | -%> 13 | 14 | 15 | 16 |
17 |
18 | <%- include('includes/header') -%> 19 |
20 |
21 | <%- include('schema/story', { story }) -%> 22 | <%- include('story/profile') -%> 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |

33 | 34 | VĂN ÁN 35 |

36 |
37 |
38 |
39 |

40 | <%= story.content %> 41 |

42 |
43 |
44 |
45 |

46 | 47 | CHƯƠNG MỤC 48 |

49 | 50 | 51 | 52 |
53 | <% if (story.adsense) {%> 54 | <%- include('includes/ads/block') -%> 55 | <%} %> 56 | <% if (chapters.length) { %> 57 | <%- include('story/list-chapter') -%> 58 | <% } else { %> 59 |
60 | 61 |
62 | <% } %> 63 |
64 | 65 |
66 |

THẢO LUẬN

67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | 76 |
77 |
78 | <%- include('includes/sidebar') -%> 79 |
80 |
81 |
82 |
83 |
84 |
85 | <%- include('includes/footer') -%> 86 |
87 | 88 | <%- include('includes/popup-session') -%> 89 |
90 | 91 | 92 |
93 | 94 |
95 | 96 | <%- include('includes/script-core') -%> 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /public/js/slick/slick-theme.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | // Default Variables 4 | 5 | // Slick icon entity codes outputs the following 6 | // "\2190" outputs ascii character "←" 7 | // "\2192" outputs ascii character "→" 8 | // "\2022" outputs ascii character "•" 9 | 10 | $slick-font-path: "./fonts/" !default; 11 | $slick-font-family: "slick" !default; 12 | $slick-loader-path: "./" !default; 13 | $slick-arrow-color: white !default; 14 | $slick-dot-color: black !default; 15 | $slick-dot-color-active: $slick-dot-color !default; 16 | $slick-prev-character: "\2190" !default; 17 | $slick-next-character: "\2192" !default; 18 | $slick-dot-character: "\2022" !default; 19 | $slick-dot-size: 6px !default; 20 | $slick-opacity-default: 0.75 !default; 21 | $slick-opacity-on-hover: 1 !default; 22 | $slick-opacity-not-active: 0.25 !default; 23 | 24 | @function slick-image-url($url) { 25 | @if function-exists(image-url) { 26 | @return image-url($url); 27 | } 28 | @else { 29 | @return url($slick-loader-path + $url); 30 | } 31 | } 32 | 33 | @function slick-font-url($url) { 34 | @if function-exists(font-url) { 35 | @return font-url($url); 36 | } 37 | @else { 38 | @return url($slick-font-path + $url); 39 | } 40 | } 41 | 42 | /* Slider */ 43 | 44 | .slick-list { 45 | .slick-loading & { 46 | background: #fff slick-image-url("ajax-loader.gif") center center no-repeat; 47 | } 48 | } 49 | 50 | /* Icons */ 51 | @if $slick-font-family == "slick" { 52 | @font-face { 53 | font-family: "slick"; 54 | src: slick-font-url("slick.eot"); 55 | src: slick-font-url("slick.eot?#iefix") format("embedded-opentype"), slick-font-url("slick.woff") format("woff"), slick-font-url("slick.ttf") format("truetype"), slick-font-url("slick.svg#slick") format("svg"); 56 | font-weight: normal; 57 | font-style: normal; 58 | } 59 | } 60 | 61 | /* Arrows */ 62 | 63 | .slick-prev, 64 | .slick-next { 65 | position: absolute; 66 | display: block; 67 | height: 20px; 68 | width: 20px; 69 | line-height: 0px; 70 | font-size: 0px; 71 | cursor: pointer; 72 | background: transparent; 73 | color: transparent; 74 | top: 50%; 75 | -webkit-transform: translate(0, -50%); 76 | -ms-transform: translate(0, -50%); 77 | transform: translate(0, -50%); 78 | padding: 0; 79 | border: none; 80 | outline: none; 81 | &:hover, &:focus { 82 | outline: none; 83 | background: transparent; 84 | color: transparent; 85 | &:before { 86 | opacity: $slick-opacity-on-hover; 87 | } 88 | } 89 | &.slick-disabled:before { 90 | opacity: $slick-opacity-not-active; 91 | } 92 | &:before { 93 | font-family: $slick-font-family; 94 | font-size: 20px; 95 | line-height: 1; 96 | color: $slick-arrow-color; 97 | opacity: $slick-opacity-default; 98 | -webkit-font-smoothing: antialiased; 99 | -moz-osx-font-smoothing: grayscale; 100 | } 101 | } 102 | 103 | .slick-prev { 104 | left: -25px; 105 | [dir="rtl"] & { 106 | left: auto; 107 | right: -25px; 108 | } 109 | &:before { 110 | content: $slick-prev-character; 111 | [dir="rtl"] & { 112 | content: $slick-next-character; 113 | } 114 | } 115 | } 116 | 117 | .slick-next { 118 | right: -25px; 119 | [dir="rtl"] & { 120 | left: -25px; 121 | right: auto; 122 | } 123 | &:before { 124 | content: $slick-next-character; 125 | [dir="rtl"] & { 126 | content: $slick-prev-character; 127 | } 128 | } 129 | } 130 | 131 | /* Dots */ 132 | 133 | .slick-dotted.slick-slider { 134 | margin-bottom: 30px; 135 | } 136 | 137 | .slick-dots { 138 | position: absolute; 139 | bottom: -25px; 140 | list-style: none; 141 | display: block; 142 | text-align: center; 143 | padding: 0; 144 | margin: 0; 145 | width: 100%; 146 | li { 147 | position: relative; 148 | display: inline-block; 149 | height: 20px; 150 | width: 20px; 151 | margin: 0 5px; 152 | padding: 0; 153 | cursor: pointer; 154 | button { 155 | border: 0; 156 | background: transparent; 157 | display: block; 158 | height: 20px; 159 | width: 20px; 160 | outline: none; 161 | line-height: 0px; 162 | font-size: 0px; 163 | color: transparent; 164 | padding: 5px; 165 | cursor: pointer; 166 | &:hover, &:focus { 167 | outline: none; 168 | &:before { 169 | opacity: $slick-opacity-on-hover; 170 | } 171 | } 172 | &:before { 173 | position: absolute; 174 | top: 0; 175 | left: 0; 176 | content: $slick-dot-character; 177 | width: 20px; 178 | height: 20px; 179 | font-family: $slick-font-family; 180 | font-size: $slick-dot-size; 181 | line-height: 20px; 182 | text-align: center; 183 | color: $slick-dot-color; 184 | opacity: $slick-opacity-not-active; 185 | -webkit-font-smoothing: antialiased; 186 | -moz-osx-font-smoothing: grayscale; 187 | } 188 | } 189 | &.slick-active button:before { 190 | color: $slick-dot-color-active; 191 | opacity: $slick-opacity-default; 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /views/stories.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include( 4 | 'includes/head', 5 | { 6 | title: 'Tất Cả Truyện' , 7 | description: '❶❶✅ Đọc truyện tranh bản dịch Full mới nhất, ảnh đẹp chất lượng cao tại ' + process.env.DOMAIN, 8 | seo_image: process.env.DOMAIN + '/images/share.jpg', 9 | showAds: true 10 | } 11 | ); 12 | -%> 13 | 14 | 15 | 16 |
17 |
18 | 19 | <%- include('includes/header') -%> 20 | 21 |
22 | <%- include('includes/genres-block', { breadcrumb: { link: `/truyen-tranh`, name: 'Truyện Tranh' } }) -%> 23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 | 31 | 32 |
33 |
34 |
35 |
36 |
37 |

38 | Truyện Tranh 39 |

40 |
41 |
42 |
43 | <%- include('includes/ads/block') -%> 44 |
45 |
46 |
47 | 48 |
49 | 50 | <%= count %> kết quả 51 |
52 | 53 |
54 | Sắp Xếp Theo 55 | 72 |
73 |
74 |
75 | 76 |
77 |
78 | <%- include('includes/loop-content', { stories }) -%> 79 | 80 |
81 |
82 |
83 | 84 |
85 | 86 |
87 |
88 | <%- include('includes/sidebar') -%> 89 |
90 |
91 |
92 |
93 |
94 | <%- include('includes/footer') -%> 95 |
96 | 97 | <%- include('includes/popup-session') -%> 98 |
99 | 100 |
101 | 102 |
103 | 104 | 105 | <%- include('includes/script-core') -%> 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /views/includes/loop-infinite.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
9 |
10 |
11 |
12 |
13 | 14 | wallhaven-550105 24 | 25 |
26 |
27 |
28 |

29 | 30 | {{ item.story.title }} 31 | 32 |

33 |
34 |
35 |
36 | 37 | 38 | 39 | 40 | 41 | 4.3 42 |
43 |
44 |
45 |
50 | 51 | 55 | {{ chapter.name }} 56 | 57 | 58 | 59 | {{ moment(chapter.createdAt).locale('vi').format('ll') }} 60 | 61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | 87 |
88 | -------------------------------------------------------------------------------- /public/lib/fontawesome/web-fonts-with-css/css/svg-with-js.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | .svg-inline--fa,svg:not(:root).svg-inline--fa{overflow:visible}.svg-inline--fa{display:inline-block;font-size:inherit;height:1em;vertical-align:-.125em}.svg-inline--fa.fa-lg{vertical-align:-.225em}.svg-inline--fa.fa-w-1{width:.0625em}.svg-inline--fa.fa-w-2{width:.125em}.svg-inline--fa.fa-w-3{width:.1875em}.svg-inline--fa.fa-w-4{width:.25em}.svg-inline--fa.fa-w-5{width:.3125em}.svg-inline--fa.fa-w-6{width:.375em}.svg-inline--fa.fa-w-7{width:.4375em}.svg-inline--fa.fa-w-8{width:.5em}.svg-inline--fa.fa-w-9{width:.5625em}.svg-inline--fa.fa-w-10{width:.625em}.svg-inline--fa.fa-w-11{width:.6875em}.svg-inline--fa.fa-w-12{width:.75em}.svg-inline--fa.fa-w-13{width:.8125em}.svg-inline--fa.fa-w-14{width:.875em}.svg-inline--fa.fa-w-15{width:.9375em}.svg-inline--fa.fa-w-16{width:1em}.svg-inline--fa.fa-w-17{width:1.0625em}.svg-inline--fa.fa-w-18{width:1.125em}.svg-inline--fa.fa-w-19{width:1.1875em}.svg-inline--fa.fa-w-20{width:1.25em}.svg-inline--fa.fa-pull-left{margin-right:.3em;width:auto}.svg-inline--fa.fa-pull-right{margin-left:.3em;width:auto}.svg-inline--fa.fa-border{height:1.5em}.svg-inline--fa.fa-li{width:2em}.svg-inline--fa.fa-fw{width:1.25em}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers-text{left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-counter{background-color:#ff253a;border-radius:1em;-webkit-box-sizing:border-box;box-sizing:border-box;color:#fff;height:1.5em;line-height:1;max-width:5em;min-width:1.5em;overflow:hidden;padding:.25em;right:0;text-overflow:ellipsis;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-bottom-right{bottom:0;right:0;top:auto;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:bottom right;transform-origin:bottom right}.fa-layers-bottom-left{bottom:0;left:0;right:auto;top:auto;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:bottom left;transform-origin:bottom left}.fa-layers-top-right{right:0;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-top-left{left:0;right:auto;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top left;transform-origin:top left}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2.5em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.svg-inline--fa.fa-stack-1x{height:1em;width:1.25em}.svg-inline--fa.fa-stack-2x{height:2em;width:2.5em}.fa-inverse{color:#fff}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.svg-inline--fa .fa-primary{fill:var(--fa-primary-color,currentColor);opacity:1;opacity:var(--fa-primary-opacity,1)}.svg-inline--fa .fa-secondary{fill:var(--fa-secondary-color,currentColor)}.svg-inline--fa .fa-secondary,.svg-inline--fa.fa-swap-opacity .fa-primary{opacity:.4;opacity:var(--fa-secondary-opacity,.4)}.svg-inline--fa.fa-swap-opacity .fa-secondary{opacity:1;opacity:var(--fa-primary-opacity,1)}.svg-inline--fa mask .fa-primary,.svg-inline--fa mask .fa-secondary{fill:#000}.fad.fa-inverse{color:#fff} -------------------------------------------------------------------------------- /views/category.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include( 4 | 'includes/head', 5 | { 6 | title: category.name, 7 | description: '❶❶✅ Đọc truyện tranh thể loại '+ category.name +' bản dịch Full mới nhất, ảnh đẹp chất lượng cao tại ' + process.env.DOMAIN, 8 | seo_image: process.env.DOMAIN + '/images/share.jpg', 9 | showAds: true 10 | } 11 | ); 12 | -%> 13 | 14 | 15 | 16 |
17 |
18 | 19 | <%- include('includes/header') -%> 20 | 21 |
22 | <%- include('includes/genres-block', { breadcrumb: { link: `/the-loai/${category.slug}.${category._id}`, name: category.name } }) -%> 23 |
24 |
25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 |

36 | <%= category.name %> 37 |

38 |
39 |
40 |
41 | <%- include('includes/ads/block') -%> 42 |
43 |
44 |
45 | 46 |
47 | 48 | <%= count %> kết quả 49 |
50 | 51 |
52 | Sắp Xếp Theo 53 | 70 |
71 |
72 |
73 | 74 |
75 | <% if (count) { %> 76 |
77 | <%- include('includes/loop-content', { stories }) -%> 78 | 79 |
80 | <% } else { %> 81 |
82 | 83 |
84 | <% } %> 85 |
86 |
87 | 88 |
89 | 90 |
91 |
92 | <%- include('includes/sidebar') -%> 93 |
94 |
95 |
96 |
97 |
98 | <%- include('includes/footer') -%> 99 |
100 | 101 | <%- include('includes/popup-session') -%> 102 |
103 | 104 |
105 | 106 |
107 | 108 | 109 | <%- include('includes/script-core') -%> 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /views/search.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include( 4 | 'includes/head', 5 | { 6 | title: 'Tìm kiếm: ' + keyword , 7 | description: '❶❶✅ Đọc truyện tranh có tên '+ keyword +' bản dịch Full mới nhất, ảnh đẹp chất lượng cao tại ' + process.env.DOMAIN, 8 | seo_image: process.env.DOMAIN + '/images/share.jpg', 9 | showAds: true 10 | } 11 | ); 12 | -%> 13 | 14 | 15 | 16 |
17 |
18 | 19 | <%- include('includes/header') -%> 20 | 21 |
22 | <%- include('includes/genres-block', { breadcrumb: { link: `/tim-kiem?keyword=` + keyword, name: 'Tìm Kiếm' } }) -%> 23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 | 31 | 32 |
33 |
34 |
35 |
36 |
37 |

38 | Tìm Kiếm: <%= keyword %> 39 |

40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 | 48 |
49 | 50 | <%= count %> kết quả 51 |
52 | 53 |
54 | Sắp Xếp Theo 55 | 72 |
73 |
74 |
75 | 76 |
77 | <% if (count) { %> 78 |
79 | <%- include('includes/loop-content', { stories }) -%> 80 | 81 |
82 | <% } else { %> 83 |
84 | 85 |
86 | <% } %> 87 |
88 |
89 | 90 |
91 | 92 |
93 |
94 | <%- include('includes/sidebar') -%> 95 |
96 |
97 |
98 |
99 |
100 | <%- include('includes/footer') -%> 101 |
102 | 103 | <%- include('includes/popup-session') -%> 104 |
105 | 106 |
107 | 108 |
109 | 110 | 111 | <%- include('includes/script-core') -%> 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Madara 2 | 3 | > Project ComicToon use Nodejs, Vue(Single File Components) and Nuxt. 4 | 5 |

6 | 7 |

8 | 9 | ## Features 10 | - Nodejs, NOSQL, VueJS, Graphql 11 | - Bootstrap 4 + Font Awesome 5 12 | - Login, register, email verification and password reset 13 | - Manga Info: Genre, Tags, Rank 14 | - Unlimited Chapters & Volumes 15 | - Facebook Comment, Views Count 16 | - Ajax Load Image, Responsive, Canvas Render 17 | - Advanced Manga Search & Filter 18 | - User Settings 19 | - Front-end User Upload 20 | 21 | ## Setup Server 22 | - Reverse proxy: [Nginx](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-20-04) 23 | - Database: [Mongodb](https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-20-04) 24 | - [Nodejs](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-20-04) - Option 2 25 | 26 | 27 | ## .env 28 | > You need to set up .env first. Example: [.env.example](https://github.com/dnstylish/madara/blob/master/.env.example) 29 | 30 | ## Storage - [BunnyCDN](https://bunny.net/) 31 | BunnyCDN is the best CDN I know. See why: [Review](https://www.techradar.com/reviews/bunny-cdn) 32 | 33 | - ### Normal Pull Zone 34 | - Story avatar, chapter avatar, user avatar,.. 35 | - Googlebot can crawl 36 | - Can share to social media 37 | ``` 38 | BUNNY_STORAGE_NAME_2= 39 | BUNNY_STORAGE_SERVER_2= 40 | BUNNY_ACCESS_KEY_2= 41 | CDN_DOMAIN_2= 42 | ``` 43 | - ### Secure Pull Zone 44 | - Chapter content 45 | ``` 46 | BUNNY_SECURITY_KEY= 47 | SECURE_ENABLE= 48 | BUNNY_STORAGE_NAME= 49 | BUNNY_STORAGE_SERVER= 50 | BUNNY_ACCESS_KEY= 51 | CDN_DOMAIN= 52 | ``` 53 | - To use secure cdn, please set SECURE_ENABLE = 1 54 | - More: [How to sign URLs](https://support.bunny.net/hc/en-us/articles/360016055099-How-to-sign-URLs-for-BunnyCDN-Token-Authentication) 55 | ## Usage 56 | 57 | ``` 58 | # install npm 59 | npm run dev 60 | 61 | # install pm2 process manager 62 | npm install -g pm2 63 | 64 | # startup script 65 | pm2 startup 66 | 67 | # start process 68 | pm2 start 69 | 70 | # save process list 71 | pm2 save 72 | 73 | # list all processes 74 | pm2 l 75 | 76 | # stop process 77 | npm stop 78 | ``` 79 | 80 | ## .env 81 | > You need to set up .env first before run project 82 | 83 | ## Watermark 84 | To change watermark, please replace files in `modules/image/lib` 85 | 86 | ## Ads 87 | - Auto Ads `views/inclues/ads/auto.ejs` 88 | - Block `views/inclues/ads/block.ejs` 89 | > You can turn off ads for per manga, page,... 90 | 91 | ## Studio: [Madara - Studio](https://github.com/dnstylish/madara-studio) 92 | - Front end user upload 93 | - Show/Hide Ads by Story 94 | - Chapter thumbnail, chapter scheduler 95 | - Crop Image, Auto upload and remove in CDN when create/update/delete 96 | - Role, permission 97 | 98 | ## Scrapper Tool 99 | > Contact [dnstylish@gmail.com](mailto:someone@yoursite.com) if you need to help. 100 | 101 | ## Backup 102 | 103 | 104 | Maintaining even a small mongodb application in production requires regular backups of remotely stored data. MongoDB gives you [three ways](http://docs.mongodb.org/manual/core/backups/) to acomplish it. In this post I'm using `monogodump` command for creating a backup and `mongorestore` for recreating the data. 105 | The purpose of this writing is to provide a simple way of periodic database dumps from a remote server to a Dropbox cloud storage. 106 | 107 | > Remember that for using `mongodump` you have to have a `mongod` process running. 108 | 109 | ### Dumping a database 110 | 111 | Suppose that you want make a backup of your `books` database. 112 | 113 | To create a dump use `mongodump -d books -o ` which will result in a `book` folder containing bson files with all collections. 114 | For backup purposes we will compress it all to one file: 115 | `tar -zcvf books.tar.gz books/`. 116 | 117 | ### Dropbox uploader 118 | 119 | To send the backup of the database to your Drobpox cloud storage install [dropbox uploader script](https://github.com/andreafabrizi/Dropbox-Uploader) on the remote server: 120 | 121 | First, download the script: 122 | ``` 123 | curl "https://raw.githubusercontent.com/andreafabrizi/Dropbox-Uploader/master/dropbox_uploader.sh" -o dropbox_uploader.sh 124 | ``` 125 | Change the permissions: 126 | ``` 127 | chmod +x dropbox_uploader.sh 128 | ``` 129 | Launch the setup process: 130 | ``` 131 | ./dropbox_uploader.sh 132 | ``` 133 | The script will guide you through all necessary steps to connect the remote machine with your Dropbox account. During the installation process you will be asked to navigate to your Dropbox web page, create an application and providing app key and app secret for the download script. 134 | 135 | After a successfull installation you can try out the connection uploading the `books`: 136 | ``` 137 | /root/downloads/dropbox_uploader.sh upload books.tar.gz / 138 | ``` 139 | 140 | The ending slash means that the file will be uploaded to the root directory of your Dropbox application. 141 | 142 | The complete script for creating an archive and uploading, let's name it `mongodb_upload.sh`: 143 | 144 | ```bash 145 | #!/usr/bin/env bash 146 | 147 | #Get current date 148 | NOW="$(date +'%d-%m-%Y_%H-%M')" 149 | 150 | # Settings: 151 | 152 | # Path to a temporary directory 153 | DIR=~/mongodb_dump/ 154 | 155 | # Path to the target dropbox directory 156 | TARGET_DIR=/ 157 | 158 | # Path do dropbox_uploader.sh file 159 | UPLOADER_SCRIPT=/root/scripts/dropbox_uploader.sh 160 | 161 | # Name of the database 162 | DB_NAME=books 163 | 164 | function mongodb_dump 165 | { 166 | # Name of the compressed file 167 | FILE="${DIR}${DB_NAME}_${NOW}.tar.gz" 168 | 169 | # Dump the database 170 | mongodump -d $DB_NAME -o $DIR 171 | 172 | # Compress 173 | tar -zcvf $FILE $DIR$DB_NAME 174 | 175 | # Remove the temporary database dump directory 176 | rm -fr $DB_NAME 177 | 178 | # Upload the file 179 | $UPLOADER_SCRIPT upload $FILE $TARGET_DIR 180 | 181 | # Remove the file 182 | rm $FILE 183 | } 184 | 185 | mongodb_dump 186 | ``` 187 | 188 | After running the script navigate to your Dropbox `Applications` folder and look for a folder named after the application you created during the installation process. The `books.tar.gz` file should be there already. 189 | 190 | ### Setting a cronjob 191 | 192 | You can have the script executed periodically by seting a cron job. To edit the crontab file responsible for registering new cron tasks run: `crontab -e`. 193 | To perform an action at 01.45 am add this line: 194 | `45 01 * * * /mongo_upload.sh` 195 | Save the file and check the list of your cron tasks: `crontab -l` 196 | More about crontab: http://v1.corenominal.org/howto-setup-a-crontab-file/ 197 | 198 | ### Restoring a backup 199 | To restore the data uncompress the file and run: 200 | `mongorestore --drop -d ` 201 | 202 | ## License 203 | - Email: contact@guen.dev 204 | - Designed by Mangabooth 205 | -------------------------------------------------------------------------------- /views/chapter.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- include( 4 | 'includes/head', 5 | { 6 | title: chapter.name + ' | ' +story.title, 7 | description: '❶❶✅ Đọc truyện tranh ' + story.title + ' ' + chapter.name + ' bản dịch Full mới nhất, ảnh đẹp chất lượng cao tại ' + process.env.DOMAIN, 8 | seo_image: webAssets(story.avatar), 9 | showAds: story.adsense 10 | } 11 | ); 12 | -%> 13 | 14 | 15 | 16 |
17 |
18 | 19 | <%- include('includes/header') -%> 20 |
21 |
22 |
23 |
24 |
25 | 86 |
87 |
88 |
89 |
90 |
91 | <%- include('includes/footer') -%> 92 |
93 | 94 | <%- include('includes/popup-session') -%> 95 |
96 | 97 |
98 | 99 |
100 | 101 | <%- include('includes/script-core') -%> 102 | 103 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /public/js/lazysizes/lazysizes.min.js: -------------------------------------------------------------------------------- 1 | /*! lazysizes - v5.3.2 */ 2 | 3 | !function(e){var t=function(u,D,f){"use strict";var k,H;if(function(){var e;var t={lazyClass:"lazyload",loadedClass:"lazyloaded",loadingClass:"lazyloading",preloadClass:"lazypreload",errorClass:"lazyerror",autosizesClass:"lazyautosizes",fastLoadedClass:"ls-is-cached",iframeLoadMode:0,srcAttr:"data-src",srcsetAttr:"data-srcset",sizesAttr:"data-sizes",minSize:40,customMedia:{},init:true,expFactor:1.5,hFac:.8,loadMode:2,loadHidden:true,ricTimeout:0,throttleDelay:125};H=u.lazySizesConfig||u.lazysizesConfig||{};for(e in t){if(!(e in H)){H[e]=t[e]}}}(),!D||!D.getElementsByClassName){return{init:function(){},cfg:H,noSupport:true}}var O=D.documentElement,i=u.HTMLPictureElement,P="addEventListener",$="getAttribute",q=u[P].bind(u),I=u.setTimeout,U=u.requestAnimationFrame||I,o=u.requestIdleCallback,j=/^picture$/i,r=["load","error","lazyincluded","_lazyloaded"],a={},G=Array.prototype.forEach,J=function(e,t){if(!a[t]){a[t]=new RegExp("(\\s|^)"+t+"(\\s|$)")}return a[t].test(e[$]("class")||"")&&a[t]},K=function(e,t){if(!J(e,t)){e.setAttribute("class",(e[$]("class")||"").trim()+" "+t)}},Q=function(e,t){var a;if(a=J(e,t)){e.setAttribute("class",(e[$]("class")||"").replace(a," "))}},V=function(t,a,e){var i=e?P:"removeEventListener";if(e){V(t,a)}r.forEach(function(e){t[i](e,a)})},X=function(e,t,a,i,r){var n=D.createEvent("Event");if(!a){a={}}a.instance=k;n.initEvent(t,!i,!r);n.detail=a;e.dispatchEvent(n);return n},Y=function(e,t){var a;if(!i&&(a=u.picturefill||H.pf)){if(t&&t.src&&!e[$]("srcset")){e.setAttribute("srcset",t.src)}a({reevaluate:true,elements:[e]})}else if(t&&t.src){e.src=t.src}},Z=function(e,t){return(getComputedStyle(e,null)||{})[t]},s=function(e,t,a){a=a||e.offsetWidth;while(a49?function(){o(t,{timeout:n});if(n!==H.ricTimeout){n=H.ricTimeout}}:te(function(){I(t)},true);return function(e){var t;if(e=e===true){n=33}if(a){return}a=true;t=r-(f.now()-i);if(t<0){t=0}if(e||t<9){s()}else{I(s,t)}}},ie=function(e){var t,a;var i=99;var r=function(){t=null;e()};var n=function(){var e=f.now()-a;if(e0;if(r&&Z(i,"overflow")!="visible"){a=i.getBoundingClientRect();r=C>a.left&&pa.top-1&&g500&&O.clientWidth>500?500:370:H.expand;k._defEx=u;f=u*H.expFactor;c=H.hFac;A=null;if(w2&&h>2&&!D.hidden){w=f;N=0}else if(h>1&&N>1&&M<6){w=u}else{w=_}}if(l!==n){y=innerWidth+n*c;z=innerHeight+n;s=n*-1;l=n}a=d[t].getBoundingClientRect();if((b=a.bottom)>=s&&(g=a.top)<=z&&(C=a.right)>=s*c&&(p=a.left)<=y&&(b||C||p||g)&&(H.loadHidden||x(d[t]))&&(m&&M<3&&!o&&(h<3||N<4)||W(d[t],n))){R(d[t]);r=true;if(M>9){break}}else if(!r&&m&&!i&&M<4&&N<4&&h>2&&(v[0]||H.preloadAfterLoad)&&(v[0]||!o&&(b||C||p||g||d[t][$](H.sizesAttr)!="auto"))){i=v[0]||d[t]}}if(i&&!r){R(i)}}};var a=ae(t);var S=function(e){var t=e.target;if(t._lazyCache){delete t._lazyCache;return}L(e);K(t,H.loadedClass);Q(t,H.loadingClass);V(t,B);X(t,"lazyloaded")};var i=te(S);var B=function(e){i({target:e.target})};var T=function(e,t){var a=e.getAttribute("data-load-mode")||H.iframeLoadMode;if(a==0){e.contentWindow.location.replace(t)}else if(a==1){e.src=t}};var F=function(e){var t;var a=e[$](H.srcsetAttr);if(t=H.customMedia[e[$]("data-media")||e[$]("media")]){e.setAttribute("media",t)}if(a){e.setAttribute("srcset",a)}};var s=te(function(t,e,a,i,r){var n,s,o,l,u,f;if(!(u=X(t,"lazybeforeunveil",e)).defaultPrevented){if(i){if(a){K(t,H.autosizesClass)}else{t.setAttribute("sizes",i)}}s=t[$](H.srcsetAttr);n=t[$](H.srcAttr);if(r){o=t.parentNode;l=o&&j.test(o.nodeName||"")}f=e.firesLoad||"src"in t&&(s||n||l);u={target:t};K(t,H.loadingClass);if(f){clearTimeout(c);c=I(L,2500);V(t,B,true)}if(l){G.call(o.getElementsByTagName("source"),F)}if(s){t.setAttribute("srcset",s)}else if(n&&!l){if(d.test(t.nodeName)){T(t,n)}else{t.src=n}}if(r&&(s||l)){Y(t,{src:n})}}if(t._lazyRace){delete t._lazyRace}Q(t,H.lazyClass);ee(function(){var e=t.complete&&t.naturalWidth>1;if(!f||e){if(e){K(t,H.fastLoadedClass)}S(u);t._lazyCache=true;I(function(){if("_lazyCache"in t){delete t._lazyCache}},9)}if(t.loading=="lazy"){M--}},true)});var R=function(e){if(e._lazyRace){return}var t;var a=n.test(e.nodeName);var i=a&&(e[$](H.sizesAttr)||e[$]("sizes"));var r=i=="auto";if((r||!m)&&a&&(e[$]("src")||e.srcset)&&!e.complete&&!J(e,H.errorClass)&&J(e,H.lazyClass)){return}t=X(e,"lazyunveilread").detail;if(r){re.updateElem(e,true,e.offsetWidth)}e._lazyRace=true;M++;s(e,t,r,i,a)};var r=ie(function(){H.loadMode=3;a()});var o=function(){if(H.loadMode==3){H.loadMode=2}r()};var l=function(){if(m){return}if(f.now()-e<999){I(l,999);return}m=true;H.loadMode=3;a();q("scroll",o,true)};return{_:function(){e=f.now();k.elements=D.getElementsByClassName(H.lazyClass);v=D.getElementsByClassName(H.lazyClass+" "+H.preloadClass);q("scroll",a,true);q("resize",a,true);q("pageshow",function(e){if(e.persisted){var t=D.querySelectorAll("."+H.loadingClass);if(t.length&&t.forEach){U(function(){t.forEach(function(e){if(e.complete){R(e)}})})}}});if(u.MutationObserver){new MutationObserver(a).observe(O,{childList:true,subtree:true,attributes:true})}else{O[P]("DOMNodeInserted",a,true);O[P]("DOMAttrModified",a,true);setInterval(a,999)}q("hashchange",a,true);["focus","mouseover","click","load","transitionend","animationend"].forEach(function(e){D[P](e,a,true)});if(/d$|^c/.test(D.readyState)){l()}else{q("load",l);D[P]("DOMContentLoaded",a);I(l,2e4)}if(k.elements.length){t();ee._lsFlush()}else{a()}},checkElems:a,unveil:R,_aLSL:o}}(),re=function(){var a;var n=te(function(e,t,a,i){var r,n,s;e._lazysizesWidth=i;i+="px";e.setAttribute("sizes",i);if(j.test(t.nodeName||"")){r=t.getElementsByTagName("source");for(n=0,s=r.length;n 2 |
3 |
4 | <% if(slider.length) {%> 5 |
6 |
7 |
8 |
9 | 34 |
35 |
36 |
37 |
38 | <%} %> 39 | 104 |
105 |
106 | 107 | -------------------------------------------------------------------------------- /public/js/770.app.min.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkserver=self.webpackChunkserver||[]).push([[770],{9770:(a,t,s)=>{s.r(t),s.d(t,{default:()=>r});var o=s(630);const e={name:"AccountSetting",props:["user"],data(){return{userData:JSON.parse(this.user),form:{name:"",email:"",avatar:"",password:"",currentPass:"",rePass:""},upload:{avatar:""},isLoading:!1,isLoadingImage:!1,showCrop:!1}},methods:{async updateInfo(a,t){this.isLoading=!0;try{const{data:{userSettings:s}}=await this.$apollo.mutate({mutation:o.t0,variables:{key:a,value:t}});this.userData=Object.assign({},this.userData,s)}catch(a){}this.isLoading=!1},async updatePassword(){this.isLoading=!0;try{await this.$apollo.mutate({mutation:o.o4,variables:{oldPass:this.form.currentPass,newPass:this.form.password}})}catch(a){}this.isLoading=!1},openCropImage(a){this.showCrop=!1,this.upload.avatar=URL.createObjectURL(a.target.files[0]),window.$("#crop-avatar-modal").modal("show"),setTimeout((()=>{this.showCrop=!0}),400)},cropAvatar(){this.$refs.cropper.getCroppedCanvas().toBlob((a=>{if(!a)return!1;{this.isLoadingImage=!0;const t=new FormData;t.append("image",a),t.append("pathName",`users/${this.userData._id}/avatar`),t.append("type","user-avatar"),this.$http.post("/upload/single",t).then((({data:a})=>{this.form.avatar=a.data,window.$("#crop-avatar-modal").modal("hide")})).catch().finally((()=>{this.isLoadingImage=!1}))}}))}}},r=(0,s(1900).Z)(e,(function(){var a=this,t=a.$createElement,s=a._self._c||t;return s("div",{attrs:{id:"form-account-settings"}},[s("div",{staticClass:"tab-group-item"},[s("div",{staticClass:"tab-item"},[s("div",{staticClass:"choose-avatar"},[s("div",{staticClass:"c-user-avatar"},[s("img",{staticClass:"avatar avatar-195 photo",attrs:{alt:"",src:a.form.avatar||a.userData.avatar,height:"195",width:"195"}})])]),a._v(" "),s("div",{staticClass:"form form-choose-avatar"},[s("div",{staticClass:"select-flie"},[a._v("\n Nhấp để tải lên\n "),s("label",{staticClass:"select-avata"},[s("input",{attrs:{type:"file",name:"wp-manga-user-avatar"},on:{change:function(t){return a.openCropImage(t)}}}),a._v(" "),s("span",{staticClass:"file-name"})]),a._v(" "),s("input",{attrs:{id:"wp-manga-upload-avatar",type:"submit",value:"Cập Nhật",name:"wp-manga-upload-avatar",disabled:!a.form.avatar||a.isLoading},on:{click:function(t){return t.preventDefault(),a.updateInfo("avatar",a.form.avatar)}}})])])]),a._v(" "),s("div",{staticClass:"tab-item"},[a._m(0),a._v(" "),s("div",{staticClass:"form-group row"},[s("label",{staticClass:"col-md-3"},[a._v("Tên Hiện Tại")]),a._v(" "),s("div",{staticClass:"col-md-9"},[s("span",{staticClass:"show"},[a._v(a._s(a.userData.name))])])]),a._v(" "),s("div",{staticClass:"form-group row"},[s("label",{staticClass:"col-md-3"},[a._v("Tên Mới")]),a._v(" "),s("div",{staticClass:"col-md-9"},[s("input",{directives:[{name:"model",rawName:"v-model",value:a.form.name,expression:"form.name"}],staticClass:"form-control",attrs:{autocomplete:"off",type:"text",name:"user-new-name"},domProps:{value:a.form.name},on:{input:function(t){t.target.composing||a.$set(a.form,"name",t.target.value)}}})])]),a._v(" "),s("div",{staticClass:"form-group row"},[s("label",{staticClass:"col-md-3",attrs:{for:"name-input-submit"}}),a._v(" "),s("div",{staticClass:"col-md-9"},[s("input",{staticClass:"form-control",attrs:{id:"name-input-submit",disabled:!a.form.name||a.isLoading,type:"submit",value:"Xác Nhận",name:"account-form-submit"},on:{click:function(t){return t.preventDefault(),a.updateInfo("name",a.form.name)}}})])])]),a._v(" "),s("div",{staticClass:"tab-item"},[a._m(1),a._v(" "),s("div",{staticClass:"form-group row"},[s("label",{staticClass:"col-md-3"},[a._v("Email Hiện Tại")]),a._v(" "),s("div",{staticClass:"col-md-9"},[s("span",{staticClass:"show"},[a._v(a._s(a.userData.email))])])]),a._v(" "),s("div",{staticClass:"form-group row"},[s("label",{staticClass:"col-md-3"},[a._v("Email Mới")]),a._v(" "),s("div",{staticClass:"col-md-9"},[s("input",{directives:[{name:"model",rawName:"v-model",value:a.form.email||a.isLoading,expression:"form.email || isLoading"}],staticClass:"form-control",attrs:{autocomplete:"off",type:"text",name:"user-new-name"},domProps:{value:a.form.email||a.isLoading},on:{input:function(t){t.target.composing||a.$set(a.form,"email || isLoading",t.target.value)}}})])]),a._v(" "),s("div",{staticClass:"form-group row"},[s("label",{staticClass:"col-md-3",attrs:{for:"name-input-submit"}}),a._v(" "),s("div",{staticClass:"col-md-9"},[s("input",{staticClass:"form-control",attrs:{disabled:!a.form.email||a.isLoading,autocomplete:"off",type:"submit",value:"Xác Nhận",name:"account-form-submit"},on:{click:function(t){return t.preventDefault(),a.updateInfo("email",a.form.email)}}})])])]),a._v(" "),s("div",{staticClass:"tab-item"},[a._m(2),a._v(" "),s("div",{staticClass:"form-group row"},[s("label",{staticClass:"col-md-3",attrs:{for:"currrent-password-input"}},[a._v("Mật khẩu hiện tại")]),a._v(" "),s("div",{staticClass:"col-md-9"},[s("input",{directives:[{name:"model",rawName:"v-model",value:a.form.currentPass,expression:"form.currentPass"}],staticClass:"form-control",attrs:{id:"currrent-password-input",type:"password",value:"",name:"user-current-password"},domProps:{value:a.form.currentPass},on:{input:function(t){t.target.composing||a.$set(a.form,"currentPass",t.target.value)}}})])]),a._v(" "),s("div",{staticClass:"form-group row"},[s("label",{staticClass:"col-md-3",attrs:{for:"new-password-input"}},[a._v(" Mật Khẩu Mới ")]),a._v(" "),s("div",{staticClass:"col-md-9"},[s("input",{directives:[{name:"model",rawName:"v-model",value:a.form.password,expression:"form.password"}],staticClass:"form-control",attrs:{id:"new-password-input",type:"password",value:"",name:"user-new-password"},domProps:{value:a.form.password},on:{input:function(t){t.target.composing||a.$set(a.form,"password",t.target.value)}}})])]),a._v(" "),s("div",{staticClass:"form-group row"},[s("label",{staticClass:"col-md-3",attrs:{for:"comfirm-password-input"}},[a._v(" Xác Nhận Mật Khẩu ")]),a._v(" "),s("div",{staticClass:"col-md-9"},[s("input",{directives:[{name:"model",rawName:"v-model",value:a.form.rePass,expression:"form.rePass"}],staticClass:"form-control",attrs:{id:"comfirm-password-input",type:"password",value:"",name:"user-new-password-confirm"},domProps:{value:a.form.rePass},on:{input:function(t){t.target.composing||a.$set(a.form,"rePass",t.target.value)}}}),a._v(" "),s("span",{attrs:{id:"password-strength"}})])]),a._v(" "),s("div",{staticClass:"form-group row"},[s("label",{staticClass:"col-md-3",attrs:{for:"password-input-submit"}}),a._v(" "),s("div",{staticClass:"col-md-9"},[s("input",{staticClass:"form-control",attrs:{id:"password-input-submit",type:"submit",value:"Thay Đổi",name:"account-form-submit",disabled:a.form.currentPass.length<6||a.form.password<6||a.form.password!==a.form.rePass||a.isLoading},on:{click:function(t){return t.preventDefault(),a.updatePassword()}}})])])])]),a._v(" "),s("div",{staticClass:"modal fade",attrs:{id:"crop-avatar-modal",tabindex:"-1",role:"dialog"}},[s("div",{staticClass:"modal-dialog",attrs:{role:"document"}},[s("div",{staticClass:"modal-content"},[a._m(3),a._v(" "),s("div",{staticClass:"modal-body"},[s("h3",[a._v("Cắt Ảnh Của Bạn")]),a._v(" "),s("div",{attrs:{id:"crop-avatar"}},[a.showCrop?s("vue-cropper",{ref:"cropper",attrs:{id:"cropImage",src:a.upload.avatar,"output-type":"jpg","crop-box-resizable":!1,"toggle-drag-mode-on-dblclick":!1,"drag-mode":"move","aspect-ratio":1,"view-mode":1,"crop-box-movable":!1}}):a._e()],1),a._v(" "),s("div",{staticClass:"list-button d-flex justify-content-center mt-3"},[s("div",{staticClass:"crop-butotn-action",on:{click:function(t){return a.$refs.cropper.rotate(-90)}}},[s("i",{staticClass:"fas fa-undo"})]),a._v(" "),s("div",{staticClass:"crop-butotn-action",on:{click:function(t){return a.$refs.cropper.relativeZoom(-.2)}}},[s("i",{staticClass:"fas fa-search-minus"})]),a._v(" "),s("div",{staticClass:"crop-butotn-action",on:{click:function(t){return a.$refs.cropper.relativeZoom(.2)}}},[s("i",{staticClass:"fas fa-search-plus"})]),a._v(" "),s("div",{staticClass:"crop-butotn-action",on:{click:function(t){return a.$refs.cropper.rotate(90)}}},[s("i",{staticClass:"fas fa-undo"})])])]),a._v(" "),s("div",{staticClass:"modal-footer pb-2"},[s("input",{staticClass:"crop-button",attrs:{type:"submit",value:"Cắt Ảnh",name:"wp-manga-upload-avatar",disabled:a.isLoadingImage},on:{click:function(t){return a.cropAvatar()}}})])])])])])}),[function(){var a=this,t=a.$createElement,s=a._self._c||t;return s("div",{staticClass:"settings-title"},[s("h3",[a._v("Tên Hiển Thị")])])},function(){var a=this,t=a.$createElement,s=a._self._c||t;return s("div",{staticClass:"settings-title"},[s("h3",[a._v("Email Đăng Ký")])])},function(){var a=this,t=a.$createElement,s=a._self._c||t;return s("div",{staticClass:"settings-title"},[s("h3",[a._v("Đổi Mật Khẩu")])])},function(){var a=this,t=a.$createElement,s=a._self._c||t;return s("div",{staticClass:"modal-header"},[s("button",{staticClass:"close",attrs:{type:"button","data-dismiss":"modal","aria-label":"Close"}},[s("span",{attrs:{"aria-hidden":"true"}},[a._v("×")])])])}],!1,null,null,null).exports}}]); --------------------------------------------------------------------------------