mirror of
https://github.com/len0rd/personal-website.git
synced 2025-03-01 03:51:57 -05:00
move recipe md -> ejs generation code to its own file so it can be run when a new recipe is added from the web page
This commit is contained in:
parent
97fb2a61fe
commit
5d8cfe39eb
184
prestart.js
184
prestart.js
|
@ -11,13 +11,10 @@ const showdown = require('showdown'),
|
||||||
mkdirp = require('mkdirp'),
|
mkdirp = require('mkdirp'),
|
||||||
projectInputDir = './project_writeups/',
|
projectInputDir = './project_writeups/',
|
||||||
projectOutputDir = './views/partials/md/projects/',
|
projectOutputDir = './views/partials/md/projects/',
|
||||||
recipeInputDir = './recipes/',
|
|
||||||
recipeOutputDir = './views/partials/md/recipes/',
|
|
||||||
recipeListGeneratedOutputDir = './views/partials/generated/',
|
|
||||||
projectClassMap = {
|
projectClassMap = {
|
||||||
h1: 'display-1' //tag type : class to add to all tags of that type (class="display-1" added to all <h1>)
|
h1: 'display-1' //tag type : class to add to all tags of that type (class="display-1" added to all <h1>)
|
||||||
};
|
};
|
||||||
const { assert } = require('console');
|
var recipeTools = require('./recipe_generator');
|
||||||
|
|
||||||
|
|
||||||
function addClassToTag(text, classMap) {
|
function addClassToTag(text, classMap) {
|
||||||
|
@ -78,185 +75,14 @@ function convertMarkdownInDirWithShowdown(inputDir, outputDir, converter) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertRecipeMarkdown(inputDir, outputDir) {
|
function convertRecipeMarkdown(inputDir) {
|
||||||
var md = require('markdown-it')()
|
|
||||||
.use(require('markdown-it-hashtag'));
|
|
||||||
|
|
||||||
md.renderer.rules.hashtag_open = function (tokens, idx) {
|
|
||||||
var tagName = tokens[idx].content.toLowerCase();
|
|
||||||
return '<a href="/recipe_navigator?tag=' + tagName + '"><span class="badge bg-secondary">';
|
|
||||||
}
|
|
||||||
|
|
||||||
md.renderer.rules.hashtag_close = function () { return '</span></a>'; }
|
|
||||||
|
|
||||||
// This is a hardcoded markdown header section number to html file name
|
|
||||||
//
|
|
||||||
// Example.md:
|
|
||||||
// """
|
|
||||||
// ... maybe some other header info here -| - exported as filename-title.ejs
|
|
||||||
// # Delicious Recipe Name -|
|
|
||||||
// Catch phrase or yield -|
|
|
||||||
// | - exported as filename-subtitle
|
|
||||||
// image of the food |
|
|
||||||
// -|
|
|
||||||
// ## Ingredients -|
|
|
||||||
// ... ingredients table, etc | - exported as filename-ingredients.ejs
|
|
||||||
// -|
|
|
||||||
// ## Instructions
|
|
||||||
// """
|
|
||||||
//
|
|
||||||
// NOTE: these titles are HARDCODED in recipe_template.ejs!
|
|
||||||
const mdSectionHtmlTitles = [
|
|
||||||
'title',
|
|
||||||
// 'subtitle',
|
|
||||||
'ingredients',
|
|
||||||
'instructions',
|
|
||||||
]
|
|
||||||
|
|
||||||
mkdirp.sync(outputDir);
|
|
||||||
fs.readdir(inputDir, (err, files) => {
|
fs.readdir(inputDir, (err, files) => {
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
if (!file.endsWith('.md')) {
|
recipeTools.generateRecipePartials(file);
|
||||||
return;
|
|
||||||
}
|
|
||||||
let fileNameNoExtension = file.slice(0, -3);
|
|
||||||
console.log('converting: ' + fileNameNoExtension);
|
|
||||||
fs.readFile(inputDir + file, 'utf8', (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ingredientTableRegex = new RegExp(`^(\\|?.*?)\\|(.*?)(\\|.*?\\|.*?)\n`, `gm`);
|
|
||||||
var ingredientDashCheck = new RegExp("^\-+$");
|
|
||||||
ingredientTableMatcher = ingredientTableRegex.exec(data);
|
|
||||||
while (ingredientTableMatcher != null) {
|
|
||||||
meas = ingredientTableMatcher[1];
|
|
||||||
let unit = ingredientTableMatcher[2].toLowerCase().trim();
|
|
||||||
if (unit != "unit" && unit && !ingredientDashCheck.test(unit)) {
|
|
||||||
meas += unit + " ";
|
|
||||||
}
|
|
||||||
data = data.replace(ingredientTableMatcher[0], `${meas}${ingredientTableMatcher[3]}\n`);
|
|
||||||
ingredientTableMatcher = ingredientTableRegex.exec(data);
|
|
||||||
}
|
|
||||||
ingredientTableMatcher = ingredientTableRegex.exec(data);
|
|
||||||
while (ingredientTableMatcher != null) {
|
|
||||||
meas = ingredientTableMatcher[1];
|
|
||||||
let unit = ingredientTableMatcher[2].toLowerCase().trim();
|
|
||||||
if (unit != "unit" && unit && !ingredientDashCheck.test(unit)) {
|
|
||||||
meas += unit + " ";
|
|
||||||
}
|
|
||||||
data = data.replace(ingredientTableMatcher[0], `${meas}${ingredientTableMatcher[3]}\n`);
|
|
||||||
ingredientTableMatcher = ingredientTableRegex.exec(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
let tokens = md.parse(data)
|
|
||||||
|
|
||||||
let sections = []
|
|
||||||
sections.push([]); // start off the array and put everything before and including the first header in title
|
|
||||||
let numSections = 0;
|
|
||||||
for (const token of tokens) {
|
|
||||||
if (token.type === 'heading_open') {
|
|
||||||
if (numSections == 0) {
|
|
||||||
numSections++;
|
|
||||||
}
|
|
||||||
else if (numSections < mdSectionHtmlTitles.length) {
|
|
||||||
numSections++;
|
|
||||||
sections.push([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sections[sections.length - 1].push(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(sections.length <= mdSectionHtmlTitles.length);
|
|
||||||
|
|
||||||
// hardcode bootstrap class attribute to add to <table> tag in ingredients
|
|
||||||
for (let ii = 0; ii < sections[1].length; ii++) {
|
|
||||||
if (sections[1][ii].type == 'table_open') {
|
|
||||||
sections[1][ii].attrs = [["class", "table table-striped table-sm table-hover"]];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let ii = 0; ii < sections.length; ii++) {
|
|
||||||
let html = md.renderer.render(sections[ii], md.options);
|
|
||||||
// hardcode making images in the title section larger
|
|
||||||
if (ii == 0) {
|
|
||||||
var regex = new RegExp(`<img (.*?)>`, `g`);
|
|
||||||
matcher = regex.exec(html);
|
|
||||||
while (matcher != null && !matcher[1].includes("w-100")) {
|
|
||||||
var restOfTag = matcher[1];
|
|
||||||
html = html.replace(matcher[0], `<img class="w-100" ${restOfTag}>`);
|
|
||||||
matcher = regex.exec(html);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fs.writeFileSync(outputDir + fileNameNoExtension + '-' + mdSectionHtmlTitles[ii] + '.ejs', html, 'utf8');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateRecipeNavigatorList(recipeSrcDir, generatedOutputDir) {
|
|
||||||
// generate a list of recipe links. While doing so generate an array
|
|
||||||
// of unique hashtags found in all recipes
|
|
||||||
mkdirp.sync(generatedOutputDir);
|
|
||||||
let recipeListPartialOut = "";
|
|
||||||
let allRecipeHashtags = [];
|
|
||||||
fs.readdir(recipeSrcDir, (err, files) => {
|
|
||||||
files.sort().forEach(file => {
|
|
||||||
if (!file.endsWith('.md')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let fileNameNoExtension = file.slice(0, -3);
|
|
||||||
|
|
||||||
const data = fs.readFileSync(recipeSrcDir + file, { encoding: 'utf8', flag: 'r' });
|
|
||||||
|
|
||||||
// find all hashtags in the file
|
|
||||||
var hashtagRegex = new RegExp(`#(\\w+)`, `g`);
|
|
||||||
hashtagMatcher = hashtagRegex.exec(data);
|
|
||||||
var recipeTags = []; // hashtags of the current recipe only
|
|
||||||
while (hashtagMatcher != null) {
|
|
||||||
var hashtag = hashtagMatcher[1].toLowerCase();
|
|
||||||
if (!allRecipeHashtags.includes(hashtag)) {
|
|
||||||
allRecipeHashtags.push(hashtag);
|
|
||||||
}
|
|
||||||
if (!recipeTags.includes(hashtag)) {
|
|
||||||
recipeTags.push(hashtag);
|
|
||||||
}
|
|
||||||
hashtagMatcher = hashtagRegex.exec(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
let combinedRecipeTags = "";
|
|
||||||
if (recipeTags.length > 0) {
|
|
||||||
combinedRecipeTags = recipeTags.join(",");
|
|
||||||
}
|
|
||||||
|
|
||||||
// get first recipe title from document
|
|
||||||
var titleRegex = new RegExp(`#\\s+(.+)\\n`, `g`);
|
|
||||||
titleMatcher = titleRegex.exec(data);
|
|
||||||
var recipeTitle = fileNameNoExtension;
|
|
||||||
if (titleMatcher != null) {
|
|
||||||
recipeTitle = titleMatcher[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
recipeListPartialOut += `<a href="recipes/${fileNameNoExtension}" class="list-group-item list-group-item-action" tags="${combinedRecipeTags}">${recipeTitle}</a>\n`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// writeout the link list partial
|
|
||||||
fs.writeFileSync(generatedOutputDir + "recipe-links.ejs", recipeListPartialOut, "utf-8");
|
|
||||||
|
|
||||||
// now generate the hashtag button list partial
|
|
||||||
// TODO: in the future sort the list by number of hashtag hits (most -> least common)
|
|
||||||
// instead of alphabetically
|
|
||||||
let tagListPartialOut = "";
|
|
||||||
allRecipeHashtags.sort().forEach(hashtag => {
|
|
||||||
tagListPartialOut += `<button type="button" class="btn btn-light btn-sm">${hashtag}</button>\n`;
|
|
||||||
});
|
|
||||||
fs.writeFileSync(generatedOutputDir + "recipe-tags.ejs", tagListPartialOut, "utf-8");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
convertMarkdownInDirWithShowdown(projectInputDir, projectOutputDir, projectsConverter);
|
convertMarkdownInDirWithShowdown(projectInputDir, projectOutputDir, projectsConverter);
|
||||||
convertRecipeMarkdown(recipeInputDir, recipeOutputDir);
|
convertRecipeMarkdown("./recipes");
|
||||||
generateRecipeNavigatorList(recipeInputDir, recipeListGeneratedOutputDir);
|
recipeTools.generateRecipeNavPartials();
|
||||||
|
|
178
recipe_generator.js
Normal file
178
recipe_generator.js
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
const fs = require('fs'),
|
||||||
|
mkdirp = require('mkdirp'),
|
||||||
|
recipeInputDir = './recipes/',
|
||||||
|
recipeOutputDir = './views/partials/md/recipes/',
|
||||||
|
recipeListGeneratedOutputDir = './views/partials/generated/';
|
||||||
|
const { assert } = require('console');
|
||||||
|
|
||||||
|
var mdIt = require('markdown-it')()
|
||||||
|
.use(require('markdown-it-hashtag'));
|
||||||
|
|
||||||
|
mdIt.renderer.rules.hashtag_open = function (tokens, idx) {
|
||||||
|
var tagName = tokens[idx].content.toLowerCase();
|
||||||
|
return '<a href="/recipe_navigator?tag=' + tagName + '"><span class="badge bg-secondary">';
|
||||||
|
}
|
||||||
|
mdIt.renderer.rules.hashtag_close = function () { return '</span></a>'; }
|
||||||
|
|
||||||
|
|
||||||
|
// This is a hardcoded markdown header section number to html file name
|
||||||
|
//
|
||||||
|
// Example.md:
|
||||||
|
// """
|
||||||
|
// ... maybe some other header info here -| - exported as filename-title.ejs
|
||||||
|
// # Delicious Recipe Name -|
|
||||||
|
// Catch phrase or yield -|
|
||||||
|
// | - exported as filename-subtitle
|
||||||
|
// image of the food |
|
||||||
|
// -|
|
||||||
|
// ## Ingredients -|
|
||||||
|
// ... ingredients table, etc | - exported as filename-ingredients.ejs
|
||||||
|
// -|
|
||||||
|
// ## Instructions
|
||||||
|
// """
|
||||||
|
//
|
||||||
|
// NOTE: these titles are HARDCODED in recipe_template.ejs!
|
||||||
|
const mdSectionHtmlTitles = [
|
||||||
|
'title',
|
||||||
|
// 'subtitle',
|
||||||
|
'ingredients',
|
||||||
|
'instructions',
|
||||||
|
]
|
||||||
|
|
||||||
|
/// @param: fileName: the markdown file to open
|
||||||
|
/// and generate into EJS partials
|
||||||
|
function generateRecipePartials(fileName) {
|
||||||
|
mkdirp.sync(recipeOutputDir);
|
||||||
|
if (!fileName.endsWith('.md')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let fileNameNoExtension = fileName.slice(0, -3);
|
||||||
|
console.log('converting: ' + fileNameNoExtension);
|
||||||
|
fs.readFile(recipeInputDir + fileName, 'utf8', (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ingredientTableRegex = new RegExp(`^(\\|?.*?)\\|(.*?)(\\|.*?\\|.*?)\r?\n`, `m`);
|
||||||
|
var ingredientDashCheck = new RegExp("^\-+$");
|
||||||
|
ingredientTableMatcher = ingredientTableRegex.exec(data);
|
||||||
|
while (ingredientTableMatcher != null) {
|
||||||
|
meas = ingredientTableMatcher[1];
|
||||||
|
let unit = ingredientTableMatcher[2].toLowerCase().trim();
|
||||||
|
if (unit != "unit" && unit && !ingredientDashCheck.test(unit)) {
|
||||||
|
meas += unit + " ";
|
||||||
|
}
|
||||||
|
data = data.replace(ingredientTableMatcher[0], `${meas}${ingredientTableMatcher[3]}\n`);
|
||||||
|
ingredientTableMatcher = ingredientTableRegex.exec(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokens = mdIt.parse(data)
|
||||||
|
|
||||||
|
let sections = []
|
||||||
|
sections.push([]); // start off the array and put everything before and including the first header in title
|
||||||
|
let numSections = 0;
|
||||||
|
for (const token of tokens) {
|
||||||
|
if (token.type === 'heading_open') {
|
||||||
|
if (numSections == 0) {
|
||||||
|
numSections++;
|
||||||
|
}
|
||||||
|
else if (numSections < mdSectionHtmlTitles.length) {
|
||||||
|
numSections++;
|
||||||
|
sections.push([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sections[sections.length - 1].push(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(sections.length <= mdSectionHtmlTitles.length);
|
||||||
|
|
||||||
|
// hardcode bootstrap class attribute to add to <table> tag in ingredients
|
||||||
|
for (let ii = 0; ii < sections[1].length; ii++) {
|
||||||
|
if (sections[1][ii].type == 'table_open') {
|
||||||
|
sections[1][ii].attrs = [["class", "table table-striped table-sm table-hover"]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let ii = 0; ii < sections.length; ii++) {
|
||||||
|
let html = mdIt.renderer.render(sections[ii], mdIt.options);
|
||||||
|
// hardcode making images in the title section larger
|
||||||
|
if (ii == 0) {
|
||||||
|
var regex = new RegExp(`<img (.*?)>`, `g`);
|
||||||
|
matcher = regex.exec(html);
|
||||||
|
while (matcher != null && !matcher[1].includes("w-100")) {
|
||||||
|
var restOfTag = matcher[1];
|
||||||
|
html = html.replace(matcher[0], `<img class="w-100" ${restOfTag}>`);
|
||||||
|
matcher = regex.exec(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.writeFileSync(recipeOutputDir + fileNameNoExtension + '-' + mdSectionHtmlTitles[ii] + '.ejs', html, 'utf8');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRecipeNavPartials() {
|
||||||
|
// generate a list of recipe links. While doing so generate an array
|
||||||
|
// of unique hashtags found in all recipes
|
||||||
|
mkdirp.sync(recipeListGeneratedOutputDir);
|
||||||
|
let recipeListPartialOut = "";
|
||||||
|
let allRecipeHashtags = [];
|
||||||
|
fs.readdir(recipeInputDir, (err, files) => {
|
||||||
|
files.sort().forEach(file => {
|
||||||
|
if (!file.endsWith('.md')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let fileNameNoExtension = file.slice(0, -3);
|
||||||
|
|
||||||
|
const data = fs.readFileSync(recipeInputDir + file, { encoding: 'utf8', flag: 'r' });
|
||||||
|
|
||||||
|
// find all hashtags in the file
|
||||||
|
var hashtagRegex = new RegExp(`#(\\w+)`, `g`);
|
||||||
|
hashtagMatcher = hashtagRegex.exec(data);
|
||||||
|
var recipeTags = []; // hashtags of the current recipe only
|
||||||
|
while (hashtagMatcher != null) {
|
||||||
|
var hashtag = hashtagMatcher[1].toLowerCase();
|
||||||
|
if (!allRecipeHashtags.includes(hashtag)) {
|
||||||
|
allRecipeHashtags.push(hashtag);
|
||||||
|
}
|
||||||
|
if (!recipeTags.includes(hashtag)) {
|
||||||
|
recipeTags.push(hashtag);
|
||||||
|
}
|
||||||
|
hashtagMatcher = hashtagRegex.exec(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
let combinedRecipeTags = "";
|
||||||
|
if (recipeTags.length > 0) {
|
||||||
|
combinedRecipeTags = recipeTags.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
// get first recipe title from document
|
||||||
|
var titleRegex = new RegExp(`#\\s+(.+)\\n`, `g`);
|
||||||
|
titleMatcher = titleRegex.exec(data);
|
||||||
|
var recipeTitle = fileNameNoExtension;
|
||||||
|
if (titleMatcher != null) {
|
||||||
|
recipeTitle = titleMatcher[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
recipeListPartialOut += `<a href="recipes/${fileNameNoExtension}" class="list-group-item list-group-item-action" tags="${combinedRecipeTags}">${recipeTitle}</a>\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// writeout the link list partial
|
||||||
|
fs.writeFileSync(recipeListGeneratedOutputDir + "recipe-links.ejs", recipeListPartialOut, "utf-8");
|
||||||
|
|
||||||
|
// now generate the hashtag button list partial
|
||||||
|
// TODO: in the future sort the list by number of hashtag hits (most -> least common)
|
||||||
|
// instead of alphabetically
|
||||||
|
let tagListPartialOut = "";
|
||||||
|
allRecipeHashtags.sort().forEach(hashtag => {
|
||||||
|
tagListPartialOut += `<button type="button" class="btn btn-light btn-sm">${hashtag}</button>\n`;
|
||||||
|
});
|
||||||
|
fs.writeFileSync(recipeListGeneratedOutputDir + "recipe-tags.ejs", tagListPartialOut, "utf-8");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateRecipePartials: generateRecipePartials,
|
||||||
|
generateRecipeNavPartials: generateRecipeNavPartials,
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
const PORT = 8090;
|
const PORT = 8090;
|
||||||
var express = require('express');
|
var express = require('express');
|
||||||
|
var recipeTools = require('./recipe_generator');
|
||||||
// formidable helps get JSON POST request content
|
// formidable helps get JSON POST request content
|
||||||
const formidable = require('express-formidable');
|
const formidable = require('express-formidable');
|
||||||
var compile = require('es6-template-strings/compile'),
|
var compile = require('es6-template-strings/compile'),
|
||||||
|
@ -22,6 +23,7 @@ app.set('view engine', 'ejs');
|
||||||
// add folder for static content:
|
// add folder for static content:
|
||||||
app.use(express.static(__dirname + '/assets'));
|
app.use(express.static(__dirname + '/assets'));
|
||||||
|
|
||||||
|
// Get method for all pages
|
||||||
app.get(/\/.*/, function (req, res) {
|
app.get(/\/.*/, function (req, res) {
|
||||||
let pathname = 'pages' + req.path;
|
let pathname = 'pages' + req.path;
|
||||||
let page = pathname.substr(pathname.lastIndexOf('/') + 1);
|
let page = pathname.substr(pathname.lastIndexOf('/') + 1);
|
||||||
|
@ -63,7 +65,7 @@ function toCamelCase(str) {
|
||||||
app.post("/new_recipe", function (req, res) {
|
app.post("/new_recipe", function (req, res) {
|
||||||
let dataIn = req.fields;
|
let dataIn = req.fields;
|
||||||
|
|
||||||
filename = recipeMarkdownDir + toCamelCase(dataIn.recipe_name_input) + ".md";
|
filename = toCamelCase(dataIn.recipe_name_input) + ".md";
|
||||||
if (fs.existsSync(recipeMarkdownDir + filename)) {
|
if (fs.existsSync(recipeMarkdownDir + filename)) {
|
||||||
res.status(409).send(`Already have a recipe with name "${dataIn.recipe_name_input}"`);
|
res.status(409).send(`Already have a recipe with name "${dataIn.recipe_name_input}"`);
|
||||||
return;
|
return;
|
||||||
|
@ -79,7 +81,10 @@ app.post("/new_recipe", function (req, res) {
|
||||||
dataIn.tags = formatted_tags
|
dataIn.tags = formatted_tags
|
||||||
|
|
||||||
formattedMarkdownRecipe = resolveToString(compiledRecipeTemplate, dataIn);
|
formattedMarkdownRecipe = resolveToString(compiledRecipeTemplate, dataIn);
|
||||||
fs.writeFileSync(filename, formattedMarkdownRecipe, 'utf-8');
|
fs.writeFileSync(recipeMarkdownDir + filename, formattedMarkdownRecipe, 'utf-8');
|
||||||
|
|
||||||
|
recipeTools.generateRecipePartials(filename);
|
||||||
|
recipeTools.generateRecipeNavPartials()
|
||||||
|
|
||||||
res.send('Successfully added!');
|
res.send('Successfully added!');
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue