diff --git a/.gitignore b/.gitignore index b476575..29aa479 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,4 @@ # OS-specific files .DS_Store -Thumbs.db - -# Backend-specific files -backend/db.js \ No newline at end of file +Thumbs.db \ No newline at end of file diff --git a/backend/db.js b/backend/db.js new file mode 100644 index 0000000..77f79a7 --- /dev/null +++ b/backend/db.js @@ -0,0 +1,19 @@ +const mariadb = require('mariadb'); +require("dotenv").config(); + +// Create a pool connection to MariaDB +const pool = mariadb.createPool({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT, 10), + database: process.env.DB_NAME, + connectionLimit: parseInt(process.env.CON_LIMIT, 10), +}); + +pool.getConnection() + .then(() => console.log("Database connection successful!")) + .catch(err => console.error("Database connection error:", err.message)); + +// Export the pool to use in other files +module.exports = pool; diff --git a/backend/index.js b/backend/index.js index 4a961c9..1196299 100644 --- a/backend/index.js +++ b/backend/index.js @@ -17,10 +17,6 @@ cert: fs.readFileSync(process.env.SSL_CERT_PATH), }; -const ORIGIN = process.env.ORIGIN ? process.env.ORIGIN.split(",") : []; - -console.log("ORIGIN:", ORIGIN); - // CORS configuration const corsOptions = { origin: process.env.ORIGIN ? process.env.ORIGIN.split(",") : [], diff --git a/backend/routes/about.js b/backend/routes/about.js index 8100f6a..8ccc51b 100644 --- a/backend/routes/about.js +++ b/backend/routes/about.js @@ -15,7 +15,7 @@ const aboutRaw = await pool.query(aboutQuery); // Log the entire result to see the structure - console.log("About Raw Result:", aboutRaw); + //console.log("About Raw Result:", aboutRaw); // If rows exist, proceed with mapping const updatedAbout = aboutRaw?.map(about => ({ diff --git a/backend/routes/contact.js b/backend/routes/contact.js index 9dc27dd..d4c4172 100644 --- a/backend/routes/contact.js +++ b/backend/routes/contact.js @@ -64,7 +64,7 @@ from: process.env.EMAIL_USER, to: email, subject: "Message Received", - text: "Your message was received, I will contact you as soon as possible!", + text: "Thank you for reaching out! I have received your message and will get back to you as soon as possible. Looking forward to connecting with you!", }; try { diff --git a/backend/routes/projects.js b/backend/routes/projects.js index 737d68a..af73b8a 100644 --- a/backend/routes/projects.js +++ b/backend/routes/projects.js @@ -12,6 +12,24 @@ // Helper function to append BASE_URL to a path const appendBaseUrl = (path) => (path ? `${BASE_URL}${path}` : null); +router.get("/my_projects", async (req, res) => { + try { + + //Query + const projectsQuery = `SELECT * FROM projects;`; + + //Queries results + const projects= await pool.query(projectsQuery); + + //Send response + res.json(projects); + + } catch (err) { + console.error("Error fetching project names: ", err.message); + res.status(500).send("Server error"); + } +}); + // Fetch all projects router.get("/projects", async (req, res) => { try { @@ -178,8 +196,8 @@ const projectDateQuery = ` SELECT - CONCAT(MONTHNAME(p.start_date), ' ', YEAR(p.start_date)) AS start_month_year, - CONCAT(MONTHNAME(p.end_date), ' ', YEAR(p.end_date)) AS end_month_year, + CONCAT(MONTHNAME(p.start_date), ' ', DAY(p.start_date), ', ', YEAR(p.start_date)) AS start_full_date, + CONCAT(MONTHNAME(p.end_date), ' ', DAY(p.end_date), ', ', YEAR(p.end_date)) AS end_full_date, l.logo_path AS date_logo FROM projects p LEFT JOIN logos l ON l.type = 'date' @@ -231,64 +249,138 @@ }; } - // Query to fetch distinct tool types - const toolTypesQuery = ` - SELECT DISTINCT t.type - FROM tools t - INNER JOIN project_tools pt ON pt.tool_id = t.id - WHERE pt.project_id = ?; - `; - - const toolTypesRows = await pool.query(toolTypesQuery, [project_id]); - - if (!toolTypesRows || toolTypesRows.length === 0) { - return res.status(404).send("No tool types found."); - } - - const toolTypes = toolTypesRows.map(row => row.type); - - // Dynamically generate tool subqueries - const toolSubqueries = toolTypes.map(type => ` - (SELECT JSON_ARRAYAGG( - JSON_OBJECT('name', t.name, 'logo', t.logo, 'type', t.type) - ) - FROM project_tools pt - INNER JOIN tools t ON pt.tool_id = t.id - WHERE pt.project_id = p.id AND t.type = '${type}') AS ${type.toLowerCase()}_tools - `).join(',\n'); - // Main query to fetch project page details const query = ` - SELECT - p.id AS project_id, - p.title AS project_title, - p.description AS project_description, - p.hook, - p.trailer, - p.thumbnail_path AS project_thumbnail, - ${toolSubqueries}, - (SELECT JSON_ARRAYAGG(JSON_OBJECT('name', t.name, 'logo', t.logo)) - FROM project_tech pt - INNER JOIN tech t ON pt.tech_id = t.id - WHERE pt.project_id = p.id) AS tech, - (SELECT JSON_ARRAYAGG(JSON_OBJECT('name', t.name, 'logo', t.logo)) - FROM project_languages pl - INNER JOIN languages t ON pl.language_id = t.id - WHERE pl.project_id = p.id) AS programming_languages, - pg.background_image AS background_path, - pg.project_logo AS logo - FROM - project_page pp - INNER JOIN - projects p ON pp.project_id = p.id - INNER JOIN - page pg ON pp.page_id = pg.id - WHERE - pp.project_id = ?; - `; + SELECT + p.id AS project_id, + p.title AS project_title, + p.description AS project_description, + p.hook, + p.trailer, + p.thumbnail_path AS project_thumbnail, + p.screenshots, + + -- Fetch tags as a JSON array + (SELECT JSON_ARRAYAGG(JSON_OBJECT('name', t.name, 'logo', t.logo)) + FROM project_tags pt + INNER JOIN tags t ON pt.tag_id = t.id + WHERE pt.project_id = p.id) AS tags, + + -- Fetch tech as a JSON array + (SELECT JSON_ARRAYAGG(JSON_OBJECT('name', t.name, 'logo', t.logo)) + FROM project_tech pt + INNER JOIN tech t ON pt.tech_id = t.id + WHERE pt.project_id = p.id) AS tech, + + -- Fetch programming languages as a JSON array + (SELECT JSON_ARRAYAGG(JSON_OBJECT('name', t.name, 'logo', t.logo)) + FROM project_languages pl + INNER JOIN languages t ON pl.language_id = t.id + WHERE pl.project_id = p.id) AS programming_languages, + + -- Fetch development tools as a JSON array + (SELECT JSON_ARRAYAGG(JSON_OBJECT('name', dt.name, 'logo', dt.logo)) + FROM project_dev_tools pdt + INNER JOIN dev_tools dt ON pdt.dev_tool_id = dt.id + WHERE pdt.project_id = p.id) AS dev_tools, + + -- Fetch management tools as a JSON array + (SELECT JSON_ARRAYAGG(JSON_OBJECT('name', mt.name, 'logo', mt.logo)) + FROM project_manage_tools pmt + INNER JOIN manage_tools mt ON pmt.manage_tool_id = mt.id + WHERE pmt.project_id = p.id) AS management_tools, + + -- Fetch design tools as a JSON array + (SELECT JSON_ARRAYAGG(JSON_OBJECT('name', dt.name, 'logo', dt.logo)) + FROM project_design_tools pdt + INNER JOIN design_tools dt ON pdt.design_tool_id = dt.id + WHERE pdt.project_id = p.id) AS des_tools, + + -- Fetch databases as a JSON array + (SELECT JSON_ARRAYAGG(JSON_OBJECT('name', d.name, 'logo', d.logo)) + FROM project_databases pd + INNER JOIN \`databases\` d ON pd.database_id = d.id + WHERE pd.project_id = p.id) AS project_databases, -- Changed alias to avoid reserved keyword + + -- Fetch category as a JSON object + JSON_OBJECT('name', c.name, 'logo', c.logo) AS category, + + -- Fetch project links as a JSON array + (SELECT JSON_ARRAYAGG(JSON_OBJECT('name', l.name, 'url', l.url, 'logo', l.logo)) + FROM links l + WHERE l.project_id = p.id) AS links, + + pg.background_image AS background_path, + pg.project_logo AS logo + FROM + project_page pp + INNER JOIN + projects p ON pp.project_id = p.id + INNER JOIN + page pg ON pp.page_id = pg.id + LEFT JOIN + categories c ON p.category_id = c.id + WHERE + pp.project_id = ?; + `; const rows = await pool.query(query, [project_id]); + const project_skills = rows.map(row => { + let skills = {}; // Initialize as empty object + + if (row.tech && row.tech.length > 0) { + skills.tech = row.tech.map(t => ({ + skill_name: "Tech", + ...t, + logo: appendBaseUrl(t.logo), + })); + } + + if (row.programming_languages && row.programming_languages.length > 0) { + skills.programming_languages = row.programming_languages.map(l => ({ + skill_name: "Programming Languages", + ...l, + logo: appendBaseUrl(l.logo), + })); + } + + if (row.development_tools && row.development_tools.length > 0) { + skills.development_tools = row.development_tools.map(dev => ({ + skill_name: "Development Tools", + ...dev, + logo: appendBaseUrl(dev.logo), + })); + } + + if (row.management_tools && row.management_tools.length > 0) { + skills.management_tools = row.management_tools.map(mgt => ({ + skill_name: "Management Tools", + ...mgt, + logo: appendBaseUrl(mgt.logo), + })); + } + + if (row.des_tools && row.des_tools.length > 0) { + skills.design_tools = row.des_tools.map(dt => ({ + skill_name: "Design Tools", + ...dt, + logo: appendBaseUrl(dt.logo), + })); + } + + if (row.project_databases && row.project_databases.length > 0) { + skills.project_databases = row.project_databases.map(db => ({ + skill_name: "Databases", + ...db, + logo: appendBaseUrl(db.logo), + })); + } + + return skills; // Return object with all accumulated skills + }); + + if (!rows || rows.length === 0) { return res.status(404).send("No page found for the given project ID."); } @@ -296,32 +388,41 @@ // Construct the final response const pageData = rows.map(row => ({ ...row, + + category: row.category ? { + ...row.category, + logo: appendBaseUrl(row.category.logo), + } : null, + + skills: project_skills, + + links: row.links ? row.links.map(link => ({ + ...link, + logo: appendBaseUrl(link.logo), + })) : null, + + tags: (row.tags || []).map(item => ({ + ...item, + logo: appendBaseUrl(item.logo), + })), background_image: appendBaseUrl(row.background_path), project_logo: appendBaseUrl(row.logo), project_thumbnail: appendBaseUrl(row.project_thumbnail), repository, - tech: row.tech.map(item => ({ - ...item, - logo: appendBaseUrl(item.logo), - })), - programming_languages: row.programming_languages.map(item => ({ - ...item, - logo: appendBaseUrl(item.logo), - })), - tools: toolTypes.map(type => ({ - type, - tools: (row[`${type.toLowerCase()}_tools`] || []).map(tool => ({ - ...tool, - logo: appendBaseUrl(tool.logo), - })), - })), context: projectContextData, // Include the project context data - start_date: projectExtraInfoResult[0]?.start_month_year, - end_date: projectExtraInfoResult[0]?.end_month_year, + + // Update date extraction using the new format + start_date: projectExtraInfoResult[0]?.start_full_date, + end_date: projectExtraInfoResult[0]?.end_full_date || "Present", // Ensure "Present" if null date_logo: appendBaseUrl(projectExtraInfoResult[0]?.date_logo), + + // Split and concatenate screenshots with the base URL + screenshots: row.screenshots + ? row.screenshots.split(",").map(path => `${BASE_URL}${path.trim()}`) + : [], })); - console.log(pageData); + //console.log(pageData); res.json(pageData[0]); } catch (err) { diff --git a/backend/routes/skills.js b/backend/routes/skills.js index 77d2ea7..cdd6c7a 100644 --- a/backend/routes/skills.js +++ b/backend/routes/skills.js @@ -67,6 +67,27 @@ l.name; `; + + const databasesQuery = ` + SELECT + d.id, + d.name, + d.logo, + MAX(spr.level) AS level, + MAX(spr.description) AS description + FROM + \`databases\` d + JOIN + databases_skill_proficiency dsp ON d.id = dsp.database_id + JOIN + skill_proficiency_ranking spr ON dsp.skill_proficiency_id = spr.id + GROUP BY + d.id, d.name, d.logo + ORDER BY + d.name; + `; + + const experienceQuery = ` SELECT e.id, @@ -98,6 +119,7 @@ const techsRaw = await pool.query(techQuery); const toolsRaw = await pool.query(toolQuery); const languagesRaw = await pool.query(languagesQuery); + const databasesRaw = await pool.query(databasesQuery); const experienceRaw = await pool.query(experienceQuery); const logosRaw = await pool.query(logosQuery); @@ -129,11 +151,19 @@ skill: `${language.description}: ${language.level}/5` })); + const updatedDatabases = databasesRaw.map(database => ({ + id: database.id, + name: database.name, + logo: database.logo ? BASE_URL + database.logo : null, + skill: `${database.description}: ${database.level}/5` + })); + // Group techs, tools, and languages into a single skills object const skills = { techs: updatedTechs, tools: updatedTools, - languages: updatedLanguages + languages: updatedLanguages, + databases: updatedDatabases }; // Format experience data diff --git a/database.sql b/database.sql new file mode 100644 index 0000000..1a49393 --- /dev/null +++ b/database.sql @@ -0,0 +1,565 @@ +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET NAMES utf8 */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + + +-- Dumping database structure for portfolio +CREATE DATABASE IF NOT EXISTS `portfolio` /*!40100 DEFAULT CHARACTER SET utf8mb4 */; +USE `portfolio`; + +-- Dumping structure for table portfolio.about +CREATE TABLE IF NOT EXISTS `about` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `title` varchar(50) NOT NULL, + `description` text NOT NULL, + `image` varchar(255) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.about: ~3 rows (approximately) +INSERT INTO `about` (`id`, `title`, `description`, `image`) VALUES + (1, 'Where I\'m From', 'I come from a small, sunny town called "Beja", located in the south of Portugal.', '/portfolio/icons/about/beja.webp'), + (2, 'Graduated in Game Development', 'I completed a Bachelor\'s degree in Game Development at IADE - Creative University, located in Lisbon, Portugal.', '/portfolio/icons/about/iade.webp'), + (3, 'Erasmus Experience in the United Kingdom', 'During the final semester of my Bachelor\'s degree, I participated in the Erasmus program at Abertay University in Dundee, Scotland.', '/portfolio/icons/about/abertay.webp'); + +-- Dumping structure for table portfolio.categories +CREATE TABLE IF NOT EXISTS `categories` ( + `id` tinyint(4) NOT NULL AUTO_INCREMENT COMMENT 'Category ID', + `name` varchar(50) DEFAULT NULL COMMENT 'Category name', + `logo` varchar(255) DEFAULT NULL COMMENT 'Category logo', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.categories: ~4 rows (approximately) +INSERT INTO `categories` (`id`, `name`, `logo`) VALUES + (1, 'Games', '/portfolio/icons/categories/games.webp'), + (2, 'Game Systems', '/portfolio/icons/categories/game_systems.webp'), + (3, 'Websites', '/portfolio/icons/categories/websites.webp'), + (4, 'Apps', '/portfolio/icons/categories/apps.webp'); + +-- Dumping structure for table portfolio.experience +CREATE TABLE IF NOT EXISTS `experience` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Experience id', + `title` varchar(255) DEFAULT NULL COMMENT 'Experience title', + `org` varchar(50) DEFAULT NULL, + `start_date` date DEFAULT NULL COMMENT 'Experice start date', + `end_date` date DEFAULT NULL COMMENT 'Experice end date', + `context` enum('Academic','Professional') DEFAULT NULL COMMENT 'Experience context', + `description` text DEFAULT NULL COMMENT 'Description about the experience', + `logo` varchar(255) DEFAULT NULL COMMENT 'Experience logo url', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.experience: ~2 rows (approximately) +INSERT INTO `experience` (`id`, `title`, `org`, `start_date`, `end_date`, `context`, `description`, `logo`) VALUES + (1, 'Bachelor\'s degree, Games Development', 'IADE - Creative University', '2020-09-16', '2024-07-20', 'Academic', ' • Designed and developed games using tools such as Unity, Love2D, and P5.js, with scripting experience in C#, Lua, and JavaScript.\\n\\n\r\n • Created a single-player Unity 2D and Unity 3D project, implementing FSM-driven AI behaviors and custom shaders while collaborating with multidisciplinary teams.\\n\\n\r\n • Built a simple multiplayer game prototype in Unreal Engine using C++ and Blueprints, gaining foundational knowledge of design patterns and server matchmaking systems.\\n\\n\r\n • Worked with basic database systems (MySQL) and backend development using Node.js to manage simple game data and server communications.\\n\\n\r\n • Applied data structures to enhance game functionality and optimize performance.\\n\\n\r\n • Learned foundational concepts in game security, including reverse engineering and memory hacking with tools like CheatEngine and DotPeek.\\n\\n\r\n • Strengthened project management and teamwork skills using tools like Trello and ClickUp to lead collaborative game development projects.', '/portfolio/icons/experience/iade.webp'), + (2, ' Bachelor\'s degree, Computer Games Application Development', 'Abertay University', '2023-01-22', '2023-06-18', 'Academic', ' • During the 6th semester of my Bachelor\'s in Game Development, I participated in an exchange program at Abertay University in Dundee, Scotland. As the game programmer in a team working with Astrodreamer Studio, I collaborated with game producers and designers to develop a Unity 3D game. This project, supervised by Abertay University, honed my skills in programming, teamwork, and project management within a professional studio environment.', '/portfolio/icons/experience/abertay.webp'); + +-- Dumping structure for table portfolio.experience_languages +CREATE TABLE IF NOT EXISTS `experience_languages` ( + `experience_id` int(11) NOT NULL, + `language_id` int(11) NOT NULL, + PRIMARY KEY (`experience_id`,`language_id`), + KEY `fk_exp_language_language_id` (`language_id`), + CONSTRAINT `fk_exp_language_experience_id` FOREIGN KEY (`experience_id`) REFERENCES `experience` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_exp_language_language_id` FOREIGN KEY (`language_id`) REFERENCES `languages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.experience_languages: ~6 rows (approximately) +INSERT INTO `experience_languages` (`experience_id`, `language_id`) VALUES + (1, 1), + (1, 2), + (1, 3), + (1, 5), + (1, 6), + (2, 2); + +-- Dumping structure for table portfolio.experience_tech +CREATE TABLE IF NOT EXISTS `experience_tech` ( + `experience_id` int(11) NOT NULL, + `tech_id` int(11) NOT NULL, + PRIMARY KEY (`experience_id`,`tech_id`), + KEY `fk_exp_tech_tech_id` (`tech_id`), + CONSTRAINT `fk_exp_tech_experience_id` FOREIGN KEY (`experience_id`) REFERENCES `experience` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_exp_tech_tech_id` FOREIGN KEY (`tech_id`) REFERENCES `tech` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.experience_tech: ~4 rows (approximately) +INSERT INTO `experience_tech` (`experience_id`, `tech_id`) VALUES + (1, 1), + (1, 2), + (1, 5), + (2, 1); + +-- Dumping structure for table portfolio.experience_tools +CREATE TABLE IF NOT EXISTS `experience_tools` ( + `experience_id` int(11) NOT NULL COMMENT 'Experience ID', + `tool_id` int(11) NOT NULL COMMENT 'Tool ID', + PRIMARY KEY (`experience_id`,`tool_id`), + KEY `fk_exp_tools_tool_id` (`tool_id`), + CONSTRAINT `fk_exp_tools_experience_id` FOREIGN KEY (`experience_id`) REFERENCES `experience` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_exp_tools_tool_id` FOREIGN KEY (`tool_id`) REFERENCES `tools` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.experience_tools: ~13 rows (approximately) +INSERT INTO `experience_tools` (`experience_id`, `tool_id`) VALUES + (1, 1), + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (1, 7), + (1, 9), + (1, 10), + (1, 11), + (1, 12), + (2, 1), + (2, 5), + (2, 8); + +-- Dumping structure for table portfolio.languages +CREATE TABLE IF NOT EXISTS `languages` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Unique identifier for each language', + `name` varchar(50) DEFAULT NULL COMMENT 'Programming language name', + `logo` varchar(255) DEFAULT NULL COMMENT 'Programming language logo path', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.languages: ~7 rows (approximately) +INSERT INTO `languages` (`id`, `name`, `logo`) VALUES + (1, 'C++', '/portfolio/icons/languages/cpp.webp'), + (2, 'C#', '/portfolio/icons/languages/csharp.webp'), + (3, 'C', '/portfolio/icons/languages/c.webp'), + (5, 'Javascript', '/portfolio/icons/languages/javascript.webp'), + (6, 'Lua', '/portfolio/icons/languages/lua.webp'), + (7, 'HTML5', '/portfolio/icons/languages/html5.webp'), + (8, 'CSS', '/portfolio/icons/languages/css.webp'); + +-- Dumping structure for table portfolio.languages_skill_proficiency +CREATE TABLE IF NOT EXISTS `languages_skill_proficiency` ( + `language_id` int(11) NOT NULL COMMENT 'Language ID', + `skill_proficiency_id` int(11) NOT NULL COMMENT 'Skill Proficiency', + PRIMARY KEY (`language_id`,`skill_proficiency_id`), + KEY `fk_languages_skill_profeciency_id` (`skill_proficiency_id`), + CONSTRAINT `fk_languages_skill_id` FOREIGN KEY (`language_id`) REFERENCES `languages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_languages_skill_profeciency_id` FOREIGN KEY (`skill_proficiency_id`) REFERENCES `skill_proficiency_ranking` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.languages_skill_proficiency: ~7 rows (approximately) +INSERT INTO `languages_skill_proficiency` (`language_id`, `skill_proficiency_id`) VALUES + (1, 2), + (2, 3), + (3, 1), + (5, 2), + (6, 3), + (7, 2), + (8, 2); + +-- Dumping structure for table portfolio.links +CREATE TABLE IF NOT EXISTS `links` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(50) DEFAULT NULL COMMENT 'Link name', + `logo` varchar(255) DEFAULT NULL COMMENT 'Link logo', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.links: ~1 rows (approximately) +INSERT INTO `links` (`id`, `name`, `logo`) VALUES + (1, 'build', NULL); + +-- Dumping structure for table portfolio.logos +CREATE TABLE IF NOT EXISTS `logos` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `type` varchar(50) DEFAULT NULL COMMENT 'Type of logo', + `reference_id` int(11) DEFAULT NULL COMMENT 'ID from related table', + `logo_path` varchar(255) DEFAULT NULL COMMENT 'Path to the logo image', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.logos: ~4 rows (approximately) +INSERT INTO `logos` (`id`, `type`, `reference_id`, `logo_path`) VALUES + (1, 'Date', NULL, '/portfolio/icons/misc/calendar.svg'), + (2, 'Academic', NULL, '/portfolio/icons/misc/academic.svg'), + (3, 'Professional', NULL, '/portfolio/icons/misc/professional.svg'), + (4, 'Personal', NULL, '/portfolio/icons/misc/personal.svg'); + +-- Dumping structure for table portfolio.page +CREATE TABLE IF NOT EXISTS `page` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(50) DEFAULT NULL, + `background_image` varchar(255) DEFAULT NULL, + `project_logo` varchar(255) DEFAULT NULL, + `page_color` varchar(50) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.page: ~6 rows (approximately) +INSERT INTO `page` (`id`, `name`, `background_image`, `project_logo`, `page_color`) VALUES + (1, 'dreamsturbia', '/portfolio/projects/dreamsturbia/images/background.jpg', '/portfolio/projects/dreamsturbia/images/logo.png', NULL), + (2, 'portfolio', '', '', NULL), + (3, 'sky_frontier', '/portfolio/projects/sky_frontier/images/background.png', NULL, NULL), + (4, 'hfsm_unity_3d', NULL, NULL, NULL), + (5, 'hardpoint', '/portfolio/projects/hardpoint/images/hardpoint_background.webp', NULL, NULL), + (6, 'the_vengeance', '/portfolio/projects/the_vengeance/images/the_vengeance_background.png', NULL, NULL); + +-- Dumping structure for table portfolio.projects +CREATE TABLE IF NOT EXISTS `projects` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Unique identifier for each project.', + `title` varchar(50) NOT NULL COMMENT ' Title of the project.', + `description` text DEFAULT NULL COMMENT 'Detailed description of the project.', + `category_id` tinyint(4) DEFAULT NULL COMMENT 'Category ID', + `hook` text DEFAULT NULL COMMENT 'Brief description of the project.', + `start_date` date DEFAULT NULL COMMENT 'Development start date.', + `end_date` date DEFAULT NULL COMMENT 'Development end date.', + `type` enum('Academic','Professional','Personal') DEFAULT NULL COMMENT 'Type of project.', + `git_rep` varchar(255) DEFAULT NULL COMMENT 'Path to the project Git Repository', + `trailer` varchar(50) DEFAULT NULL COMMENT 'YouTube link for the trailer.', + `thumbnail_path` varchar(255) DEFAULT NULL COMMENT 'Path to the folder containing project images.', + PRIMARY KEY (`id`), + KEY `fk_project_category_id` (`category_id`), + CONSTRAINT `fk_project_category_id` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.projects: ~6 rows (approximately) +INSERT INTO `projects` (`id`, `title`, `description`, `category_id`, `hook`, `start_date`, `end_date`, `type`, `git_rep`, `trailer`, `thumbnail_path`) VALUES + (1, 'Dreamsturbia', 'Dreamsturbia is a captivating 3D action-puzzle-adventure prototype that explores the haunting world of insomnia and the inner demons it creates. Players step into the role of a protagonist tormented by traumatic memories and relentless phobias, preventing her from finding peace in sleep. Seeking help from a therapist, she undergoes a trance-induced lucid dream where she must confront manifestations of her deepest fears.\r\n\r\nThis prototype was developed during my 3rd semester at university and was my first Unity 3D project. It was created in collaboration with a multidisciplinary team, where I worked alongside a design team. On this project i was responsible to create part of the NPC\'s AI, game core mechanics and level concept and design.', 1, 'A 3D action-puzzle-adventure exploring insomnia and its haunting inner demons.', '2022-02-14', '2022-06-20', 'Academic', 'Dreamsturbia-Project-IADE-Unity3D', 'https://www.youtube.com/embed/aFNQjcJDlnI', '/portfolio/projects/dreamsturbia/images/thumbnail.webp'), + (2, 'Portfolio', 'I fully developed this website myself as my first project using React and Tailwind. It includes sections about me, my skills, and my educational qualifications. I also built the backend using Node.js, and this was my first time hosting both the frontend and backend on my NAS (Network-Attached Storage) using Docker. For data storage, I used MariaDB and managed the database with HeidiSQL.', 3, 'My portfolio website!', '2025-01-07', NULL, 'Personal', 'My-Portfolio', NULL, '/portfolio/projects/portfolio/images/portfolio_thumbnail.webp'), + (3, 'The Vengeance', 'The Vengeance is a Unity 2D vertical slice action-adventure single-player game set in a medieval fantasy world. I originally developed this project for my 3rd semester at university. Later, I reworked it, adding more content and polishing it further by implementing finite state machines for NPCs. I also integrated a dialogue system using Ink and reworked the core mechanics of the prototype.', 1, 'The Vengeance is a 2D action-adventure single-player game set in a medieval fantasy world.', '2024-09-01', NULL, 'Academic', 'TheVengeance-Project-IADE-Unity2D', NULL, '/portfolio/projects/the_vengeance/images/the_vengeance_thumbnail.png'), + (4, 'Sky Frontier', 'Sky Frontier is a 3D multiplayer shooter developed in Unreal Engine 4. This was my first project in UE4, created during my 5th university semester. It also introduced me to using C++ in game development.', 1, 'Sky Frontier is a 3D multiplayer shooter. Where Air force pilots enter a virtual reality world to experiment with different classes of planes and train their skills in the high skies by destroying targets and fighting against other pilots.', '2022-09-10', '2023-01-10', 'Academic', 'SkyFrontier-Project-IADE-UE4-3D', 'https://www.youtube.com/embed/2VbHDiMxcCw', '/portfolio/projects/sky_frontier/images/sky_frontier_thumbnail.webp'), + (8, 'Hierarchical Finite State Machines', 'A simple Hierarchical State Machine system created in Unity 3D using ScriptableObjects. This is a small project I developed myself. I created this project after working with finite state machines and decided to adapt my previous implementations, applying the same philosophy to a Hierarchical Finite State Machine.', 2, 'Hierarchical State Machine system created in Unity 3D.', '2024-10-14', '2024-10-21', 'Personal', 'HierarchicalFSM-Unity3D', NULL, '/portfolio/projects/hfsm_unity_3d/images/hfsm_thumbnail.webp'), + (9, 'HardPoint', 'Hardpoint is a 3D roguelike action-adventure shooter developed in Unity 3D during my Erasmus semester at Abertay University. The game features procedurally generated levels composed of interconnected rooms, a teleporter system for strategic loot management, and a dynamic combat system with two playable characters. I contributed to refining the game’s mechanics, implementing enemy AI.', 1, 'Hardpoint is a 3D roguelike action game', '2023-02-01', '2023-06-05', 'Academic', 'HardPoint-Project-Abertay-University-Unity3D', NULL, '/portfolio/projects/hardpoint/images/hardpoint_thumbnail.webp'); + +-- Dumping structure for table portfolio.project_languages +CREATE TABLE IF NOT EXISTS `project_languages` ( + `project_id` int(11) NOT NULL COMMENT 'Foreign key referencing projects.id', + `language_id` int(11) NOT NULL COMMENT 'Foreign key referencing languages.id.', + PRIMARY KEY (`project_id`,`language_id`), + KEY `fk_projects_languages_language_id` (`language_id`), + CONSTRAINT `fk_projects_languages_language_id` FOREIGN KEY (`language_id`) REFERENCES `languages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_projects_languages_project_id` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.project_languages: ~8 rows (approximately) +INSERT INTO `project_languages` (`project_id`, `language_id`) VALUES + (1, 2), + (2, 5), + (2, 7), + (2, 8), + (3, 2), + (4, 1), + (8, 2), + (9, 2); + +-- Dumping structure for table portfolio.project_page +CREATE TABLE IF NOT EXISTS `project_page` ( + `project_id` int(11) NOT NULL, + `page_id` int(11) NOT NULL, + PRIMARY KEY (`project_id`,`page_id`) USING BTREE, + KEY `fk_project_page_page_id` (`page_id`), + CONSTRAINT `fk_project_page_page_id` FOREIGN KEY (`page_id`) REFERENCES `page` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_project_page_project_id` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.project_page: ~6 rows (approximately) +INSERT INTO `project_page` (`project_id`, `page_id`) VALUES + (1, 1), + (2, 2), + (3, 6), + (4, 3), + (8, 4), + (9, 5); + +-- Dumping structure for table portfolio.project_tags +CREATE TABLE IF NOT EXISTS `project_tags` ( + `project_id` int(11) NOT NULL, + `tag_id` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`project_id`,`tag_id`), + KEY `fk_project_tag_tag_id` (`tag_id`), + CONSTRAINT `fk_project_tag_project_id` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_project_tag_tag_id` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.project_tags: ~9 rows (approximately) +INSERT INTO `project_tags` (`project_id`, `tag_id`) VALUES + (1, 2), + (1, 4), + (3, 1), + (3, 4), + (4, 2), + (4, 3), + (8, 7), + (9, 2), + (9, 4); + +-- Dumping structure for table portfolio.project_tech +CREATE TABLE IF NOT EXISTS `project_tech` ( + `project_id` int(11) NOT NULL COMMENT 'Foreign key referencing projects.id', + `tech_id` int(11) NOT NULL COMMENT 'Foreign key referencing technologies.id', + PRIMARY KEY (`project_id`,`tech_id`) USING BTREE, + KEY `fk_project_technologies_technology_id` (`tech_id`) USING BTREE, + CONSTRAINT `fk_project_technologies_project_id` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_project_technologies_technology_id` FOREIGN KEY (`tech_id`) REFERENCES `tech` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.project_tech: ~12 rows (approximately) +INSERT INTO `project_tech` (`project_id`, `tech_id`) VALUES + (1, 1), + (1, 6), + (2, 3), + (2, 4), + (2, 9), + (3, 1), + (3, 6), + (4, 2), + (8, 1), + (8, 6), + (9, 1), + (9, 6); + +-- Dumping structure for table portfolio.project_tools +CREATE TABLE IF NOT EXISTS `project_tools` ( + `project_id` int(11) NOT NULL COMMENT 'Foreign key referencing projects.id.', + `tool_id` int(11) NOT NULL COMMENT 'Foreign key referencing tools.id.', + PRIMARY KEY (`project_id`,`tool_id`), + KEY `fk_project_tools_tool_id` (`tool_id`) USING BTREE, + CONSTRAINT `fk_project_tools_project_id` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_project_tools_tool_id` FOREIGN KEY (`tool_id`) REFERENCES `tools` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.project_tools: ~20 rows (approximately) +INSERT INTO `project_tools` (`project_id`, `tool_id`) VALUES + (1, 1), + (1, 5), + (2, 13), + (3, 1), + (3, 2), + (3, 5), + (3, 6), + (3, 10), + (4, 1), + (4, 2), + (4, 4), + (4, 10), + (4, 12), + (4, 14), + (8, 5), + (8, 6), + (9, 1), + (9, 2), + (9, 5), + (9, 8); + +-- Dumping structure for table portfolio.skill_proficiency_ranking +CREATE TABLE IF NOT EXISTS `skill_proficiency_ranking` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id', + `level` tinyint(4) unsigned NOT NULL COMMENT 'Skill Proficiency Level', + `description` varchar(50) NOT NULL COMMENT 'Skill Proficiency Description', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.skill_proficiency_ranking: ~5 rows (approximately) +INSERT INTO `skill_proficiency_ranking` (`id`, `level`, `description`) VALUES + (1, 1, 'Beginner'), + (2, 2, 'Familiar'), + (3, 3, 'Comfortable'), + (4, 4, 'Proficient'), + (5, 5, 'Expert'); + +-- Dumping structure for table portfolio.social_media +CREATE TABLE IF NOT EXISTS `social_media` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(50) DEFAULT NULL COMMENT 'Social Media name', + `username` varchar(50) DEFAULT NULL, + `url` varchar(255) DEFAULT NULL COMMENT 'Social Media url', + `logo` varchar(255) DEFAULT NULL COMMENT 'Social Media logo url', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.social_media: ~5 rows (approximately) +INSERT INTO `social_media` (`id`, `name`, `username`, `url`, `logo`) VALUES + (1, 'instagram', 'nuno97teixeira', 'https://www.instagram.com/nuno97teixeira/', '/portfolio/icons/socials/instagram.webp'), + (2, 'linkedin', 'nunoteixeira97', 'https://www.linkedin.com/in/nunoteixeira97/', '/portfolio/icons/socials/linkedin.webp'), + (3, 'gmail', 'nuno97teixeira@gmail.com', NULL, '/portfolio/icons/socials/gmail.webp'), + (5, 'facebook', 'nuno.teixeira.940', 'https://www.facebook.com/nuno.teixeira.940/', '/portfolio/icons/socials/facebook.webp'), + (6, 'gitbucket', NULL, 'https://jmpteixeira.myasustor.com:8443/Nuno_Teixeira', '/portfolio/icons/socials/gitbucket.webp'); + +-- Dumping structure for table portfolio.tags +CREATE TABLE IF NOT EXISTS `tags` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Tag ID', + `name` varchar(50) DEFAULT NULL COMMENT 'Tag Name', + `category_id` tinyint(4) DEFAULT NULL COMMENT 'Category ID', + PRIMARY KEY (`id`), + KEY `fk_category_id` (`category_id`), + CONSTRAINT `fk_category_id` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.tags: ~8 rows (approximately) +INSERT INTO `tags` (`id`, `name`, `category_id`) VALUES + (1, '2D', 1), + (2, '3D', 1), + (3, 'Multiplayer', 1), + (4, 'Singleplayer', 1), + (5, 'Unity', 1), + (6, 'Unreal Engine', 1), + (7, 'AI', 2), + (8, 'Physics', 2); + +-- Dumping structure for table portfolio.tag_logo +CREATE TABLE IF NOT EXISTS `tag_logo` ( + `tag_id` int(11) NOT NULL COMMENT 'Tag id', + `logo_id` int(11) NOT NULL COMMENT 'Logo id', + PRIMARY KEY (`tag_id`,`logo_id`), + KEY `fk_tag_logo_logo_id` (`logo_id`), + CONSTRAINT `fk_tag_logo_logo_id` FOREIGN KEY (`logo_id`) REFERENCES `logos` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_tag_logo_tag_id` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.tag_logo: ~0 rows (approximately) + +-- Dumping structure for table portfolio.tech +CREATE TABLE IF NOT EXISTS `tech` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Unique identifier for each technology', + `name` varchar(100) DEFAULT NULL COMMENT ' Technology name', + `logo` varchar(255) DEFAULT NULL COMMENT 'Technology logo path', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.tech: ~8 rows (approximately) +INSERT INTO `tech` (`id`, `name`, `logo`) VALUES + (1, 'Unity', '/portfolio/icons/tech/unity.webp'), + (2, 'Unreal Engine', '/portfolio/icons/tech/unreal_engine.webp'), + (3, 'React', '/portfolio/icons/tech/react.webp'), + (4, 'Tailwind', '/portfolio/icons/tech/tailwind.webp'), + (5, 'Love2D', '/portfolio/icons/tech/love2D.webp'), + (6, 'Git', '/portfolio/icons/tech/git.webp'), + (7, 'p5.js', '/portfolio/icons/tech/p5_js.webp'), + (9, 'node.js', '/portfolio/icons/tech/nodejs.webp'); + +-- Dumping structure for table portfolio.tech_skill_proficiency +CREATE TABLE IF NOT EXISTS `tech_skill_proficiency` ( + `tech_id` int(11) NOT NULL COMMENT 'Tech ID', + `skill_proficiency_id` int(11) NOT NULL COMMENT 'Skill Proficiency ID', + PRIMARY KEY (`tech_id`,`skill_proficiency_id`), + KEY `fk_tech_skill_proficiency_id` (`skill_proficiency_id`), + CONSTRAINT `fk_tech_skill_id` FOREIGN KEY (`tech_id`) REFERENCES `tech` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_tech_skill_proficiency_id` FOREIGN KEY (`skill_proficiency_id`) REFERENCES `skill_proficiency_ranking` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.tech_skill_proficiency: ~8 rows (approximately) +INSERT INTO `tech_skill_proficiency` (`tech_id`, `skill_proficiency_id`) VALUES + (1, 3), + (2, 2), + (3, 1), + (4, 1), + (5, 2), + (6, 2), + (7, 1), + (9, 2); + +-- Dumping structure for table portfolio.tools +CREATE TABLE IF NOT EXISTS `tools` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Unique identifier for each tool.', + `name` varchar(50) DEFAULT NULL COMMENT 'Tool name', + `type` enum('Development','Managment','Design') DEFAULT NULL COMMENT 'Tool type', + `logo` varchar(255) DEFAULT NULL COMMENT 'Logo path', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.tools: ~14 rows (approximately) +INSERT INTO `tools` (`id`, `name`, `type`, `logo`) VALUES + (1, 'GitHub', 'Development', '/portfolio/icons/tools/github.webp'), + (2, 'Google Docs', 'Managment', '/portfolio/icons/tools/google_docs.webp'), + (3, 'ClickUp', 'Managment', '/portfolio/icons/tools/clickup.webp'), + (4, 'Photoshop', 'Design', '/portfolio/icons/tools/photoshop.webp'), + (5, 'Visual Studio', 'Development', '/portfolio/icons/tools/visual_studio.webp'), + (6, 'GitBucket', 'Development', '/portfolio/icons/tools/gitbucket.webp'), + (7, 'DragonBones', 'Design', '/portfolio/icons/tools/dragonbones.webp'), + (8, 'Jira', 'Managment', '/portfolio/icons/tools/jira.webp'), + (9, 'Visual Studio Code', 'Development', '/portfolio/icons/tools/vscode.webp'), + (10, 'Trello', 'Managment', '/portfolio/icons/tools/trello.webp'), + (11, 'Adobe Illustrator', 'Design', '/portfolio/icons/tools/adobe_illustrator.webp'), + (12, 'JetBrains Rider', 'Development', '/portfolio/icons/tools/rider.webp'), + (13, 'HeidiSQL', 'Development', '/portfolio/icons/tools/heidi.webp'), + (14, 'Vegas Pro', 'Design', '/portfolio/icons/tools/vegas_pro.webp'); + +-- Dumping structure for table portfolio.tools_skill_proficiency +CREATE TABLE IF NOT EXISTS `tools_skill_proficiency` ( + `tool_id` int(11) NOT NULL COMMENT 'Tool ID', + `skill_proficiency_id` int(11) NOT NULL COMMENT 'Skill Proficiency ID', + PRIMARY KEY (`tool_id`,`skill_proficiency_id`), + KEY `fk_tools_skill_profeciency_id` (`skill_proficiency_id`), + CONSTRAINT `fk_tools_skill_id` FOREIGN KEY (`tool_id`) REFERENCES `tools` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_tools_skill_profeciency_id` FOREIGN KEY (`skill_proficiency_id`) REFERENCES `skill_proficiency_ranking` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dumping data for table portfolio.tools_skill_proficiency: ~14 rows (approximately) +INSERT INTO `tools_skill_proficiency` (`tool_id`, `skill_proficiency_id`) VALUES + (1, 2), + (2, 4), + (3, 2), + (4, 1), + (5, 3), + (6, 3), + (7, 1), + (8, 2), + (9, 3), + (10, 4), + (11, 1), + (12, 3), + (13, 3), + (14, 2); + +-- Dumping structure for trigger portfolio.languages_before_insert_logo +SET @OLDTMP_SQL_MODE=@@SQL_MODE, SQL_MODE=''; +DELIMITER // +CREATE Teixeira`@`%` TRIGGER `languages_before_insert_logo` BEFORE UPDATE ON `languages` FOR EACH ROW BEGIN + IF NEW.logo IS NOT NULL OR NEW.logo = '' THEN + SET NEW.logo = CONCAT('/portfolio/icons/languages/', NEW.logo); + END IF; +END// +DELIMITER ; +SET SQL_MODE=@OLDTMP_SQL_MODE; + +-- Dumping structure for trigger portfolio.page_before_insert_images +SET @OLDTMP_SQL_MODE=@@SQL_MODE, SQL_MODE=''; +DELIMITER // +CREATE Teixeira`@`%` TRIGGER `page_before_insert_images` BEFORE UPDATE ON `page` FOR EACH ROW BEGIN + /*IF NEW.background_image IS NOT NULL OR NEW.background_image = '' THEN + SET NEW.background_image = CONCAT('/portfolio/projects/', NEW.background_image); + END IF;*/ +END// +DELIMITER ; +SET SQL_MODE=@OLDTMP_SQL_MODE; + +-- Dumping structure for trigger portfolio.projects_before_insert_thumbail +SET @OLDTMP_SQL_MODE=@@SQL_MODE, SQL_MODE=''; +DELIMITER // +CREATE Teixeira`@`%` TRIGGER `projects_before_insert_thumbail` BEFORE INSERT ON `projects` FOR EACH ROW BEGIN + IF NEW.thumbnail_path IS NOT NULL OR NEW.thumbnail_path = '' THEN + SET NEW.thumbnail_path = CONCAT('/portfolio/projects/', NEW.thumbnail_path); + END IF; +END// +DELIMITER ; +SET SQL_MODE=@OLDTMP_SQL_MODE; + +-- Dumping structure for trigger portfolio.tech_before_insert_logo +SET @OLDTMP_SQL_MODE=@@SQL_MODE, SQL_MODE=''; +DELIMITER // +CREATE Teixeira`@`%` TRIGGER `tech_before_insert_logo` BEFORE UPDATE ON `tech` FOR EACH ROW BEGIN + IF NEW.logo IS NOT NULL OR NEW.logo = '' THEN + SET NEW.logo = CONCAT('/portfolio/icons/tech/', NEW.logo); + END IF; +END// +DELIMITER ; +SET SQL_MODE=@OLDTMP_SQL_MODE; + +/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */; +/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */; +/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */; diff --git a/frontend/index.html b/frontend/index.html index 9b55a96..1de8176 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,9 @@ - + - portfolio + Nuno Teixeira | Portfolio
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2ac49d2..5715e89 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "clsx": "^2.1.1", "dotenv": "^16.4.7", + "lucide-react": "^0.483.0", "motion": "^11.16.4", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -3646,6 +3647,14 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.483.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.483.0.tgz", + "integrity": "sha512-WldsY17Qb/T3VZdMnVQ9C3DDIP7h1ViDTHVdVGnLZcvHNg30zH/MTQ04RTORjexoGmpsXroiQXZ4QyR0kBy0FA==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3673348..8093976 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "dependencies": { "clsx": "^2.1.1", "dotenv": "^16.4.7", + "lucide-react": "^0.483.0", "motion": "^11.16.4", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 31e5e60..45ed5ee 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,15 +1,22 @@ +//React import React from "react"; import { Routes, Route } from "react-router-dom"; + +//Framer Motion +import { AnimatePresence } from "framer-motion"; + +//Components import Header from "./components/Header"; import MainPage from "./components/sections/MainPage"; import Project from "./components/sections/projects/Project"; -import { AnimatePresence } from "framer-motion"; import { LoadingProvider } from "./components/utils/LoadingContext"; + +//Styles import "./index.css"; const App = () => { return ( -
+
{/* Header */}
diff --git a/frontend/src/assets/favicon.png b/frontend/src/assets/favicon.png deleted file mode 100644 index f1ef087..0000000 --- a/frontend/src/assets/favicon.png +++ /dev/null Binary files differ diff --git a/frontend/src/assets/favicon.webp b/frontend/src/assets/favicon.webp new file mode 100644 index 0000000..7e592b8 --- /dev/null +++ b/frontend/src/assets/favicon.webp Binary files differ diff --git a/frontend/src/assets/icons/apps.webp b/frontend/src/assets/icons/apps.webp deleted file mode 100644 index cb41b3e..0000000 --- a/frontend/src/assets/icons/apps.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/icons/clock.svg b/frontend/src/assets/icons/clock.svg new file mode 100644 index 0000000..b7b4224 --- /dev/null +++ b/frontend/src/assets/icons/clock.svg @@ -0,0 +1,7 @@ + + Time Streamline Icon: https://streamlinehq.com + + \ No newline at end of file diff --git a/frontend/src/assets/icons/facebook.svg b/frontend/src/assets/icons/facebook.svg new file mode 100644 index 0000000..92cd1f9 --- /dev/null +++ b/frontend/src/assets/icons/facebook.svg @@ -0,0 +1,7 @@ + + Facebook Streamline Icon: https://streamlinehq.com + + \ No newline at end of file diff --git a/frontend/src/assets/icons/game_systems.webp b/frontend/src/assets/icons/game_systems.webp deleted file mode 100644 index 5403e2b..0000000 --- a/frontend/src/assets/icons/game_systems.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/icons/games.webp b/frontend/src/assets/icons/games.webp deleted file mode 100644 index 51eaff3..0000000 --- a/frontend/src/assets/icons/games.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/icons/gitbucket.svg b/frontend/src/assets/icons/gitbucket.svg new file mode 100644 index 0000000..05c1830 --- /dev/null +++ b/frontend/src/assets/icons/gitbucket.svg @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/icons/hamburger.svg b/frontend/src/assets/icons/hamburger.svg deleted file mode 100644 index 76fcbfb..0000000 --- a/frontend/src/assets/icons/hamburger.svg +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/icons/instagram.svg b/frontend/src/assets/icons/instagram.svg new file mode 100644 index 0000000..42ffd8f --- /dev/null +++ b/frontend/src/assets/icons/instagram.svg @@ -0,0 +1,8 @@ + + Instagram Streamline Icon: https://streamlinehq.com + Instagram + + \ No newline at end of file diff --git a/frontend/src/assets/icons/linkedin.svg b/frontend/src/assets/icons/linkedin.svg new file mode 100644 index 0000000..fba3b06 --- /dev/null +++ b/frontend/src/assets/icons/linkedin.svg @@ -0,0 +1,8 @@ + + Linkedin Streamline Icon: https://streamlinehq.com + LinkedIn + + \ No newline at end of file diff --git a/frontend/src/assets/icons/websites.webp b/frontend/src/assets/icons/websites.webp deleted file mode 100644 index e3e902a..0000000 --- a/frontend/src/assets/icons/websites.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/icons/youtube.svg b/frontend/src/assets/icons/youtube.svg new file mode 100644 index 0000000..fb12530 --- /dev/null +++ b/frontend/src/assets/icons/youtube.svg @@ -0,0 +1,8 @@ + + Youtube Streamline Icon: https://streamlinehq.com + YouTube + + \ No newline at end of file diff --git a/frontend/src/assets/index.js b/frontend/src/assets/index.js index 050a8e8..3f29f6a 100644 --- a/frontend/src/assets/index.js +++ b/frontend/src/assets/index.js @@ -1,5 +1,10 @@ //Social Media -import proflie_picture from "./personal/myphoto.jpg"; +import proflie_picture from "./personal/myphoto.webp"; +import facebook from "./icons/facebook.svg"; +import instagram from "./icons/instagram.svg"; +import gitbucket from "./icons/gitbucket.svg"; +import linkedin from "./icons/linkedin.svg"; +import youtube from "./icons/youtube.svg"; //Tools import pdf from "./personal/pdf.png"; @@ -8,6 +13,16 @@ import all_tags from "./icons/alltags.webp"; import tag_icon from "./icons/tag_icon.svg"; import peek_icon from "./icons/peek_icon.svg"; +import clock_icon from "./icons/clock.svg"; + +//Misc Logos +import joystick from "./misc/joystick.svg"; +import unity from "./misc/unity.svg"; +import dpad from "./misc/dpad.svg"; +import controller from "./misc/controller.svg"; +import unreal from "./misc/unreal.svg"; +import x from "./misc/x.svg"; +import gameboy from "./misc/gameboy.svg"; export { //icons @@ -15,7 +30,24 @@ all_tags, tag_icon, pdf, + clock_icon, //picture proflie_picture, + + //logos + facebook, + instagram, + gitbucket, + linkedin, + youtube, + + //Misc Logos + joystick, + unity, + dpad, + controller, + unreal, + x, + gameboy, }; diff --git a/frontend/src/assets/languages/cpp.webp b/frontend/src/assets/languages/cpp.webp deleted file mode 100644 index 6ce58db..0000000 --- a/frontend/src/assets/languages/cpp.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/languages/csharp.webp b/frontend/src/assets/languages/csharp.webp deleted file mode 100644 index 17ab5ec..0000000 --- a/frontend/src/assets/languages/csharp.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/languages/html5.webp b/frontend/src/assets/languages/html5.webp deleted file mode 100644 index cbdebb2..0000000 --- a/frontend/src/assets/languages/html5.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/languages/javascript.webp b/frontend/src/assets/languages/javascript.webp deleted file mode 100644 index ef9cc17..0000000 --- a/frontend/src/assets/languages/javascript.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/languages/lua.webp b/frontend/src/assets/languages/lua.webp deleted file mode 100644 index bd681af..0000000 --- a/frontend/src/assets/languages/lua.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/misc/controller.svg b/frontend/src/assets/misc/controller.svg new file mode 100644 index 0000000..798dfd7 --- /dev/null +++ b/frontend/src/assets/misc/controller.svg @@ -0,0 +1,9 @@ + + Controller 1 Streamline Icon: https://streamlinehq.com + + + + \ No newline at end of file diff --git a/frontend/src/assets/misc/dpad.svg b/frontend/src/assets/misc/dpad.svg new file mode 100644 index 0000000..9f340ec --- /dev/null +++ b/frontend/src/assets/misc/dpad.svg @@ -0,0 +1,7 @@ + + Dpad Fill Streamline Icon: https://streamlinehq.com + + \ No newline at end of file diff --git a/frontend/src/assets/misc/gameboy.svg b/frontend/src/assets/misc/gameboy.svg new file mode 100644 index 0000000..316f4c5 --- /dev/null +++ b/frontend/src/assets/misc/gameboy.svg @@ -0,0 +1,9 @@ + + Gameboy Fill Streamline Icon: https://streamlinehq.com + + + + \ No newline at end of file diff --git a/frontend/src/assets/misc/joystick.svg b/frontend/src/assets/misc/joystick.svg new file mode 100644 index 0000000..74caf59 --- /dev/null +++ b/frontend/src/assets/misc/joystick.svg @@ -0,0 +1,10 @@ + + Joystick Streamline Icon: https://streamlinehq.com + + + \ No newline at end of file diff --git a/frontend/src/assets/misc/unity.svg b/frontend/src/assets/misc/unity.svg new file mode 100644 index 0000000..51c2415 --- /dev/null +++ b/frontend/src/assets/misc/unity.svg @@ -0,0 +1,8 @@ + + Unity Streamline Icon: https://streamlinehq.com + + \ No newline at end of file diff --git a/frontend/src/assets/misc/unreal.svg b/frontend/src/assets/misc/unreal.svg new file mode 100644 index 0000000..9602e29 --- /dev/null +++ b/frontend/src/assets/misc/unreal.svg @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/assets/misc/x.svg b/frontend/src/assets/misc/x.svg new file mode 100644 index 0000000..adfe9c2 --- /dev/null +++ b/frontend/src/assets/misc/x.svg @@ -0,0 +1,7 @@ + + Icons Streamline Icon: https://streamlinehq.com + + \ No newline at end of file diff --git a/frontend/src/assets/personal/myphoto.jpg b/frontend/src/assets/personal/myphoto.jpg deleted file mode 100644 index 8618693..0000000 --- a/frontend/src/assets/personal/myphoto.jpg +++ /dev/null Binary files differ diff --git a/frontend/src/assets/personal/myphoto.webp b/frontend/src/assets/personal/myphoto.webp new file mode 100644 index 0000000..bb32aad --- /dev/null +++ b/frontend/src/assets/personal/myphoto.webp Binary files differ diff --git a/frontend/src/assets/personal/myphoto_tp.png b/frontend/src/assets/personal/myphoto_tp.png deleted file mode 100644 index a7d2bb6..0000000 --- a/frontend/src/assets/personal/myphoto_tp.png +++ /dev/null Binary files differ diff --git a/frontend/src/assets/svg/ButtonSvg.jsx b/frontend/src/assets/svg/ButtonSvg.jsx index bdab7cb..efe1f08 100644 --- a/frontend/src/assets/svg/ButtonSvg.jsx +++ b/frontend/src/assets/svg/ButtonSvg.jsx @@ -8,7 +8,7 @@ > @@ -29,12 +29,12 @@ ) : ( <> diff --git a/frontend/src/assets/svg/PlusSvg.jsx b/frontend/src/assets/svg/PlusSvg.jsx deleted file mode 100644 index c07be13..0000000 --- a/frontend/src/assets/svg/PlusSvg.jsx +++ /dev/null @@ -1,12 +0,0 @@ -const PlusSvg = ({ className = "" }) => { - return ( - - - - ); -}; - -export default PlusSvg; diff --git a/frontend/src/assets/svg/SectionSvg.jsx b/frontend/src/assets/svg/SectionSvg.jsx deleted file mode 100644 index 409e828..0000000 --- a/frontend/src/assets/svg/SectionSvg.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import PlusSvg from "./PlusSvg"; - -const SectionSvg = ({ crossesOffset }) => { - return ( - <> - - - - - ); -}; - -export default SectionSvg; diff --git a/frontend/src/assets/tech/git.webp b/frontend/src/assets/tech/git.webp deleted file mode 100644 index a4aa930..0000000 --- a/frontend/src/assets/tech/git.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tech/love2D.webp b/frontend/src/assets/tech/love2D.webp deleted file mode 100644 index 5e9d72a..0000000 --- a/frontend/src/assets/tech/love2D.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tech/p5_js.webp b/frontend/src/assets/tech/p5_js.webp deleted file mode 100644 index d8ea691..0000000 --- a/frontend/src/assets/tech/p5_js.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tech/react.webp b/frontend/src/assets/tech/react.webp deleted file mode 100644 index e6444e2..0000000 --- a/frontend/src/assets/tech/react.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tech/unity.webp b/frontend/src/assets/tech/unity.webp deleted file mode 100644 index d533d77..0000000 --- a/frontend/src/assets/tech/unity.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tech/unreal_engine.webp b/frontend/src/assets/tech/unreal_engine.webp deleted file mode 100644 index a18c092..0000000 --- a/frontend/src/assets/tech/unreal_engine.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tools/adobe_illustrator.webp b/frontend/src/assets/tools/adobe_illustrator.webp deleted file mode 100644 index d7e03eb..0000000 --- a/frontend/src/assets/tools/adobe_illustrator.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tools/adobe_photoshop.webp b/frontend/src/assets/tools/adobe_photoshop.webp deleted file mode 100644 index 47c3c36..0000000 --- a/frontend/src/assets/tools/adobe_photoshop.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tools/dragonbones.webp b/frontend/src/assets/tools/dragonbones.webp deleted file mode 100644 index f683b2c..0000000 --- a/frontend/src/assets/tools/dragonbones.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tools/github.webp b/frontend/src/assets/tools/github.webp deleted file mode 100644 index dc07948..0000000 --- a/frontend/src/assets/tools/github.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tools/github_white.webp b/frontend/src/assets/tools/github_white.webp deleted file mode 100644 index bf5bcad..0000000 --- a/frontend/src/assets/tools/github_white.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tools/google_docs.webp b/frontend/src/assets/tools/google_docs.webp deleted file mode 100644 index 94af786..0000000 --- a/frontend/src/assets/tools/google_docs.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tools/jetbrains_raider.webp b/frontend/src/assets/tools/jetbrains_raider.webp deleted file mode 100644 index b839eba..0000000 --- a/frontend/src/assets/tools/jetbrains_raider.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tools/jira.webp b/frontend/src/assets/tools/jira.webp deleted file mode 100644 index 69557b3..0000000 --- a/frontend/src/assets/tools/jira.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tools/trello.webp b/frontend/src/assets/tools/trello.webp deleted file mode 100644 index bbfb4a1..0000000 --- a/frontend/src/assets/tools/trello.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tools/vegas_pro.webp b/frontend/src/assets/tools/vegas_pro.webp deleted file mode 100644 index 77ca0d6..0000000 --- a/frontend/src/assets/tools/vegas_pro.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tools/visual_studio.webp b/frontend/src/assets/tools/visual_studio.webp deleted file mode 100644 index ef3ded4..0000000 --- a/frontend/src/assets/tools/visual_studio.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tools/vscode.webp b/frontend/src/assets/tools/vscode.webp deleted file mode 100644 index 4305a96..0000000 --- a/frontend/src/assets/tools/vscode.webp +++ /dev/null Binary files differ diff --git a/frontend/src/assets/tools/word.webp b/frontend/src/assets/tools/word.webp deleted file mode 100644 index 6bf0daf..0000000 --- a/frontend/src/assets/tools/word.webp +++ /dev/null Binary files differ diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 3186ec5..cac3685 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -51,7 +51,7 @@ if (timeElapsed < duration) { requestAnimationFrame(animateScroll); } else { - window.scrollTo(0, targetPosition); // Ensure it reaches the target at the end + window.scrollTo(0, targetPosition); } }; diff --git a/frontend/src/components/Heading.jsx b/frontend/src/components/Heading.jsx index 0fa5483..66a992e 100644 --- a/frontend/src/components/Heading.jsx +++ b/frontend/src/components/Heading.jsx @@ -1,11 +1,43 @@ -const Heading = ({ title }) => { +const Heading = ({ + title = "", + highlightWords = [], + className = "relative text-4xl lg:text-5xl font-bold mb-16", +}) => { + // Function to apply highlights to substrings + const getHighlightedText = (text) => { + if (!highlightWords.length) return text; + + let result = []; + let lastIndex = 0; + + highlightWords.forEach(({ word, color }) => { + let startIndex = text.indexOf(word); + if (startIndex !== -1) { + // Push text before the match + if (startIndex > lastIndex) { + result.push(text.substring(lastIndex, startIndex)); + } + // Push the highlighted match + result.push( + + {word} + + ); + lastIndex = startIndex + word.length; + } + }); + + // Push remaining text + if (lastIndex < text.length) { + result.push(text.substring(lastIndex)); + } + + return result; + }; + return (
- {title && ( -
- {title} -
- )} +
{getHighlightedText(title)}
); }; diff --git a/frontend/src/components/MouseTracker.jsx b/frontend/src/components/MouseTracker.jsx index 6c6b454..29f72b0 100644 --- a/frontend/src/components/MouseTracker.jsx +++ b/frontend/src/components/MouseTracker.jsx @@ -1,39 +1,50 @@ -import React, { useEffect, useRef } from "react"; -import { createPortal } from "react-dom"; +import { useState, useEffect } from "react"; -const MouseTracker = ({ children, offset = { x: 0, y: 0 } }) => { - const element = useRef(null); +const MouseTracker = ({ text = "Tooltip" }) => { + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [visible, setVisible] = useState(false); useEffect(() => { - const handler = (ev) => { - if (element.current) { - const e = ev.touches ? ev.touches[0] : ev; - const x = e.clientX + offset.x; - const y = e.clientY + offset.y; - element.current.style.transform = `translate(${x}px, ${y}px)`; - element.current.style.visibility = "visible"; - } + const handleMouseMove = (e) => { + setPosition({ x: e.clientX, y: e.clientY - 60 }); + setVisible(true); }; - document.addEventListener("mousemove", handler); - document.addEventListener("touchmove", handler); + const handleMouseLeave = () => { + setVisible(false); + }; + + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseleave", handleMouseLeave); return () => { - document.removeEventListener("mousemove", handler); - document.removeEventListener("touchmove", handler); + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseleave", handleMouseLeave); }; - }, [offset]); + }, []); - return createPortal( + return visible && text ? (
- {children} -
, - document.body - ); + {text} +
+ ) : null; }; export default MouseTracker; diff --git a/frontend/src/components/design/FloatingSVG.jsx b/frontend/src/components/design/FloatingSVG.jsx new file mode 100644 index 0000000..6dae5fd --- /dev/null +++ b/frontend/src/components/design/FloatingSVG.jsx @@ -0,0 +1,127 @@ +import React, { useEffect, useRef, useState } from "react"; +import useIsMobile from "../../constants/useIsMobile"; // Ensure this hook is correctly implemented + +const easeInOutSine = (t) => -(Math.cos(Math.PI * t) - 1) / 2; + +const FloatingSVG = ({ + svg, + position, + speed = 0.5, + amplitude = 20, + rotationRange = 10, + resetTrigger, // Prop to trigger reset +}) => { + const svgRef = useRef(null); + const animationRef = useRef(null); + const startTimeRef = useRef(null); + const [isExploding, setIsExploding] = useState(true); + const isMobile = useIsMobile(); // Detect if the screen is mobile + + // Reset the explosion animation when resetTrigger changes + useEffect(() => { + if (!resetTrigger) { + // Section is not visible, reset to center + const svgElement = svgRef.current; + const centerX = window.innerWidth / 2 - (isMobile ? 7.5 : 15); // Adjust center based on size + const centerY = window.innerHeight / 2 - (isMobile ? 7.5 : 15); // Adjust center based on size + svgElement.style.transition = + "left 1s ease-out, top 1s ease-out, opacity 1s ease-out"; + svgElement.style.left = `${centerX}px`; + svgElement.style.top = `${centerY}px`; + svgElement.style.opacity = "0"; // Fade out + setIsExploding(true); // Prepare for next explosion + } else { + // Section is visible, start explosion animation + setIsExploding(true); + } + }, [resetTrigger, isMobile]); + + // Explosion and floating animation + useEffect(() => { + const svgElement = svgRef.current; + + if (isExploding && resetTrigger) { + // Set initial position to the center of the screen + const centerX = window.innerWidth / 2 - (isMobile ? 7.5 : 15); // Adjust center based on size + const centerY = window.innerHeight / 2 - (isMobile ? 7.5 : 15); // Adjust center based on size + svgElement.style.left = `${centerX}px`; + svgElement.style.top = `${centerY}px`; + + // Move to the target position with a smooth transition + svgElement.style.transition = + "left 1s ease-out, top 1s ease-out, opacity 1s ease-out"; + svgElement.style.opacity = "0"; // Start invisible + setTimeout(() => { + svgElement.style.left = position.x; + svgElement.style.top = position.y; + svgElement.style.opacity = "1"; + setIsExploding(false); + }, Math.random() * 500); // Random delay between 0 and 500ms + } + + // Floating animation + const animate = (timestamp) => { + if (!startTimeRef.current) startTimeRef.current = timestamp; + const elapsed = (timestamp - startTimeRef.current) / 1000; // Convert to seconds + + // Simulate floating with multiple sine waves + const offsetY = + Math.sin(elapsed * speed * 1.8) * amplitude * 0.6 + // Primary wave + Math.sin(elapsed * speed * 1.2) * amplitude * 0.4; // Secondary wave + + const offsetX = + Math.cos(elapsed * speed * 1.3) * amplitude * 0.3 + // Horizontal drift + Math.sin(elapsed * speed * 0.8) * amplitude * 0.2; // Subtle side drift + + // Eased Y motion for more natural feel + const t = (Math.sin(elapsed * speed) + 1) / 2; // Normalize to 0-1 + const easedOffsetY = easeInOutSine(t) * amplitude * 2 - amplitude; + + // Add smooth rotation + const rotation = Math.sin(elapsed * speed * 0.6) * rotationRange; + + // Apply transformation + svgElement.style.transform = `translate(${offsetX}px, ${easedOffsetY}px) rotate(${rotation}deg)`; + + animationRef.current = requestAnimationFrame(animate); + }; + + // Start floating animation after explosion is complete + if (!isExploding && resetTrigger) { + animationRef.current = requestAnimationFrame(animate); + } + + return () => cancelAnimationFrame(animationRef.current); + }, [ + position, + speed, + amplitude, + rotationRange, + isExploding, + resetTrigger, + isMobile, + ]); + + // Set size based on screen type (mobile or desktop) + const size = isMobile ? "20px" : "30px"; + + return ( +
+ floating-svg +
+ ); +}; + +export default FloatingSVG; diff --git a/frontend/src/components/sections/About.jsx b/frontend/src/components/sections/About.jsx index 876c2b6..c98ffab 100644 --- a/frontend/src/components/sections/About.jsx +++ b/frontend/src/components/sections/About.jsx @@ -3,10 +3,11 @@ import { motion } from "framer-motion"; import "./Home.css"; import { LoadingContext } from "../utils/LoadingContext"; +import NavigationDots from "../utils/NavigationDots"; const About = ({ isVisible, aboutMe }) => { const [error, setError] = useState(null); - const { isLoading } = useContext(LoadingContext); // Use the global loading state + const { isLoading } = useContext(LoadingContext); if (isLoading) return
Loading...
; @@ -22,7 +23,16 @@ return (
- + + {/* Animation container */} ( {/* Inner Content */}
-
+
{item.title}
@@ -59,7 +69,7 @@ )}

diff --git a/frontend/src/components/sections/Activity.jsx b/frontend/src/components/sections/Activity.jsx index c1e44d7..4a1b4e7 100644 --- a/frontend/src/components/sections/Activity.jsx +++ b/frontend/src/components/sections/Activity.jsx @@ -5,6 +5,7 @@ import LeftArrow from "../../assets/buttons/LeftArrow.svg"; import styles from "./Activity.module.css"; import { LoadingContext } from "../utils/LoadingContext"; +import NavigationDots from "../utils/NavigationDots"; const Activity = ({ isVisible, activityData }) => { const [currentIndex, setCurrentIndex] = useState(0); @@ -48,13 +49,17 @@ return (
- + + {/* Content Wrapper */}
diff --git a/frontend/src/components/sections/Contact.jsx b/frontend/src/components/sections/Contact.jsx index 59a3d39..84849e3 100644 --- a/frontend/src/components/sections/Contact.jsx +++ b/frontend/src/components/sections/Contact.jsx @@ -1,9 +1,19 @@ +//React import { useState, useEffect, useContext } from "react"; + +//Framer Motion import { motion } from "framer-motion"; + +//API import { postData } from "../../api"; -import Heading from "../Heading"; -import styles from "./Contact.module.css"; + +//Utils import { LoadingContext } from "../utils/LoadingContext"; +import Heading from "../Heading"; +import NavigationDots from "../utils/NavigationDots"; + +//CSS +import styles from "./Contact.module.css"; const Contact = ({ isVisible, contactData }) => { const [formData, setFormData] = useState({ @@ -60,94 +70,100 @@ return ( -
- +
+ + - {/* Social Media Buttons */} -
- {contactData.map((contact) => ( - - {contact.name} - - {contact.username} - - - ))} +
+ {/* Social Media Buttons */} +
+ {contactData.map((contact) => ( + + {contact.name} + + {contact.username} + + + ))} +
+ +
+ + + + + {/* Success/Error Message Below Message Field */} +
+ {error &&

{error}

} + {success &&

{success}

} +
+ +
+ +
+
+ + {/* Disclaimer */} +

+ Disclaimer: Please avoid sharing sensitive or confidential + information in your message. +

- -
- - - - - {/* Success/Error Message Below Message Field */} -
- {error &&

{error}

} - {success &&

{success}

} -
- -
- -
-
- - {/* Disclaimer */} -

- Disclaimer: Please avoid sharing sensitive or confidential information - in your message. -

{/* Rights */} -

+

@2025 Nuno Teixeira
All rights reserved

diff --git a/frontend/src/components/sections/ElementObserver.jsx b/frontend/src/components/sections/ElementObserver.jsx new file mode 100644 index 0000000..b222259 --- /dev/null +++ b/frontend/src/components/sections/ElementObserver.jsx @@ -0,0 +1,35 @@ +import { useEffect, useState } from "react"; + +const ElementObserver = (selectors, options = { threshold: 0.2 }) => { + const [visibleElements, setVisibleElements] = useState(new Set()); + + useEffect(() => { + const elements = selectors.flatMap((selector) => + Array.from(document.querySelectorAll(selector)) + ); + if (elements.length === 0) return; + + const handleIntersection = (entries) => { + entries.forEach((entry) => { + setVisibleElements((prevVisibleElements) => { + const newVisibleElements = new Set(prevVisibleElements); + if (entry.isIntersecting) { + newVisibleElements.add(entry.target); + } else { + newVisibleElements.delete(entry.target); + } + return newVisibleElements; + }); + }); + }; + + const observer = new IntersectionObserver(handleIntersection, options); + elements.forEach((element) => observer.observe(element)); + + return () => elements.forEach((element) => observer.unobserve(element)); + }, [selectors, options]); + + return (element) => visibleElements.has(element); +}; + +export default ElementObserver; diff --git a/frontend/src/components/sections/Home.css b/frontend/src/components/sections/Home.css index fc9a7de..e69de29 100644 --- a/frontend/src/components/sections/Home.css +++ b/frontend/src/components/sections/Home.css @@ -1,47 +0,0 @@ -/* CSS for the social and clock container */ -.social-container { - opacity: 0; -} - -.clock-container { - opacity: 0; -} - -.animate-glow { - animation: glow 2s infinite alternate; -} - -/* Rainbow Animation */ -@keyframes rainbow { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.animate-rainbow { - background-image: linear-gradient(to right, red, orange, yellow, green, cyan); - background-size: 300% 300%; - background-clip: text; - -webkit-background-clip: text; /* For WebKit browsers */ - color: transparent; /* Ensures text takes on the gradient */ - animation: rainbow 10s ease infinite; -} - -.animate-glow { - animation: glow 2s infinite alternate; -} - -@keyframes glow { - from { - filter: drop-shadow(0 0 8px rgba(0, 255, 255, 0.7)); - } - to { - filter: drop-shadow(0 0 16px rgba(0, 255, 255, 1)); - } -} diff --git a/frontend/src/components/sections/Home.jsx b/frontend/src/components/sections/Home.jsx index 699a2f5..12a76a6 100644 --- a/frontend/src/components/sections/Home.jsx +++ b/frontend/src/components/sections/Home.jsx @@ -1,9 +1,11 @@ import React, { useEffect, useState, useContext } from "react"; import { motion } from "framer-motion"; -import { fetchData } from "../../api"; import "./Home.css"; import { proflie_picture } from "../../assets/index"; import { LoadingContext } from "../utils/LoadingContext"; +import NavigationDots from "../utils/NavigationDots"; +import { floattingImages } from "../../constants"; +import FloatingSVG from "../design/FloatingSVG"; const Home = ({ isVisible, introData }) => { const [currentTime, setCurrentTime] = useState(new Date()); @@ -33,6 +35,19 @@ className="relative min-h-screen flex flex-col items-center pt-36 lg:pt-60 pb-16 sm:pb-8 md:pb-24 lg:pb-60 text-white" id="home" > + {/* Add floating SVG images to the background */} + {floattingImages.map((image, index) => ( + + ))} + + {/* ProfileContainer with animation */}
{ const { isLoading, setIsLoading } = useContext(LoadingContext); const activeSections = SectionObserver(); + const isMobile = useIsMobile(); // State to store fetched data const [introData, setIntroData] = useState([]); @@ -52,7 +56,7 @@ } catch (error) { console.error("Error fetching data:", error); } finally { - setIsLoading(false); // Set loading to false after fetching data + setIsLoading(false); } }; @@ -60,7 +64,7 @@ }, [setIsLoading]); if (isLoading) { - return
Loading...
; // Or your custom loading spinner + return
Loading...
; } return ( @@ -188,6 +192,34 @@ contactData={contactData} /> + +
+ {!isMobile && + static_social_media.map((social, index) => ( + + + + + + ))} +
); }; diff --git a/frontend/src/components/sections/Section.jsx b/frontend/src/components/sections/Section.jsx new file mode 100644 index 0000000..eedf2da --- /dev/null +++ b/frontend/src/components/sections/Section.jsx @@ -0,0 +1,68 @@ +import React from "react"; + +const Section = ({ + id, + bgColor, + textColor, + children, + hasTopSeparator, + hasBottomSeparator, + nextBgColor, +}) => { + return ( +
+ {/* Top Separator (only for middle sections) */} + {hasTopSeparator && ( + + + + + )} + + {/* Section Content */} + {children} + + {/* Bottom Separator (for all sections except the last) */} + {hasBottomSeparator && ( + + + + + )} +
+ ); +}; + +export default Section; diff --git a/frontend/src/components/sections/Skills.jsx b/frontend/src/components/sections/Skills.jsx index 4e9b736..407a5f5 100644 --- a/frontend/src/components/sections/Skills.jsx +++ b/frontend/src/components/sections/Skills.jsx @@ -5,11 +5,13 @@ import Heading from "../Heading"; import { pdf } from "../../assets/index"; import { LoadingContext } from "../utils/LoadingContext"; +import NavigationDots from "../utils/NavigationDots"; const Skills = ({ isVisible, skillsData }) => { - const [hoveredTech, setHoveredTech] = useState(null); - const [hoveredLanguage, setHoveredLanguage] = useState(null); - const [hoveredTools, setHoveredTools] = useState({}); + const [hoveredItem, setHoveredItem] = useState({ + section: null, + index: null, + }); // Track section and index const { isLoading } = useContext(LoadingContext); // Fallback if skillsData or its nested properties are undefined @@ -31,7 +33,11 @@ animate={isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 100 }} transition={{ duration: 0.2, ease: "easeOut" }} > - + +
{/* Tech Section */} @@ -45,8 +51,9 @@ title="Tech" items={skillsData.skills.techs} color="orange" - hoveredItem={hoveredTech} - setHoveredItem={setHoveredTech} + hoveredItem={hoveredItem} + setHoveredItem={setHoveredItem} + section="tech" // Unique section identifier getKey={(item, index) => `tech-${index}`} isVisible={isVisible} /> @@ -65,10 +72,9 @@ title={`${toolType} Tools`} items={tools} color="orange" - hoveredItem={hoveredTools[toolType]} - setHoveredItem={(item) => - setHoveredTools((prev) => ({ ...prev, [toolType]: item })) - } + hoveredItem={hoveredItem} + setHoveredItem={setHoveredItem} + section={`tools-${toolType}`} // Unique section identifier getKey={(item, index) => `${toolType}-${index}`} isVisible={isVisible} /> @@ -86,9 +92,30 @@ title="Languages" items={skillsData.skills.languages} color="orange" - hoveredItem={hoveredLanguage} - setHoveredItem={setHoveredLanguage} - getKey={(item, index) => `language-${index}`} + hoveredItem={hoveredItem} + setHoveredItem={setHoveredItem} + section="languages" // Unique section identifier + getKey={(item, index) => `language-${item.id || index}`} + isVisible={isVisible} + /> + + )} + + {/* Databases Section */} + {skillsData.skills.databases && ( + + `database-${item.id || index}`} isVisible={isVisible} /> @@ -96,7 +123,11 @@
{/* Experience Section */} - +
{/* Academic Experience */} {skillsData.experience?.academic?.length > 0 && ( @@ -268,7 +299,7 @@

Resume Download

-
+
{ const [activeCategory, setActiveCategory] = useState({ @@ -68,13 +80,17 @@ return ( - + +
{ +const Container = ({ + children, + className, + shadow = true, + whileHover, + transition = { duration: 0.5 }, +}) => { return ( {children} diff --git a/frontend/src/components/sections/projects/HoverableIcon.jsx b/frontend/src/components/sections/projects/HoverableIcon.jsx index 9b18a1b..354d2c3 100644 --- a/frontend/src/components/sections/projects/HoverableIcon.jsx +++ b/frontend/src/components/sections/projects/HoverableIcon.jsx @@ -1,48 +1,107 @@ -import React from "react"; +import React, { useState, useEffect, useRef } from "react"; import { motion } from "framer-motion"; import clsx from "clsx"; // Import clsx for handling dynamic classes const HoverableIconList = ({ title, items, - color = "red", // Default color set to "pink" - hoveredItem, - setHoveredItem, + color = "red", getKey, + onHoverStart, // New prop + onHoverEnd, // New prop + sectionId, // New prop to identify section }) => { + const [hoveredItem, setHoveredItem] = useState(null); // Track hovered item index + const [isMobile, setIsMobile] = useState(false); // Track if the device is mobile + const iconRefs = useRef([]); // Refs for each icon + + // Detect if the device is mobile + useEffect(() => { + const checkIsMobile = () => { + setIsMobile(window.innerWidth <= 768); // Adjust breakpoint as needed + }; + + checkIsMobile(); // Check on initial render + window.addEventListener("resize", checkIsMobile); // Check on window resize + + return () => { + window.removeEventListener("resize", checkIsMobile); // Cleanup + }; + }, []); + + // Handle click outside to clear hoveredItem + useEffect(() => { + const handleClickOutside = (event) => { + if ( + isMobile && + hoveredItem !== null && + !iconRefs.current[hoveredItem]?.contains(event.target) + ) { + setHoveredItem(null); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isMobile, hoveredItem]); + // Dynamically constructing class names using clsx - const borderClass = `${color}-400`; // e.g., "yellow-400" - const shadowClass = `${color}-500`; // e.g., "yellow-500" + const borderClass = `${color}`; // e.g., "yellow-400" + const shadowClass = `[0_0_30px_rgba(255,255,255,0.6)]`; // e.g., "yellow-500" return (

{title}

- {items.map((item, index) => ( + {(items ?? []).map((item, index) => (
setHoveredItem(index)} - onMouseLeave={() => setHoveredItem(null)} + onMouseEnter={() => { + if (!isMobile) { + onHoverStart?.(sectionId, index, item.name); + setHoveredItem(index); + } + }} + onMouseLeave={() => { + if (!isMobile) { + onHoverEnd?.(); + setHoveredItem(null); + } + }} + onClick={() => { + if (isMobile) { + const newHovered = hoveredItem === index ? null : index; + setHoveredItem(newHovered); + if (newHovered !== null) { + onHoverStart?.(sectionId, index, item.name); + } else { + onHoverEnd?.(); + } + } + }} > - {hoveredItem === index && ( -
+ {/* Inline Tooltip for Mobile */} + {isMobile && hoveredItem === index && ( +
{item.name}
)} diff --git a/frontend/src/components/sections/projects/HoverableSkill.jsx b/frontend/src/components/sections/projects/HoverableSkill.jsx index 2d56386..a6440fc 100644 --- a/frontend/src/components/sections/projects/HoverableSkill.jsx +++ b/frontend/src/components/sections/projects/HoverableSkill.jsx @@ -1,19 +1,36 @@ -import React, { useEffect, useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { motion, useAnimation } from "framer-motion"; import clsx from "clsx"; +import MouseTracker from "../../MouseTracker"; // Import the MouseTracker component const HoverableSkillList = ({ title, - items, + items = [], // Default to an empty array to avoid undefined errors color = "red", hoveredItem, setHoveredItem, + section, getKey, isVisible, }) => { const borderClass = `text-${color}-400`; const controls = useAnimation(); const skillLevelRefs = useRef([]); + const [isMobile, setIsMobile] = useState(false); + + // Detect if the device is mobile + useEffect(() => { + const checkIsMobile = () => { + setIsMobile(window.innerWidth <= 768); // Adjust breakpoint as needed + }; + + checkIsMobile(); // Check on initial render + window.addEventListener("resize", checkIsMobile); // Check on window resize + + return () => { + window.removeEventListener("resize", checkIsMobile); // Cleanup + }; + }, []); useEffect(() => { if (isVisible) { @@ -33,6 +50,10 @@ } }, [isVisible, controls]); + // Check if the current item is hovered + const isHovered = + hoveredItem.section === section && hoveredItem.index !== null; + return (

{items.map((item, index) => { - const circumference = 2 * Math.PI * 40; // Adjusted for better scaling + const circumference = 2 * Math.PI * 40; return (
setHoveredItem(index)} - onMouseLeave={() => setHoveredItem(null)} + onMouseEnter={() => + !isMobile && setHoveredItem({ section, index }) + } + onMouseLeave={() => + !isMobile && setHoveredItem({ section: null, index: null }) + } + onClick={() => isMobile && setHoveredItem({ section, index })} > -
+
{" "} {/* Adjusted size for mobile */}

{item.name}

- {hoveredItem === index && ( + + {/* Inline Tooltip for Mobile */} + {isMobile && isHovered && hoveredItem.index === index && (
{item.skill}
@@ -103,6 +133,11 @@ ); })}
+ + {/* MouseTracker for Desktop Tooltip */} + {!isMobile && isHovered && ( + + )}
); }; diff --git a/frontend/src/components/sections/projects/Project.jsx b/frontend/src/components/sections/projects/Project.jsx index d0d9c99..02eeca5 100644 --- a/frontend/src/components/sections/projects/Project.jsx +++ b/frontend/src/components/sections/projects/Project.jsx @@ -2,33 +2,84 @@ import { motion } from "framer-motion"; import { useParams } from "react-router-dom"; import { fetchData } from "../../../api"; -import Container from "./Container"; import HoverableIconList from "./HoverableIcon"; -import styles from "./Project.module.css"; - -//SVG +import Container from "./Container"; +import Slideshow from "./Slideshow"; +import ElementObserver from "../ElementObserver"; +import MouseTracker from "../../MouseTracker"; import RightArrow from "../../../assets/buttons/RightArrow.svg"; import LeftArrow from "../../../assets/buttons/LeftArrow.svg"; +import { clock_icon } from "../../../assets"; +import styles from "./Project.module.css"; +import useIsMobile from "../../../constants/useIsMobile"; +import { floattingImages } from "../../../constants"; +import FloatingSVG from "../../design/FloatingSVG"; const Project = () => { const { title } = useParams(); const [projectData, setProjectData] = useState(null); + const [projectDetails, setProjectDetails] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [hoveredTech, setHoveredTech] = useState(null); - const [hoveredLanguage, setHoveredLanguage] = useState(null); - const [hoveredTools, setHoveredTools] = useState({}); - const [toolsByType, setToolsByType] = useState({}); + const [hoverState, setHoverState] = useState({ + section: null, + index: null, + text: null, + }); const [currentIndex, setCurrentIndex] = useState(0); const [direction, setDirection] = useState(1); const intervalRef = useRef(null); + const GET_PROJECTS = import.meta.env.VITE_GET_PROJECTS; + const GET_WORK_PAGE = import.meta.env.VITE_GET_WORK_PAGE; + const isMobile = useIsMobile(); + + // Define selectors for the sections you want to animate + const sectionSelectors = [ + ".description-section", + ".trailer-section", + ".skill-section", + ".details-section", + ".links-section", + ]; + + { + /* Framer Motion Variants */ + } + const containerVariants = { + visible: { + opacity: 1, + scale: 1, + transition: { + staggerChildren: 0.2, // Stagger delay for each child + }, + }, + hidden: { + opacity: 0, + scale: 0, + }, + }; + + const itemVariants = { + visible: { + opacity: 1, + scale: 1, + }, + hidden: { + opacity: 0, + scale: 0, + }, + }; + + // Use ElementObserver to track visibility of sections + const isElementVisible = ElementObserver(sectionSelectors); + useEffect(() => { const fetchProjectData = async () => { try { // Fetch all projects and find the one with the matching title - const allProjects = await fetchData("projects"); - const project = allProjects.projects.find((p) => { + const allProjects = await fetchData(GET_PROJECTS); + const project = allProjects.find((p) => { const projectSlug = p.title .toLowerCase() .replace(/[^a-z0-9 -]/g, "") @@ -38,15 +89,8 @@ }); if (project) { - const data = await fetchData(`project_page/${project.id}`); + const data = await fetchData(`${GET_WORK_PAGE}/${project.id}`); setProjectData(data); - - const groupedTools = data?.tools.reduce((acc, tool) => { - if (!acc[tool.type]) acc[tool.type] = []; - acc[tool.type].push(tool); - return acc; - }, {}); - setToolsByType(groupedTools); } else { setError("Project not found"); } @@ -73,6 +117,126 @@ useEffect(() => { if (!projectData) return; + // Reset projectDetails before adding new data + setProjectDetails([]); + + if (projectData?.start_date) { + const parseDate = (dateString) => { + const parts = dateString.split(" "); + + if (parts.length === 3) { + // Format: "Month Day, Year" (e.g., "March 15, 2024") + return new Date(dateString); + } else if (parts.length === 2) { + // Format: "Month Year" (Default to 1st of the month) + const [month, year] = parts; + return new Date(`${month} 1, ${year}`); + } + + return null; + }; + + const startDate = parseDate(projectData.start_date); + const isPresent = projectData.end_date === "Present"; + const endDate = isPresent ? "Present" : parseDate(projectData.end_date); + const lastDate = isPresent ? new Date() : endDate; + + const formatDate = (date) => { + return date.toLocaleString("default", { + month: "long", + day: "numeric", + year: "numeric", + }); + }; + + const partialFormatDate = (date) => { + return date.toLocaleString("default", { + month: "long", + year: "numeric", + }); + }; + + const partialDate = isPresent + ? `${partialFormatDate(startDate)} - Present` + : `${partialFormatDate(startDate)} - ${partialFormatDate(endDate)}`; + + const date = isPresent + ? `${formatDate(startDate)} - Present` + : `${formatDate(startDate)} - ${formatDate(endDate)}`; + + const dateDetail = { text: partialDate, logo: projectData.date_logo }; + setProjectDetails((prevDetails) => [...prevDetails, dateDetail]); + + // Calculate the total time difference in days + const timeDifferenceInDays = Math.floor( + (lastDate - startDate) / (1000 * 60 * 60 * 24) + ); + + // Convert days to weeks and months + const timeDifferenceInWeeks = Math.floor(timeDifferenceInDays / 7); + const timeDifferenceInMonths = + (lastDate.getFullYear() - startDate.getFullYear()) * 12 + + (lastDate.getMonth() - startDate.getMonth()); + + let durationText = ""; + + if (timeDifferenceInMonths === 0) { + // If in the same month, show weeks (or days if less than a week) + if (timeDifferenceInWeeks > 0) { + durationText = `${timeDifferenceInWeeks} week${ + timeDifferenceInWeeks > 1 ? "s" : "" + }`; + } else { + durationText = `${timeDifferenceInDays} day${ + timeDifferenceInDays > 1 ? "s" : "" + }`; + } + } else if (timeDifferenceInMonths < 12) { + durationText = `${timeDifferenceInMonths} month${ + timeDifferenceInMonths > 1 ? "s" : "" + }`; + } else { + const years = Math.floor(timeDifferenceInMonths / 12); + const months = timeDifferenceInMonths % 12; + durationText = `${years} year${years > 1 ? "s" : ""}${ + months > 0 ? ` ${months} month${months > 1 ? "s" : ""}` : "" + }`; + } + + const durationDetail = { + text: durationText, + logo: clock_icon, + }; + + setProjectDetails((prevDetails) => [...prevDetails, durationDetail]); + } + + if (projectData?.context && projectData.context.project_context) { + const contextDetail = { + text: projectData.context.project_context, + logo: projectData.context.context_logo, + }; + setProjectDetails((prevDetails) => [...prevDetails, contextDetail]); + } + + if (projectData?.category) { + const detail = { + text: projectData.category.name, + logo: projectData.category.logo, + }; + setProjectDetails((prevDetails) => [...prevDetails, detail]); + } + + if (projectData?.tags && projectData.tags.length > 0) { + const tagsDetails = projectData.tags + .filter((tag) => tag.logo) + .map((tag) => ({ + text: tag.name, + logo: tag.logo, + })); + setProjectDetails((prevDetails) => [...prevDetails, ...tagsDetails]); + } + resetTimer(); // Start the interval when data loads return () => clearInterval(intervalRef.current); // Cleanup on unmount @@ -102,6 +266,8 @@ return (
+ {/* MouseTracker */} + {!isMobile && } {/* Background Image */}
{projectData?.background_image ? ( @@ -111,7 +277,19 @@ alt="Project Background" /> ) : ( -
// Fallback to background color if no image +
+ {/* Add floating SVG images to the background */} + {floattingImages.map((image, index) => ( + + ))} +
)}
@@ -128,115 +306,165 @@

- -
- {/* Thumbnail */} - Project Thumbnail + {/* Project Description */} + + +
+ {/* Thumbnail */} + Project Thumbnail - {/* Description */} -
-

- Description -

- {projectData?.project_description} + {/* Description */} +
+

+ Description +

+

{projectData?.project_description}

+
-
-
+ +
{/* Trailer */} - {projectData?.trailer && ( - -

- Trailer -

-
- -
-
- )} - - {/* Tech, Languages, and Tools Section */} -
- {/* Tech Section */} - - `tech-${index}`} - /> - - - {/* Programming Languages Section */} - - `language-${index}`} - /> - - - {/* Tools Section */} - {Object.entries(toolsByType).map(([toolType, tools]) => ( - - tool.tools)} - hoveredItem={hoveredTools[toolType]} - setHoveredItem={(item) => - setHoveredTools((prev) => ({ ...prev, [toolType]: item })) - } - getKey={(item, index) => `${toolType}-${index}`} - /> - - ))} -
- -
- {/* Detailed Info Section */} - - Date Logo - -

- {projectData?.start_date} - {projectData?.end_date} -

-
- - {projectData?.context && ( - - Context Logo - -

- {projectData?.context.project_context} + + {projectData?.trailer && ( + +

+ Trailer

+
+ +
)} +
+ + {/* Tech, Languages, Tools and Databases Section */} +
+ + {projectData?.skills.length > 0 && ( +
+ {projectData.skills.map((skillCategory, categoryIndex) => + Object.keys(skillCategory).map((categoryKey) => { + const groupedItems = skillCategory[categoryKey].reduce( + (acc, item) => { + if (!acc[item.skill_name]) acc[item.skill_name] = []; + acc[item.skill_name].push(item); + return acc; + }, + {} + ); + + return Object.entries(groupedItems).map( + ([skillName, items], groupIndex) => ( + +

+ {skillName} +

+
+ {items.map((item, itemIndex) => ( + icon.id} + sectionId={`skills-${categoryKey}-${groupIndex}`} + onHoverStart={(section, index, text) => + setHoverState({ section, index, text }) + } + onHoverEnd={() => + setHoverState({ + section: null, + index: null, + text: null, + }) + } + /> + ))} +
+
+ ) + ); + }) + )} +
+ )} +
+ + {/* Details Section */} + +
+ {projectDetails.map((detail, index) => ( + + Detail Logo +

+ {detail.text} +

+
+ ))} +
+
{projectData?.repository && projectData?.repository?.url && ( -
+
-

+

Development Activity

)} - {projectData?.repository && projectData?.repository?.url && ( - + {projectData?.screenshots && projectData.screenshots.length > 0 && ( + )} + + {/* Links Section */} + + {projectData?.links && projectData.links.length > 0 && ( + + {projectData.links.map((link, index) => ( + + setHoverState({ + section: "links", + index, + text: link.name, + }) + } + onMouseLeave={() => + setHoverState({ + section: null, + index: null, + text: null, + }) + } + > +
+ {link.name} +
+
+ ))} +
+ )} +
); diff --git a/frontend/src/components/sections/projects/Project.module.css b/frontend/src/components/sections/projects/Project.module.css index 0099934..db2ce20 100644 --- a/frontend/src/components/sections/projects/Project.module.css +++ b/frontend/src/components/sections/projects/Project.module.css @@ -11,12 +11,9 @@ /* Activity Card */ .activity-card { - padding-right: 1rem; - padding-left: 1rem; - padding-top: 1.5rem; - padding-bottom: 1.5rem; + padding: 1.5rem 1rem; border-radius: 10px; - background: rgba(0, 0, 0, 0.85); + background: rgba(0, 0, 0, 0.95); box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2); width: 100%; max-width: 1000px; @@ -36,31 +33,27 @@ align-items: center; justify-content: center; gap: 1.5rem; + height: 230px; + overflow: hidden; } /* Activity Logo */ .activity-logo { - width: 80px; - height: 80px; + width: 60px; + height: 60px; flex-shrink: 0; margin-top: 0.5rem; } /* Activity Title */ .activity-title { - font-size: 1.25rem; + font-size: 1.35rem; font-weight: bold; margin-bottom: 0.5rem; text-align: center; word-break: break-word; } -@media (min-width: 640px) { - .activity-title { - font-size: auto 2.5rem; - } -} - /* Activity Details */ .activity-details { font-size: 0.75rem; @@ -124,8 +117,8 @@ background-color: rgba(0, 0, 0, 0.9); border-radius: 50%; padding: 1rem; - width: 6rem; /* Default for larger screens */ - height: 6rem; + width: 4.5rem; /* Default for larger screens */ + height: 4.5rem; display: flex; justify-content: center; align-items: center; @@ -135,20 +128,20 @@ /* Hover effect */ .button-container:hover { - box-shadow: 0 0 30px rgba(255, 255, 255, 1); + box-shadow: 0 0 30px rgba(255, 255, 255, 0.6); transform: scale(1.1); } /* Adjust for mobile screens (smaller than 768px) */ @media (max-width: 768px) { .button-container { - width: 5.5rem; - height: 5.5rem; - padding: 0.8rem; + width: 3.5rem; + height: 3.5rem; + padding: 0.7rem; } .button-container:hover { - box-shadow: 0 0 15px rgba(255, 255, 255, 1); + box-shadow: 0 0 15px rgba(255, 255, 255, 0.6); transform: scale(1.1); } diff --git a/frontend/src/components/sections/projects/Slideshow.jsx b/frontend/src/components/sections/projects/Slideshow.jsx new file mode 100644 index 0000000..e0fe172 --- /dev/null +++ b/frontend/src/components/sections/projects/Slideshow.jsx @@ -0,0 +1,125 @@ +import React, { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import LeftArrow from "../../../assets/buttons/LeftArrow.svg"; +import RightArrow from "../../../assets/buttons/RightArrow.svg"; +import { X } from "lucide-react"; +import Heading from "../../Heading"; + +const Slideshow = ({ images, projectName }) => { + const [currentIndex, setCurrentIndex] = useState(0); + const [isFullscreen, setIsFullscreen] = useState(false); + + if (!images || images.length === 0) return null; // No images available + + const preloadImage = (index) => { + const img = new Image(); + img.src = images[index]; + }; + + const nextSlide = () => { + const nextIndex = (currentIndex + 1) % images.length; + preloadImage(nextIndex); + setCurrentIndex(nextIndex); + }; + + const prevSlide = () => { + const prevIndex = currentIndex === 0 ? images.length - 1 : currentIndex - 1; + preloadImage(prevIndex); + setCurrentIndex(prevIndex); + }; + + return ( +
+ + + {/* Slideshow Thumbnail */} +
setIsFullscreen(true)} + > + + + + + {/* Navigation Buttons */} + + + +
+ + {/* Fullscreen Mode*/} + {isFullscreen && ( +
+ + + + {/* Close Button */} + + + {/* Navigation Buttons */} + + + + +
+ )} +
+ ); +}; + +export default Slideshow; diff --git a/frontend/src/components/utils/NavigationDots.jsx b/frontend/src/components/utils/NavigationDots.jsx new file mode 100644 index 0000000..dc571dd --- /dev/null +++ b/frontend/src/components/utils/NavigationDots.jsx @@ -0,0 +1,70 @@ +import React from "react"; +import { navigation } from "../../constants/index"; +import useIsMobile from "../../constants/useIsMobile"; + +const NavigationDots = ({ currentSectionId }) => { + const smoothScroll = (target, duration = 75) => { + const header = document.querySelector(".header"); + const headerHeight = header ? header.offsetHeight : 0; + + const targetPosition = + target.getBoundingClientRect().top + window.scrollY - headerHeight; + const startPosition = window.scrollY; + const distance = targetPosition - startPosition; + let startTime = null; + + const easeInOutQuad = (t, b, c, d) => { + t /= d / 2; + if (t < 1) return (c / 2) * t * t + b; + t--; + return (-c / 2) * (t * (t - 2) - 1) + b; + }; + + const animateScroll = (currentTime) => { + if (startTime === null) startTime = currentTime; + const timeElapsed = currentTime - startTime; + const run = easeInOutQuad(timeElapsed, startPosition, distance, duration); + + window.scrollTo(0, run); + + if (timeElapsed < duration) { + requestAnimationFrame(animateScroll); + } else { + window.scrollTo(0, targetPosition); + } + }; + + requestAnimationFrame(animateScroll); + }; + + const handleDotClick = (sectionId) => { + const section = document.getElementById(sectionId); + if (section) { + smoothScroll(section); + } + }; + + const isMobile = useIsMobile(); + + return ( +
+ {!isMobile && + navigation.map((item, index) => { + const sectionId = item.url.replace("/#", ""); + return ( +
+ ); +}; + +export default NavigationDots; diff --git a/frontend/src/components/utils/helpers.jsx b/frontend/src/components/utils/helpers.jsx deleted file mode 100644 index d26fc9d..0000000 --- a/frontend/src/components/utils/helpers.jsx +++ /dev/null @@ -1,63 +0,0 @@ -import { - alltags, - apps, - game_systems, - games, - tag_icon, - websites, -} from "../../assets"; - -// Filter projects based on selected tags -export const filterProjects = (projects, primaryTag, subtags) => { - if (primaryTag === "All") return projects; - - return projects.filter((project) => { - const matchesPrimaryTag = project.primaryTag === primaryTag; - const matchesSubtags = - subtags.length === 0 || - subtags.every((subtag) => project.subtags.includes(subtag)); - - return matchesPrimaryTag && matchesSubtags; - }); -}; - -// Define tag hierarchy (primary tags and their subtags) -export const TAG_HIERARCHY = { - All: { - subtags: [], - logo: alltags, - }, - - Games: { - subtags: [ - "2D", - "3D", - "Unity", - "Unreal Engine", - "Singleplayer", - "Multiplayer", - ], - logo: games, - }, - - "Game Systems": { - subtags: [], - logo: game_systems, - }, - - Websites: { - subtags: [], - logo: websites, - }, - - Apps: { - subtags: [], - logo: apps, - }, -}; - -// Helper to get a tag icon or default fallback -export const getTagIcon = (tag) => { - const tagData = Object.entries(TAG_HIERARCHY).find(([key]) => key === tag); - return tagData ? tagData[1].logo : tag_icon; -}; diff --git a/frontend/src/constants/index.js b/frontend/src/constants/index.js index be562f5..3ec2f76 100644 --- a/frontend/src/constants/index.js +++ b/frontend/src/constants/index.js @@ -1,3 +1,14 @@ +import { linkedin, gitbucket, youtube, instagram, facebook } from "../assets"; +import { + joystick, + unity, + dpad, + controller, + unreal, + x, + gameboy, +} from "../assets"; + export const navigation = [ { id: 0, title: "Home", url: "/#home" }, { id: 1, title: "About", url: "/#about" }, @@ -6,3 +17,92 @@ { id: 4, title: "Activity", url: "/#activity" }, { id: 5, title: "Contact", url: "/#contact" }, ]; + +export const static_social_media = [ + { + id: 0, + name: "linkedin", + url: "https://www.linkedin.com/in/nunoteixeira97/", + logo: linkedin, + }, + + { + id: 1, + name: "gitbucket", + url: "https://jmpteixeira.myasustor.com:8443/Nuno_Teixeira", + logo: gitbucket, + }, + + { + id: 2, + name: "youtube", + url: "https://www.youtube.com/@nuno97teixeira", + logo: youtube, + }, + + { + id: 3, + name: "instagram", + url: "https://www.instagram.com/nuno97teixeira/", + logo: instagram, + }, + + { + id: 4, + name: "facebook", + url: "https://www.facebook.com/nuno.teixeira.940/", + logo: facebook, + }, +]; + +//floating svg's +export const floattingImages = [ + { + svg: joystick, + position: { x: "20%", y: "20%" }, + speed: 0.77, + amplitude: 60, + }, + + { + svg: unity, + position: { x: "80%", y: "60%" }, + speed: 0.8, + amplitude: 40, + }, + + { + svg: dpad, + position: { x: "40%", y: "70%" }, + speed: 0.85, + amplitude: 50, + }, + + { + svg: controller, + position: { x: "70%", y: "15%" }, + speed: 0.93, + amplitude: 32, + }, + + { + svg: unreal, + position: { x: "75%", y: "85%" }, + speed: 1.02, + amplitude: 30, + }, + + { + svg: x, + position: { x: "25%", y: "85%" }, + speed: 0.81, + amplitude: 25, + }, + + { + svg: gameboy, + position: { x: "55%", y: "85%" }, + speed: 0.81, + amplitude: 60, + }, +]; diff --git a/frontend/src/constants/useIsMobile.js b/frontend/src/constants/useIsMobile.js new file mode 100644 index 0000000..1b792da --- /dev/null +++ b/frontend/src/constants/useIsMobile.js @@ -0,0 +1,18 @@ +import { useState, useEffect } from "react"; + +const useIsMobile = (breakpoint = 768) => { + const [isMobile, setIsMobile] = useState(window.innerWidth < breakpoint); + + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < breakpoint); + }; + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, [breakpoint]); + + return isMobile; +}; + +export default useIsMobile; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 2c87af8..4f516a0 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,17 +1,21 @@ -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; -// https://vite.dev/config/ -export default defineConfig({ - server: { - port: 80, - proxy: { - "/api": { - target: "https://jmpteixeira.myasustor.com:3001", - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, ""), +export default ({ mode }) => { + // Load environment variables + const env = loadEnv(mode, process.cwd(), ""); + + return defineConfig({ + server: { + port: Number(env.VITE_BUILD_PORT), + proxy: { + "/api": { + target: env.VITE_TARGET_URL, + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ""), + }, }, }, - }, - plugins: [react()], -}); + plugins: [react()], + }); +};