diff --git a/README.md b/README.md index c8632ff60143544df3a7d594587ec02768894071..8efe1adcf22b67b68c52e7666e4183de8b2fbc0c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ # ImmoWeb -ImmoWeb est un essai de site web classique pour gérer des annonces immobilières afin de tester entres autres la technologie Express \ No newline at end of file +ImmoWeb est un essai de site web classique pour gérer des annonces immobilières afin de tester entres autres la technologie Express + +## Installation + +run + +```shell +npm install +``` + +before anything else \ No newline at end of file diff --git a/app.js b/app.js index 320e81e258369a574ab694d65a14fb1c33af8d6c..5feccbda521531a042640be44b0e27279a071927 100644 --- a/app.js +++ b/app.js @@ -8,15 +8,16 @@ const passport = require('passport'); const flash = require('connect-flash'); const LocalStrategy = require('passport-local').Strategy; -const authRouter = require('./routes/auth'); -const indexRouter = require('./routes/index'); +const authRouter = require("./routes/auth"); const usersRouter = require('./routes/users'); +const adsRouter = require('./routes/ads'); const app = express(); const mongoose = require('mongoose'); -require('dotenv').config(); +require('./src/helpers').addHelpers(); +require('dotenv').config() mongoose.connect(process.env.DB_CONNECTION_STRING, { useNewUrlParser: true, @@ -77,9 +78,12 @@ app.use(flash()); app.use(passport.initialize()); app.use(passport.session()); -app.use('/', indexRouter); app.use('/', authRouter); app.use('/users', usersRouter); +app.use('/ads', adsRouter) +.get('/', function(req, res, next) { + res.redirect('/ads/'); +}); // catch 404 and forward to error handler app.use(function(req, res, next) { diff --git a/models/ad.js b/models/ad.js index 3b20b094f91d2e8601f523dc4dc0dd4de1585f0d..72d5f2ee3e02a7865e6dbfd380b6bfaa6c72bc74 100644 --- a/models/ad.js +++ b/models/ad.js @@ -1,48 +1,94 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; -const postTemplate = { - author: String, - body: String, - date: Date, -}; - const AD_TYPE = { - SALE: 'sale', - RENTAL: 'rental', + VALUE: { + SALE: "sale", + RENTAL: "rental" + }, + LANG: { + SALE: "A la vente", + RENTAL: "A la location" + } }; const AD_TRANSACTION_STATUS = { - AVAILABLE: 'available', - RENTED: 'rented', - SOLD: 'sold', + VALUE: { + AVAILABLE: "available", + RENTED: "rented", + SOLD: "sold" + }, + LANG: { + AVAILABLE: "Disponible", + RENTED: "Louée", + SOLD: "Vendue" + } }; +const postTemplate = { + author: { + type: String, + trim: true + }, + body: { + type: String, + trim: true, + required: [true, 'Le corps du message ne peut être vide !'] + }, + date: { + type: Date, + get: d => d && d.toLocaleDateString() + }, +} + const adSchema = new Schema({ - title: String, - type: String, - published: Boolean, - transactionStatus: String, - description: String, - price: Number, - availabilityDate: Date, - pictures: [{body: Buffer, title: String}], - questions: [ - { - ...postTemplate, - answers: [ - { - ...postTemplate, - }, - ], - }, - ], + title: { + type: String, + required: [true, 'Titre requis'], + trim: true + }, + type: { + type: String, + required: [true, 'Type de bien requis'], + enum: Object.values(AD_TYPE.VALUE) + }, + published: { + type: Boolean, + required: [true, 'État de publication requis'] + }, + transactionStatus: { + type: String, + required: [true, 'Statut de transaction requis'], + enum: Object.values(AD_TRANSACTION_STATUS.VALUE) + }, + price: { + type: Number, + required: [true, 'Prix requis'], + validate: { + validator: _ => { + return String(_).match(/^\d+(\.\d{2})?$/); + }, + msg: 'Le prix doit avoir au maximum 2 chiffres de centimes' + } + }, + description: { + type: String, + trim: true + }, + availabilityDate: { + type: Date + }, + pictures: [{ body: Buffer, title: String }], + questions: [{ + ...postTemplate, + answers: [{ + ...postTemplate + }] + }] }); -const Ad = mongoose.model('Ad', adSchema); - module.exports = { - AD_TYPE: AD_TYPE, - AD_TRANSACTIONSTATUS: AD_TRANSACTION_STATUS, - Ad: Ad, -}; + AD_TYPE: AD_TYPE, + AD_TRANSACTIONSTATUS: AD_TRANSACTION_STATUS, + Ad: mongoose.model('Ad', adSchema) +} diff --git a/public/javascript/adDelete.js b/public/javascript/adDelete.js new file mode 100644 index 0000000000000000000000000000000000000000..5741ee7796fd9df8aae36feb1c8aa9b896464e28 --- /dev/null +++ b/public/javascript/adDelete.js @@ -0,0 +1,17 @@ +function deleteAd(id) { + const response = fetch(`delete/${id}`, { + method: 'DELETE', + mode: 'cors', + cache: 'no-cache', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json' + }, + redirect: 'follow', + referrer: '' + }); + + response + .then(value => window.location = value.url) + .catch(reason => console.error(reason)); +} \ No newline at end of file diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index bf1c2248c04ba7dfa32fb4e6ed958315d408a90f..991b01ea8ac6de81bacac6e62b4f490737b7e294 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -14,6 +14,23 @@ body { min-width: 150px; } +#create-form { + overflow: hidden; + width: min-content; +} + +form > fieldset, form > button { + margin-top: 5%; +} + +div.form-input { + display: inline; +} + +span.error-message { + color: red; +} + .msg { display: flex; justify-content: space-between; diff --git a/routes/ads.js b/routes/ads.js new file mode 100644 index 0000000000000000000000000000000000000000..67cdec5e28a12c0d7af67ea503415ae406661099 --- /dev/null +++ b/routes/ads.js @@ -0,0 +1,94 @@ +const express = require('express'); +const router = express.Router(); + +const adModel = require('../models/ad'); + +/* GET to get to get to the ad creation form */ +router.get('/create', function(req, res, next) { + res.render('ad_create'); +}) +.post('/create', function(req, res, next) { + //TODO : gérer l'upload de fichier (avec le middleware multiparty par exemple) + const body = req.body; + const id = body.id; + + const formData = { + title: body.title, + type: body.type, + transactionStatus: body.transactionStatus, + price: body.price.replace(',', '.'), + published: body.published && true, + description: body.description, + availabilityDate: body.availabilityDate === '' ? null : body.availabilityDate + }; + + if(id) { + //Peut-être charger l'objet en amont et le retourner si erreur ? + + adModel.Ad.updateOne({_id: id}, { $set: formData }).exec() + .then(value => res.redirect('/ads/')) + .catch(reason => { + res.render('ad_create', reason); + }); + } else { + const newAd = new adModel.Ad(formData); + + newAd.save() + .then(value => res.redirect('/ads/')) + .catch(reason => { + console.log(reason); + res.render('ad_create', reason); + }); + } +}) +.get('/', function(req, res, next) { + adModel.Ad.find({ published: true }).exec() + .then(value => { + // on trie les annonces par ordre alphabétique + res.render('ad_view', { ads: value.sort((a, b) => a.title.localeCompare(b.title)) }); + }) + .catch(_ => { + console.log(_); + res.render('ad_view', { ads: [], errors: { load: 'Un problème est survenu lors du chargement des données' }}); + }); +}) +.get('/update/:id', function(req, res, next) { + const id = req.params.id; + + adModel.Ad.findOne({ _id: id }, function (err, ad) { + const errors = []; + if (err) { errors.push(err.message) } + if (!ad) { errors.push("L'annonce cherchée n'a pas été trouvée") } + + res.render('ad_create', { ad: ad, errors_update: errors}); + }); +}) +.delete('/delete/:id', function(req, res, next) { + const id = req.params.id; + + adModel.Ad.deleteOne({_id: id}).exec() + .then(value => { + req.flash('success', "L'annonce a bien été supprimée"); + }) + .catch(reason => { + req.flash('error', "L'annonce n'a pas pu être supprimée"); + }) + .finally(() => res.redirect(303, '/ads/')); +}); + +/*adModelToAdViewModel = e => { + return { + _id: e._id, + title: e.title, + type: adModel.AD_TYPE.LANG[e.type.toUpperCase()], + transactionStatus: adModel.AD_TRANSACTIONSTATUS.LANG[e.transactionStatus.toUpperCase()], + price: e.price, + published: e.published, + description: e.description, + availabilityDate: e.availabilityDate.toLocaleDateString(), + pictures: e.pictures, + questions: e.questions, + } +}*/ + +module.exports = router; diff --git a/routes/index.js b/routes/index.js deleted file mode 100644 index d77d5c6a20658b9bd69922d494a71dddceada549..0000000000000000000000000000000000000000 --- a/routes/index.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require('express'); -const router = new express.Router(); - -/* GET home page. */ -router.get('/', function(req, res, next) { - res.render('index', {title: 'Express', messages: req.flash('info')}); -}); - -module.exports = router; diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..f4389d6b84a4e7cfd3a107f4492c99be851d0548 --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,36 @@ +const hbs = require('hbs'); + +const addHelpers = function() { + hbs.registerHelper('ifEquals', function(arg1, arg2, options) { + return (arg1 === arg2) ? options.fn(this) : options.inverse(this); + }); + + hbs.registerHelper('getEnumValue', function(enumName, key, options) { + const adModule = require('../models/ad'); + + switch(enumName) { + case "type": + return adModule.AD_TYPE.LANG[key.toUpperCase()]; + case "transactionStatus": + return adModule.AD_TRANSACTIONSTATUS.LANG[key.toUpperCase()]; + default: + return "Ressource inconnue" + }; + }); + + hbs.registerHelper('dateToLocaleString', function(date, options) { + return date && date.toLocaleDateString(); + }); + + hbs.registerHelper('dateToISO', function(date, options) { + return date && date.toISOString().split('T')[0]; + }); + + hbs.registerHelper('formatPrice', function(price, options) { + return price && price.toFixed(2).replace('.', ','); + }) +} + +module.exports = { + addHelpers: addHelpers +} \ No newline at end of file diff --git a/views/ad_create.hbs b/views/ad_create.hbs new file mode 100644 index 0000000000000000000000000000000000000000..b59489641b65a87de0b2fbc53c530819487f0748 --- /dev/null +++ b/views/ad_create.hbs @@ -0,0 +1,87 @@ +
{{dateToLocaleString availabilityDate}}
+ +Welcome to {{title}}{{#if user}}, {{user.name}}{{/if}}