diff --git a/backend/index.js b/backend/index.js index f52fe56..4a961c9 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,3 +1,4 @@ +require("dotenv").config(); const https = require('https'); const fs = require('fs'); const express = require('express'); @@ -11,15 +12,18 @@ const aboutRoutes = require('./routes/about'); // Set up SSL certificate files -// Define the paths to your certificate and key files const options = { - key: fs.readFileSync('/app/certificates/ssl.key'), - cert: fs.readFileSync('/app/certificates/ssl.crt'), + key: fs.readFileSync(process.env.SSL_KEY_PATH), + 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: ['https://api.nunoteixeira.dev', 'https://nunoteixeira.dev', 'http://localhost'], // Allow both production and local frontend + origin: process.env.ORIGIN ? process.env.ORIGIN.split(",") : [], methods: 'GET,POST,PUT,DELETE', credentials: true, }; @@ -45,6 +49,6 @@ app.use('/', aboutRoutes); // Start the HTTPS server -https.createServer(options, app).listen(3001, () => { - console.log('Server is running on https://www.jmpteixeira.myasustor.com:3001'); +https.createServer(options, app).listen(process.env.PORT, () => { + console.log(`Server is running on: ${process.env.BASE_URL}:${process.env.PORT}`); }); \ No newline at end of file diff --git a/backend/routes/projects.js b/backend/routes/projects.js index 9a7c3e6..737d68a 100644 --- a/backend/routes/projects.js +++ b/backend/routes/projects.js @@ -13,66 +13,82 @@ const appendBaseUrl = (path) => (path ? `${BASE_URL}${path}` : null); // Fetch all projects -router.get("/project_cards", async (req, res) => { +router.get("/projects", async (req, res) => { try { - const query = ` - SELECT - p.id, - p.title, - p.hook, - p.category, - p.tags, + const categoriesAndTagsQuery = `SELECT + c.id AS category_id, + c.name AS category_name, + c.logo AS category_logo, + GROUP_CONCAT(t.name) AS tags + FROM + categories c + LEFT JOIN + tags t ON c.id = t.category_id + GROUP BY + c.id;`; - (SELECT JSON_ARRAYAGG(t1.name) + const projectsQuery = ` SELECT + p.id, + p.title, + p.hook, + c.name AS category, + p.thumbnail_path AS thumbnail, - FROM project_tools pt + -- Fetch tags as a JSON array + (SELECT JSON_ARRAYAGG(t.name) + FROM project_tags pt + INNER JOIN tags t ON pt.tag_id = t.id + WHERE pt.project_id = p.id) AS tags, - INNER JOIN tools t1 ON pt.tool_id = t1.id + -- Fetch tools as a JSON array + (SELECT JSON_ARRAYAGG(t1.name) + FROM project_tools pt + INNER JOIN tools t1 ON pt.tool_id = t1.id + WHERE pt.project_id = p.id) AS tools, - WHERE pt.project_id = p.id) AS tools, + -- Fetch tech as a JSON array + (SELECT JSON_ARRAYAGG(t2.name) + FROM project_tech pt2 + INNER JOIN tech t2 ON pt2.tech_id = t2.id + WHERE pt2.project_id = p.id) AS tech, - (SELECT JSON_ARRAYAGG(t2.name) + -- Fetch programming languages as a JSON array + (SELECT JSON_ARRAYAGG(t3.name) + FROM project_languages pt3 + INNER JOIN languages t3 ON pt3.language_id = t3.id + WHERE pt3.project_id = p.id) AS programming_languages, - FROM project_tech pt2 + -- Fetch tech logos as a JSON array + (SELECT JSON_ARRAYAGG(t2.logo) + FROM project_tech pt2 + INNER JOIN tech t2 ON pt2.tech_id = t2.id + WHERE pt2.project_id = p.id) AS tech_logo, - INNER JOIN tech t2 ON pt2.tech_id = t2.id + -- Fetch programming language logos as a JSON array + (SELECT JSON_ARRAYAGG(t3.logo) + FROM project_languages pt3 + INNER JOIN languages t3 ON pt3.language_id = t3.id + WHERE pt3.project_id = p.id) AS programming_languages_logo - WHERE pt2.project_id = p.id) AS tech, - - (SELECT JSON_ARRAYAGG(t3.name) - - FROM project_languages pt3 - - INNER JOIN languages t3 ON pt3.language_id = t3.id - - WHERE pt3.project_id = p.id) AS programming_languages, - - (SELECT JSON_ARRAYAGG(t2.logo) - - FROM project_tech pt2 - - INNER JOIN tech t2 ON pt2.tech_id = t2.id - - WHERE pt2.project_id = p.id) AS tech_logo, - - (SELECT JSON_ARRAYAGG(t3.logo) - - FROM project_languages pt3 - - INNER JOIN languages t3 ON pt3.language_id = t3.id - - WHERE pt3.project_id = p.id) AS programming_languages_logo, - - p.thumbnail_path AS thumbnail - FROM - projects p - GROUP BY - p.id, p.title, p.hook, p.category, p.tags, p.thumbnail_path; - `; + FROM + projects p + LEFT JOIN + categories c ON p.category_id = c.id + GROUP BY + p.id, p.title, p.hook, p.thumbnail_path, c.name;`; - const rows = await pool.query(query); + //Queries results + const projectsRaw = await pool.query(projectsQuery); + const categoriesAndTagsRaw = await pool.query(categoriesAndTagsQuery); - const projectsWithImageURLs = rows.map(project => ({ + const categories = categoriesAndTagsRaw.map(category => ({ + ...category, + category_logo: category.category_logo ? `${BASE_URL}${category.category_logo}` : null, + tags: category.tags ? category.tags.split(',') : [] + } + )); + + const projects = projectsRaw.map(project => ({ ...project, image_url: `${BASE_URL}${project.thumbnail}`, // Construct full URL for thumbnail logos: [ @@ -85,11 +101,14 @@ //console.log("Query result with image URLs:", projectsWithImageURLs); // Check if rows are returned - if (!rows || rows.length === 0) { + if (!projectsRaw || projectsRaw.length === 0) { return res.status(404).send("No projects found."); } - res.json(projectsWithImageURLs); // Send the result with image URLs + //console.log("Work result: ", {categories, projects}); + + res.json({categories, projects}); + } catch (err) { console.error("Error fetching projects:", err.message); res.status(500).send("Server error"); diff --git a/frontend/src/assets/index.js b/frontend/src/assets/index.js index 7a6d908..050a8e8 100644 --- a/frontend/src/assets/index.js +++ b/frontend/src/assets/index.js @@ -5,25 +5,15 @@ import pdf from "./personal/pdf.png"; //icons -import hamburger from "./icons/hamburger.svg"; -import alltags from "./icons/alltags.webp"; +import all_tags from "./icons/alltags.webp"; import tag_icon from "./icons/tag_icon.svg"; -import games from "./icons/games.webp"; -import game_systems from "./icons/game_systems.webp"; -import websites from "./icons/websites.webp"; -import apps from "./icons/apps.webp"; import peek_icon from "./icons/peek_icon.svg"; export { //icons - hamburger, peek_icon, - games, - alltags, + all_tags, tag_icon, - game_systems, - websites, - apps, pdf, //picture diff --git a/frontend/src/components/ProjectCard.jsx b/frontend/src/components/ProjectCard.jsx index 37eb89d..b148ee0 100644 --- a/frontend/src/components/ProjectCard.jsx +++ b/frontend/src/components/ProjectCard.jsx @@ -29,7 +29,7 @@ onMouseEnter={() => setHoveredProjectId(project.id)} onMouseLeave={() => setHoveredProjectId(null)} > - {/* Only the image is inside the link */} + {/* Project Image with the link*/} - Please avoid sharing sensitive or confidential information in your - message. + 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/Home.css b/frontend/src/components/sections/Home.css index f407d73..fc9a7de 100644 --- a/frontend/src/components/sections/Home.css +++ b/frontend/src/components/sections/Home.css @@ -33,68 +33,6 @@ animation: rainbow 10s ease infinite; } -.container-shape { - position: relative; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 2rem; - background: black; - color: white; - overflow: hidden; - isolation: isolate; /* Ensures pseudo-elements stack properly */ -} - -.container-shape::before, -.container-shape::after { - content: ""; - position: absolute; - border: 2px solid cyan; /* Neon border */ - border-radius: 0; /* Optional, depends on desired style */ - box-shadow: 0 0 20px cyan; - transition: all 0.3s ease-in-out; -} - -.container-shape::before { - top: -4px; - left: -4px; - width: calc(100% + 8px); - height: calc(100% + 8px); - clip-path: polygon( - 10% 0, - 90% 0, - 100% 10%, - 100% 90%, - 90% 100%, - 10% 100%, - 0 90%, - 0 10% - ); -} - -.container-shape::after { - top: 4px; - left: 4px; - width: calc(100% - 8px); - height: calc(100% - 8px); - clip-path: polygon( - 10% 0, - 90% 0, - 100% 10%, - 100% 90%, - 90% 100%, - 10% 100%, - 0 90%, - 0 10% - ); -} - -.container-shape:hover::before, -.container-shape:hover::after { - transform: scale(1.02); -} - .animate-glow { animation: glow 2s infinite alternate; } @@ -107,10 +45,3 @@ filter: drop-shadow(0 0 16px rgba(0, 255, 255, 1)); } } - -/* Ensure the flex container for large screens still keeps the elements stacked vertically */ -@media (min-width: 1024px) { - #home { - flex-direction: column !important; /* Force column layout for larger screens */ - } -} diff --git a/frontend/src/components/sections/Home.jsx b/frontend/src/components/sections/Home.jsx index a089962..699a2f5 100644 --- a/frontend/src/components/sections/Home.jsx +++ b/frontend/src/components/sections/Home.jsx @@ -30,7 +30,7 @@ return (
{/* ProfileContainer with animation */} @@ -45,7 +45,7 @@ stiffness: 300, damping: 25, }} - className="w-full flex flex-col justify-center mb-8 md:mb-16 flex-grow" + className="w-full flex flex-col justify-center mb-8" >
- {introData.socials.map((social, index) => ( + {introData.socials.map((social) => ( ))} @@ -135,9 +135,9 @@ className="clock-container relative bottom-4 md:bottom-10 text-center w-full mt-10" initial={{ opacity: 0 }} animate={isVisible ? { opacity: 1 } : { opacity: 0 }} - transition={{ duration: 1, delay: 1.2 }} // Stagger clock animation + transition={{ duration: 1, delay: 1.2 }} > -

+

{formattedTime}

diff --git a/frontend/src/components/sections/MainPage.jsx b/frontend/src/components/sections/MainPage.jsx index df8191d..0f680d4 100644 --- a/frontend/src/components/sections/MainPage.jsx +++ b/frontend/src/components/sections/MainPage.jsx @@ -23,7 +23,7 @@ // State to store fetched data const [introData, setIntroData] = useState([]); const [aboutMe, setAboutData] = useState([]); - const [projects, setWorkData] = useState([]); + const [workData, setWorkData] = useState([]); const [skillsData, setSkillsData] = useState([]); const [activityData, setActivityData] = useState([]); const [contactData, setContactData] = useState([]); @@ -111,7 +111,7 @@ {/* Work Section */}

- + {/* Diagonal separator */} { - const [activeCategory, setActiveCategory] = useState("All"); +const Work = ({ isVisible, work }) => { + const [activeCategory, setActiveCategory] = useState({ + category_id: "All", + category_name: "All", + category_logo: all_tags, + tags: [], + }); const [selectedTags, setSelectedTags] = useState([]); const [hoveredProjectId, setHoveredProjectId] = useState(null); const [filteredProjects, setFilteredProjects] = useState([]); - const { isLoading } = useContext(LoadingContext); // Use the global loading state + const { isLoading } = useContext(LoadingContext); + + if (!work.categories) { + return
Loading...
; + } if (isLoading) return
Loading...
; + const allCategory = { + category_id: "All", + category_name: "All", + category_logo: all_tags, + tags: [], + }; + + const categoriesWithAll = [allCategory, ...work.categories]; + useEffect(() => { const filterProjectsByTags = () => { - const filtered = projects.filter((project) => { + const filtered = work.projects.filter((project) => { const matchesCategory = - activeCategory === "All" || - project.category === activeCategory || - project.tech === activeCategory; + activeCategory.category_name === "All" || + project.category === activeCategory.category_name; const matchesTags = selectedTags.length === 0 || @@ -37,7 +53,7 @@ }; filterProjectsByTags(); - }, [projects, activeCategory, selectedTags]); + }, [work.projects, activeCategory, selectedTags]); const handleCategoryClick = (category) => { setActiveCategory(category); @@ -71,57 +87,72 @@ }} transition={{ staggerChildren: 0.1 }} > - {Object.entries(TAG_HIERARCHY).map(([category, data]) => ( + {categoriesWithAll.map((category) => ( handleCategoryClick(category)} whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} className={`flex items-center gap-2 px-6 lg:px-10 py-4 text-xl rounded-lg transition-all duration-200 ${ - activeCategory === category + activeCategory.category_id === category.category_id ? "bg-orange-500 text-white font-bold" : "bg-black text-gray-200 hover:bg-gray-600" }`} > - {`${category} - {category} + {category.category_logo && ( // Only render the logo if it exists + {`${category.category_name} + )} + {category.category_name} ))} - {TAG_HIERARCHY[activeCategory]?.subtags?.length > 0 && ( - - {TAG_HIERARCHY[activeCategory].subtags.map((tag) => ( - handleTagClick(tag)} - whileHover={{ scale: 1.1 }} - whileTap={{ scale: 0.9 }} - className={`flex items-center px-4 py-1 rounded-lg transition-all duration-200 ${ - selectedTags.includes(tag) - ? "bg-orange-500 text-white" - : "bg-black text-gray-200 hover:bg-gray-600" - }`} - > - Tag Logo - {tag} - - ))} - - )} + {activeCategory.category_name !== "All" && + work.categories.find( + (cat) => cat.category_id === activeCategory.category_id + )?.tags?.length > 0 && ( + + {work.categories + .find((cat) => cat.category_id === activeCategory.category_id) + ?.tags?.map( + ( + tag // Map over its tags + ) => ( + handleTagClick(tag)} + whileHover={{ scale: 1.1 }} + whileTap={{ scale: 0.9 }} + className={`flex items-center px-4 py-1 rounded-lg transition-all duration-200 ${ + selectedTags.includes(tag) + ? "bg-orange-500 text-white" + : "bg-black text-gray-200 hover:bg-gray-600" + }`} + > + Tag Logo + {tag} + + ) + )} + + )} { try { // Fetch all projects and find the one with the matching title - const allProjects = await fetchData("project_cards"); - const project = allProjects.find((p) => { + const allProjects = await fetchData("projects"); + const project = allProjects.projects.find((p) => { const projectSlug = p.title .toLowerCase() .replace(/[^a-z0-9 -]/g, "")