Integrate hugo static generator
This commit is contained in:
25
backend/app.ts
Normal file
25
backend/app.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import express from "express";
|
||||
|
||||
const hostname = '127.0.0.1';
|
||||
const httpPort = 4080;
|
||||
|
||||
const app = express();
|
||||
|
||||
app.set('views', 'views');
|
||||
app.set('view engine', 'hbs');
|
||||
|
||||
// import morgan from 'morgan'
|
||||
// app.use(morgan('dev'));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(express.static('public'));
|
||||
|
||||
import mainRouter from './routes/main';
|
||||
import apiRouter from './routes/api/apiRouter';
|
||||
|
||||
app.use('/', mainRouter);
|
||||
app.use('/api', apiRouter);
|
||||
|
||||
app.listen(httpPort, () => {
|
||||
console.log(`Server running at http://${hostname}:${httpPort}/`);
|
||||
});
|
226
backend/controllers/mediaController.ts
Normal file
226
backend/controllers/mediaController.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { type Request, type Response } from "express";
|
||||
import UserModel, { values } from '../models/userModel';
|
||||
import MediaModel, { Table, Media } from '../models/mediaModel';
|
||||
|
||||
interface omdbRes{
|
||||
Title: string,
|
||||
Released: string,
|
||||
Response: string,
|
||||
Poster: string,
|
||||
Type: string
|
||||
}
|
||||
|
||||
function fromStringToTable(value: string): (Table | undefined) {
|
||||
if (value.localeCompare("games") == 0) return Table.games;
|
||||
if (value.localeCompare("movies") == 0) return Table.movies;
|
||||
if (value.localeCompare("series") == 0) return Table.series;
|
||||
return;
|
||||
}
|
||||
|
||||
async function downloadImage(mData: Media, type: Table) {
|
||||
// Specify the path where you want to save the image
|
||||
const outputPath = '/poster/' + type + '/' + mData.code + '.jpg';
|
||||
|
||||
// Use Bun's built-in fetch to download the image
|
||||
const response = await fetch(mData.webImg);
|
||||
|
||||
// Check if the request was successful
|
||||
if (!response.ok) {
|
||||
console.log("fetch image error");
|
||||
return
|
||||
}
|
||||
|
||||
// Convert the response to a blob
|
||||
const imageBlob = await response.blob();
|
||||
// Use Bun's write to save the image to a file
|
||||
await Bun.write('./public/' + outputPath, await imageBlob.arrayBuffer());
|
||||
MediaModel.updateWebImg(type, mData.code, outputPath);
|
||||
|
||||
}
|
||||
|
||||
async function createMed(req: Request, res: Response) {
|
||||
const mediaCode: string = req.body.code;
|
||||
|
||||
const omdb_key = UserModel.getValue(values.omdb_key);
|
||||
|
||||
if (!omdb_key) {
|
||||
return res.status(500).json({ message: 'Error when creating media' });
|
||||
}
|
||||
|
||||
// remove the tt in front in DB its stored as number
|
||||
const sub = mediaCode.substring(2);
|
||||
const cleanCode = parseInt(sub);
|
||||
|
||||
try {
|
||||
|
||||
const uri = `http://www.omdbapi.com/?i=${mediaCode}&apikey=${omdb_key}`;
|
||||
const mJson = await fetch(uri);
|
||||
const mData: omdbRes = await mJson.json();
|
||||
|
||||
if (mData.Response == 'False') {
|
||||
return res.status(404).json({ message: 'wrong code' });
|
||||
}
|
||||
|
||||
const media: Media = {
|
||||
id:0,
|
||||
code: cleanCode,
|
||||
title: mData.Title,
|
||||
released: mData.Released,
|
||||
webImg: mData.Poster,
|
||||
};
|
||||
|
||||
const seriesFound = MediaModel.findOne(Table.series, cleanCode);
|
||||
if (seriesFound.length != 0) {
|
||||
res.status(409).json({ message: 'Media already exists' });
|
||||
await downloadImage(media, Table.series);
|
||||
return;
|
||||
}
|
||||
|
||||
const moviesFound = MediaModel.findOne(Table.movies, cleanCode);
|
||||
if (moviesFound.length != 0) {
|
||||
res.status(409).json({ message: 'Media already exists' });
|
||||
await downloadImage(media, Table.movies);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mData.Type.localeCompare("movie") == 0) {
|
||||
const savedMedia = MediaModel.save(Table.movies, cleanCode, mData.Title, mData.Released, mData.Poster);
|
||||
await downloadImage(media, Table.movies);
|
||||
}
|
||||
else if (mData.Type.localeCompare("series") == 0) {
|
||||
const savedMedia = MediaModel.save(Table.series, cleanCode, mData.Title, mData.Released, mData.Poster);
|
||||
await downloadImage(media, Table.series);
|
||||
}
|
||||
|
||||
res.status(201).json(media);
|
||||
} catch (err) {
|
||||
return res.status(500).json({ message: 'Error when creating media' });
|
||||
}
|
||||
}
|
||||
|
||||
async function createGame(req: Request, res: Response) {
|
||||
var gameCode = req.body.code;
|
||||
|
||||
const twitch_client_id = UserModel.getValue(values.twitch_client_id);
|
||||
const twitch_client_secret = UserModel.getValue(values.twitch_client_secret);
|
||||
|
||||
if (!twitch_client_id || !twitch_client_secret) {
|
||||
return res.status(500).json({ message: 'Error when creating game' });
|
||||
}
|
||||
|
||||
try {
|
||||
const gameFound = MediaModel.findOne(Table.games, gameCode);
|
||||
if (gameFound) {
|
||||
return res.status(409).json({ message: 'Game already exists' });
|
||||
}
|
||||
|
||||
const uri = "https://id.twitch.tv/oauth2/token?client_id=" + twitch_client_id + "&client_secret=" + twitch_client_secret + "&grant_type=client_credentials";
|
||||
var response = await fetch(uri, { method: 'POST' });
|
||||
const mData = await response.json();
|
||||
|
||||
const mheaders: HeadersInit = {
|
||||
'Accept': 'application/json',
|
||||
'Client-ID': twitch_client_id,
|
||||
'Authorization': 'Bearer ' + mData.access_token
|
||||
}
|
||||
|
||||
gameCode = parseInt(gameCode)
|
||||
|
||||
response = await fetch(
|
||||
"https://api.igdb.com/v4/games",
|
||||
{
|
||||
method: 'POST',
|
||||
headers: mheaders,
|
||||
body: `fields name, first_release_date; where id = ${gameCode};`
|
||||
}
|
||||
)
|
||||
const gameData = await response.json()
|
||||
if (gameData.length == 0) {
|
||||
return res.status(404).json({ message: 'wrong code' });
|
||||
}
|
||||
|
||||
const date = new Date(gameData[0].first_release_date * 1000);
|
||||
const options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short', year: 'numeric' }
|
||||
const dateStr = date.toLocaleDateString(undefined, options);
|
||||
|
||||
|
||||
response = await fetch(
|
||||
"https://api.igdb.com/v4/covers",
|
||||
{
|
||||
method: 'POST',
|
||||
headers: mheaders,
|
||||
body: `fields image_id; where game = ${gameCode};`
|
||||
}
|
||||
)
|
||||
const coverData = await response.json()
|
||||
|
||||
const game: Media = {
|
||||
id: 0,
|
||||
code: gameCode,
|
||||
title: gameData[0].name,
|
||||
released: dateStr,
|
||||
webImg: `https://images.igdb.com/igdb/image/upload/t_cover_big/${coverData[0].image_id}.jpg`,
|
||||
};
|
||||
|
||||
const savedGame = MediaModel.save(Table.games, game.code, game.title, game.released, game.webImg);
|
||||
return res.status(201).json(game);
|
||||
|
||||
} catch (error) {
|
||||
|
||||
return res.status(500).json({ message: 'Error when creating game', error: error });
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* mediaController.list()
|
||||
*/
|
||||
list: function (req: Request, res: Response) {
|
||||
const mediaTable = fromStringToTable(req.params.mediaType);
|
||||
if (!mediaTable) {
|
||||
return res.status(500).json({
|
||||
message: 'Error when getting media.'
|
||||
});
|
||||
}
|
||||
|
||||
const media = MediaModel.find(mediaTable);
|
||||
return res.json(media);
|
||||
},
|
||||
|
||||
create: async function (req: Request, res: Response) {
|
||||
const mediaCode: string = req.body.code;
|
||||
if (mediaCode.startsWith("t")) {
|
||||
return await createMed(req, res);
|
||||
} else {
|
||||
return await createGame(req, res);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* mediaController.delete()
|
||||
*/
|
||||
remove: function (req: Request, res: Response) {
|
||||
const mediaTable = fromStringToTable(req.params.mediaType);
|
||||
if (!mediaTable) {
|
||||
return res.status(500).json({
|
||||
message: 'Error when deleting the media.'
|
||||
});
|
||||
}
|
||||
|
||||
const code = req.body.code;
|
||||
|
||||
try {
|
||||
const mediaTable = req.baseUrl.includes('movies') ? Table.movies : Table.series;
|
||||
const media = MediaModel.findOneAndDelete(mediaTable, code);
|
||||
if (!media) {
|
||||
return res.status(404).json({ message: 'No such media' });
|
||||
}
|
||||
|
||||
return res.status(204).json();
|
||||
}
|
||||
catch (err) {
|
||||
return res.status(500).json({ message: 'Error when deleting the media.' });
|
||||
}
|
||||
},
|
||||
};
|
50
backend/controllers/userController.ts
Normal file
50
backend/controllers/userController.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { type Request, type Response } from "express";
|
||||
import UserModel, { values } from '../models/userModel';
|
||||
|
||||
export default {
|
||||
|
||||
render: function (req: Request, res: Response) {
|
||||
res.render('user', { keys: UserModel.namesOfValues });
|
||||
},
|
||||
|
||||
create: function (req: Request, res: Response) {
|
||||
|
||||
const reqPassword: string = req.body.reqPassword;
|
||||
if (!reqPassword) return res.render('user', { keys: UserModel.namesOfValues });
|
||||
|
||||
const password = UserModel.getValue(values.pass);
|
||||
|
||||
// if no password in db save reqPassword
|
||||
if (!password) {
|
||||
const affectedRows = UserModel.updateValue("pass", reqPassword);
|
||||
if (affectedRows > 0) {
|
||||
return res.redirect('/list');
|
||||
}
|
||||
return res.render('user', { keys: UserModel.namesOfValues });
|
||||
}
|
||||
// check if passwords equal
|
||||
if (password != reqPassword) {
|
||||
return res.render('user', { keys: UserModel.namesOfValues });
|
||||
}
|
||||
|
||||
// update
|
||||
const name: string = req.body.name;
|
||||
const value: string = req.body.value;
|
||||
|
||||
if (!name || !value) {
|
||||
return res.render('user', { keys: UserModel.namesOfValues });
|
||||
}
|
||||
|
||||
const affectedRows = UserModel.updateValue(name, value);
|
||||
if (affectedRows == 0) {
|
||||
return res.render('user', { keys: UserModel.namesOfValues });
|
||||
}
|
||||
|
||||
return res.redirect('/list');
|
||||
},
|
||||
|
||||
get: function (req: Request, res: Response) {
|
||||
const usersFound = UserModel.getAll();
|
||||
return res.status(200).json(usersFound);
|
||||
},
|
||||
};
|
15
backend/miscellaneous/checkAuthenticated.ts
Normal file
15
backend/miscellaneous/checkAuthenticated.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { type NextFunction, type Request, type Response } from "express";
|
||||
import userModel, { values } from 'backend/models/userModel';
|
||||
|
||||
function checkAuthenticated(req: Request, res: Response, next: NextFunction) {
|
||||
const pass = req.body.pass;
|
||||
const password = userModel.getValue(values.pass);
|
||||
if (pass && password) {
|
||||
if (pass == password) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
return res.status(500).json({ message: 'Error when getting transactions.' });
|
||||
}
|
||||
|
||||
export default checkAuthenticated;
|
65
backend/miscellaneous/db.ts
Normal file
65
backend/miscellaneous/db.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
|
||||
const pool = new Database("mydb.sqlite", { strict: true });
|
||||
|
||||
pool.exec(`
|
||||
CREATE TABLE IF NOT EXISTS series (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code INT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
released TEXT NOT NULL,
|
||||
webImg TEXT NOT NULL,
|
||||
UNIQUE (code)
|
||||
);
|
||||
`);
|
||||
|
||||
pool.exec(`
|
||||
CREATE TABLE IF NOT EXISTS movies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code INT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
released TEXT NOT NULL,
|
||||
webImg TEXT NOT NULL,
|
||||
UNIQUE (code)
|
||||
);
|
||||
`);
|
||||
|
||||
pool.exec(`
|
||||
CREATE TABLE IF NOT EXISTS games (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code INT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
released TEXT NOT NULL,
|
||||
webImg TEXT NOT NULL,
|
||||
UNIQUE (code)
|
||||
);
|
||||
`);
|
||||
|
||||
pool.exec(`
|
||||
CREATE TABLE IF NOT EXISTS userData (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
function inset_keys() {
|
||||
class co {
|
||||
count!: number;
|
||||
}
|
||||
const result = pool.query("SELECT count(*) as count FROM userData;").as(co).get();
|
||||
if(result && result.count >= 4){
|
||||
return;
|
||||
}
|
||||
|
||||
pool.exec(`
|
||||
INSERT INTO userData (name, value) VALUES ("pass", "");
|
||||
INSERT INTO userData (name, value) VALUES ("omdb_key", "");
|
||||
INSERT INTO userData (name, value) VALUES ("twitch_client_id", "");
|
||||
INSERT INTO userData (name, value) VALUES ("twitch_client_secret", "");
|
||||
`);
|
||||
}
|
||||
|
||||
inset_keys();
|
||||
|
||||
export default pool;
|
82
backend/models/mediaModel.ts
Normal file
82
backend/models/mediaModel.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import pool from 'backend/miscellaneous/db'
|
||||
|
||||
|
||||
export class Media {
|
||||
id!: number;
|
||||
code!: number;
|
||||
title!: string;
|
||||
released!: string;
|
||||
webImg!: string;
|
||||
}
|
||||
|
||||
export enum Table {
|
||||
movies = "movies",
|
||||
series = "series",
|
||||
games = "games",
|
||||
}
|
||||
|
||||
function save(table: Table, code: number, title: string, released: string, webImg: string): number {
|
||||
try {
|
||||
const sql = "INSERT INTO " + table + " (code, title, released, webImg) VALUES (?,?,?,?)";
|
||||
|
||||
const result = pool.query(sql).run(code, title, released, webImg);
|
||||
return result.changes;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function updateWebImg(table: Table, code: number, webImg: string): number {
|
||||
try {
|
||||
const sql = "UPDATE " + table + " SET webImg = ? WHERE code = ?;";
|
||||
const result = pool.query(sql).run(webImg, code);
|
||||
return result.changes;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function findOneAndDelete(table: Table, code: number): number {
|
||||
try {
|
||||
const result = pool.query("DELETE FROM " + table + " WHERE code = ?;").run(code);
|
||||
return result.changes;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function findOne(table: Table, code: number): Media[] {
|
||||
try {
|
||||
const rows = pool.query("SELECT * FROM " + table + " WHERE code = ?;").as(Media).all(code);
|
||||
return rows;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function find(table: Table): Media[] {
|
||||
try {
|
||||
const rows = pool.query("SELECT * FROM " + table + ";").as(Media).all();
|
||||
return rows;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export default {
|
||||
save,
|
||||
updateWebImg,
|
||||
findOneAndDelete,
|
||||
findOne,
|
||||
find
|
||||
};
|
56
backend/models/userModel.ts
Normal file
56
backend/models/userModel.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import pool from 'backend/miscellaneous/db'
|
||||
|
||||
class UserD {
|
||||
name?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export enum values {
|
||||
pass = 1,
|
||||
omdb_key,
|
||||
twitch_client_id,
|
||||
twitch_client_secret,
|
||||
}
|
||||
|
||||
const namesOfValues: string[] = ["", "pass", "omdb_key", "twitch_client_id", "twitch_client_secret"];
|
||||
|
||||
function getValue(name: values): string | undefined {
|
||||
try {
|
||||
const rows = pool.query("SELECT name, value FROM userData where id = ?;").as(UserD).all(name);
|
||||
if (rows.length > 0)
|
||||
return rows[0].value;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function updateValue(name: string, value: string): number {
|
||||
try {
|
||||
const result = pool.query("UPDATE userData SET value = ? WHERE name = ?").run(value, name);
|
||||
return result.changes;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getAll(): UserD[] {
|
||||
try {
|
||||
const rows = pool.query("SELECT name, value FROM userData;").as(UserD).all();
|
||||
return rows;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export default {
|
||||
getValue,
|
||||
updateValue,
|
||||
getAll,
|
||||
namesOfValues
|
||||
};
|
12
backend/routes/api/apiRouter.ts
Normal file
12
backend/routes/api/apiRouter.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import express, { type Request, type Response } from "express";
|
||||
import mediaRouter from './mediaRouter';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use('/media', mediaRouter);
|
||||
|
||||
router.get('/', function (req: Request, res: Response) {
|
||||
res.status(200).json({ message: 'API is working' });
|
||||
});
|
||||
|
||||
export default router;
|
13
backend/routes/api/mediaRouter.ts
Normal file
13
backend/routes/api/mediaRouter.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import express from "express";
|
||||
import mediaController from '../../controllers/mediaController.js';
|
||||
import checkAuthenticated from '../../miscellaneous/checkAuthenticated.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/:mediaType', mediaController.list);
|
||||
|
||||
router.post('/:mediaType', checkAuthenticated, mediaController.create);
|
||||
|
||||
router.delete('/:mediaType', checkAuthenticated, mediaController.remove);
|
||||
|
||||
export default router;
|
26
backend/routes/main.ts
Normal file
26
backend/routes/main.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import express, { type Request, type Response } from "express";
|
||||
import userData from '../userKnowledge.json';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/2_0', function (req: Request, res: Response) {
|
||||
res.render('main/2_0', { userData });
|
||||
});
|
||||
|
||||
router.get('/cv', function (req: Request, res: Response) {
|
||||
res.render('cv', { userData });
|
||||
});
|
||||
|
||||
router.get('/1_0', function (req: Request, res: Response) {
|
||||
res.render('main/1_0');
|
||||
});
|
||||
|
||||
router.get('/list', function (req: Request, res: Response) {
|
||||
res.render('list');
|
||||
});
|
||||
|
||||
//import userRouter from './user';
|
||||
//router.use('/user', userRouter);
|
||||
|
||||
export default router;
|
14
backend/routes/user.ts
Normal file
14
backend/routes/user.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import express from "express";
|
||||
import userController from 'controllers/userController';
|
||||
import checkAuthenticated from 'backend/miscellaneous/checkAuthenticated';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', userController.render);
|
||||
|
||||
router.post('/', userController.create);
|
||||
|
||||
router.put('/', checkAuthenticated, userController.get);
|
||||
|
||||
export default router;
|
111
backend/userKnowledge.json
Normal file
111
backend/userKnowledge.json
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"first_name": "Nikola",
|
||||
"last_name": "Petrov",
|
||||
"phone_number": "+38670749506",
|
||||
"occupation": "Student",
|
||||
"birth": "14, November, 2000",
|
||||
"living_location": "Ljubljana, Slovenia",
|
||||
"web_link": "https://petrovv.com",
|
||||
"git_link": "https://git.petrovv.com/explore",
|
||||
"email": "nikola@petrovv.com",
|
||||
"instagram_handle":"@nikolainsta7",
|
||||
"instagram_link":"https://www.instagram.com/nikolainsta7",
|
||||
"about_me": [
|
||||
"I am Nikola, currently pursuing my studies at the Faculty of Electrical Engineering and Computer Science (FERI) in Maribor. My academic journey is largely driven by my interest in application and web development. I find the process of creating functional and user-friendly digital solutions both challenging and rewarding. This field allows me to blend creativity with technical skills, which I find particularly engaging.",
|
||||
"Recently, I have developed an interest in the game of Go. The strategic depth and complexity of the game have captivated my attention, providing a stimulating mental exercise. Additionally, I have started exploring photography. Capturing moments and expressing visual stories through a lens has become a newfound passion, offering a different kind of creative outlet that complements my technical pursuits."
|
||||
],
|
||||
"project": [
|
||||
{
|
||||
"img": "/images/projects/password_manager.jpeg",
|
||||
"title": "Password manager",
|
||||
"des": "CLI app",
|
||||
"link": "https://git.petrovv.com/nikola/password_manager"
|
||||
},
|
||||
{
|
||||
"img": "/images/projects/list.jpeg",
|
||||
"title": "My watch/game list",
|
||||
"des": "",
|
||||
"link": "/list"
|
||||
},
|
||||
{
|
||||
"img": "/images/logo.png",
|
||||
"title": "Server",
|
||||
"des": "Everything running on my server",
|
||||
"link": "https://git.petrovv.com/nikola/personal_website"
|
||||
},
|
||||
{
|
||||
"img": "/images/projects/projektna_naloga.jpeg",
|
||||
"title": "Highway Tracker",
|
||||
"des": "School project",
|
||||
"link": "https://git.petrovv.com/nikola/school/src/branch/master/projektna_naloga"
|
||||
},
|
||||
{
|
||||
"img": "/images/projects/bitshift.jpeg",
|
||||
"title": "BitShifters",
|
||||
"des": "unity",
|
||||
"link": "https://git.petrovv.com/nikola/school/src/branch/master/semester_4/razvoj_programskih_sistemov/bitshifters"
|
||||
},
|
||||
{
|
||||
"img": "/images/projects/tetris.jpeg",
|
||||
"title": "Tetris",
|
||||
"des": "WPF",
|
||||
"link": "https://git.petrovv.com/nikola/school/src/branch/master/semester_4/razvoj_programskih_sistemov/tetris"
|
||||
}
|
||||
],
|
||||
"experience": [
|
||||
{
|
||||
"title": "HW Developer",
|
||||
"company": "Spica International",
|
||||
"time": "17/03/2025 - CURRENT",
|
||||
"des": "Working on access menegment systems."
|
||||
},
|
||||
{
|
||||
"title": "Backend/Frontend",
|
||||
"company": "RRC d.o.o",
|
||||
"time": "01/09/2024 - 31/12/2024",
|
||||
"des": "Worked on goverment websites for collage enrolment and student dorm requests."
|
||||
},
|
||||
{
|
||||
"title": "Developer",
|
||||
"company": "RRC d.o.o",
|
||||
"time": "18/03/2024 - 31/05/2024",
|
||||
"des": "Student practicum. Backend in java with frontend in ext JS and jQuery."
|
||||
},
|
||||
{
|
||||
"title": "Developer/IT",
|
||||
"company": "LightAct",
|
||||
"time": "01/07/2022 - 01/09/2022",
|
||||
"des": "I helped maintaining data base, worked on the application (integrated a capture card and IP camera), assembled new server rack, installed new UTP/power connectors in the office."
|
||||
},
|
||||
{
|
||||
"title": "Mentor",
|
||||
"company": "Institute 404",
|
||||
"time": "08/06/2020 - 19/06/2020",
|
||||
"des": "I helped primary school children with their projects with soldering, laser cutting, and building."
|
||||
},
|
||||
{
|
||||
"title": "Maintenance technician",
|
||||
"company": "Hella Saturnos d.o.o.",
|
||||
"time": "04/09/2018 - 18/01/2019",
|
||||
"des": "I maintained and repaired machines from plastic presses to personal stations."
|
||||
},
|
||||
{
|
||||
"title": "Maintenance technician",
|
||||
"company": "Best Western Premier Hotel Slon",
|
||||
"time": "01/03/2018 - 04/05/2018",
|
||||
"des": "I helped with setting up the conference/event rooms. I helped customers and fixed problems like replacing light bulbs, wall sockets, hair-dryers."
|
||||
}
|
||||
],
|
||||
"education": [
|
||||
{
|
||||
"title": "(FERI) Faculty of Electrical Engineering and Computer Science, University of Maribor",
|
||||
"time": "01/10/2021 - CURRENT",
|
||||
"des": "Graduate engineer of computer science and information technology."
|
||||
},
|
||||
{
|
||||
"title": "(SSTS Siska) Secondary school of technical professions siska",
|
||||
"time": "01/09/2016 - 07/07/2021",
|
||||
"des": "Electrotechnician."
|
||||
}
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user