consolidate all repos to one for archive

This commit is contained in:
2025-01-28 13:46:42 +01:00
commit a6610fbc7a
5350 changed files with 2705721 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@@ -0,0 +1,79 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
// vključimo mongoose in ga povežemo z MongoDB
var mongoose = require('mongoose');
var mongoDB = "mongodb://127.0.0.1/vaja3";
mongoose.connect(mongoDB);
mongoose.set('strictQuery', false);
mongoose.Promise = global.Promise;
var db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
// vključimo routerje
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/userRoutes');
var photosRouter = require('./routes/photoRoutes');
var mdataRouter = require('./routes/mdataRoutes');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'hbs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
/**
* Vključimo session in connect-mongo.
* Connect-mongo skrbi, da se session hrani v bazi.
* Posledično ostanemo prijavljeni, tudi ko spremenimo kodo (restartamo strežnik)
*/
var session = require('express-session');
var MongoStore = require('connect-mongo');
app.use(session({
secret: 'work hard',
resave: true,
saveUninitialized: false,
store: MongoStore.create({mongoUrl: mongoDB})
}));
//Shranimo sejne spremenljivke v locals
//Tako lahko do njih dostopamo v vseh view-ih (glej layout.hbs)
app.use(function (req, res, next) {
res.locals.session = req.session;
next();
});
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/photos', photosRouter);
app.use('/mdata', mdataRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
app.listen(4000, function () {
console.log('Example app listening on port 4000!');
});
module.exports = app;

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('vaja3:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

View File

@@ -0,0 +1,186 @@
const { now } = require('mongoose');
var MdataModel = require('../models/mdataModel.js');
/**
* mdataController.js
*
* @description :: Server-side logic for managing mdatas.
*/
module.exports = {
/**
* mdataController.list()
*/
list: function (req, res) {
MdataModel.find({answering_id: null})
.sort({timestamp: -1})
.exec( function (err, mdatas)
{
if (err) {
return res.status(500).json({
message: 'Error when getting mdata.',
error: err
});
}
var data =[];
data.mdatas = mdatas;
return res.render('mdata/list', data);
});
},
/**
* mdataController.show()
*/
show: function (req, res) {
var id = req.params.id;
MdataModel.findOne({_id: id}, function (err, mdata) {
if (err) {
return res.status(500).json({
message: 'Error when getting mdata.',
error: err
});
}
if (!mdata) {
return res.status(404).json({
message: 'No such mdata'
});
}
MdataModel.findOne({_id: mdata.answer}, function (err, currectAns) {
if (err) {
return res.status(500).json({
message: 'Error when getting mdata.',
error: err
});
}
mdata.cur = currectAns;
MdataModel.find({answering_id: id, _id:{$ne: mdata.answer }})
.sort({timestamp:-1})
.exec( function (err, answers) {
if (err) {
return res.status(500).json({
message: 'Error when getting mdata.',
error: err
});
}
mdata.answers = answers;
mdata.selectAns = (mdata.user_id == req.session.userId)
return res.render('mdata/show', mdata);
});
});
});
},
listmy: function (req, res) {
MdataModel.find( {user_id : req.session.userId }, function (err, mdatas) {
if (err) {
return res.status(500).json({
message: 'Error when getting mdata.',
error: err
});
}
var data =[];
data.mdatas = mdatas;
return res.render('mdata/my', data);
});
},
/**
* mdataController.create()
*/
create: function (req, res) {
var mdata = new MdataModel({
user_id : req.session.userId,
title : req.body.title,
question : req.body.question,
timestamp : Date.now(),
answering_id : req.body.answering_id
});
mdata.save(function (err, mdata) {
if (err) {
return res.status(500).json({
message: 'Error when creating mdata',
error: err
});
}
return res.redirect('/mdata');
//return res.status(201).json(mdata);
});
},
/**
* mdataController.update()
*/
update: function (req, res) {
var id = req.params.id;
var ans = req.params.ans;
MdataModel.findOne({_id: id}, function (err, mdata) {
if (err) {
return res.status(500).json({
message: 'Error when getting mdata',
error: err
});
}
if (!mdata) {
return res.status(404).json({
message: 'No such mdata'
});
}
mdata.user_id = req.body.user_id ? req.body.user_id : mdata.user_id;
mdata.title = req.body.title ? req.body.title : mdata.title;
mdata.question = req.body.question ? req.body.question : mdata.question;
mdata.timestamp = req.body.timestamp ? req.body.timestamp : mdata.timestamp;
mdata.tags = req.body.tags ? req.body.tags : mdata.tags;
mdata.answering_id = req.body.answering_id ? req.body.answering_id : mdata.answering_id;
mdata.answer = ans ? ans : mdata.answer;
mdata.save(function (err, mdata) {
if (err) {
return res.status(500).json({
message: 'Error when updating mdata.',
error: err
});
}
return res.redirect('/mdata');
});
});
},
/**
* mdataController.remove()
*/
remove: function (req, res) {
var id = req.params.id;
MdataModel.findByIdAndRemove(id, function (err, mdata) {
if (err) {
return res.status(500).json({
message: 'Error when deleting the mdata.',
error: err
});
}
return res.redirect('/mdata/my');
});
},
publish: function(req, res){
return res.render('mdata/publish');
}
};

View File

@@ -0,0 +1,138 @@
var PhotoModel = require('../models/photoModel.js');
/**
* photoController.js
*
* @description :: Server-side logic for managing photos.
*/
module.exports = {
/**
* photoController.list()
*/
list: function (req, res) {
PhotoModel.find()
.populate('postedBy')
.exec(function (err, photos) {
if (err) {
return res.status(500).json({
message: 'Error when getting photo.',
error: err
});
}
var data = [];
data.photos = photos;
return res.render('photo/list', data);
});
},
/**
* photoController.show()
*/
show: function (req, res) {
var id = req.params.id;
PhotoModel.findOne({_id: id}, function (err, photo) {
if (err) {
return res.status(500).json({
message: 'Error when getting photo.',
error: err
});
}
if (!photo) {
return res.status(404).json({
message: 'No such photo'
});
}
return res.json(photo);
});
},
/**
* photoController.create()
*/
create: function (req, res) {
var photo = new PhotoModel({
name : req.body.name,
path : "/images/"+req.file.filename,
postedBy : req.session.userId,
views : 0,
likes : 0
});
photo.save(function (err, photo) {
if (err) {
return res.status(500).json({
message: 'Error when creating photo',
error: err
});
}
//return res.status(201).json(photo);
return res.redirect('/mdata');
});
},
/**
* photoController.update()
*/
update: function (req, res) {
var id = req.params.id;
PhotoModel.findOne({_id: id}, function (err, photo) {
if (err) {
return res.status(500).json({
message: 'Error when getting photo',
error: err
});
}
if (!photo) {
return res.status(404).json({
message: 'No such photo'
});
}
photo.name = req.body.name ? req.body.name : photo.name;
photo.path = req.body.path ? req.body.path : photo.path;
photo.postedBy = req.body.postedBy ? req.body.postedBy : photo.postedBy;
photo.views = req.body.views ? req.body.views : photo.views;
photo.likes = req.body.likes ? req.body.likes : photo.likes;
photo.save(function (err, photo) {
if (err) {
return res.status(500).json({
message: 'Error when updating photo.',
error: err
});
}
return res.json(photo);
});
});
},
/**
* photoController.remove()
*/
remove: function (req, res) {
var id = req.params.id;
PhotoModel.findByIdAndRemove(id, function (err, photo) {
if (err) {
return res.status(500).json({
message: 'Error when deleting the photo.',
error: err
});
}
return res.status(204).json();
});
},
publish: function(req, res){
return res.render('photo/publish');
}
};

View File

@@ -0,0 +1,122 @@
var TagsModel = require('../models/tagsModel.js');
/**
* tagsController.js
*
* @description :: Server-side logic for managing tagss.
*/
module.exports = {
/**
* tagsController.list()
*/
list: function (req, res) {
TagsModel.find(function (err, tagss) {
if (err) {
return res.status(500).json({
message: 'Error when getting tags.',
error: err
});
}
return res.json(tagss);
});
},
/**
* tagsController.show()
*/
show: function (req, res) {
var id = req.params.id;
TagsModel.findOne({_id: id}, function (err, tags) {
if (err) {
return res.status(500).json({
message: 'Error when getting tags.',
error: err
});
}
if (!tags) {
return res.status(404).json({
message: 'No such tags'
});
}
return res.json(tags);
});
},
/**
* tagsController.create()
*/
create: function (req, res) {
var tags = new TagsModel({
name : req.body.name
});
tags.save(function (err, tags) {
if (err) {
return res.status(500).json({
message: 'Error when creating tags',
error: err
});
}
return res.status(201).json(tags);
});
},
/**
* tagsController.update()
*/
update: function (req, res) {
var id = req.params.id;
TagsModel.findOne({_id: id}, function (err, tags) {
if (err) {
return res.status(500).json({
message: 'Error when getting tags',
error: err
});
}
if (!tags) {
return res.status(404).json({
message: 'No such tags'
});
}
tags.name = req.body.name ? req.body.name : tags.name;
tags.save(function (err, tags) {
if (err) {
return res.status(500).json({
message: 'Error when updating tags.',
error: err
});
}
return res.json(tags);
});
});
},
/**
* tagsController.remove()
*/
remove: function (req, res) {
var id = req.params.id;
TagsModel.findByIdAndRemove(id, function (err, tags) {
if (err) {
return res.status(500).json({
message: 'Error when deleting the tags.',
error: err
});
}
return res.status(204).json();
});
}
};

View File

@@ -0,0 +1,161 @@
var UserModel = require('../models/userModel.js');
/**
* userController.js
*
* @description :: Server-side logic for managing users.
*/
module.exports = {
/**
* userController.list()
*/
list: function (req, res) {
UserModel.find(function (err, users) {
if (err) {
return res.status(500).json({
message: 'Error when getting user.',
error: err
});
}
return res.json(users);
});
},
/**
* userController.show()
*/
show: function (req, res) {
var id = req.params.id;
UserModel.findOne({_id: id}, function (err, user) {
if (err) {
return res.status(500).json({
message: 'Error when getting user.',
error: err
});
}
if (!user) {
return res.status(404).json({
message: 'No such user'
});
}
return res.json(user);
});
},
/**
* userController.create()
*/
create: function (req, res) {
var user = new UserModel({
username : req.body.username,
password : req.body.password,
email : req.body.email
});
user.save(function (err, user) {
if (err) {
return res.status(500).json({
message: 'Error when creating user',
error: err
});
}
//return res.status(201).json(user);
return res.redirect('/users/login');
});
},
/**
* userController.update()
*/
update: function (req, res) {
var id = req.params.id;
UserModel.findOne({_id: id}, function (err, user) {
if (err) {
return res.status(500).json({
message: 'Error when getting user',
error: err
});
}
if (!user) {
return res.status(404).json({
message: 'No such user'
});
}
user.username = req.body.username ? req.body.username : user.username;
user.password = req.body.password ? req.body.password : user.password;
user.email = req.body.email ? req.body.email : user.email;
user.save(function (err, user) {
if (err) {
return res.status(500).json({
message: 'Error when updating user.',
error: err
});
}
return res.json(user);
});
});
},
/**
* userController.remove()
*/
remove: function (req, res) {
var id = req.params.id;
UserModel.findByIdAndRemove(id, function (err, user) {
if (err) {
return res.status(500).json({
message: 'Error when deleting the user.',
error: err
});
}
return res.status(204).json();
});
},
showRegister: function(req, res){
res.render('user/register');
},
showLogin: function(req, res){
res.render('user/login');
},
login: function(req, res, next){
UserModel.authenticate(req.body.username, req.body.password, function(err, user){
if(err || !user){
var err = new Error('Wrong username or paassword');
err.status = 401;
return next(err);
}
req.session.userId = user._id;
res.redirect('/mdata');
});
},
logout: function(req, res, next){
if(req.session){
req.session.destroy(function(err){
if(err){
return next(err);
} else{
return res.redirect('/');
}
});
}
}
};

View File

@@ -0,0 +1,23 @@
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var mdataSchema = new Schema({
'user_id' : {
type: Schema.Types.ObjectId,
ref: 'user'
},
'title' : String,
'question' : String,
'timestamp' : Date,
'tags' : Array,
'answering_id' : {
type: Schema.Types.ObjectId,
ref: 'mdata'
},
'answer' : {
type: Schema.Types.ObjectId,
ref: 'mdata'
}
});
module.exports = mongoose.model('mdata', mdataSchema);

View File

@@ -0,0 +1,15 @@
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var photoSchema = new Schema({
'name' : String,
'path' : String,
'postedBy' : {
type: Schema.Types.ObjectId,
ref: 'user'
},
'views' : Number,
'likes' : Number
});
module.exports = mongoose.model('photo', photoSchema);

View File

@@ -0,0 +1,8 @@
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var tagsSchema = new Schema({
'name' : String
});
module.exports = mongoose.model('tags', tagsSchema);

View File

@@ -0,0 +1,44 @@
var mongoose = require('mongoose');
var bcrypt = require('bcrypt');
var Schema = mongoose.Schema;
var userSchema = new Schema({
'username' : String,
'password' : String,
'email' : String
});
userSchema.pre('save', function(next){
var user = this;
bcrypt.hash(user.password, 10, function(err, hash){
if(err){
return next(err);
}
user.password = hash;
next();
});
});
userSchema.statics.authenticate = function(username, password, callback){
User.findOne({username: username})
.exec(function(err, user){
if(err){
return callback(err);
} else if(!user) {
var err = new Error("User not found.");
err.status = 401;
return callback(err);
}
bcrypt.compare(password, user.password, function(err, result){
if(result === true){
return callback(null, user);
} else{
return callback();
}
});
});
}
var User = mongoose.model('user', userSchema);
module.exports = User;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
{
"name": "vaja3",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www",
"dev": "nodemon ./bin/www"
},
"dependencies": {
"bcrypt": "^5.0.1",
"connect-mongo": "^4.6.0",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"express-session": "^1.17.2",
"hbs": "~4.0.4",
"http-errors": "~1.6.3",
"mongoose": "^6.2.8",
"morgan": "~1.9.1",
"multer": "^1.4.4"
}
}

View File

@@ -0,0 +1,18 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}
nav ul{
list-style: none;
display: flex;
gap: 20px;
}
nav ul li a{
text-decoration: none;
}

View File

@@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.redirect('/mdata');
});
module.exports = router;

View File

@@ -0,0 +1,44 @@
var express = require('express');
var router = express.Router();
var mdataController = require('../controllers/mdataController.js');
function requiresLogin(req, res, next){
if(req.session && req.session.userId){
return next();
} else{
return res.redirect('/mdata');
}
}
/*
* GET
*/
router.get('/', mdataController.list);
router.get('/publish', requiresLogin, mdataController.publish);
router.get('/my', requiresLogin, mdataController.listmy);
router.get('/delete/:id', requiresLogin, mdataController.remove);
router.get('/update/:id/:ans', requiresLogin, mdataController.update);
/*
* GET
*/
router.get('/:id', mdataController.show);
/*
* POST
*/
router.post('/', mdataController.create);
/*
* PUT
*/
router.put('/:id', mdataController.update);
/*
* DELETE
*/
router.delete('/:id', mdataController.remove);
module.exports = router;

View File

@@ -0,0 +1,29 @@
var express = require('express');
// Vključimo multer za file upload
var multer = require('multer');
var upload = multer({dest: 'public/images/'});
var router = express.Router();
var photoController = require('../controllers/photoController.js');
function requiresLogin(req, res, next){
if(req.session && req.session.userId){
return next();
} else{
var err = new Error("You must be logged in to view this page");
err.status = 401;
return next(err);
}
}
router.get('/list', photoController.list);
router.get('/publish', requiresLogin, photoController.publish);
router.get('/:id', photoController.show);
router.post('/', requiresLogin, upload.single('image'), photoController.create);
router.put('/:id', photoController.update);
router.delete('/:id', photoController.remove);
module.exports = router;

View File

@@ -0,0 +1,30 @@
var express = require('express');
var router = express.Router();
var tagsController = require('../controllers/tagsController.js');
/*
* GET
*/
router.get('/', tagsController.list);
/*
* GET
*/
router.get('/:id', tagsController.show);
/*
* POST
*/
router.post('/', tagsController.create);
/*
* PUT
*/
router.put('/:id', tagsController.update);
/*
* DELETE
*/
router.delete('/:id', tagsController.remove);
module.exports = router;

View File

@@ -0,0 +1,19 @@
var express = require('express');
var router = express.Router();
var userController = require('../controllers/userController.js');
router.get('/', userController.list);
router.get('/register', userController.showRegister);
router.get('/login', userController.showLogin);
router.get('/logout', userController.logout);
router.get('/:id', userController.show);
router.post('/', userController.create);
router.post('/login', userController.login);
router.put('/:id', userController.update);
router.delete('/:id', userController.remove);
module.exports = router;

View File

@@ -0,0 +1,3 @@
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>{{title}}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
<script defer src="https://unpkg.com/htmx.org@1.8.6"></script>
</head>
<body class="container">
</br>
<nav class="navbar bg-light-subtle">
<div class="container-fluid navbar-expand-lg">
<div class="navbar-nav">
<a class="nav-link" href='/mdata'>Domov</a>
{{#if session.userId}}
<a class="nav-link" href='/mdata/publish'>Objavi uprasanje</a>
<a class="nav-link" href='/mdata/my'>Moji</a>
<a class="nav-link" href='/users/logout'>Odjava</a>
{{else}}
<a class="nav-link" href='/users/login'>Prijava</a>
<a class="nav-link" href='/users/register'>Registracija</a>
{{/if}}
</div>
</div>
</nav>
</br>
{{{body}}}
</body>
</html>

View File

@@ -0,0 +1,11 @@
<h1>Uprasanja:</h1>
<hr>
{{#each mdatas}}
<div class="card my-3">
<div class="card-body">
<h3 class="card-title">{{title}}</h3>
<p class="card-text">{{question}}</p>
<a href="/mdata/{{id}}" class="btn btn-primary">Podrobno</a>
</div>
</div>
{{/each}}

View File

@@ -0,0 +1,28 @@
<h1>Moji Uprasanja:</h1>
<hr/>
<div class="container">
{{#each mdatas}}
<div class="row m-1">
<div class="col-10">
<h2>{{title}}</h2>
</div>
<div class="col-2 text-end" >
<a class="btn btn-primary" href="/mdata/{{id}}">Podrobno</a>
</div>
</div>
<div class="row m-1">
<div class="col-10" >
<p>{{question}}</p>
</div>
<div class="col-2 text-end" >
<a class="btn btn-danger" href="/mdata/delete/{{id}}">izbrisi</a>
</div>
</div>
<hr/>
{{/each}}
</div>

View File

@@ -0,0 +1,14 @@
<h1>Dodaj uprasanje</h1>
<form action="/mdata" method="post">
<label class="form-label" >Naslov</label>
<input class="form-control" type="text" name="title">
<br/>
<label class="form-label" >Uprasanje</label>
<textarea class="form-control" rows="3" name="question"></textarea>
<br/>
<input class="btn btn-primary" type="submit" value="Dodaj">
</form>

View File

@@ -0,0 +1,57 @@
{{#if answering_id}}
<h3>Odgovor: </h3>
{{else}}
<h3>Vprasanje: </h3>
{{/if}}
<div class="card my-3">
<div class="card-body">
<h3 class="card-title">{{title}}</h3>
<p class="card-text">{{question}}</p>
</div>
</div>
<hr>
{{#if cur}}
<h3>Pravi odgovor: </h3>
<div class="card my-3 bg-warning">
<div class="card-body">
<p class="card-text">{{cur.question}}</p>
<a class="btn btn-primary me-2" href="/mdata/{{cur.id}}">Podrobno</a>
</div>
</div>
<hr>
{{/if}}
{{#if answers}}
<h3>Odgovori: </h3>
{{#each answers}}
<div class="card my-3">
<div class="card-body">
<p class="card-text">{{question}}</p>
<a class="btn btn-primary me-2" href="/mdata/{{id}}">Podrobno</a>
{{#if ../selectAns}}
<a class="btn btn-primary" href="/mdata/update/{{../id}}/{{id}}">Pravi</a>
{{/if}}
</div>
</div>
{{/each}}
<hr>
{{/if}}
{{#if session.userId}}
<form action="/mdata" method="post" class="mt-5">
<input type="hidden" name="answering_id" value="{{id}}">
<div class="mb-3">
<label class="form-label">Odgovor</label>
<textarea class="form-control" rows="3" name="question" required></textarea>
</div>
<button class="btn btn-primary" type="submit">Dodaj</button>
</form>
{{/if}}

View File

@@ -0,0 +1,8 @@
<h1>Slike:</h1>
{{#each photos}}
<h2>slika:</h2>
<img src="{{path}}" title="{{name}}" width="400"/><br/>
Objavil: <span>{{postedBy.username}}</span>
Objavil: <span>{{id}}</span>
<hr/>
{{/each}}

View File

@@ -0,0 +1,8 @@
<h1>Dodaj sliko</h1>
<form action="/photos" method="post" enctype="multipart/form-data">
<input type="text" name="name" placeholder="ime slike" required>
<input type="file" name="image">
<div class="tp">
<input type="submit" value="Dodaj">
</div>
</form>

View File

@@ -0,0 +1,17 @@
<h1 class="text-center mb-5">Prijava</h1>
<form action="login" method="post" class="mx-auto w-50">
<div class="mb-3">
<label class="form-label" >Uporabniško ime</label>
<input class="form-control" type="text" name="username" placeholder="Uporabniško ime" required>
</div>
<div class="mb-3">
<label class="form-label" >Geslo</label>
<input class="form-control" type="password" name="password" placeholder="Geslo" required>
</div>
<br/>
<div class="text-center">
<input class="btn btn-primary" type="submit" value="prijava">
</div>
</form>

View File

@@ -0,0 +1,22 @@
<h1 class="text-center mb-5">Registracija</h1>
<form action="/users" method="post" class="mx-auto w-50">
<div class="mb-3">
<label class="form-label">Email</label>
<input class="form-control" type="text" name="email" placeholder="E-mail" required>
</div>
<div class="mb-3">
<label class="form-label">Uporabniško ime</label>
<input class="form-control" type="text" name="username" placeholder="Username" required>
</div>
<div class="mb-3">
<label class="form-label">Geslo</label>
<input class="form-control" type="password" name="password" placeholder="Password" required>
</div>
<div class="text-center">
<button class="btn btn-primary" type="submit">REGISTER</button>
</div>
</form>

View File

@@ -0,0 +1 @@
.vscode

View File

@@ -0,0 +1,78 @@
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
-- Zbirka podatkov: `vaja1`
CREATE DATABASE IF NOT EXISTS `baza` DEFAULT CHARACTER SET utf32 COLLATE utf32_slovenian_ci;
USE `baza`;
-- Struktura tabele `users`
DROP TABLE IF EXISTS `users`;
CREATE TABLE IF NOT EXISTS `users` (
`id` int NOT NULL AUTO_INCREMENT,
`username` text COLLATE utf32_slovenian_ci NOT NULL,
`password` text COLLATE utf32_slovenian_ci NOT NULL,
`email` text COLLATE utf32_slovenian_ci,
`ime` text COLLATE utf32_slovenian_ci,
`priimek` text COLLATE utf32_slovenian_ci,
`naslov` text COLLATE utf32_slovenian_ci,
`posta` text COLLATE utf32_slovenian_ci,
`telefon` text COLLATE utf32_slovenian_ci,
`adm` int NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf32 COLLATE=utf32_slovenian_ci;
-- Struktura tabele `ads`
DROP TABLE IF EXISTS `ads`;
CREATE TABLE IF NOT EXISTS `ads` (
`id` int NOT NULL AUTO_INCREMENT,
`title` text COLLATE utf32_slovenian_ci NOT NULL,
`description` text COLLATE utf32_slovenian_ci NOT NULL,
`user_id` int NOT NULL,
`date` DATETIME COLLATE utf32_slovenian_ci NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_ads_users` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf32 COLLATE=utf32_slovenian_ci;
-- Struktura tabele `categorys`
DROP TABLE IF EXISTS `categorys`;
CREATE TABLE IF NOT EXISTS `categorys` (
`id` int NOT NULL AUTO_INCREMENT,
`title` text COLLATE utf32_slovenian_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf32 COLLATE=utf32_slovenian_ci;
INSERT INTO categorys (title)
VALUES ("Telefon"),("Avto"),("Narava"),("Hisa"),("Motor");
DROP TABLE IF EXISTS `ads_categorys`;
CREATE TABLE IF NOT EXISTS `ads_categorys` (
`id` int NOT NULL AUTO_INCREMENT,
`ads_id` int NOT NULL,
`categorys_id` int NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_cat_ad` FOREIGN KEY (`ads_id`) REFERENCES `ads`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_ad_cat` FOREIGN KEY (`categorys_id`) REFERENCES `categorys`(`id`) ON DELETE CASCADE
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf32 COLLATE=utf32_slovenian_ci;
DROP TABLE IF EXISTS `comments`;
CREATE TABLE IF NOT EXISTS `comments` (
`id` int NOT NULL AUTO_INCREMENT,
`ads_id` int NOT NULL,
`users_id` int NOT NULL,
`content` text COLLATE utf32_slovenian_ci,
`date` DATETIME COLLATE utf32_slovenian_ci NOT NULL,
`country` text COLLATE utf32_slovenian_ci,
PRIMARY KEY (`id`),
CONSTRAINT `fk_ad_comm` FOREIGN KEY (`ads_id`) REFERENCES `ads`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_comm_us` FOREIGN KEY (`users_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf32 COLLATE=utf32_slovenian_ci;
DROP TABLE IF EXISTS `ads_image`;
CREATE TABLE IF NOT EXISTS `ads_image` (
`id` int NOT NULL AUTO_INCREMENT,
`ads_id` int NOT NULL,
`image` text COLLATE utf32_slovenian_ci NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_ad_img` FOREIGN KEY (`ads_id`) REFERENCES `ads`(`id`) ON DELETE CASCADE
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf32 COLLATE=UTF32_SLOVENIAN_CI;

View File

@@ -0,0 +1,18 @@
<?php
//razred, ki skrbi za povezavo z bazo (Vzorec MVC zagovarja principe OOP)
class Db
{
private static $instance = NULL;
//Funkcija getInstance vrne povezavo z bazo. Ob prvem klicu ustvari povezavo in jo shrani v statični spremenljivki. Ob nadaljnjih klicih vrača povezavo iz spomina
public static function getInstance()
{
if (!isset(self::$instance)) {
self::$instance = mysqli_connect("localhost", "root", "", "baza");
self::$instance->set_charset("UTF8");
}
return self::$instance;
}
}

View File

@@ -0,0 +1,158 @@
<?php
/*
Controller za oglase. Vključuje naslednje standardne akcije:
index: izpiše vse oglase
show: izpiše posamezen oglas
create: izpiše obrazec za vstavljanje oglasa
store: vstavi obrazec v bazo
edit: izpiše vmesnik za urejanje oglasa
update: posodobi oglas v bazi
delete: izbriše oglas iz baze
*/
class ads_controller
{
public function index()
{
//s pomočjo statične metode modela, dobimo seznam vseh oglasov
//$ads bo na voljo v pogledu za vse oglase index.php
$ads = Ad::all();
//pogled bo oblikoval seznam vseh oglasov v html kodo
require_once('views/ads/index.php');
}
public function show()
{
//preverimo, če je uporabnik podal informacijo, o oglasu, ki ga želi pogledati
if (!isset($_GET['id'])) {
if(isset($_SESSION["USER_ID"]))
{
$ads = Ad::findUser($_SESSION["USER_ID"]);
require_once('views/ads/myAdds.php');
}
else
{
return call('pages', 'error');
}
}
else
{
$ad = Ad::find($_GET['id']);
require_once('views/ads/show.php');
}
}
public function create()
{
if(!isset($_SESSION["USER_ID"])){
header("Location: /admin/index.php");
die();
}
// Izpišemo pogled z obrazcem za vstavljanje oglasa
$categorys = Categorys::all();
require_once('views/ads/create.php');
}
public function store()
{
if(!isset($_SESSION["USER_ID"])){
header("Location: /admin/index.php");
die();
}
// Obdelamo podatke iz obrazca (views/ads/create.php), akcija pričakuje da so podatki v $_POST
// Tukaj bi morali podatke še validirati, preden jih dodamo v bazo
// Pokličemo metodo za ustvarjanje novega oglasa
$ad = Ad::insert($_POST["title"], $_POST["description"], $_FILES["image"], $_POST['categorys']);
//ko je oglas dodan, imamo v $ad podatke o tem novem oglasu
//uporabniku lahko pokažemo pogled, ki ga bo obvestil o uspešnosti oddaje oglasa
call('ads', 'show');
}
public function edit()
{
if(!isset($_SESSION["USER_ID"])){
header("Location: /admin/index.php");
die();
}
// Ob klicu akcije se v URL poda GET parameter z ID-jem oglasa, ki ga urejamo
// Od modela pridobimo podatke o oglasu, da lahko predizpolnimo vnosna polja v obrazcu
if (!isset($_GET['id'])) {
return call('pages', 'error');
}
$ad = Ad::find($_GET['id']);
require_once('views/ads/edit.php');
}
public function update()
{
if(!isset($_SESSION["USER_ID"])){
header("Location: /admin/index.php");
die();
}
// Obdelamo podatke iz obrazca (views/ads/edit.php), ki pridejo v $_POST.
// Pričakujemo, da je v $_POST podan tudi ID oglasa, ki ga posodabljamo.
if (!isset($_POST['id'])) {
return call('pages', 'error');
}
// Naložimo oglas
$ad = Ad::find($_POST['id']);
// Pokličemo metodo, ki posodobi obstoječi oglas v bazi
$ad->update($_POST["title"], $_POST["description"]);
header("Location: /admin/index.php?controller=ads&action=show");
die();
}
public function addImg()
{
if(!isset($_SESSION["USER_ID"])){
header("Location: /admin/index.php");
die();
}
if (!isset($_GET['id'])) {
return call('pages', 'error');
}
if(isset($_POST['submit']))
{
$ad = Ad::find($_GET['id']);
$id = $ad->addImg($_FILES["image"]);
header("Location: /admin/index.php?controller=ads&action=show");
die();
}
else
{
$ad = Ad::find($_GET['id']);
require_once('views/ads/addImg.php');
}
}
public function delete()
{
if(!isset($_SESSION["USER_ID"])){
header("Location: /admin/index.php");
die();
}
// Obdelamo zahtevo za brisanje oglasa. Akcija pričakuje, da je v URL-ju podan ID oglasa.
if (!isset($_GET['id'])) {
return call('pages', 'error');
}
// Poiščemo oglas
$ad = Ad::find($_GET['id']);
// Kličemo metodo za izbris oglasa iz baze.
$ad->delete();
header("Location: /admin/index.php?controller=ads&action=show");
die();
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
Enostaven primer controlletja, ki ne uporablja modela.
Njegova naloga je, da vrača statične HTML strani, kot je stran z napako.
Uporabili smo ga tudi za prikaz vmesnika, ki demonstrira uporabi API-ja.
*/
class pages_controller {
public function error() {
// Izpiše pogled s sporočilom o napaki
require_once('views/pages/error.php');
}
public function api(){
if(isset($_SESSION["USER_ID"]) && $_SESSION["ADM"] == 1){
//Prikaži vmesnik za upravljanje z API-jem
require_once('views/pages/api.php');
}
}
public function api1(){
if(isset($_SESSION["USER_ID"]) && $_SESSION["ADM"] == 1){
//Prikaži vmesnik za upravljanje z API-jem
require_once('views/pages/api1.php');
}
}
public function logout(){
require_once('views/pages/logout.php');
}
public function login(){
if(isset($_POST["submit"])){
//Preveri prijavne podatke
if(($user = User::validate_login($_POST["username"], $_POST["password"])) != null){
//Zapomni si prijavljenega uporabnika v seji in preusmeri na index.php
$_SESSION["USER_ID"] = $user->id;
$_SESSION["ADM"] = $user->adm;
header("Location: /admin/index.php");
die();
}
}
require_once('views/pages/login.php');
}
public function register(){
$error = "";
if(isset($_POST["submit"]))
{
//Preveri, če uporabniško ime že obstaja
if(User::username_exists($_POST["username"]))
{
$error = "Uporabniško ime že obstaja!";
}
else if($_POST["password"] != $_POST["repeat_password"])
{
$error = "Gesli se ne ujemata!";
}
else{
User::createUser($_POST["username"], $_POST["password"], $_POST["email"], $_POST["ime"], $_POST["priimek"], $_POST["naslov"], $_POST["posta"], $_POST["telefon"]);
header("Location: /admin/index.php?controller=pages&action=login");
die();
}
}
require_once('views/pages/register.php');
}
}
?>

View File

@@ -0,0 +1,32 @@
<?php
/*
Vstopna točka naše aplikacije. Vse zahteve gredo skozi index.php, ki poskrbi za ustrezno obravnavo.
V URL-ju se bosta podala dva parametra: controller in action, ki bosta določala, katera akcija se izvede.
S pomočjo .htaccess lahko skrajšamo URL naslove (več v .htaccess datoteki).
*/
require_once('connection.php');
session_start();
// Seja poteče po 30 minutah - avtomatsko odjavi neaktivnega uporabnika
if(isset($_SESSION['LAST_ACTIVITY']) && time() - $_SESSION['LAST_ACTIVITY'] > 1800){
session_unset();
session_destroy();
header("Location: /admin/index.php");
die();
}
$_SESSION['LAST_ACTIVITY'] = time();
// Razberemo namero uporabnika preko query string parametrov controller in action
if (isset($_GET['controller']) && isset($_GET['action'])) {
$controller = $_GET['controller'];
$action = $_GET['action'];
} else {
// Če uporabnik ni podal svoje zahteve v pravilni obliki, ga preusmerimo na privzeto akcijo
$controller = 'ads';
$action = 'index';
}
// Vključimo layout, torej splošni izgled strani, layout pa vključuje router (routes.php)
require_once('views/layout.php');

View File

@@ -0,0 +1,169 @@
<?php
/*
Model za oglas. Vsebuje lastnosti, ki definirajo strukturo oglasa in sovpadajo s stolpci v bazi.
Nekatere metode so statične, ker niso vezane na posamezen oglas: poišči vse oglase, vstavi nov oglas, ...
Druge so statične, ker so vezane na posamezen oglas: posodobi oglas, izbriši oglas, ...
V modelu moramo definirati tudi relacije oz. povezane entitete/modele. V primeru oglasa je to $user, ki
povezuje oglas z uporabnikom, ki je oglas objavil. Relacija nam poskrbi za nalaganje podatkov o uporabniku,
da nimamo samo user_id, ampak tudi username, ...
*/
require_once 'users.php'; // Vključimo model za uporabnike
require_once 'ads_image.php'; // Vključimo model za slike
require_once 'categorys.php'; // Vključimo model za kategorije
class Ad
{
public $id;
public $title;
public $description;
public $user;
public $ad_img;
public $ads_categorys;
// Konstruktor
public function __construct($id, $title, $description, $user_id)
{
$this->id = $id;
$this->title = $title;
$this->description = $description;
$this->user = User::findUserName($user_id);
$this->ad_img = ads_image::find($id);
$this->ads_categorys = Categorys::find($id);
}
// Metoda, ki iz baze vrne vse oglase
public static function all()
{
$db = Db::getInstance(); // pridobimo instanco baze
$query = "SELECT * FROM ads ORDER BY ads.date DESC;"; // pripravimo query
$res = $db->query($query); // poženemo query
$ads = array();
while ($ad = $res->fetch_object()) {
// Za vsak rezultat iz baze ustvarimo objekt (kličemo konstuktor) in ga dodamo v array $ads
array_push($ads, new Ad($ad->id, $ad->title, $ad->description, $ad->user_id));
}
return $ads;
}
public static function findUser($id)
{
$db = Db::getInstance();
$id = mysqli_real_escape_string($db, $id);
$query = "SELECT * FROM ads WHERE ads.user_id = '$id';";
$res = $db->query($query);
$ads = array();
while ($ad = $res->fetch_object()) {
// Za vsak rezultat iz baze ustvarimo objekt (kličemo konstuktor) in ga dodamo v array $ads
array_push($ads, new Ad($ad->id, $ad->title, $ad->description, $ad->user_id));
}
return $ads;
}
// Metoda, ki vrne en oglas z specifičnim id-jem iz baze
public static function find($id)
{
$db = Db::getInstance();
$id = mysqli_real_escape_string($db, $id);
$query = "SELECT * FROM ads WHERE ads.id = '$id';";
$res = $db->query($query);
if ($ad = $res->fetch_object()) {
return new Ad($ad->id, $ad->title, $ad->description, $ad->user_id);
}
return null;
}
// Metoda, ki doda nov oglas v bazo
public static function insert($title, $desc, $img, $categorys)
{
$db = Db::getInstance();
$title = mysqli_real_escape_string($db, $title);
$desc = mysqli_real_escape_string($db, $desc);
$user_id = $_SESSION["USER_ID"]; // user_id vzamemo iz seje (prijavljen uporabnik)
$query = "INSERT INTO ads (title, description, user_id, date)
VALUES('$title', '$desc', '$user_id', NOW());";
if($db->query($query))
{
$id = $db->insert_id;
if(isset($categorys)){
foreach($categorys as $category)
{
$query = "INSERT INTO ads_categorys (ads_id, categorys_id)
VALUES ($id, $category)";
$db->query($query);
}
}
if(isset($img)){
$date = date("YmdHms");
$img_path = "../slike/" . $date . $img['name'];
move_uploaded_file($img['tmp_name'], $img_path);
$img_path = "slike/" . $date . $img['name'];
$img_path = "/" . $img_path;
$img_path = mysqli_real_escape_string($db, $img_path);
$query = "INSERT INTO ads_image (ads_id, image) VALUES ('$id', '$img_path')";
$db->query($query);
}
return Ad::find($id);
}
else
{
return null; // v primeru napake vrnemo null
}
}
// Metoda, ki posodobi obstoječ oglas v bazi
public function update($title, $desc)
{
$db = Db::getInstance();
$id = mysqli_real_escape_string($db, $this->id);
$title = mysqli_real_escape_string($db, $title);
$desc = mysqli_real_escape_string($db, $desc);
$query = "UPDATE ads SET title = '$title', description = '$desc' WHERE id = '$id'";
if ($db->query($query)) {
return $id; //iz baze pridobimo posodobljen oglas in ga vrnemo controllerju
} else {
return null;
}
}
public function addImg($img)
{
$db = Db::getInstance();
$date = date("YmdHms");
$img_path = "../slike/" . $date . $img['name'];
move_uploaded_file($img['tmp_name'], $img_path);
$img_path = "slike/" . $date . $img['name'];
$img_path = "/" . $img_path;
$img_path = mysqli_real_escape_string($db, $img_path);
$query = "INSERT INTO ads_image (ads_id, image) VALUES ('$this->id', '$img_path')";
if ($db->query($query)) {
return $this->id; //iz baze pridobimo posodobljen oglas in ga vrnemo controllerju
} else {
return null;
}
}
// Metoda, ki izbriše oglas iz baze
public function delete()
{
$db = Db::getInstance();
$id = mysqli_real_escape_string($db, $this->id);
$query = "DELETE FROM ads WHERE id = '$id'";
if ($db->query($query)) {
return true;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
class ads_image
{
public $id;
public $ads_id;
public $image;
// Konstruktor
public function __construct($id, $ads_id, $image)
{
$this->id = $id;
$this->ads_id = $ads_id;
$this->image = $image;
}
// Metoda, ki vrne uporabnika z določenim ID-jem iz baze
public static function find($ads_id)
{
$db = Db::getInstance();
$id = mysqli_real_escape_string($db, $ads_id);
$query = "SELECT * FROM ads_image WHERE ads_image.ads_id = '$ads_id'";
$res = $db->query($query);
$images = array();
while ($img = $res->fetch_object()) {
array_push($images, new ads_image($img->id, $img->ads_id, $img->image));
}
return $images;
}
}

View File

@@ -0,0 +1,44 @@
<?php
class Categorys
{
public $id;
public $title;
// Konstruktor
public function __construct($id, $title)
{
$this->id = $id;
$this->title = $title;
}
// Metoda, ki vrne uporabnika z določenim ID-jem iz baze
public static function all()
{
$db = Db::getInstance();
$query = "SELECT * FROM categorys";
$res = $db->query($query);
$categorys = array();
while ($category = $res->fetch_object()) {
array_push($categorys, new Categorys($category->id, $category->title));
}
return $categorys;
}
public static function find($id)
{
$db = Db::getInstance();
$query = "SELECT categorys.id, categorys.title FROM ads_categorys, categorys
WHERE ads_categorys.ads_id = '$id'
AND ads_categorys.categorys_id = categorys.id";
$res = $db->query($query);
$categorys = array();
while ($category = $res->fetch_object()) {
array_push($categorys, new Categorys($category->id, $category->title));
}
return $categorys;
}
}

View File

@@ -0,0 +1,94 @@
<?php
require_once 'users.php';
class Comments
{
public $id;
public $ads_id;
public $user;
public $content;
public $country;
// Konstruktor
public function __construct($id, $ads_id, $users_id, $content, $country)
{
$this->id = $id;
$this->ads_id = $ads_id;
$this->user = User::findUserName($users_id);;
$this->content = $content;
$this->country = $country;
}
public static function insert($ads_id, $users_id, $content)
{
$url = "http://ip-api.com/json/" . $_SERVER['REMOTE_ADDR'] . "?fields=16385";
$data = file_get_contents($url);
$data = json_decode($data, true);
if($data['status'] == 'success'){
$country = $data['country'];
} else {
$country = 'localHost';
}
$db = Db::getInstance();
$content = mysqli_real_escape_string($db, $content);
$ads_id = mysqli_real_escape_string($db, $ads_id);
$query = "INSERT INTO comments (ads_id, users_id, content, date, country)
VALUES ('$ads_id', '$users_id', '$content', NOW(), '$country');";
if($res = $db->query($query)) {
return new Comments($db->insert_id, $ads_id, $users_id, $content, $country);
} else {
return array();
}
}
public static function all()
{
$db = Db::getInstance();
$query = "SELECT * FROM comments ORDER BY comments.date DESC LIMIT 5";
$res = $db->query($query);
$comments = array();
while ($commnet = $res->fetch_object()) {
array_push($comments, new Comments($commnet->id, $commnet->ads_id, $commnet->users_id, $commnet->content, $commnet->country));
}
return $comments;
}
public static function findForAd($ads_id)
{
$db = Db::getInstance();
$id = mysqli_real_escape_string($db, $ads_id);
$query = "SELECT * FROM comments WHERE comments.ads_id = '$ads_id'";
$res = $db->query($query);
$comments = array();
while ($commnet = $res->fetch_object()) {
array_push($comments, new Comments($commnet->id, $commnet->ads_id, $commnet->users_id, $commnet->content, $commnet->country));
}
return $comments;
}
public static function find($id)
{
$db = Db::getInstance();
$query = "SELECT * FROM comments WHERE comments.id = '$id'";
$res = $db->query($query);
if ($commnet = $res->fetch_object()) {
return new Comments($commnet->id, $commnet->ads_id, $commnet->users_id, $commnet->content, $commnet->country);
}
}
public function delete()
{
$db = Db::getInstance();
$id = mysqli_real_escape_string($db, $this->id);
$query = "DELETE FROM comments WHERE id = '$id'";
if ($db->query($query)) {
return true;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,8 @@
<?php
/*
Prazna datoteka, ki simulira model za 'pages_controller'.
Potrebujemo jo, ker bi sicer dobili napako pri dinamičnem nalaganju datotek v funkciji call (routes.php).
Kot alternativa, bi lahko modele nalagali v controllerjih, namesto v funkciji call. Potem te datoteke ne bi potrebovali.
*/
require_once 'models/users.php';

View File

@@ -0,0 +1,168 @@
<?php
// Model za uporabnika
/*
Model z uporabniki.
Čeprav nimamo users_controller-ja, ta model potrebujemo pri oglasih,
saj oglas vsebuje podatke o uporabniku, ki je oglas objavil.
Razred implementira metodo find, ki jo uporablja Ads model zato, da
user_id zamenja z instanco objekta User z vsemi podatki o uporabniku.
*/
class User
{
public $id;
public $username;
public $email;
public $ime;
public $priimek;
public $naslov;
public $posta;
public $telefon;
public $adm;
// Konstruktor
public function __construct($id, $username, $adm, $email, $ime, $priimek, $naslov, $posta, $telefon)
{
$this->id = $id;
$this->username = $username;
$this->adm = $adm;
$this->email = $email;
$this->ime = $ime;
$this->priimek = $priimek;
$this->naslov = $naslov;
$this->posta = $posta;
$this->telefon = $telefon;
}
// Metoda, ki vrne uporabnika z določenim ID-jem iz baze
public static function find($id)
{
$db = Db::getInstance();
$id = mysqli_real_escape_string($db, $id);
$query = "SELECT * FROM users WHERE id = $id;";
$res = $db->query($query);
if ($user = $res->fetch_object()) {
return new User($user->id, $user->username, $user->adm, $user->email, $user->ime, $user->priimek, $user->naslov, $user->posta, $user->telefon);
}
return null;
}
public static function findUserName($id)
{
$db = Db::getInstance();
$id = mysqli_real_escape_string($db, $id);
$query = "SELECT users.username FROM users WHERE id = $id;";
$res = $db->query($query);
if ($user = $res->fetch_object()) {
return $user->username;
}
return null;
}
public static function all()
{
$list = array();
$db = Db::getInstance();
$query = "SELECT * FROM users;";
$res = $db->query($query);
while ($user = $res->fetch_object()) {
$list[] = new User($user->id, $user->username, $user->adm, $user->email, $user->ime, $user->priimek, $user->naslov, $user->posta, $user->telefon);
}
return $list;
}
public static function validate_login($username, $password)
{
$db = Db::getInstance();
$username = mysqli_real_escape_string($db, $username);
$pass = sha1($password);
$query = "SELECT * FROM users WHERE username='$username' AND password='$pass'";
$res = $db->query($query);
if($user_obj = $res->fetch_object()){
return new User($user_obj->id, $user_obj->username, $user_obj->adm, $user_obj->email, $user_obj->ime, $user_obj->priimek, $user_obj->naslov, $user_obj->posta, $user_obj->telefon);
}
return null;
}
public static function username_exists($username){
$db = Db::getInstance();
$username = mysqli_real_escape_string($db, $username);
$query = "SELECT * FROM users WHERE username='$username'";
$res = $db->query($query);
return mysqli_num_rows($res) > 0;
}
public static function createUser($username, $password, $email, $ime, $priimek, $naslov, $posta, $telefon){
$db = Db::getInstance();
$username = mysqli_real_escape_string($db, $username);
$password = sha1($password);
$email = mysqli_real_escape_string($db, $email);
$ime = mysqli_real_escape_string($db, $ime);
$priimek = mysqli_real_escape_string($db, $priimek);
$naslov = mysqli_real_escape_string($db, $naslov);
$posta = mysqli_real_escape_string($db, $posta);
$telefon = mysqli_real_escape_string($db, $telefon);
$query = "INSERT INTO users (username, password, email, ime, priimek, naslov, posta, telefon)
VALUES ('$username', '$password', '$email', '$ime', '$priimek', '$naslov', '$posta', '$telefon');";
if($db->query($query)){
return true;
}
else{
return false;
}
}
public static function insert($username, $password){
$db = Db::getInstance();
$username = mysqli_real_escape_string($db, $username);
$password = sha1($password);
$query = "INSERT INTO users (username, password, email, ime, priimek, naslov, posta, telefon)
VALUES ('$username', '$password', '', '', '', '', '', '');";
if($db->query($query)){
return $db->insert_id;
}
else{
return false;
}
}
public function delete()
{
$db = Db::getInstance();
$id = mysqli_real_escape_string($db, $this->id);
$query = "DELETE FROM users WHERE id = '$id'";
if ($db->query($query)) {
$query = "DELETE FROM comments WHERE comments.users_id = '$id'";
$db->query($query);
$query = "DELETE FROM ads WHERE ads.user_id = '$id'";
$db->query($query);
return true;
} else {
return false;
}
}
public function update( $username, $email, $ime, $priimek, $naslov, $posta, $telefon)
{
$db = Db::getInstance();
$id = mysqli_real_escape_string($db, $this->id);
$username = mysqli_real_escape_string($db, $username);
$email = mysqli_real_escape_string($db, $email);
$ime = mysqli_real_escape_string($db, $ime);
$priimek = mysqli_real_escape_string($db, $priimek);
$naslov = mysqli_real_escape_string($db, $naslov);
$posta = mysqli_real_escape_string($db, $posta);
$telefon = mysqli_real_escape_string($db, $telefon);
$query = "UPDATE users SET
username = '$username', email = '$email', ime = '$ime', priimek = '$priimek', naslov = '$naslov', posta = '$posta', telefon = '$telefon' WHERE id = '$id'";
if ($db->query($query)) {
return true;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
Usmerjevalnik (router) skrbi za obravnavo HTTP zahtev. Glede na zahtevo,
pokliče ustrezno akcijo v zahtevanem controllerju.
*/
// Funkcija, ki kliče kontrolerje in hkrati vključuje njihovo kodo in kodo modela
function call($controller, $action)
{
// Vključimo kodo controllerja in modela (pazimo na poimenovanje datotek)
require_once('controllers/' . $controller . '_controller.php');
require_once('models/' . $controller . '.php');
// Ustvarimo kontroler
$o = $controller . "_controller"; //generiramo ime razreda controllerja
$controller = new $o; //ustvarimo instanco razreda (ime razreda je string spremenljivka)
//pokličemo akcijo na kontrolerju (ime funkcije je string spremenljivka)
$controller->{$action}();
}
// Seznam vseh dovoljenih controllerjev in njihovih akcij. Z njegovo pomočjo bi
// lahko definirali tudi pravice (ustrezno zmanjšali nabor akcij pod določenimi pogoji)
$controllers = array(
'pages' => ['error', 'api', 'login', 'register', 'logout'],
'ads' => ['index', 'show', 'create', 'store', 'edit', 'update', 'delete', 'addImg']
);
// Preverimo, če zahteva kliče controller in akcijo iz zgornjega seznama
if (
array_key_exists($controller, $controllers)
&& in_array($action, $controllers[$controller])
) {
// Pokličemo akcijo
call($controller, $action);
} else {
// Izpišemo stran z napako
call('pages', 'error');
}

View File

@@ -0,0 +1,8 @@
<h2>Dodaj sliko</h2>
<form action="?controller=ads&action=addImg&id=<?php echo $ad->id; ?>" method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label">Slika</label>
<input class="form-control" type="file" name="image" required>
</div>
<input type="submit" name="submit" value="Dodaj sliko" class="btn btn-primary"/>
</form>

View File

@@ -0,0 +1,29 @@
<h2>Objavi oglas</h2>
<form action="?controller=ads&action=store" method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label">Naslov</label>
<input type="text" class="form-control" name="title" required>
</div>
<div class="mb-3">
<label class="form-label">Vsebina</label>
<textarea name="description" class="form-control" rows="3" required></textarea>
</div>
<?php foreach($categorys as $category) : ?>
<div class="form-check">
<input name="categorys[]" value="<?php echo $category->id; ?>" class="form-check-input" type="checkbox">
<label class="form-check-label">
<?php echo $category->title; ?>
</label>
</div>
<?php endforeach?>
<div class="mb-3">
<label class="form-label">Slika</label>
<input class="form-control" type="file" name="image" required>
</div>
<input class="btn btn-primary" type="submit" name="submit" value="Objavi" /> <br />
</form>

View File

@@ -0,0 +1,65 @@
<h2>Uredi oglas</h2>
<form action="?controller=ads&action=update" method="POST" enctype="multipart/form-data">
<!-- ID od oglasa, ki ga želimo urediti, pošljemo v POST s pomočjo avtomatsko izpolnjenega skritega vnosnega polja <input type='hidden'>-->
<input type="hidden" name="id" value="<?php echo $ad->id; ?>" />
<label>Naslov</label>
<input class="form-control" type="text" name="title" value="<?php echo $ad->title; ?>" /> <br />
<label>Vsebina</label>
<textarea class="form-control" name="description" rows="10" cols="50"><?php echo $ad->description; ?></textarea>
<br/>
<input class="btn btn-primary" type="submit" name="submit" value="Shrani" /> <br />
</form>
<table class="table table-striped">
<thead>
<tr>
<th>Username</th>
<th>Content</th>
<th></th>
</tr>
</thead>
<tbody id="comments">
</tbody>
</table>
<script>
$(document).ready(async function() {
await loadComments();
$(".delete_comment_btn").click(deleteClickHandler);
});
function deleteClickHandler()
{
var row = $(this).closest("tr");
deleteComment(row);
row.remove();
}
function deleteComment(row) {
var id = row.attr("id");
$.ajax({
url: '/api/index.php/comments/' + id,
method: 'DELETE'
});
}
async function loadComments() {
await $.get("/api/index.php/comments/<?php echo $ad->id ?>", renderComments);
}
function renderComments(comments) {
comments.forEach(function(comment) {
var row = document.createElement("tr");
row.id = comment.id;
row.innerHTML = "<td>" + comment.user +
"</td><td>" + comment.content +
"</td><td><button class='btn btn-danger delete_comment_btn'>Izbriši</button></td>";
$("#comments").append(row);
});
}
</script>

View File

@@ -0,0 +1,50 @@
<table class="table table-striped">
<thead>
<tr>
<th>Username</th>
<th>Content</th>
<th>Country</th>
<th></th>
</tr>
</thead>
<tbody id="comments">
</tbody>
</table>
<hr/>
<?php foreach ($ads as $ad): ?>
<h2><?php echo $ad->title;?></h2>
<p>
<?php foreach ($ad->ads_categorys as $category): ?>
<?php echo "$category->title ";?>
<?php endforeach; ?>
</p>
<p><img width="200" src="<?php echo $ad->ad_img[0]->image?>"/></p>
<a href='?controller=ads&action=show&id=<?php echo $ad->id; ?>'><button class="btn btn-primary">Prikaži</button></a>
<hr/>
<?php endforeach; ?>
<script>
$(document).ready(async function() {
await loadComments();
});
async function loadComments() {
await $.get("/api/index.php/comments", renderComments);
}
function renderComments(comments) {
comments.forEach(function(comment) {
var row = document.createElement("tr");
row.id = comment.id;
row.innerHTML = "<td>" + comment.user +
"</td><td>" + comment.content +
"</td><td>" + comment.country +
"</td><td><a href='?controller=ads&action=show&id=" + comment.id +"'><button class='btn btn-primary'>Poglej</button></a></td>";
$("#comments").append(row);
});
}
</script>

View File

@@ -0,0 +1,14 @@
<?php foreach($ads as $ad): ?>
<h2><?php echo $ad->title;?></h2>
<?php foreach ($ad->ad_img as $img):?>
<p><img src="<?php echo $img->image;?>" width="200"/></p>
<?php endforeach; ?>
<a href="?controller=ads&action=edit&id=<?php echo $ad->id;?>"><button class="btn btn-primary">Uredi</button></a>
<a href="?controller=ads&action=addImg&id=<?php echo $ad->id;?>"><button class="btn btn-primary">Dodaj sliko</button></a>
<a href="?controller=ads&action=delete&id=<?php echo $ad->id;?>"><button class="btn btn-danger">Odstrani</button></a>
<hr/>
<?php endforeach; ?>

View File

@@ -0,0 +1,74 @@
<h4><?php echo $ad->title; ?></h4>
<p><?php echo $ad->description; ?></p>
<p>
<?php foreach ($ad->ads_categorys as $cat):?>
<?php echo "$cat->title "; ?>
<?php endforeach; ?>
</p>
<?php foreach ($ad->ad_img as $img):?>
<p><img src="<?php echo $img->image;?>" width="400"/></p>
<?php endforeach; ?>
<p>Objavil: <?php echo $ad->user; ?></p>
<a href="/admin/index.php"><button class="btn btn-primary">Nazaj</button></a>
<?php if(isset($_SESSION['USER_ID'])):?>
<hr/>
<div class="row">
<div class="col-md-6">
<h4>Komentarji</h4>
<div class="form-group">
<label>Vsebina</label>
<textarea class="form-control" id="comment_content" rows="3"></textarea>
</div>
<button id="submit_comment_btn" class="btn btn-primary">Dodaj</button>
</div>
</div>
<?php endif; ?>
<hr/>
<div id="comments">
</div>
<script>
$(document).ready(async function() {
await loadComments();
$("#submit_comment_btn").click(submitComment);
});
function submitComment() {
var data = {
content: $("#comment_content").val(),
ad_id: <?php echo "'$ad->id'"; ?>,
};
$("#comment_content").val("");
$.post('/api/index.php/comments/', data, function(data) {
var row = document.createElement("div");
row.id = data.id;
row.innerHTML = "<div class='row'><h4>" + data.user +
"</h4><p>" + data.content +
"</p></div>";
$("#comments").append(row);
});
}
async function loadComments() {
await $.get("/api/index.php/comments/<?php echo $ad->id ?>", renderComments);
}
function renderComments(comments) {
comments.forEach(function(comment) {
var row = document.createElement("div");
row.id = comment.id;
row.innerHTML = "<div class='row'><h4>" + comment.user +
"</h4><p>" + comment.content +
"</p></div>";
$("#comments").append(row);
});
}
</script>

View File

@@ -0,0 +1,39 @@
<html class="bg-dark-subtle">
<head>
<title>PHP</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
</head>
<body class="bg-dark-subtle">
<div class="container">
<h1 class="display-1" >Oglasnik</h1>
<br/>
<nav class="navbar bg-light-subtle">
<div class="container-fluid navbar-expand-lg">
<div class="navbar-nav">
<a class="nav-link" aria-current="page" href="/admin/index.php">Domov</a>
<?php if(isset($_SESSION["USER_ID"])): ?>
<a class="nav-link" href="/admin/index.php?controller=ads&action=create">Objavi oglas</a>
<a class="nav-link" href="/admin/index.php?controller=ads&action=show">Moji Oglasi</a>
<a class="nav-link" href="/admin/index.php?controller=pages&action=logout">Odjava</a>
<?php else: ?>
<a class="nav-link" href="/admin/index.php?controller=pages&action=login">Prijava</a>
<a class="nav-link" href="/admin/index.php?controller=pages&action=register">Registracija</a>
<?php endif; ?>
<?php if(isset($_SESSION["ADM"]) && $_SESSION["ADM"] > 0): ?>
<a class="nav-link" href="/admin/index.php?controller=pages&action=api">Uporabniki</a>
<?php endif; ?>
</div>
</div>
</nav>
<br/>
<!-- tukaj se bo vključevala koda pogledov, ki jih bodo nalagali kontrolerji -->
<!-- klic akcije iz routes bo na tem mestu zgeneriral html kodo, ki bo zalepnjena v našo predlogo -->
<?php require_once('routes.php'); ?>
</div>
</body>
</html>

View File

@@ -0,0 +1,147 @@
<!-- pogled za pregeld vseh oglasov-->
<!-- na vrhu damu uporabniku gumb, s katerim proži akcijo create, da lahko dodaja nove uporabnike -->
<h3>Ustvari oglas:</h3>
Uporabnisko ime: <input type="text" name="username" id="create_ad_username" />
Geslo: <input type="text" name="password" id="create_ad_password" />
<button id="create_user_btn">Dodaj</button>
<hr />
<h3>Seznam vseh oglasov</h3>
<table class="table table-striped">
<thead>
<tr>
<th>Username</th>
<th>Ime</th>
<th>Priimek</th>
<th>Email</th>
<th>Naslov</th>
<th>Posta</th>
<th>Telefon</th>
<th></th>
</tr>
</thead>
<tbody id="ads_tbody">
</tbody>
</table>
<button hidden id="confrm_user_btn" class="btn btn-primary">Potrdi</button>
<button hidden id="cancel_user_btn" class="btn btn-primary">Preklici</button>
<script>
$(document).ready(async function() {
await loadAds();
$("#create_user_btn").click(createAd);
$(".edit_user_btn").click(editClickHandler);
$(".delete_user_btn").click(deleteClickHandler);
$("#confrm_user_btn").click(confrmClickHandler);
$("#cancel_user_btn").click(cancelClickHandler);
});
async function loadAds() {
await $.get("/api/index.php/users", renderAds);
}
function renderAds(ads) {
ads.forEach(function(ad) {
var row = document.createElement("tr");
row.id = ad.id;
row.innerHTML = "<td>" + ad.username +
"</td><td>" + ad.ime +
"</td><td>" + ad.priimek +
"</td><td>" + ad.email +
"</td><td>" + ad.naslov +
"</td><td>" + ad.posta +
"</td><td>" + ad.telefon +
"</td>";
row.innerHTML += "<td><button class='btn btn-primary edit_user_btn'>Uredi</button>" +
"<button class='btn btn-primary delete_user_btn'>Izbriši</button></td>";
$("#ads_tbody").append(row);
});
}
function createAd() {
var data = {
username: $("#create_ad_username").val(),
password: $("#create_ad_password").val()
};
$("#create_ad_username").val("");
$("#create_ad_password").val("");
$.post('/api/index.php/users/', data, function(data) {
var row = document.createElement("tr");
row.id = data.id;
row.innerHTML = "<td>" + data.username +
"</td><td>" + data.ime +
"</td><td>" + data.priimek +
"</td><td>" + data.email +
"</td><td>" + data.naslov +
"</td><td>" + data.posta +
"</td><td>" + data.telefon +
"</td>";
row.innerHTML += "<td><button class='btn btn-primary edit_user_btn'>Uredi</button>" +
"<button class='btn btn-primary delete_user_btn'>Izbriši</button></td>";
$(".edit_user_btn", row).click(editClickHandler);
$(".delete_user_btn", row).click(deleteClickHandler);
$("#ads_tbody").append(row);
});
}
function editClickHandler() {
var row = $(this).closest("tr");
if ($(this).text() == "Uredi") {
$(this).text("Shrani");
row.find('td:not(:nth-last-child(-n+1)').attr('contenteditable', true);
} else {
$(this).text("Uredi");
row.find('td:not(:nth-last-child(-n+1))').attr('contenteditable', false);
updateAd(row);
}
}
function updateAd(row) {
var id = row.attr("id");
var data = {
username: row.find('td:nth-child(1)').text(),
ime: row.find('td:nth-child(2)').text(),
priimek: row.find('td:nth-child(3)').text(),
email: row.find('td:nth-child(4)').text(),
naslov: row.find('td:nth-child(5)').text(),
posta: row.find('td:nth-child(6)').text(),
telefon: row.find('td:nth-child(7)').text()
};
$.ajax({
url: '/api/index.php/users/' + id,
method: 'PUT',
data: JSON.stringify(data),
contentType: 'application/json'
});
}
var rowToBeDeleted;
function deleteClickHandler() {
rowToBeDeleted = $(this).closest("tr");
$("#confrm_user_btn").attr("hidden", false);
$("#cancel_user_btn").attr("hidden", false);
}
function cancelClickHandler() {
$("#confrm_user_btn").attr("hidden", true);
$("#cancel_user_btn").attr("hidden", true);
}
function confrmClickHandler() {
deleteAd(rowToBeDeleted);
rowToBeDeleted.remove();
$("#confrm_user_btn").attr("hidden", true);
$("#cancel_user_btn").attr("hidden", true);
}
function deleteAd(row) {
var id = row.attr("id");
$.ajax({
url: '/api/index.php/users/' + id,
method: 'DELETE'
});
}
</script>

View File

@@ -0,0 +1 @@
<p>Prišlo je do napake.</p>

View File

@@ -0,0 +1,11 @@
<h2>Prijava</h2>
<form action="?controller=pages&action=login" method="POST">
<label class="form-label" >Uporabniško ime</label>
<input class="form-control w-25" type="text" name="username" />
<label class="form-label" >Geslo</label>
<input class="form-control w-25" type="password" name="password" />
<br/>
<input class="btn btn-primary" type="submit" name="submit" value="Pošlji" />
<br/>
</form>

View File

@@ -0,0 +1,5 @@
<?php
session_unset(); //Odstrani sejne spremenljivke
session_destroy(); //Uniči sejo
header("Location: /admin/index.php"); //Preusmeri na index.php
die();

View File

@@ -0,0 +1,35 @@
<h2>Registracija</h2>
<form action="?controller=pages&action=register" method="POST">
<label class="form-label">Uporabniško ime</label>
<input class="form-control w-25" type="text" name="username" required/>
<label class="form-label">Geslo</label>
<input class="form-control w-25" type="password" name="password" required/>
<label class="form-label">Ponovi geslo</label>
<input class="form-control w-25" type="password" name="repeat_password" required/>
<label class="form-label">Email</label>
<input class="form-control w-25" type="email" name="email" required/>
<label class="form-label">Ime</label>
<input class="form-control w-25" type="text" name="ime" required/>
<label class="form-label">Priimek</label>
<input class="form-control w-25" type="text" name="priimek" required/>
<label class="form-label">Naslov</label>
<input class="form-control w-25" type="text" name="naslov" />
<label class="form-label">Posta</label>
<input class="form-control w-25" type="text" name="posta" />
<label class="form-label">Telefon</label>
<input class="form-control w-25" type="text" name="telefon" />
<br/>
<input class="btn btn-primary" type="submit" name="submit" value="Pošlji" />
<br/>
<?php echo $error; ?>
</form>

View File

@@ -0,0 +1,40 @@
<?php
class comments_controller_json
{
public function index()
{
// Iz modela pidobimo vse oglase
$comments = Comments::all();
//izpišemo $ads v JSON formatu
echo json_encode($comments);
}
public function show($id)
{
$comments = Comments::findForAd($id);
echo json_encode($comments);
}
public function store()
{
$comment = Comments::insert($_POST["ad_id"], $_SESSION["USER_ID"], $_POST["content"]);
echo json_encode($comment);
}
public function update($id)
{
}
public function delete($id)
{
// Poiščemo in izbrišemo oglas
$comment = Comments::find($id);
$comment->delete();
// Vrnemo podatke iz izbrisanega oglasa
echo json_encode($comment);
}
}

View File

@@ -0,0 +1,53 @@
<?php
class users_controller_json
{
public function index()
{
// Iz modela pidobimo vse oglase
$users = User::all();
//izpišemo $ads v JSON formatu
echo json_encode($users);
}
public function show($id)
{
$user = User::find($id);
echo json_encode($user);
}
public function store()
{
// Store se pokliče z POST, zato so podatki iz obrazca na voljo v $_POST
$id = User::insert($_POST["username"], $_POST["password"]);
$user = User::find($id);
// Vrnemo vstavljen oglas
echo json_encode($user);
}
public function update($id)
{
// Update se pokliče z PUT, zato nima podatkov v formData ($_POST).
// Namesto tega smo jih poslali v body-u HTTP zahtevka v JSON formatu.
$data = file_get_contents('php://input'); //preberemo body iz zahtevka
$data = json_decode($data, true); //dekodiramo JSON string v PHP array
// Poiščemo in posodobimo oglas
$user = User::find($id);
$user = $user->update($data['username'], $data['email'], $data['ime'], $data['priimek'], $data['naslov'], $data['posta'], $data['telefon']);
// Vrnemo posodobljen oglas
echo json_encode($user);
}
public function delete($id)
{
// Poiščemo in izbrišemo oglas
$user = User::find($id);
$user->delete();
// Vrnemo podatke iz izbrisanega oglasa
echo json_encode($user);
}
}

View File

@@ -0,0 +1,91 @@
<?php
/*
Vstopna točka za našo spletno storitev. Podobno kot pri MVC, bodo tudi vse zahteve na API šle skozi index.php,
ki bo poskrbel za njihovo obravnavo.
Index.php ima tako vlogo routerja, ki na podlagi HTTP zahteve sproži ustrezne akcije.
Za razliko od MVC, bo poleg URL-ja pomembna tudi HTTP metoda v zahtevi, saj REST predpisuje akcije, ki jih prožijo določene metode.
ENDPOINTI:
api/ads/:id/
PUT -> posodobi
GET -> vrni oglas
DELETE -> zbriši oglas
api/ads
POST -> dodaj nov oglas
GET-> vrni vse oglase
S pomočjo .htaccess preslikamo URL-je iz /api.php/foo/bar => /api/foo/bar (več v datoteki .htaccess)
*/
require_once "../admin/connection.php"; //uporabimo povezavo na bazo iz MVC
session_start();
//nastavimo glave odgovora tako, da brskalniku sporočimo, da mu vračamo json
header('Content-Type: application/json');
//omgočimo zahtevo iz različnih domen
header("Access-Control-Allow-Origin: *");
// Kot odgovor iz API-ja izpišemo JSON string s pomočjo funkcije json_encode
// preberemo HTTP metodo iz zahteve
$method = $_SERVER['REQUEST_METHOD'];
// Razberemo parametre iz URL - razbijemo URL po '/'
// tako dobimo iz zahteve api/first/second/third => $request = array("first", "second", "third")
if(isset($_SERVER['PATH_INFO']))
$request = explode('/', trim($_SERVER['PATH_INFO'],'/'));
else
$request="";
$controllers = array( 'users', 'comments' );
// Najprej potrebujemo 'router', ki bo razpoznal zahtevo in sprožil ustrezne akcije
// Preverimo, če je v url-ju prva pot 'ads'
if(!isset($request[0]) || !in_array($request[0], $controllers)){
echo json_encode((object)["status"=>"404", "message"=>"Not found"]);
die();
}
require_once ('../admin/models/' . $request[0] . '.php'); //uporabimo model Ad iz MVC
require_once ('controllers/' . $request[0] . '_controller_json.php'); //vključimo API controller
$o = $request[0] . "_controller_json"; //generiramo ime razreda controllerja
$controller = new $o; //ustvarimo instanco razreda (ime razreda je string spremenljivka)
// Odvisno od metode pokličemo ustrezen controller action
switch($method){
case "GET":
// Če je v zahtevi nastavljen :id, kličemo akcijo show (en oglas), sicer pa index (vsi oglasi)
if(isset($request[1])){
$controller->show($request[1]);
} else {
$controller->index();
}
break;
case "POST":
$controller->store();
break;
case "PUT":
if(!isset($request[1])){
// Če ni podan :id v zahtevi, izpišemo napako
echo json_encode((object)["status"=>"500", "message"=>"Invalid parameters"]);
die();
}
$controller->update($request[1]);
break;
case "DELETE":
if(!isset($request[1])){
// Če ni podan :id v zahtevi, izpišemo napako
echo json_encode((object)["status"=>"500", "message"=>"Invalid parameters"]);
die();
}
$controller->delete($request[1]);
break;
default:
break;
}

View File

@@ -0,0 +1,3 @@
<?php
header("Location: /admin/index.php");
?>

View File

@@ -0,0 +1,130 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@@ -0,0 +1,88 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
// vključimo mongoose in ga povežemo z MongoDB
var mongoose = require('mongoose');
var mongoDB = "mongodb://127.0.0.1/vaja4";
mongoose.connect(mongoDB);
mongoose.Promise = global.Promise;
var db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
// vključimo routerje
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/userRoutes');
var photosRouter = require('./routes/photoRoutes');
var app = express();
var cors = require('cors');
var allowedOrigins = ['http://localhost:3000', 'http://localhost:3001'];
app.use(cors({
credentials: true,
origin: function(origin, callback){
// Allow requests with no origin (mobile apps, curl)
if(!origin) return callback(null, true);
if(allowedOrigins.indexOf(origin)===-1){
var msg = "The CORS policy does not allow access from the specified Origin.";
return callback(new Error(msg), false);
}
return callback(null, true);
}
}));
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'hbs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
/**
* Vključimo session in connect-mongo.
* Connect-mongo skrbi, da se session hrani v bazi.
* Posledično ostanemo prijavljeni, tudi ko spremenimo kodo (restartamo strežnik)
*/
var session = require('express-session');
var MongoStore = require('connect-mongo');
app.use(session({
secret: 'work hard',
resave: true,
saveUninitialized: false,
store: MongoStore.create({mongoUrl: mongoDB})
}));
//Shranimo sejne spremenljivke v locals
//Tako lahko do njih dostopamo v vseh view-ih (glej layout.hbs)
app.use(function (req, res, next) {
res.locals.session = req.session;
next();
});
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/photos', photosRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
//res.render('error');
res.json(err);
});
module.exports = app;

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('vaja3:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3001');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

View File

@@ -0,0 +1,209 @@
var PhotoModel = require('../models/photoModel.js');
/**
* photoController.js
*
* @description :: Server-side logic for managing photos.
*/
module.exports = {
/**
* photoController.list()
*/
list: function (req, res) {
PhotoModel.find()
.populate('postedBy')
.sort({createdAt: -1})
.exec(function (err, photos) {
if (err) {
return res.status(500).json({
message: 'Error when getting photo.',
error: err
});
}
var data = [];
data.photos = photos;
//return res.render('photo/list', data);
return res.json(photos);
});
},
/**
* photoController.show()
*/
show: function (req, res) {
var id = req.params.id;
PhotoModel.findOne({_id: id}).populate('postedBy').exec(function (err, photo) {
if (err) {
return res.status(500).json({
message: 'Error when getting photo.',
error: err
});
}
if (!photo) {
return res.status(404).json({
message: 'No such photo'
});
}
return res.json(photo);
});
},
/**
* photoController.create()
*/
create: function (req, res) {
var photo = new PhotoModel({
name : req.body.name,
path : "/images/"+req.file.filename,
postedBy : req.session.userId,
reports : 0,
likes : 0,
comments : []
});
photo.save(function (err, photo) {
if (err) {
return res.status(500).json({
message: 'Error when creating photo',
error: err
});
}
return res.status(201).json(photo);
//return res.redirect('/photos');
});
},
/**
* photoController.update()
*/
update: function (req, res) {
var id = req.params.id;
PhotoModel.findOne({_id: id}, function (err, photo) {
if (err) {
return res.status(500).json({
message: 'Error when getting photo',
error: err
});
}
if (!photo) {
return res.status(404).json({
message: 'No such photo'
});
}
photo.name = req.body.name ? req.body.name : photo.name;
photo.path = req.body.path ? req.body.path : photo.path;
photo.postedBy = req.body.postedBy ? req.body.postedBy : photo.postedBy;
photo.views = req.body.views ? req.body.views : photo.views;
photo.likes = req.body.likes ? req.body.likes : photo.likes;
photo.save(function (err, photo) {
if (err) {
return res.status(500).json({
message: 'Error when updating photo.',
error: err
});
}
return res.json(photo);
});
});
},
/**
* photoController.remove()
*/
remove: function (req, res) {
var id = req.params.id;
PhotoModel.findByIdAndRemove(id, function (err, photo) {
if (err) {
return res.status(500).json({
message: 'Error when deleting the photo.',
error: err
});
}
return res.status(204).json();
});
},
publish: function(req, res){
return res.render('photo/publish');
},
increaseValue: async function (req, res) {
const id = req.params.id;
const type = req.params.type;
try {
if(type == "like"){
const photo = await PhotoModel.findByIdAndUpdate(
{ _id: id },
{ $inc: { likes: 1 } },
{ new: true }
);
}else if(type == "report"){
const photo = await PhotoModel.findByIdAndUpdate(
{ _id: id },
{ $inc: { reports: 1 } },
{ new: true }
);
if(photo.reports >= 5){
await PhotoModel.findByIdAndRemove(id);
}
}else{
return res.status(500).json({
message: 'Error when updating photo increment.',
});
}
if (!photo) {
return res.status(404).json({
message: 'No such photo'
});
}
return res.status(200).json(photo);
} catch (err) {
return res.status(500).json({
message: 'Error when updating photo.',
error: err
});
}
},
addComment: async function (req, res) {
const id = req.params.id;
const comment = req.body.comment;
try {
const photo = await PhotoModel.findByIdAndUpdate(
{ _id: id },
{ $push: { comments: comment } },
{ new: true }
);
if (!photo) {
return res.status(404).json({
message: 'No such photo'
});
}
return res.json(photo);
} catch (err) {
return res.status(500).json({
message: 'Error when updating photo.',
error: err
});
}
}
};

View File

@@ -0,0 +1,211 @@
var UserModel = require('../models/userModel.js');
var PhotoModel = require('../models/photoModel.js');
const { use } = require('../routes/userRoutes.js');
var mongoose = require('mongoose');
/**
* userController.js
*
* @description :: Server-side logic for managing users.
*/
module.exports = {
/**
* userController.list()
*/
list: function (req, res) {
UserModel.find(function (err, users) {
if (err) {
return res.status(500).json({
message: 'Error when getting user.',
error: err
});
}
return res.json(users);
});
},
/**
* userController.show()
*/
show: function (req, res) {
var id = req.params.id;
UserModel.findOne({_id: id}, function (err, user) {
if (err) {
return res.status(500).json({
message: 'Error when getting user.',
error: err
});
}
if (!user) {
return res.status(404).json({
message: 'No such user'
});
}
return res.json(user);
});
},
/**
* userController.create()
*/
create: function (req, res) {
var user = new UserModel({
username : req.body.username,
password : req.body.password,
email : req.body.email,
posts : 0,
totalLikes : 0
});
user.save(function (err, user) {
if (err) {
return res.status(500).json({
message: 'Error when creating user',
error: err
});
}
return res.status(201).json(user);
//return res.redirect('/users/login');
});
},
/**
* userController.update()
*/
update: function (req, res) {
var id = req.params.id;
UserModel.findOne({_id: id}, function (err, user) {
if (err) {
return res.status(500).json({
message: 'Error when getting user',
error: err
});
}
if (!user) {
return res.status(404).json({
message: 'No such user'
});
}
user.username = req.body.username ? req.body.username : user.username;
user.password = req.body.password ? req.body.password : user.password;
user.email = req.body.email ? req.body.email : user.email;
user.save(function (err, user) {
if (err) {
return res.status(500).json({
message: 'Error when updating user.',
error: err
});
}
return res.json(user);
});
});
},
/**
* userController.remove()
*/
remove: function (req, res) {
var id = req.params.id;
UserModel.findByIdAndRemove(id, function (err, user) {
if (err) {
return res.status(500).json({
message: 'Error when deleting the user.',
error: err
});
}
return res.status(204).json();
});
},
showRegister: function(req, res){
res.render('user/register');
},
showLogin: function(req, res){
res.render('user/login');
},
login: function(req, res, next){
UserModel.authenticate(req.body.username, req.body.password, function(err, user){
if(err || !user){
var err = new Error('Wrong username or paassword');
err.status = 402;
return next(err);
}
req.session.userId = user._id;
//res.redirect('/users/profile');
return res.json(user);
});
},
profile: async function(req, res,next){
const userId = req.session.userId;
UserModel.findById(req.session.userId)
.exec(function(error, user){
if(error){
return next(error);
} else{
if(user===null){
var err = new Error('Not authorized, go back!');
err.status = 400;
return next(err);
} else{
// Counting the number of posts based on the user id
PhotoModel.countDocuments({ postedBy: userId })
.then((count) => {
// Update the user's post count
user.posts = count;
return user.save();
}).catch((error) => {
console.error('Error counting likes:', error);
return next(error);
});
// const objectId = Types.objectId(userId);
var objectId = mongoose.Types.ObjectId(userId);
PhotoModel.aggregate([
{ $match: { postedBy: objectId } },
{ $group: { _id: null, totalLikes: { $sum: "$likes" } } }
]).then((result) => {
// Update the user's total likes
user.totalLikes = result[0].totalLikes;
return user.save();
}).catch((error) => {
console.error('Error counting likes:', error);
return next(error);
});
return res.json(user);
}
}
});
},
logout: function(req, res, next){
if(req.session){
req.session.destroy(function(err){
if(err){
return next(err);
} else{
//return res.redirect('/');
return res.status(201).json({});
}
});
}
}
};

View File

@@ -0,0 +1,16 @@
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var photoSchema = new Schema({
'name' : String,
'path' : String,
'postedBy' : {
type: Schema.Types.ObjectId,
ref: 'user'
},
'reports' : Number,
'likes' : Number,
'comments' : Array
}, {timestamps: true});
module.exports = mongoose.model('photo', photoSchema);

View File

@@ -0,0 +1,48 @@
var mongoose = require('mongoose');
var bcrypt = require('bcrypt');
var Schema = mongoose.Schema;
var userSchema = new Schema({
'username' : String,
'password' : String,
'email' : String,
'posts' : Number,
'totalLikes' : Number
});
// userSchema.pre('save', function(next){
// var user = this;
// bcrypt.hash(user.password, 10, function(err, hash){
// if(err){
// return next(err);
// }
// user.password = hash;
// next();
// });
// });
userSchema.statics.authenticate = function(username, password, callback){
User.findOne({username: username})
.exec(function(err, user){
if(err){
return callback(err);
} else if(!user) {
var err = new Error("User not found.");
err.status = 401;
return callback(err);
}
// bcrypt.compare(password, user.password, function(err, result){
// if(result === true){
// return callback(null, user);
// } else{
// return callback();
// }
// });
return callback(null, user);
});
}
var User = mongoose.model('user', userSchema);
module.exports = User;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
{
"name": "vaja3",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www",
"dev": "nodemon ./bin/www"
},
"dependencies": {
"bcrypt": "^5.0.1",
"connect-mongo": "^4.6.0",
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"express": "~4.16.1",
"express-session": "^1.17.2",
"hbs": "~4.0.4",
"http-errors": "~1.6.3",
"mongoose": "^6.2.8",
"morgan": "~1.9.1",
"multer": "^1.4.4"
}
}

View File

@@ -0,0 +1,18 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}
nav ul{
list-style: none;
display: flex;
gap: 20px;
}
nav ul li a{
text-decoration: none;
}

View File

@@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;

View File

@@ -0,0 +1,33 @@
var express = require('express');
// Vključimo multer za file upload
var multer = require('multer');
var upload = multer({dest: 'public/images/'});
var router = express.Router();
var photoController = require('../controllers/photoController.js');
function requiresLogin(req, res, next){
if(req.session && req.session.userId){
return next();
} else{
var err = new Error("You must be logged in to view this page");
err.status = 401;
return next(err);
}
}
router.get('/', photoController.list);
//router.get('/publish', requiresLogin, photoController.publish);
router.get('/increase/:type/:id', photoController.show);
router.get('/comment/:id', photoController.show);
router.get('/:id', photoController.show);
router.post('/', requiresLogin, upload.single('image'), photoController.create);
router.put('/increase/:type/:id', photoController.increaseValue);
router.put('/comment/:id', photoController.addComment);
router.put('/:id', photoController.update);
router.delete('/:id', photoController.remove);
module.exports = router;

View File

@@ -0,0 +1,20 @@
var express = require('express');
var router = express.Router();
var userController = require('../controllers/userController.js');
router.get('/', userController.list);
//router.get('/register', userController.showRegister);
//router.get('/login', userController.showLogin);
router.get('/profile', userController.profile);
router.get('/logout', userController.logout);
router.get('/:id', userController.show);
router.post('/', userController.create);
router.post('/login', userController.login);
router.put('/:id', userController.update);
router.delete('/:id', userController.remove);
module.exports = router;

View File

@@ -0,0 +1,3 @@
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>

View File

@@ -0,0 +1,2 @@
<h1>{{title}}</h1>
<p>Welcome to {{title}}</p>

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>{{title}}</title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<!--
<nav>
<ul>
<li><a href='/'>Domov</a></li>
{{#if session.userId}}
<li><a href='/users/profile'>Profil</a></li>
<li><a href='/photos/publish'>Objavi sliko</a></li>
<li><a href='/users/logout'>Odjava</a></li>
{{else}}
<li><a href='/users/login'>Prijava</a></li>
<li><a href='/users/register'>Registracija</a></li>
{{/if}}
</ul>
</nav>
-->
{{{body}}}
</body>
</html>

View File

@@ -0,0 +1,7 @@
<h1>Slike:</h1>
{{#each photos}}
<h2>slika:</h2>
<img src="{{path}}" title="{{name}}" width="400"/><br/>
Objavil: <span>{{postedBy.username}}</span>
<hr/>
{{/each}}

View File

@@ -0,0 +1,8 @@
<h1>Dodaj sliko</h1>
<form action="/photos" method="post" enctype="multipart/form-data">
<input type="text" name="name" placeholder="ime slike" required>
<input type="file" name="image">
<div class="tp">
<input type="submit" value="Dodaj">
</div>
</form>

View File

@@ -0,0 +1,8 @@
<h1>Prijava</h1>
<form action="login" method="post">
<input type="text" name="username" placeholder="Uporabniško ime" required>
<input type="password" name="password" placeholder="Geslo" required>
<div class="tp">
<input type="submit" value="prijava">
</div>
</form>

View File

@@ -0,0 +1,6 @@
<h1>Profil uporabnika</h1>
<h2>Ime: </h2>
<h3>{{username}}</h3>
<h2>Mail:</h2>
<h3>{{email}}</h3>
<br><a type="button" href="logout">Odjava </a>

View File

@@ -0,0 +1,8 @@
<h1>Registracija</h1>
<form action="/users" method="post">
<input type="text" name="email" placeholder="E-mail" required="">
<input type="text" name="username" placeholder="Username" required="">
<input type="password" name="password" placeholder="Password" required="">
<input type="submit" value="REGISTER">
</form>

View File

@@ -0,0 +1,38 @@
.DS_STORE
node_modules
scripts/flow/*/.flowconfig
.flowconfig
*~
*.pyc
.grunt
_SpecRunner.html
__benchmarks__
build/
remote-repo/
coverage/
.module-cache
fixtures/dom/public/react-dom.js
fixtures/dom/public/react.js
test/the-files-to-test.generated.js
*.log*
chrome-user-data
*.sublime-project
*.sublime-workspace
.idea
*.iml
.vscode
*.swp
*.swo
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build
packages/react-devtools-extensions/chrome/*.crx
packages/react-devtools-extensions/chrome/*.pem
packages/react-devtools-extensions/firefox/build
packages/react-devtools-extensions/firefox/*.xpi
packages/react-devtools-extensions/firefox/*.pem
packages/react-devtools-extensions/shared/build
packages/react-devtools-extensions/.tempUserDataDir
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^13.5.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<title>Photos</title>
</head>
<body class="container">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,66 @@
import { useState, useEffect } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { UserContext } from "./userContext";
import Header from "./components/Header";
import Photos from "./components/Photos";
import Login from "./components/Login";
import Register from "./components/Register";
import Profile from "./components/Profile";
import Logout from "./components/Logout";
import AddPhoto from "./components/AddPhoto";
import Detail from './components/Detail';
function App() {
/**
* Podatek o tem, ali je uporabnik prijavljen ali ne, bomo potrebovali v vseh komponentah.
* State je dosegljiv samo znotraj trenutne komponente. Če želimo deliti spremenljivke z
* ostalimi komponentami, moramo uporabiti Context.
* Vsebino Contexta smo definirali v datoteki userContext.js. Poleg objekta 'user', potrebujemo
* še funkcijo, ki bo omogočala posodabljanje te vrednosti. To funkcijo definiramo v komponenti App
* (updateUserData). V render metodi pripravimo UserContext.Provider, naš Context je potem dosegljiv
* v vseh komponentah, ki se nahajajo znotraj tega providerja.
* V komponenti Login ob uspešni prijavi nastavimo userContext na objekt s trenutno prijavljenim uporabnikom.
* Ostale komponente (npr. Header) lahko uporabijo UserContext.Consumer, da dostopajo do prijavljenega
* uporabnika.
* Context se osveži, vsakič ko osvežimo aplikacijo v brskalniku. Da preprečimo neželeno odjavo uporabnika,
* lahko context trajno hranimo v localStorage v brskalniku.
*/
const [user, setUser] = useState(localStorage.user ? JSON.parse(localStorage.user) : null);
const updateUserData = (userInfo) => {
localStorage.setItem("user", JSON.stringify(userInfo));
setUser(userInfo);
}
/**
* Na vrhu vključimo komponento Header, z naslovom in menijem.
* Nato vključimo Router, ki prikaže ustrezno komponento v odvisnosti od URL naslova.
* Pomembno je, da za navigacijo in preusmeritve uporabljamo komponenti Link in Navigate, ki sta
* definirani v react-router-dom modulu. Na ta način izvedemo navigacijo brez osveževanja
* strani. Klasične metode (<a href=""> in window.location) bi pomenile osvežitev aplikacije
* in s tem dodatno obremenitev (ponovni izris komponente Header, ponastavitev Contextov,...)
*/
return (
<BrowserRouter>
<UserContext.Provider value={{
user: user,
setUserContext: updateUserData
}}>
<div className="App">
<Header title="Instagram"></Header>
<br />
<Routes>
<Route path="/" exact element={<Photos />}></Route>
<Route path="/login" exact element={<Login />}></Route>
<Route path="/register" element={<Register />}></Route>
<Route path="/publish" element={<AddPhoto />}></Route>
<Route path="/profile" element={<Profile />}></Route>
<Route path="/logout" element={<Logout />}></Route>
<Route path="/Detail/:id" element={<Detail />}></Route>
</Routes>
</div>
</UserContext.Provider>
</BrowserRouter>
);
}
export default App;

View File

@@ -0,0 +1,53 @@
import { useContext, useState } from 'react'
import { Navigate } from 'react-router';
import { UserContext } from '../userContext';
function AddPhoto(props) {
const userContext = useContext(UserContext);
const[name, setName] = useState('');
const[file, setFile] = useState('');
const[uploaded, setUploaded] = useState(false);
async function onSubmit(e){
e.preventDefault();
if(!name){
alert("Vnesite ime!");
return;
}
const formData = new FormData();
formData.append('name', name);
formData.append('image', file);
const res = await fetch('http://localhost:3001/photos', {
method: 'POST',
credentials: 'include',
body: formData
});
const data = await res.json();
setUploaded(true);
}
return (
<form className="form-group mx-auto w-50" onSubmit={onSubmit} >
{!userContext.user ? <Navigate replace to="/login" /> : ""}
{uploaded ? <Navigate replace to="/" /> : ""}
<div className="mb-3">
<label>Ime slike</label>
<input type="text" className="form-control" name="ime" placeholder="Ime slike"
value={name} onChange={(e)=>{setName(e.target.value)}} required/>
</div>
<div className="mb-3">
<label>Izberi sliko: </label>
<input style={{marginLeft: 5 + 'px'}} type="file" id="file"
onChange={(e)=>{setFile(e.target.files[0])}} required/>
</div>
<input className="btn btn-primary" type="submit" name="submit" value="Naloži" />
</form>
)
}
export default AddPhoto;

View File

@@ -0,0 +1,12 @@
function Comments(props){
return(
<div className="card my-3">
<div className="card-body">
<p className="card-text">{props.comment}</p>
</div>
</div>
)
}
export default Comments;

View File

@@ -0,0 +1,111 @@
import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { UserContext } from '../userContext';
import Comments from './Comments';
import IncrementButton from './IncrementButton';
function Detail() {
const [photo, setPhoto] = useState(null);
const [likes, setLikes] = useState(0);
const [commentInput, setCommentInput] = useState('');
const [comments, setComments] = useState([]);
const { id } = useParams();
useEffect(() => {
const getPhoto = async () => {
const res = await fetch(`http://localhost:3001/photos/${id}`);
const data = await res.json();
setPhoto(data);
setLikes(data.likes);
setComments(data.comments);
};
getPhoto();
}, [id]);
const addToLike = useCallback(() => {
setLikes(likes + 1);
}, [likes]);
if (!photo) {
return <div>Loading...</div>;
}
const handleCommentChange = (event) => {
setCommentInput(event.target.value);
};
const handleCommentSubmit = (event) => {
setComments([...comments, commentInput]);
setCommentInput('');
};
const addComment = async () => {
// Make a request to the backend API to add the comment
fetch(`http://localhost:3001/photos/comment/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ comment: commentInput }),
});
// Update the comments state variable
handleCommentSubmit();
};
return (
<div>
<div className="card">
<img className="card-img" src={"http://localhost:3001/" + photo.path} alt={photo.name} />
<ul className="list-group list-group-flush">
<li className="list-group-item">Naslov: {photo.name}</li>
<li className="list-group-item">Avtor: {photo.postedBy.username}</li>
<li className="list-group-item">Objavljen: {photo.createdAt}</li>
<li className="list-group-item">Vsecki: {likes}</li>
</ul>
<UserContext.Consumer>
{context => (
context.user ?
<div className="card-body">
<IncrementButton text="Like" type="like" className="btn btn-primary" setLikes={addToLike} amount={likes} source={id}></IncrementButton>
<IncrementButton text="Report" type="report" className="btn btn-danger" amount={photo.reports} source={id}></IncrementButton>
</div>
:
<></>
)}
</UserContext.Consumer>
</div>
<br />
<h3>Komentar:</h3>
<div style={{marginBottom: 10 + 'px'}}>
{comments.map((comment, index) => <Comments key={comments.indexOf(comment)} comment={comment}></Comments>)}
</div>
<UserContext.Consumer>
{context => (
context.user ?
<div>
<textarea className="form-control" placeholder="Add a comment" rows="3"
value={commentInput} onChange={handleCommentChange}>
</textarea>
<br/>
<button className='btn btn-primary' onClick={addComment}>Dodaj komentar</button>
</div>
:
<></>
)}
</UserContext.Consumer>
</div>
);
}
export default Detail;

View File

@@ -0,0 +1,35 @@
import { useContext } from "react";
import { UserContext } from "../userContext";
import { Link } from "react-router-dom";
function Header(props) {
return (
<header>
<nav className="navbar bg-light-subtle">
<div className="container-fluid navbar-expand-lg">
<div className="navbar-nav">
<Link className="nav-link" to='/'>Domov</Link>
<UserContext.Consumer>
{context => (
context.user ?
<>
<Link className="nav-link" to='/publish'>Objavi sliko</Link>
<Link className="nav-link" to='/profile'>Profil</Link>
<Link className="nav-link" to='/logout'>Odjava</Link>
</>
:
<>
<Link className="nav-link" to='/login'>Prijava</Link>
<Link className="nav-link" to='/register'>Registracija</Link>
</>
)}
</UserContext.Consumer>
</div>
</div>
</nav>
</header>
);
}
export default Header;

View File

@@ -0,0 +1,47 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
function IncrementButton(props) {
const [reports, setReports] = useState(0);
const navigate = useNavigate();
const incrementLikes = async () => {
props.setLikes((prevLikes) => prevLikes + 1);
await fetch(`http://localhost:3001/photos/increase/${props.type}/${props.source}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ likes: props.likes + 1 }),
});
};
const incrementReports = async () => {
setReports((prevReports) => {
const updatedReports = prevReports + 1;
if (updatedReports === 5) {
navigate('/'); // Redirect to the home page
}
return updatedReports;
});
await fetch(`http://localhost:3001/photos/increase/${props.type}/${props.source}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ reports: reports + 1 }),
});
};
return (
<span style={{marginRight: 10 + 'px'}} className={props.class} onClick={(props.text === 'Report') ? incrementReports : incrementLikes}>
{props.text}
</span>
);
}
export default IncrementButton;

View File

@@ -0,0 +1,60 @@
import { useContext, useState } from 'react';
import { UserContext } from '../userContext';
import { Navigate } from 'react-router-dom';
function Login(){
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const userContext = useContext(UserContext);
async function Login(e){
e.preventDefault();
const res = await fetch("http://localhost:3001/users/login", {
method: "POST",
credentials: "include",
headers: { 'Content-Type': 'application/json'},
body: JSON.stringify({
username: username,
password: password
})
});
const data = await res.json();
if(data._id !== undefined){
userContext.setUserContext(data);
} else {
setUsername("");
setPassword("");
setError("Invalid username or password");
}
}
return (
<form onSubmit={Login} className="mx-auto w-50">
{userContext.user ? <Navigate replace to="/" /> : ""}
<div className="mb-3">
<label className="form-label" >Uporabniško ime</label>
<input className="form-control" type="text" name="username" placeholder="Uporabniško ime"
value={username} onChange={(e)=>(setUsername(e.target.value))}/>
</div>
<div className="mb-3">
<label className="form-label" >Geslo</label>
<input className="form-control" type="password" name="password" placeholder="Geslo"
value={password} onChange={(e)=>(setPassword(e.target.value))}/>
</div>
<br/>
<div className="text-center">
<input className="btn btn-primary" type="submit" name="submit" value="Log in"/>
</div>
<label>{error}</label>
</form>
);
}
export default Login;

View File

@@ -0,0 +1,20 @@
import { useEffect, useContext } from 'react';
import { UserContext } from '../userContext';
import { Navigate } from 'react-router-dom';
function Logout(){
const userContext = useContext(UserContext);
useEffect(function(){
const logout = async function(){
userContext.setUserContext(null);
const res = await fetch("http://localhost:3001/users/logout");
}
logout();
}, []);
return (
<Navigate replace to="/" />
);
}
export default Logout;

View File

@@ -0,0 +1,23 @@
import { Link } from 'react-router-dom';
function Photo(props) {
return (
<div>
<div className="card w-50">
<img className="card-img" src={"http://localhost:3001/" + props.photo.path} alt={props.photo.name} />
<ul className="list-group list-group-flush">
<li className="list-group-item">Naslov: {props.photo.name}</li>
<li className="list-group-item">Avtor: {props.photo.postedBy.username}</li>
<li className="list-group-item">Objavljen: {props.photo.createdAt}</li>
<li className="list-group-item">Vsecki: {props.photo.likes}</li>
</ul>
<div className="card-body">
<Link className="btn btn-primary" to={`/Detail/${props.photo._id}`}>Podrobnosti</Link>
</div>
</div>
<br />
</div>
);
}
export default Photo;

View File

@@ -0,0 +1,23 @@
import { useState, useEffect } from 'react';
import Photo from './Photo';
function Photos(){
const [photos, setPhotos] = useState([]);
useEffect(function(){
const getPhotos = async function(){
const res = await fetch("http://localhost:3001/photos");
const data = await res.json();
setPhotos(data);
}
getPhotos();
}, []);
return(
<div>
<h3>Slike:</h3>
{photos.map(photo=>(<Photo photo={photo} key={photo._id}></Photo>))}
</div>
);
}
export default Photos;

View File

@@ -0,0 +1,42 @@
import { useContext, useEffect, useState } from 'react';
import { UserContext } from '../userContext';
import { Navigate } from 'react-router-dom';
function Profile(){
const userContext = useContext(UserContext);
const [profile, setProfile] = useState({});
const [totalLikes, setTotalLikes] = useState(0);
const [posts, setPosts] = useState(0);
useEffect(function(){
const getProfile = async function(){
const res = await fetch("http://localhost:3001/users/profile", {credentials: "include"});
const data = await res.json();
setProfile(data);
setTotalLikes(data.totalLikes);
setPosts(data.posts);
}
getProfile();
}, []);
useEffect(() => {
// This useEffect will be triggered whenever totalLikes or posts change,
// and you can use it to update the state variables with the latest values.
setTotalLikes(profile.totalLikes);
setPosts(profile.posts);
}, [profile.totalLikes, profile.posts]);
return (
<>
{!userContext.user ? <Navigate replace to="/login" /> : ""}
<h1>Profil</h1>
<h5>Uporabnisko ime: {profile.username}</h5>
<h5>Email: {profile.email}</h5>
<h6>Stevilo objav: {posts}</h6>
<h6>Stevilo likov: {totalLikes} </h6>
</>
);
}
export default Profile;

View File

@@ -0,0 +1,63 @@
import { useState } from 'react';
function Register() {
const [username, setUsername] = useState([]);
const [password, setPassword] = useState([]);
const [email, setEmail] = useState([]);
const [error, setError] = useState([]);
async function Register(e){
e.preventDefault();
const res = await fetch("http://localhost:3001/users", {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email,
username: username,
password: password
})
});
const data = await res.json();
if(data._id !== undefined){
window.location.href="/";
}
else{
setUsername("");
setPassword("");
setEmail("");
setError("Registration failed");
}
}
return(
<form className="mx-auto w-50" onSubmit={Register}>
<div className="mb-3">
<label className="form-label">Uporabniško ime</label>
<input className="form-control" type="text" name="username" placeholder="Uporabniško ime" required
value={username} onChange={(e)=>(setUsername(e.target.value))}/>
</div>
<div className="mb-3">
<label className="form-label">Geslo</label>
<input className="form-control" type="password" name="password" placeholder="Geslo" required
value={password} onChange={(e)=>(setPassword(e.target.value))}/>
</div>
<div className="mb-3">
<label className="form-label">Email</label>
<input className="form-control" type="text" name="email" placeholder="E-mail" required
value={email} onChange={(e)=>(setEmail(e.target.value))}/>
</div>
<div className="text-center">
<input className="btn btn-primary" type="submit" name="submit" value="Registracija" />
</div>
<label>{error}</label>
</form>
);
}
export default Register;

Some files were not shown because too many files have changed in this diff Show More