├── 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 |
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 |
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 |
43 |
44 |
45 |
49 |
50 |
51 | Chơi Game Cùng Ad Không ?
52 |
53 |
54 | Audition PC: lun_1m2
55 |
56 |
57 | PUBG MOBILE: 51363328630
58 | Tặng gà cho Ad nha...hihi
59 |
60 |
61 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/views/chapter/entry-header.ejs:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
26 |
27 | <%= story.title ;%>
28 | - <%= chapter.name ;%><%= chapter.nameExtend ? " - " + chapter.nameExtend : "" ;%>
29 |
30 |
31 |
32 | <%- include('chapter/entry-header') -%>
33 |
34 |
35 |
36 | <% if (story.adsense) { %>
37 | <%- include('includes/ads/block') -%>
38 | <% } %>
39 |
40 | <%= chapter.note || process.env.NOTE_DEFAULT?.toUpperCase() %>
41 |
42 |
43 | <% for (let i = 0; i < chapter.content.length; i++) { %>
44 |
45 |
51 |
52 | <% if (story.adsense && [15, 50, 75].includes(i)) { %>
53 | <%- include('includes/ads/block') -%>
54 | <% } %>
55 | <% } %>
56 |
57 | <% if (story.adsense) { %>
58 | <%- include('includes/ads/block') -%>
59 | <% } %>
60 |
61 |
62 |
63 | <%- include('chapter/entry-header') -%>
64 |
65 |
66 |
67 |
80 |
81 |
82 | <%- include('includes/sidebar') -%>
83 |
84 |
85 |
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