feat: initialize frontend with Next.js, Tailwind CSS, and internationalization support

- Added package.json for project dependencies and scripts
- Configured PostCSS with Tailwind CSS
- Created global styles and theme context for dark mode support
- Implemented layout and routing for internationalization using next-intl
- Developed main components: Header, Footer, Hero, About, Skills, Projects, Contact
- Added language switcher and contact form with validation
- Created project card component to display project details
- Set up localization files for English and Portuguese
- Configured Tailwind CSS for styling and responsive design
- Added favicon and logo assets
This commit is contained in:
2025-06-09 09:26:30 -03:00
parent b4f67449b5
commit a16374afd0
37 changed files with 8107 additions and 0 deletions

871
backend/package-lock.json generated Normal file
View File

@@ -0,0 +1,871 @@
{
"name": "backend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "backend",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"nodemailer": "^7.0.3"
}
},
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"license": "MIT",
"dependencies": {
"mime-types": "^3.0.0",
"negotiator": "^1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/body-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
"debug": "^4.4.0",
"http-errors": "^2.0.0",
"iconv-lite": "^0.6.3",
"on-finished": "^2.4.1",
"qs": "^6.14.0",
"raw-body": "^3.0.0",
"type-is": "^2.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"license": "MIT",
"engines": {
"node": ">=6.6.0"
}
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/dotenv": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.0",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"finalhandler": "^2.1.0",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"merge-descriptors": "^2.0.0",
"mime-types": "^3.0.0",
"on-finished": "^2.4.1",
"once": "^1.4.0",
"parseurl": "^1.3.3",
"proxy-addr": "^2.0.7",
"qs": "^6.14.0",
"range-parser": "^1.2.1",
"router": "^2.2.0",
"send": "^1.1.0",
"serve-static": "^2.2.0",
"statuses": "^2.0.1",
"type-is": "^2.0.1",
"vary": "^1.1.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"on-finished": "^2.4.1",
"parseurl": "^1.3.3",
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/http-errors/node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/nodemailer": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz",
"integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.6.3",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"depd": "^2.0.0",
"is-promise": "^4.0.0",
"parseurl": "^1.3.3",
"path-to-regexp": "^8.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.5",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"mime-types": "^3.0.1",
"ms": "^2.1.3",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
"license": "MIT",
"dependencies": {
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"parseurl": "^1.3.3",
"send": "^1.2.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
"media-typer": "^1.1.0",
"mime-types": "^3.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
}
}
}

19
backend/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"nodemailer": "^7.0.3"
}
}

53
backend/routes/email.js Normal file
View File

@@ -0,0 +1,53 @@
const express = require('express');
const nodemailer = require('nodemailer');
const router = express.Router();
router.post('/send', async (req, res) => {
const { name, email, message } = req.body;
if (!name ||!email ||!message) {
return res.status(400).json({ message: 'All fields are required.' });
}
if (!/\S+@\S+\.\S+/.test(email)) {
return res.status(400).json({ message: 'Invalid email address.' });
}
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || '587', 10),
secure: process.env.SMTP_SECURE === 'true', // true for 465, false for other ports
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
// If using self-signed certificates or having issues with TLS:
// tls: {
// rejectUnauthorized: false // Use with caution, only for development/testing
// }
});
const mailOptions = {
from: `"${name}" <${process.env.SMTP_FROM_EMAIL || process.env.SMTP_USER}>`, // Use a configured FROM email or fallback
replyTo: email,
to: process.env.YOUR_RECEIVING_EMAIL, // Your email address to receive submissions
subject: `New Portfolio Contact: ${name}`,
text: `Name: ${name}\nEmail: ${email}\nMessage: ${message}`,
html: `<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p>
<p><strong>Message:</strong></p>
<p>${message.replace(/\n/g, '<br>')}</p>`,
};
try {
await transporter.sendMail(mailOptions);
res.status(200).json({ message: 'Message sent successfully!' });
} catch (error) {
console.error('Error sending email:', error);
// Provide a more generic error message to the client
res.status(500).json({ message: 'Failed to send message. Please try again later.' });
}
});
module.exports = router;

26
backend/server.js Normal file
View File

@@ -0,0 +1,26 @@
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const emailRoutes = require('./routes/email');
const app = express();
const PORT = process.env.BACKEND_PORT || 3001;
// Middleware
app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:3000', // Adjust for your frontend URL
}));
app.use(express.json());
// Routes
app.use('/api/email', emailRoutes); // All email routes will be prefixed with /api/email
app.get('/api/health', (req, res) => { // Health check endpoint
res.status(200).json({ status: 'UP', message: 'Backend is running' });
});
app.listen(PORT, () => {
console.log(`Backend server running on port ${PORT}`);
});

36
frontend/README.md Normal file
View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

5
frontend/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

7
frontend/next.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
import createNextIntlPlugin from 'next-intl/plugin';
const nextConfig: NextConfig = {};
const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);

6291
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
frontend/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@heroicons/react": "^2.2.0",
"next": "15.3.3",
"next-intl": "^4.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.3",
"tailwindcss": "^4",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

BIN
frontend/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

View File

@@ -0,0 +1,58 @@
import { Inter } from "next/font/google";
import Header from "@/app/components/Header"; // Usando o Header que forneci
import Footer from "@/app/components/Footer";
import "@/app/globals.css"; // Importando o CSS global
// Importações importantes do next-intl
import {hasLocale, Locale, NextIntlClientProvider} from 'next-intl';
import { ThemeProvider } from "@/configuration/ThemeContext";
import {getTranslations, setRequestLocale} from 'next-intl/server';
import { ReactNode } from "react";
import { routing } from "@/i18n/routing";
import { notFound } from "next/navigation";
const inter = Inter({ subsets: ["latin"] });
type Props = {
children: ReactNode;
params: Promise<{locale: Locale}>;
};
export function generateStaticParams() {
return routing.locales.map((locale) => ({locale}));
}
export async function generateMetadata(props: Omit<Props, 'children'>) {
const {locale} = await props.params;
const t = await getTranslations({locale, namespace: 'metadata'});
return {
title: t('home_title'),
description: t('home_description')
};
}
export default async function RootLayout({children, params}: Props) {
const {locale} = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
setRequestLocale(locale);
return (
<html lang={locale} className="scroll-smooth">
<body className={`${inter.className} bg-background text-text-primary`}>
<ThemeProvider>
<NextIntlClientProvider locale={locale}>
<Header />
<main className="flex flex-col items-center">
{children}
</main>
<Footer />
</NextIntlClientProvider>
</ThemeProvider>
</body>
</html>
);
}

View File

@@ -0,0 +1,28 @@
import Hero from "@/app/components/sections/Hero";
import About from "@/app/components/sections/About";
import Skills from "@/app/components/sections/Skills";
import Projects from "@/app/components/sections/Projects";
import Contact from "@/app/components/sections/Contact";
import { Locale } from "next-intl";
import { use } from "react";
import { setRequestLocale } from "next-intl/server";
export default function Home({params}: {
params: Promise<{locale: Locale}>;
}) {
const {locale} = use(params);
// Enable static rendering
setRequestLocale(locale);
return (
<div className="container mx-auto px-4">
<Hero />
<About />
<Skills />
<Projects />
<Contact />
</div>
);
}

View File

@@ -0,0 +1,74 @@
'use client';
import React, { useState, FormEvent } from 'react';
import { useTranslations } from 'next-intl';
export default function ContactForm() {
const t = useTranslations('ContactForm');
const [formData, setFormData] = useState<{ name: string; email: string; message: string }>({ name: '', email: '', message: '' });
const [status, setStatus] = useState<string>('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setStatus(t('sending'));
if (!formData.name ||!formData.email ||!formData.message) {
setStatus(t('errorAllFields'));
return;
}
if (!/\S+@\S+\.\S+/.test(formData.email)) {
setStatus(t('errorInvalidEmail'));
return;
}
try {
// The backend will run on a different port or subpath, proxied by Nginx
const response = await fetch('/api/email/send', { // Adjusted API path
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (response.ok) {
setStatus(t('success'));
setFormData({ name: '', email: '', message: '' });
} else {
const errorData = await response.json();
setStatus(errorData.message || t('errorFailed'));
}
} catch (error) {
console.error('Contact form error:', error);
setStatus(t('errorGeneric'));
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6 max-w-lg mx-auto p-8 bg-white dark:bg-gray-800 shadow-xl rounded-lg">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('nameLabel')}</label>
<input type="text" name="name" id="name" value={formData.name} onChange={handleChange} required
className="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm dark:bg-gray-700 dark:text-white" />
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('emailLabel')}</label>
<input type="email" name="email" id="email" value={formData.email} onChange={handleChange} required
className="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm dark:bg-gray-700 dark:text-white" />
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300">{t('messageLabel')}</label>
<textarea name="message" id="message" rows={4} value={formData.message} onChange={handleChange} required
className="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary sm:text-sm dark:bg-gray-700 dark:text-white"></textarea>
</div>
<div>
<button type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-dark transition-colors">
{t('sendButton')}
</button>
</div>
{status && <p className={`mt-4 text-sm text-center ${status.includes('success') || status.includes('sucesso') ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>{status}</p>}
</form>
);
}

View File

@@ -0,0 +1,26 @@
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { FaGithub, FaLinkedin, FaTwitter } from 'react-icons/fa';
export default function Footer() {
const t = useTranslations('footer');
return (
<footer className="border-t border-[var(--color-border)] py-8 text-[var(--color-text-secondary)]">
<div className="container mx-auto flex flex-col md:flex-row justify-between items-center gap-4 px-4 sm:px-6 lg:px-8">
<p className="text-sm">&copy; {new Date().getFullYear()} João Loureiro. {t('copyright')}</p>
<div className="flex items-center space-x-4">
<Link href="#" aria-label="GitHub" target="_blank" className="hover:text-[var(--color-primary)] transition-colors">
<FaGithub size={20} />
</Link>
<Link href="#" aria-label="LinkedIn" target="_blank" className="hover:text-[var(--color-primary)] transition-colors">
<FaLinkedin size={20} />
</Link>
<Link href="#" aria-label="Twitter" target="_blank" className="hover:text-[var(--color-primary)] transition-colors">
<FaTwitter size={20} />
</Link>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,36 @@
'use client';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { useTheme } from '@/configuration/ThemeContext';
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline'; // Install @heroicons/react
import Image from 'next/image';
import LanguageSwitcher from "@/app/components/LanguageSwitcher";
export default function Header() {
const t = useTranslations('navigation');
const { theme, toggleTheme } = useTheme();
return (
<header className="sticky top-0 z-50 border-b border-[var(--color-border)] bg-[var(--color-background)]/90 backdrop-blur-sm">
<nav className="container mx-auto flex items-center justify-between py-4 px-4 sm:px-6 lg:px-8">
<Link href="/">
<Image src="/logo.png" alt="João Loureiro Logo" width={40} height={40} priority />
</Link>
<div className="hidden md:flex items-center space-x-6 text-[var(--color-text-secondary)]">
<Link href="#about" className="hover:text-[var(--color-primary)] transition-colors">{t('about')}</Link>
<Link href="#skills" className="hover:text-[var(--color-primary)] transition-colors">{t('tech')}</Link>
<Link href="#projects" className="hover:text-[var(--color-primary)] transition-colors">{t('projects')}</Link>
<Link href="#contact" className="hover:text-[var(--color-primary)] transition-colors">{t('contact')}</Link>
</div>
<div className="hidden md:flex items-center space-x-6 text-[var(--color-text-secondary)]">
{/* Theme Toggle */}
<button onClick={toggleTheme} className="p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700">
{theme === 'dark'? <SunIcon className="h-6 w-6 text-yellow-400" /> : <MoonIcon className="h-6 w-6 text-gray-600" />}
</button>
<LanguageSwitcher />
</div>
</nav>
</header>
);
}

View File

@@ -0,0 +1,32 @@
'use client';
import { useLocale } from 'next-intl';
import { usePathname, useRouter } from 'next/navigation';
import { useTransition } from 'react';
export default function LanguageSwitcher() {
const [isPending, startTransition] = useTransition();
const router = useRouter();
const pathname = usePathname().replaceAll(/^\/(pt|en)/g, ''); // Remove locale prefix from pathname
const locale = useLocale();
const onSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const nextLocale = e.target.value;
startTransition(() => {
router.replace(`/${nextLocale}${pathname}`);
});
};
return (
<select
defaultValue={locale}
onChange={onSelectChange}
disabled={isPending}
className="bg-[var(--color-card)] text-[var(--color-text-secondary)] border border-[var(--color-border)] rounded-md focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
>
<option value="pt">🇧🇷 Português</option>
<option value="en">🇺🇸 English</option>
</select>
);
}

View File

@@ -0,0 +1,37 @@
import Link from 'next/link';
import {useTranslations} from 'next-intl';
import { FaGithub, FaArrowUpRightFromSquare } from 'react-icons/fa6';
type ProjectCardProps = {
title: string;
description: string;
tech: string[];
};
export default function ProjectCard({ title, description, tech }: ProjectCardProps) {
const t = useTranslations('projects');
return (
<div className="bg-[var(--color-card)] rounded-lg p-6 flex flex-col border border-[var(--color-border)] hover:border-[var(--color-primary)]/50 transition-colors duration-300">
<h3 className="text-[var(--color-text-primary)] mb-2">{title}</h3>
<p className="text-[var(--color-text-secondary)] mb-4 flex-grow">{description}</p>
<div className="flex flex-wrap gap-2 mb-4">
{tech.map(t => (
<span key={t} className="bg-[var(--color-border)]/50 text-[var(--color-text-secondary)] text-xs font-semibold px-2.5 py-1 rounded-full">
{t}
</span>
))}
</div>
<div className="flex items-center space-x-8 mt-auto pt-4 border-t border-[var(--color-border)]">
<Link href="#" target="_blank" className="flex gap-2 items-center text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors">
{t('live_link')}
<FaArrowUpRightFromSquare size={12} />
</Link>
<Link href="#" target="_blank" className="flex gap-2 items-center text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors">
<FaGithub size={20} />
{t('repo_link')}
</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import {useTranslations} from 'next-intl';
export default function About() {
const t = useTranslations('about');
return (
<section id="about" className="container mx-auto py-24 md:py-32 lg:py-48">
<h2 className="text-3xl md:text-4xl font-bold text-center text-[var(--color-text-primary)] mb-12">{t('title')}</h2>
<div className="max-w-3xl mx-auto text-[var(--color-text-secondary)] space-y-6 text-lg text-left md:text-justify">
<p>{t('paragraph1')}</p>
<p>{t('paragraph2')}</p>
<p>{t('paragraph3')}</p>
</div>
</section>
);
}

View File

@@ -0,0 +1,40 @@
'use client';
import {useTranslations} from 'next-intl';
export default function Contact() {
const t = useTranslations('contact');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
alert('Contact form submission logic needs to be implemented!');
};
return (
<section id="contact" className="container mx-auto py-24 md:py-32 lg:py-48">
<div className="text-center max-w-2xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] mb-4">{t('title')}</h2>
<p className="text-[var(--color-text-secondary)] mb-10">{t('subtitle')}</p>
</div>
<form onSubmit={handleSubmit} className="max-w-xl mx-auto">
<div className="mb-4">
<label htmlFor="name" className="block mb-2 text-sm font-medium text-[var(--color-text-secondary)]">{t('form_name')}</label>
<input type="text" id="name" className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5" required />
</div>
<div className="mb-4">
<label htmlFor="email" className="block mb-2 text-sm font-medium text-[var(--color-text-secondary)]">{t('form_email')}</label>
<input type="email" id="email" className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5" required />
</div>
<div className="mb-6">
<label htmlFor="message" className="block mb-2 text-sm font-medium text-[var(--color-text-secondary)]">{t('form_message')}</label>
<textarea id="message" rows={5} className="bg-[var(--color-card)] border border-[var(--color-border)] text-[var(--color-text-primary)] text-sm rounded-lg focus:ring-[var(--color-primary)] focus:border-[var(--color-primary)] block w-full p-2.5" required></textarea>
</div>
<div className="text-center">
<button type="submit" className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white font-bold py-3 px-8 rounded-full transition-colors">
{t('submit_button')}
</button>
</div>
</form>
</section>
);
}

View File

@@ -0,0 +1,20 @@
import {useTranslations} from 'next-intl';
import Link from 'next/link';
export default function Hero() {
const t = useTranslations('hero');
return (
<section id="home" className="container mx-auto text-center py-24 md:py-32 lg:py-48">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-text-primary)]">
{t('greeting')} <span className="block text-[var(--color-primary)]">{t('title')}</span>
</h1>
<p className="text-lg md:text-xl text-[var(--color-text-secondary)] max-w-2xl mx-auto mt-6 mb-8">
{t('subtitle')}
</p>
<Link href="#contact" className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white font-bold py-3 px-8 rounded-full transition-colors">
{t('cta_button')}
</Link>
</section>
);
}

View File

@@ -0,0 +1,29 @@
import {useTranslations} from 'next-intl';
import ProjectCard from '../ProjectCard';
// Você pode popular isso com seus dados reais
const projectsData = [
{ id: 1, tech: ["Next.js", "Stripe", "Tailwind CSS", "Prisma"] },
{ id: 2, tech: ["Socket.IO", "Node.js", "React", "Express"] },
{ id: 3, tech: ["Next.js", "MDX", "Tailwind CSS", "Vercel"] }
];
export default function Projects() {
const t = useTranslations('projects');
return (
<section id="projects" className="container mx-auto py-24 md:py-32 lg:py-48">
<h2 className="text-3xl md:text-4xl font-bold text-center text-[var(--color-text-primary)] mb-12">{t('title')}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{projectsData.map(project => (
<ProjectCard
key={project.id}
title={t(`project_${project.id}_title`)}
description={t(`project_${project.id}_description`)}
tech={project.tech}
/>
))}
</div>
</section>
);
}

View File

@@ -0,0 +1,37 @@
import {useTranslations} from 'next-intl';
const skillsData = {
languages: ["JavaScript (ES6+)", "TypeScript", "HTML5", "CSS3", "Python"],
frameworks: ["React", "Next.js", "Node.js", "Express", "Tailwind CSS", "tRPC"],
tools: ["Git", "GitHub", "Docker", "VS Code", "Figma", "Postman", "Vercel"],
databases: ["PostgreSQL", "MongoDB", "Redis", "Prisma ORM"]
};
const SkillCategory = ({ title, skills }: { title: string, skills: string[] }) => (
<div className="bg-[var(--color-card)] p-6 rounded-lg border border-[var(--color-border)]">
<h3 className="text-[var(--color-primary)] mb-4">{title}</h3>
<div className="flex flex-wrap gap-2">
{skills.map(skill => (
<span key={skill} className="bg-[var(--color-border)]/50 text-[var(--color-text-secondary)] px-3 py-1 rounded-full text-sm font-medium">
{skill}
</span>
))}
</div>
</div>
);
export default function Skills() {
const t = useTranslations('skills');
return (
<section id="skills" className="container mx-auto py-24 md:py-32 lg:py-48">
<h2 className="text-3xl md:text-4xl font-bold text-center text-[var(--color-text-primary)] mb-12">{t('title')}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<SkillCategory title={t('languages')} skills={skillsData.languages} />
<SkillCategory title={t('frameworks')} skills={skillsData.frameworks} />
<SkillCategory title={t('tools')} skills={skillsData.tools} />
<SkillCategory title={t('databases')} skills={skillsData.databases} />
</div>
</section>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

View File

@@ -0,0 +1,29 @@
@import "tailwindcss";
@import "tailwindcss";
@tailwind utilities;
@theme {
--color-background: #0A0A0A;
--color-card: #121212;
--color-primary: #0ea5e9;
--color-text-primary: #F4F4F5;
--color-text-secondary: #A1A1AA;
--color-border: #27272A;
}
body {
color: var(--color-text-primary);
background-color: var(--color-background);
font-family: Inter, sans-serif; /* Adicione sua fonte preferida */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
line-height: 1.2;
}
h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.75rem; }

View File

@@ -0,0 +1,11 @@
import {ReactNode} from 'react';
type Props = {
children: ReactNode;
};
// Since we have a `not-found.tsx` page on the root, a layout file
// is required, even if it's just passing children through.
export default function RootLayout({children}: Props) {
return children;
}

View File

@@ -0,0 +1,6 @@
import {redirect} from 'next/navigation';
// This page only renders when the app is built statically (output: 'export')
export default function RootPage() {
redirect('/pt');
}

View File

@@ -0,0 +1,43 @@
'use client';
import React, { createContext, useState, useEffect, useContext, ReactNode } from 'react';
interface ThemeContextType {
theme: string;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useState<string>('dark'); // Default to dark mode
useEffect(() => {
const storedTheme = localStorage.getItem('theme');
if (storedTheme) {
setTheme(storedTheme);
}
// Apply theme to HTML element for Tailwind's 'class' strategy
document.documentElement.classList.toggle('dark', theme === 'dark');
}, [theme]);
const toggleTheme = () => {
const newTheme = theme === 'light'? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
document.documentElement.classList.toggle('dark', newTheme === 'dark');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};

View File

@@ -0,0 +1,11 @@
import createMiddleware from 'next-intl/middleware';
import {routing} from './routing';
export default createMiddleware(routing);
export const config = {
// Match all pathnames except for
// - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
// - … the ones containing a dot (e.g. `favicon.ico`)
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};

View File

@@ -0,0 +1,5 @@
import {createNavigation} from 'next-intl/navigation';
import {routing} from './routing';
export const {Link, getPathname, redirect, usePathname, useRouter} =
createNavigation(routing);

View File

@@ -0,0 +1,39 @@
import {hasLocale} from 'next-intl';
import {getRequestConfig} from 'next-intl/server';
import {routing} from './routing';
export default getRequestConfig(async ({requestLocale}) => {
const requested = await requestLocale;
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
return {
locale: locale,
messages: (await import(`../locales/${locale}.json`)).default,
// Set a default time zone if you plan to use time-related formatting.
// timeZone: 'America/Sao_Paulo',
// Set a default now value if you plan to use relative time formatting.
// now: new Date(),
// Set a default onError handler to make debugging easier.
onError: (error) => {
if (
error.message.includes('MISSING_MESSAGE') ||
error.message.includes('INVALID_MESSAGE')
) {
// Potentially log missing messages
console.warn(error.message);
} else {
console.error(JSON.stringify(error.message));
}
},
getMessageFallback: ({namespace, key, error}) => {
const path = [namespace, key].filter((part) => part!= null).join('.');
if (error.code === 'MISSING_MESSAGE') {
return `${path} is not yet translated`;
} else {
return `Dear translator, please fix this message: ${path}`;
}
}
};
});

View File

@@ -0,0 +1,6 @@
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'pt'],
defaultLocale: 'pt'
});

View File

@@ -0,0 +1,62 @@
{
"metadata": {
"home_title": "Home | João Loureiro - Software Developer",
"home_description": "Welcome to the portfolio of João Loureiro, a passionate software developer specializing in modern web technologies.",
"projects_title": "Projects | João Loureiro",
"projects_description": "Explore a selection of projects built by João Loureiro, showcasing skills in front-end and back-end development.",
"about_title": "About Me | João Loureiro",
"about_description": "Learn more about João Loureiro's journey into software development, his technical philosophy, and personal interests."
},
"navigation": {
"home": "Home",
"about": "About",
"tech": "Technologies",
"projects": "Projects",
"contact": "Contact"
},
"hero": {
"greeting": "Hello, I'm João Loureiro.",
"title": "Software Developer & Web Enthusiast",
"subtitle": "I build modern, responsive, and user-friendly web applications from front-end to back-end.",
"cta_button": "See My Work"
},
"about": {
"title": "About Me",
"paragraph1": "Driven by a passion for creating elegant solutions to complex problems, my journey in software development began with a deep curiosity for how things work. I thrive on turning ideas into reality through clean, efficient, and scalable code.",
"paragraph2": "I specialize in the JavaScript ecosystem, with a strong focus on technologies like React, Next.js, and Node.js. I'm a lifelong learner, always eager to explore new frameworks and programming paradigms to stay at the forefront of technology.",
"paragraph3": "When I'm not at my keyboard, I enjoy exploring the outdoors, reading about new tech trends, and contributing to open-source projects. I believe the best products are built at the intersection of great technology and human-centric design."
},
"skills": {
"title": "My Tech Stack",
"languages": "Languages",
"frameworks": "Frameworks & Libraries",
"tools": "Developer Tools",
"databases": "Databases"
},
"projects": {
"title": "Projects I've Built",
"project_1_title": "E-commerce Platform 'ShopNext'",
"project_1_description": "A full-featured e-commerce website built with Next.js, featuring product catalogs, a shopping cart, user authentication, and a Stripe integration for payments.",
"project_2_title": "Real-time Chat Application 'Converse'",
"project_2_description": "A web-based chat app using Socket.IO and Node.js, allowing users to join rooms and exchange messages in real-time. Features include typing indicators and user presence.",
"project_3_title": "Personal Portfolio & Blog",
"project_3_description": "The very site you are on now! Built with Next.js and Tailwind CSS, statically exported for maximum performance. Includes a blog powered by MDX.",
"tech_used": "Technologies Used:",
"live_link": "Live Demo",
"repo_link": "View Code"
},
"contact": {
"title": "Let's Connect",
"subtitle": "Have a project in mind or just want to say hello? My inbox is always open. Fill out the form below or find me on social media.",
"form_name": "Your Name",
"form_email": "Your Email",
"form_message": "Your Message",
"submit_button": "Send Message",
"success_message": "Thank you! Your message has been sent successfully.",
"error_message": "Oops! Something went wrong. Please try again later."
},
"footer": {
"copyright": "All rights reserved.",
"socials_title": "Find me on"
}
}

View File

@@ -0,0 +1,62 @@
{
"metadata": {
"home_title": "Início | João Loureiro - Desenvolvedor de Software",
"home_description": "Bem-vindo ao portfólio de João Loureiro, um desenvolvedor de software apaixonado e especializado em tecnologias web modernas.",
"projects_title": "Projetos | João Loureiro",
"projects_description": "Explore uma seleção de projetos construídos por João Loureiro, demonstrando habilidades em desenvolvimento front-end e back-end.",
"about_title": "Sobre Mim | João Loureiro",
"about_description": "Saiba mais sobre a jornada de João Loureiro na programação, sua filosofia técnica e interesses pessoais."
},
"navigation": {
"home": "Início",
"about": "Sobre",
"tech": "Tecnologias",
"projects": "Projetos",
"contact": "Contato"
},
"hero": {
"greeting": "Olá, eu sou o João Loureiro.",
"title": "Desenvolvedor de Software & Entusiasta Web",
"subtitle": "Eu construo aplicações web modernas, responsivas e fáceis de usar, do front-end ao back-end.",
"cta_button": "Veja Meus Projetos"
},
"about": {
"title": "Sobre Mim",
"paragraph1": "Movido pela paixão de criar soluções elegantes para problemas complexos, minha jornada no desenvolvimento de software começou com uma profunda curiosidade sobre como as coisas funcionam. Adoro transformar ideias em realidade através de código limpo, eficiente e escalável.",
"paragraph2": "Sou especialista no ecossistema JavaScript, com forte foco em tecnologias como React, Next.js e Node.js. Estou em constante aprendizado, sempre ansioso para explorar novos frameworks e paradigmas de programação para me manter na vanguarda da tecnologia.",
"paragraph3": "Quando não estou no meu teclado, gosto de explorar a natureza, ler sobre novas tendências tecnológicas e contribuir para projetos de código aberto. Acredito que os melhores produtos são construídos na interseção de ótima tecnologia e design centrado no ser humano."
},
"skills": {
"title": "Minhas Tecnologias",
"languages": "Linguagens",
"frameworks": "Frameworks & Bibliotecas",
"tools": "Ferramentas de Dev",
"databases": "Bancos de Dados"
},
"projects": {
"title": "Projetos que Construí",
"project_1_title": "Plataforma de E-commerce 'ShopNext'",
"project_1_description": "Um site de e-commerce completo construído com Next.js, com catálogos de produtos, carrinho de compras, autenticação de usuários e integração com Stripe para pagamentos.",
"project_2_title": "Aplicação de Chat em Tempo Real 'Converse'",
"project_2_description": "Um aplicativo de chat baseado na web usando Socket.IO e Node.js, permitindo que usuários entrem em salas e troquem mensagens em tempo real. Inclui indicadores de digitação e presença de usuário.",
"project_3_title": "Portfólio Pessoal & Blog",
"project_3_description": "O próprio site em que você está agora! Construído com Next.js e Tailwind CSS, exportado estaticamente para máxima performance. Inclui um blog com tecnologia MDX.",
"tech_used": "Tecnologias Utilizadas:",
"live_link": "Ver ao Vivo",
"repo_link": "Ver Código"
},
"contact": {
"title": "Vamos Conversar",
"subtitle": "Tem um projeto em mente ou só quer dizer um oi? Minha caixa de entrada está sempre aberta. Preencha o formulário abaixo ou me encontre nas redes sociais.",
"form_name": "Seu Nome",
"form_email": "Seu Email",
"form_message": "Sua Mensagem",
"submit_button": "Enviar Mensagem",
"success_message": "Obrigado! Sua mensagem foi enviada com sucesso.",
"error_message": "Oops! Algo deu errado. Por favor, tente novamente mais tarde."
},
"footer": {
"copyright": "Todos os direitos reservados.",
"socials_title": "Me encontre em"
}
}

View File

@@ -0,0 +1,14 @@
import type { Config } from "tailwindcss";
const config: Config = {
// O modo dark é automático na v4, mas pode manter para clareza
darkMode: "class",
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
// REMOVA A CHAVE 'theme' INTEIRA.
// plugins: [], // Plugins, se você tiver, continuam aqui.
};
export default config;

27
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}