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,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;

View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,6 @@
import { createContext } from 'react';
export const UserContext = createContext({
user: null,
setUserContext: () => {}
});