Browse Source

deck management

develop
Rob Colbert 2 years ago
parent
commit
b10921cbd4
  1. 1
      app/controllers/admin.js
  2. 113
      app/controllers/admin/deck.js
  3. 23
      app/models/deck.js
  4. 45
      app/models/playing-card.js
  5. 99
      app/services/deck.js
  6. 11
      app/views/admin/components/menu.pug
  7. 24
      app/views/admin/deck/editor.pug
  8. 6
      app/views/admin/deck/index.pug
  9. 4
      app/views/admin/deck/view.pug

1
app/controllers/admin.js

@ -45,6 +45,7 @@ class AdminController extends SiteController {
);
router.use('/content-report',await this.loadChild(path.join(__dirname, 'admin', 'content-report')));
router.use('/deck',await this.loadChild(path.join(__dirname, 'admin', 'deck')));
router.use('/host',await this.loadChild(path.join(__dirname, 'admin', 'host')));
router.use('/job-queue', await this.loadChild(path.join(__dirname, 'admin', 'job-queue')));
router.use('/log', await this.loadChild(path.join(__dirname, 'admin', 'log')));

113
app/controllers/admin/deck.js

@ -0,0 +1,113 @@
// admin/deck.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const DTP_COMPONENT_NAME = 'admin:deck';
const express = require('express');
// const mongoose = require('mongoose');
const { SiteController, SiteError } = require('../../../lib/site-lib');
class DeckController extends SiteController {
constructor (dtp) {
super(dtp, DTP_COMPONENT_NAME);
}
async start ( ) {
const router = express.Router();
router.use(async (req, res, next) => {
res.locals.currentView = 'admin';
res.locals.adminView = 'deck';
return next();
});
router.param('deckId', this.populateDeckId.bind(this));
router.post('/:deckId', this.postDeckUpdate.bind(this));
router.post('/', this.postDeckCreate.bind(this));
router.get('/create', this.getDeckCreateForm.bind(this));
router.get('/:deckId', this.getDeckView.bind(this));
router.get('/', this.getHomeView.bind(this));
return router;
}
async populateDeckId (req, res, next, deckId) {
const { deck: deckService } = this.dtp.services;
try {
res.locals.deck = await deckService.getById(deckId);
if (!res.locals.deck) {
throw new SiteError(404, 'Deck not found');
}
return next();
} catch (error) {
this.log.error('failed to populate deck', { deckId, error });
return next(error);
}
}
async postDeckUpdate (req, res, next) {
const { deck: deckService } = this.dtp.services;
try {
res.locals.deck = await deckService.update(res.locals.deck, req.body);
res.redirect(`/admin/deck/${res.locals.deck._id}`);
} catch (error) {
this.log.error('failed to update deck', {
deckId: res.locals.deck._id,
error,
});
return next(error);
}
}
async postDeckCreate (req, res, next) {
const { deck: deckService } = this.dtp.services;
try {
res.locals.deck = await deckService.create(req.user, req.body);
res.redirect(`/admin/deck/${res.locals.deck._id}`);
} catch (error) {
this.log.error('failed to create deck', {
deckId: res.locals.deck._id,
error,
});
return next(error);
}
}
async getDeckView (req, res) {
res.render('admin/deck/view');
}
async getDeckCreateForm (req, res) {
res.render('admin/deck/editor');
}
async getHomeView (req, res, next) {
const { deck: deckService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.decks = await deckService.getDecks(res.locals.pagination);
res.render('admin/deck/index');
} catch (error) {
this.log.error('failed to create deck', {
deckId: res.locals.deck._id,
error,
});
return next(error);
}
}
}
module.exports = async (dtp) => {
let controller = new DeckController(dtp);
return controller;
};

23
app/models/deck.js

@ -0,0 +1,23 @@
// deck.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const DeckSchema = new Schema({
created: { type: Date, required: true, index: -1 },
owner: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' },
name: { type: String, required: true },
name_lc: { type: String, required: true, lowercase: true, index: 1 },
description: { type: String, required: true },
tags: { type: [String] },
flags: {
isOfficial: { type: Boolean, default: true, required: true, index: 1 },
},
});
module.exports = mongoose.model('Deck', DeckSchema);

45
app/models/playing-card.js

@ -0,0 +1,45 @@
// playing-card.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const CARD_TYPE_LIST = ['question', 'answer'];
const MediaMetadataSchema = new Schema({
type: { type: String },
size: { type: Number },
bitRate: { type: Number },
duration: { type: Number },
video: {
width: { type: Number },
height: { type: Number },
fps: { type: Number },
},
});
const PlayingCardSchema = new Schema({
deck: { type: Schema.ObjectId, required: true, index: 1 },
type: { type: String, enum: CARD_TYPE_LIST, required: true, index: 1 },
content: {
text: { type: String },
image: { type: Schema.ObjectId, ref: 'Image' },
video: {
bucket: { type: String },
key: { type: String },
metadata: { type: MediaMetadataSchema },
},
},
options: {
blankCount: { type: Number, required: true, min: 0 },
},
flags: {
isOfficial: { type: Boolean, default: true, required: true, index: 1 },
},
});
module.exports = mongoose.model('PlayingCard', PlayingCardSchema);

99
app/services/deck.js

@ -0,0 +1,99 @@
// cache.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
// const PlayingCard = mongoose.model('PlayingCard');
const Deck = mongoose.model('Deck');
const striptags = require('striptags');
const { SiteService } = require('../../lib/site-lib');
class DeckService extends SiteService {
constructor (dtp) {
super(dtp, module.exports);
this.populateDeck = [
{
path: 'owner',
select: 'created username username_lc displayName picture',
},
];
}
async create (owner, deckDefinition) {
const NOW = new Date();
const deck = new Deck();
deck.created = NOW;
deck.owner = owner._id;
deck.name = striptags(deckDefinition.name.trim());
deck.name_lc = deck.name.toLowerCase();
deck.description = striptags(deckDefinition.description.trim());
if (deckDefinition.tags) {
deck.tags = deckDefinition.tags.split(',').map((tag) => tag.trim());
}
deck.flags = {
isOfficial: deckDefinition.isOfficial === 'on',
};
await deck.save();
return deck.toObject();
}
async update (deck, deckDefinition) {
deckDefinition.name = striptags(deckDefinition.name.trim());
deckDefinition.name_lc = deckDefinition.name.toLowerCase();
deckDefinition.description = striptags(deckDefinition.description.trim());
if (deckDefinition.tags) {
deckDefinition.tags = deckDefinition.tags.split(',').map((tag) => tag.trim());
}
await Deck.updateOne(
{ _id: deck._id },
{
$set: {
name: deckDefinition.name,
name_lc: deckDefinition.name_lc,
description: deckDefinition.description,
tags: deckDefinition.tags,
'flags.isOfficial': deckDefinition.isOfficial === 'on',
},
},
);
}
async getDecks (pagination) {
const decks = await Deck
.find()
.sort({ created: -1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populateDeck)
.lean();
return decks;
}
async getById (deckId) {
const deck = await Deck
.findOne({ _id: deckId })
.populate(this.populateDeck)
.lean();
return deck;
}
}
module.exports = {
slug: 'deck',
name: 'deck',
create: (dtp) => { return new DeckService(dtp); },
};

11
app/views/admin/components/menu.pug

@ -14,6 +14,17 @@ ul.uk-nav.uk-nav-default
li.uk-nav-divider
li(class={ 'uk-active': (adminView === 'deck') })
a(href="/admin/deck")
span.nav-item-icon
i.fas.fa-user
span.uk-margin-small-left Decks
li(class={ 'uk-active': (adminView === 'card') })
a(href="/admin/card")
span.nav-item-icon
i.fas.fa-user
span.uk-margin-small-left Cards
li(class={ 'uk-active': (adminView === 'user') })
a(href="/admin/user")
span.nav-item-icon

24
app/views/admin/deck/editor.pug

@ -0,0 +1,24 @@
extends ../layouts/main
block content
- var actionUrl = deck ? `/admin/deck/${deck._id}` : `/admin/deck`;
form(method="POST", action= actionUrl).uk-form
.uk-card.uk-card-default.uk-card-small
.uk-card-header
h1.uk-card-title Create New Deck
.uk-card-body
.uk-margin
label(for="name").uk-form-label Deck name
input(id="name", name="name", type="text", placeholder="Enter deck name").uk-input
.uk-margin
label(for="description").uk-form-label Description
textarea(id="description", name="description", rows="3", placeholder="Enter deck description").uk-textarea
.uk-margin
label(for="tags").uk-form-label Tags
input(id="tags", name="tags", type="text", placeholder="Enter search tags").uk-input
.uk-text-small.uk-text-muted Separate multiple tags with a comma
.uk-card-footer
button(type="submit").uk-button.uk-button-primary= deck ? 'Update Deck' : 'Create Deck'

6
app/views/admin/deck/index.pug

@ -0,0 +1,6 @@
extends ../layouts/main
block content
h1 Deck Index
pre= JSON.stringify(decks, null, 2)

4
app/views/admin/deck/view.pug

@ -0,0 +1,4 @@
extends ../layouts/main
block content
h1 Deck View
Loading…
Cancel
Save