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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 (
+
+

+
+ );
+};
+
+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.username}
-
-
- ))}
+
+ {/* Social Media Buttons */}
+
+
+
+
+ {/* Disclaimer */}
+
+ Disclaimer: Please avoid sharing sensitive or confidential
+ information in your message.
+
-
-
-
- {/* 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 Description */}
+
+
+
+ {/* 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 */}
-
-
-
-
- {projectData?.start_date} - {projectData?.end_date}
-
-
-
- {projectData?.context && (
-
-
-
-
- {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.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,
+ })
+ }
+ >
+
+

+
+
+ ))}
+
+ )}
+
);
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()],
+ });
+};