Getting things ready for dnd

This commit is contained in:
BinarySandia04 2024-08-09 01:29:08 +02:00
parent 5875b69089
commit 168d683b13
26 changed files with 437 additions and 60 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@ -0,0 +1,12 @@
{
"id": "dnd-5e",
"title": "Dungeons & Dragons 5e",
"description": "Dungeons & Dragons Fifth edition game system support",
"authors": [
{
"name": "Aran Roig"
}
],
"version": "1.0.0",
"color": "#e92026"
}

View File

@ -6,10 +6,43 @@ import { RouterLink, RouterView } from 'vue-router'
import { GetUser, UserStatus, LoadUser } from '@/services/User.js' import { GetUser, UserStatus, LoadUser } from '@/services/User.js'
import { IsAdmin } from './services/User' import { IsAdmin } from './services/User'
import useEmitter from '@/services/Emitter';
const emitter = useEmitter();
import { DisplayToast, SetEmitter } from './services/Dragonroll'
import { ImportModule, GetModulesToLoad } from './services/Modules'
import { CreateWindow } from './services/Windows';
console.clear(); console.clear();
console.log("%cLoaded!!!", "color: #22ff22; font-size: 24px"); console.log("%cLoaded!!!", "color: #22ff22; font-size: 24px");
LoadUser(); LoadUser();
SetEmitter(emitter);
onMounted(() => {
async function preloadModules(){
const modules = GetModulesToLoad();
let moduleLoads = [];
modules.forEach(moduleName => {
moduleLoads.push(ImportModule(moduleName));
});
await Promise.all(moduleLoads);
DisplayToast('aqua', 'All modules loaded successfully');
if(GetUser()){
CreateWindow('main_menu')
// CreateWindow('test');
DisplayToast('green', 'Logged in successfully as ' + GetUser().username + '!', 3000)
return;
}
CreateWindow('login');
}
preloadModules();
})
</script> </script>
<template> <template>

View File

@ -27,14 +27,11 @@ import 'prismjs/components/prism-csharp';
import 'prismjs/components/prism-ruby'; import 'prismjs/components/prism-ruby';
import 'prismjs/components/prism-bash'; import 'prismjs/components/prism-bash';
VueMarkdownEditor.lang.use('es-Es', esEs); VueMarkdownEditor.lang.use('es-Es', esEs);
VueMarkdownEditor.use(vuepressTheme, { Prism }); VueMarkdownEditor.use(vuepressTheme, { Prism });
VueMarkdownEditor.use(createKatexPlugin()); VueMarkdownEditor.use(createKatexPlugin());
const app = createApp(App).use(VueMarkdownEditor); const app = createApp(App).use(VueMarkdownEditor);
app.config.globalProperties.emitter = emitter app.config.globalProperties.emitter = emitter
app.config.globalProperties.rollWindows = { app.config.globalProperties.rollWindows = {
login: reactive([]), login: reactive([]),
@ -47,3 +44,4 @@ app.config.globalProperties.rollWindows = {
app.use(router) app.use(router)
app.mount('#app') app.mount('#app')

View File

@ -5,6 +5,7 @@ import Api from '@/services/Api'
import { backendUrl } from './BackendURL'; import { backendUrl } from './BackendURL';
import { GetUser } from './User'; import { GetUser } from './User';
import { ExitGame } from './Game'; import { ExitGame } from './Game';
import { GetModule } from './Modules';
let emitter; let emitter;
@ -12,6 +13,8 @@ function SetEmitter(newEmitter){
emitter = newEmitter emitter = newEmitter
} }
let GetEmitter = () => emitter;
function DisplayToast(color, text, duration = 1000){ function DisplayToast(color, text, duration = 1000){
emitter.emit("toast", {color, text, duration}); emitter.emit("toast", {color, text, duration});
} }
@ -109,8 +112,13 @@ function GetPlayer(player_campaign){
if(index != -1) return players.value[index]; if(index != -1) return players.value[index];
} }
function GetSystem(){
if(currentCampaign) return GetModule(currentCampaign.system)
}
export { export {
SetEmitter, SetEmitter,
GetEmitter,
DisplayToast, DisplayToast,
@ -122,6 +130,7 @@ export {
GetClient, GetClient,
GetPlayerList, GetPlayerList,
GetPlayer, GetPlayer,
GetSystem,
GetChatRef, GetChatRef,
SendMessage SendMessage

View File

@ -0,0 +1,32 @@
let modulesToLoad = [
"dnd-5e"
]
let modules = [];
async function GetJson(url){
let obj = await (await fetch(url)).json();
return obj;
}
async function ImportModule(moduleFolder) {
let moduleInfo = await GetJson('/modules/' + moduleFolder + '/module.json');
modules.push(moduleInfo);
}
let GetModules = () => modules;
let GetModulesToLoad = () => modulesToLoad;
function GetModule(id){
let module = null;
modules.forEach(mod => {
if(mod.id == id) module = mod;
})
return module;
}
export {
ImportModule,
GetModules,
GetModule,
GetModulesToLoad,
}

View File

@ -4,6 +4,11 @@ import { Disconnect } from './Dragonroll';
const windows = ref([]) const windows = ref([])
const defValues = { const defValues = {
'test': {
id: "example",
title: "Example",
close: true
},
'login': { 'login': {
id: 'login', id: 'login',
title: 'Login', title: 'Login',
@ -74,6 +79,11 @@ const defValues = {
id: 'environment', id: 'environment',
title: 'Edit environment', title: 'Edit environment',
close: true close: true
},
'system_selector': {
id: 'system-selector',
title: "Select a game system",
close: true
} }
} }
@ -214,8 +224,8 @@ function ClearAll(){
} }
function ClearWindows(data){ function ClearWindows(data){
for (let i = 0; i < windows[data.type].value.length; i++) { for (let i = 0; i < windows.value.length; i++) {
ClearWindow(windows[data.type].value[i].id); ClearWindow(windows.value[i].id);
} }
// reload.value += 1; // reload.value += 1;
} }

View File

@ -2,9 +2,6 @@
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { RouterLink, RouterView } from 'vue-router' import { RouterLink, RouterView } from 'vue-router'
import useEmitter from '@/services/Emitter';
const emitter = useEmitter();
import WindowManager from '@/views/managers/WindowManager.vue' import WindowManager from '@/views/managers/WindowManager.vue'
import { GetUser } from '@/services/User' import { GetUser } from '@/services/User'
@ -13,17 +10,6 @@ import Toast from './partials/Toast.vue';
import { DisplayToast, SetEmitter } from '../services/Dragonroll'; import { DisplayToast, SetEmitter } from '../services/Dragonroll';
import GameManager from './managers/GameManager.vue'; import GameManager from './managers/GameManager.vue';
onMounted(() => {
SetEmitter(emitter);
if(GetUser()){
CreateWindow('main_menu')
DisplayToast('green', 'Logged in successfully as ' + GetUser().username + '!', 3000)
return;
}
CreateWindow('login');
}
);
</script> </script>

View File

@ -19,12 +19,14 @@ import ChatWindow from '../windows/game/ChatWindow.vue'
import DiceWindow from '../windows/game/DiceWindow.vue' import DiceWindow from '../windows/game/DiceWindow.vue'
import MapButtons from '../windows/dm/MapButtons.vue' import MapButtons from '../windows/dm/MapButtons.vue'
import EnvironmentWindow from '../windows/dm/EnvironmentWindow.vue' import EnvironmentWindow from '../windows/dm/EnvironmentWindow.vue'
import SystemSelectorWindow from '../windows/campaigns/SystemSelectorWindow.vue'
// Gestionem ventanas // Gestionem ventanas
const reload = ReloadRef(); const reload = ReloadRef();
const windows = Windows(); const windows = Windows();
const WindowMap = { const WindowMap = {
test: ExampleWindow,
login: LoginWindow, login: LoginWindow,
main_menu: MainMenuWindow, main_menu: MainMenuWindow,
register: RegisterWindow, register: RegisterWindow,
@ -37,7 +39,8 @@ const WindowMap = {
chat: ChatWindow, chat: ChatWindow,
dice_menu: DiceWindow, dice_menu: DiceWindow,
map_buttons: MapButtons, map_buttons: MapButtons,
environment: EnvironmentWindow environment: EnvironmentWindow,
system_selector: SystemSelectorWindow
}; };
</script> </script>

View File

@ -0,0 +1,68 @@
<script setup>
import { inject, onMounted, ref } from 'vue';
import { GetEmitter } from '../../services/Dragonroll';
const props = defineProps(['data']);
const data = props.data;
const title = ref("");
const image = ref(null);
const clearParent = inject('clearParent');
function Select(){
if(data.id){
GetEmitter().emit("select", data.id);
clearParent();
}
}
onMounted(() => {
title.value = data.title;
image.value.src = "modules/" + data.id + "/icon.png"
})
</script>
<template>
<div class="system-container" v-on:click="Select">
<img class="system-icon" ref="image">
<div class="system-content">
<span class="title">{{ title }}</span>
</div>
</div>
</template>
<style scoped lang="scss">
.system-content {
margin-left: 10px;
width: 100%;
text-align: left;
align-items: center;
display: flex;
}
.title {
font-weight: bold;
}
.system-container {
display: flex;
padding: 10px;
user-select: none;
transition: background-color .2s;
&:hover {
background-color: var(--color-background-softer);
}
}
.system-icon {
width: 32px;
margin-right: auto;
}
</style>

View File

@ -0,0 +1,75 @@
<script setup>
import { onMounted, onUpdated, provide, ref, watch } from 'vue';
import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Windows';
import { CreateChildWindow } from '../../services/Windows';
import { GetModules } from '../../services/Modules';
const selectedSystem = ref("");
const selectedImage = ref(null);
const systemTitle = ref("")
const props = defineProps(['windowId']);
let windowId = props.windowId;
function DisplaySystemSelector(){
CreateChildWindow(windowId, 'system_selector');
}
defineExpose({ selectedSystem });
watch(selectedSystem, () => {
let modules = GetModules();
let module = null;
modules.forEach(mod => {
if(mod.id == selectedSystem.value) module = mod;
});
if(module){
selectedImage.value.src = "modules/" + module.id + "/icon.png"
systemTitle.value = module.title;
}
});
</script>
<template>
<div class="system-selector" v-on:click="DisplaySystemSelector">
<span v-show="selectedSystem == ''" class="none">No game system selected</span>
<div v-show="selectedSystem != ''" class="yes">
<img ref="selectedImage" class="system-icon">
{{ systemTitle }}
</div>
</div>
</template>
<style scoped lang="scss">
.system-selector {
user-select: none;
margin-top: 5px;
margin-bottom: 5px;
padding: 14px;
font-size: 15px;
border-radius: 6px;
outline: none;
border: none;
transition: 300ms background-color;
background-color: var(--color-background-softer);
color: var(--color-text);
cursor: pointer;
}
.none {
color: var(--text-disabled)
}
.yes {
display: flex;
align-items: center;
font-weight: bold;
}
.system-icon {
width: 32px;
margin-right: auto;
}
</style>

View File

@ -8,7 +8,16 @@ const emitter = useEmitter();
const text = ref(""); const text = ref("");
const toast = ref(null); const toast = ref(null);
emitter.on('toast', data => { let toastQueue = [];
let displayingToast = false;
function DisplayToast(){
if(displayingToast) return;
if(toastQueue.length == 0) return;
displayingToast = true;
let data = toastQueue.pop();
text.value = data.text; text.value = data.text;
toast.value.classList.add(data.color); toast.value.classList.add(data.color);
@ -20,8 +29,15 @@ emitter.on('toast', data => {
toast.value.classList.remove("show"); toast.value.classList.remove("show");
toast.value.classList.remove("sliding"); toast.value.classList.remove("sliding");
toast.value.classList.remove(data.color); toast.value.classList.remove(data.color);
displayingToast = false;
DisplayToast();
}, 400); }, 400);
}, data.duration); }, data.duration);
}
emitter.on('toast', data => {
toastQueue.push(data);
DisplayToast();
}); });
</script> </script>

View File

@ -28,7 +28,6 @@ onMounted(() => {
<style scoped lang="scss"> <style scoped lang="scss">
.book-list { .book-list {
height: 100%;
background-color: var(--color-background); background-color: var(--color-background);
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, onUpdated, ref } from 'vue'; import { onMounted, onUpdated, ref, compile, render, h } from 'vue';
import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Windows'; import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Windows';
import WindowHandle from '@/views/partials/WindowHandle.vue'; import WindowHandle from '@/views/partials/WindowHandle.vue';
@ -10,6 +10,9 @@ const props = defineProps(['data']);
const data = props.data; const data = props.data;
let id = data.id; let id = data.id;
const test = ref(null)
onMounted(() => { onMounted(() => {
SetupHandle(id, handle); SetupHandle(id, handle);
SetSize(id, {x: 500, y: 380}); SetSize(id, {x: 500, y: 380});
@ -23,6 +26,7 @@ onMounted(() => {
<WindowHandle :window="id" ref="handle"></WindowHandle> <WindowHandle :window="id" ref="handle"></WindowHandle>
<!-- Body --> <!-- Body -->
<div ref="test"></div>
</div> </div>
</template> </template>
@ -30,29 +34,9 @@ onMounted(() => {
<style scoped> <style scoped>
.window-wrapper { .window-wrapper {
min-width: 700px;
min-height: 630px;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.splash-image {
width: 600px;
height: 250px;
}
.form-field {
padding: 10px;
display: flex;
align-items: left;
flex-direction: column;
justify-content: left;
width: 600px;
}
label {
text-align: left;
}
</style> </style>

View File

@ -7,6 +7,7 @@ import { CreateWindow, CreateChildWindow } from '../../../services/Windows';
import Api from '@/services/Api.js' import Api from '@/services/Api.js'
import CampaignEntry from '../../partials/CampaignEntry.vue'; import CampaignEntry from '../../partials/CampaignEntry.vue';
import { GetEmitter } from '../../../services/Dragonroll';
const handle = ref(null); const handle = ref(null);
@ -23,6 +24,8 @@ onMounted(() => {
ResetPosition(id, "center"); ResetPosition(id, "center");
RefreshCampaigns(); RefreshCampaigns();
GetEmitter().on('refresh_campaign', () => { RefreshCampaigns() });
}); });
function CreateCampaign(){ function CreateCampaign(){

View File

@ -10,6 +10,8 @@ import { ClearAll, ClearWindow, CreateWindow } from '../../../services/Windows';
import { LaunchGame } from '../../../services/Game'; import { LaunchGame } from '../../../services/Game';
import { AddSound } from '../../../services/Sound'; import { AddSound } from '../../../services/Sound';
import ChatComponent from '../../partials/ChatComponent.vue'; import ChatComponent from '../../partials/ChatComponent.vue';
import GameSystem from '@/views/partials/GameSystem.vue'
import { GetModule } from '../../../services/Modules';
const handle = ref(null); const handle = ref(null);
@ -18,6 +20,7 @@ const data = props.data;
const hide_start = ref(false); const hide_start = ref(false);
const hide_chat = ref(false); const hide_chat = ref(false);
const campaign_title = ref(null);
const container = ref(null); const container = ref(null);
@ -37,6 +40,8 @@ onMounted(() => {
hide_start.value = data.hide_start; hide_start.value = data.hide_start;
AddSound(container.value) AddSound(container.value)
campaign_title.value.style.backgroundColor = GetModule(data.campaign.system).color ? GetModule(data.campaign.system).color : "#1f1f1f";
}); });
function CopyCode(){ function CopyCode(){
@ -74,10 +79,10 @@ function Exit(){
</div> </div>
</div> </div>
<div class="campaign-preview-column center"> <div class="campaign-preview-column center">
<h1 class="campaign-title">{{ data.campaign.name }}</h1> <h1 class="campaign-title" ref="campaign_title">{{ data.campaign.name }}</h1>
<div class="campaign-main-container"> <div class="campaign-main-container">
<div class="campaign-main-container-scroll"> <div class="campaign-main-container-scroll">
<div class="">Dnd 5e</div> <GameSystem :data="GetModule(data.campaign.system)"></GameSystem>
<h2>Books</h2> <h2>Books</h2>
<CampaignBookList class="small-book-list"></CampaignBookList> <CampaignBookList class="small-book-list"></CampaignBookList>
</div> </div>
@ -98,7 +103,6 @@ function Exit(){
<style scoped lang="scss"> <style scoped lang="scss">
.small-book-list { .small-book-list {
height: 400px;
margin: 20px; margin: 20px;
overflow: auto; overflow: auto;
} }
@ -137,10 +141,10 @@ function Exit(){
height: 100%; height: 100%;
display: grid; display: grid;
grid-template-columns: 3fr 5fr 4fr; grid-template-columns: 2fr 4fr 3fr;
&.campaign-preview-compact { &.campaign-preview-compact {
grid-template-columns: 2fr 3fr; grid-template-columns: 2fr 4fr;
} }
} }
@ -173,4 +177,8 @@ h1, h2 {
font-family: MrEavesRemake; font-family: MrEavesRemake;
} }
h1 {
background-color: rgb(143, 39, 39);
}
</style> </style>

View File

@ -1,10 +1,13 @@
<script setup> <script setup>
import { onMounted, onUpdated, ref } from 'vue'; import { onMounted, onUpdated, ref, provide } from 'vue';
import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Windows'; import { SetupHandle, SetSize, SetPosition, ResetPosition, ClearWindow } from '@/services/Windows';
import WindowHandle from '@/views/partials/WindowHandle.vue'; import WindowHandle from '@/views/partials/WindowHandle.vue';
import ErrorMessage from '@/components/partials/ErrorMessage.vue'
import Api from '@/services/Api.js' import Api from '@/services/Api.js'
import SystemSelector from '../../partials/SystemSelector.vue';
import { GetEmitter } from '../../../services/Dragonroll';
const handle = ref(null); const handle = ref(null);
@ -12,20 +15,41 @@ const props = defineProps(['data']);
const data = props.data; const data = props.data;
const campaignName = ref(""); const campaignName = ref("");
const systemSelector = ref(null);
const errorMessage = ref("");
let id = data.id; let id = data.id;
let system = "";
onMounted(() => { onMounted(() => {
SetupHandle(id, handle); SetupHandle(id, handle);
SetSize(id, {x: 300, y: 150}); SetSize(id, {x: 300, y: 240});
ResetPosition(id, "center"); ResetPosition(id, "center");
GetEmitter().on('select', (system_id) => Select(system_id))
console.log(system);
}); });
function Select(system_id){
system = system_id;
try {
systemSelector.value.selectedSystem = system_id;
} catch {}
}
function NewCampaign(){ function NewCampaign(){
Api().post('/campaign/create', { Api().post('/campaign/create', {
name: campaignName.value name: campaignName.value,
system
}).then((response) => { }).then((response) => {
console.log(response); if(response.data.status == "error"){
errorMessage.value = response.data.msg;
} else {
ClearWindow(id);
GetEmitter().emit('refresh_campaign');
}
}); });
} }
</script> </script>
@ -40,10 +64,15 @@ function NewCampaign(){
<div class="form-field"> <div class="form-field">
<input id="username-field" type="text" placeholder="Enter campaign name..." name="campaignName" v-model="campaignName" autocomplete="off" > <input id="username-field" type="text" placeholder="Enter campaign name..." name="campaignName" v-model="campaignName" autocomplete="off" >
</div> </div>
<div class="form-field">
<SystemSelector :windowId="id" ref="systemSelector"></SystemSelector>
</div>
<div class="form-field"> <div class="form-field">
<button class="btn-primary sound-click">Create</button> <button class="btn-primary sound-click">Create</button>
</div> </div>
</form> </form>
<ErrorMessage v-if="errorMessage">{{ errorMessage }}</ErrorMessage>
</div> </div>
</template> </template>

View File

@ -0,0 +1,63 @@
<script setup>
import { onMounted, onUpdated, provide, ref, inject } from 'vue';
import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Windows';
import WindowHandle from '@/views/partials/WindowHandle.vue';
import Api from '@/services/Api.js'
import SystemSelector from '../../partials/SystemSelector.vue';
import { GetModules } from '../../../services/Modules';
import GameSystem from '../../partials/GameSystem.vue';
import { ClearWindow } from '../../../services/Windows';
const handle = ref(null);
const props = defineProps(['data']);
const data = props.data;
const campaignName = ref("");
let id = data.id;
let systems = ref(GetModules());
function Clear(){
ClearWindow(id);
}
provide('clearParent', Clear);
onMounted(() => {
SetupHandle(id, handle);
SetSize(id, {x: 300, y: 600});
ResetPosition(id, "center");
console.log(systems.value)
});
</script>
<template>
<div class="window-wrapper" :id="'window-wrapper-' + id">
<WindowHandle :window="id" ref="handle"></WindowHandle>
<!-- Body -->
<div class="system-list">
<GameSystem v-for="system in systems" :id="system.id" :data="system"></GameSystem>
</div>
</div>
</template>
<style scoped lang="scss">
.window-wrapper {
display: flex;
align-items: center;
}
.system-list {
display: flex;
flex-direction: column;
width: 100%;
}
</style>

View File

@ -11,6 +11,7 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),
'vue': 'vue/dist/vue.esm-bundler.js'
} }
}, },
proxy: { proxy: {

12
server/models/Book.js Normal file
View File

@ -0,0 +1,12 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const BookSchema = new Schema({
title: {type: String, required: true},
authors: { type: [String] },
system: {type: String, required: true},
image: { type: String },
data: { type: Object },
});
module.exports = mongoose.model('Book', BookSchema);

View File

@ -3,6 +3,7 @@ const Schema = mongoose.Schema;
const CampaignSchema = new Schema({ const CampaignSchema = new Schema({
name: {type: String, required: true}, name: {type: String, required: true},
system: {type: String, required: true},
creation_date: { type: Date, default: Date.now}, creation_date: { type: Date, default: Date.now},
last_opened: { type: Date, default: Date.now}, last_opened: { type: Date, default: Date.now},
invite_code: { type: String, unique: true }, invite_code: { type: String, unique: true },

View File

@ -0,0 +1,11 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const CharacterSchema = new Schema({
name: {type: String, required: true},
data: { type: Object },
campaign_user: {type: mongoose.Types.ObjectId, ref: "CampaignUser"},
image: { type: String },
});
module.exports = mongoose.model('Character', CharacterSchema);

11
server/models/Entity.js Normal file
View File

@ -0,0 +1,11 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const EntitySchema = new Schema({
name: {type: String, required: true},
data: { type: Object },
campaign: {type: mongoose.Types.ObjectId, ref: "Campaign"},
image: { type: String },
});
module.exports = mongoose.model('Entity', EntitySchema);

12
server/models/Map.js Normal file
View File

@ -0,0 +1,12 @@
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const MapSchema = new Schema({
name: {type: String, required: true},
data: { type: Object }, // Data del format dd2vtt
campaign: {type: mongoose.Types.ObjectId, ref: "Campaign"},
image: { type: String },
entities: { type: Object }
});
module.exports = mongoose.model('Entity', EntitySchema);

View File

@ -16,10 +16,11 @@ router.post('/register', passport.authenticate('jwt', {session: false}), rateLim
router.post('/create', passport.authenticate('jwt', {session: false}), rateLimitMiddleware, (req, res) => { router.post('/create', passport.authenticate('jwt', {session: false}), rateLimitMiddleware, (req, res) => {
let { let {
name name,
system
} = req.body; } = req.body;
if(!(name)){ if(!(name && system)){
res.json({ res.json({
status: "error", status: "error",
msg: "params" msg: "params"
@ -28,7 +29,7 @@ router.post('/create', passport.authenticate('jwt', {session: false}), rateLimit
} }
// Create the campaign // Create the campaign
let campaign = new Campaign({name}); let campaign = new Campaign({name, system});
campaign.invite_code = Campaign.generateInvite(); campaign.invite_code = Campaign.generateInvite();
campaign.save().then(campaign => { campaign.save().then(campaign => {