diff --git a/README.md b/README.md index e215bc4..21327eb 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,68 @@ -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). +# DevFolio: Your Customizable Developer Portfolio -## Getting Started +DevFolio is a modern, responsive, and customizable portfolio template for developers. With easy-to-edit markdown files, you can showcase your projects, skills, and experience in a professional and visually appealing way. -First, run the development server: +## Features -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +- 🎨 Modern and clean design +- 🌓 Dark mode support +- 📱 Fully responsive +- ⚡ Built with Next.js for optimal performance +- 🎭 Easy customization through markdown files +- 📄 Automatic CV/resume generation +- 🔗 Social media integration -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +## Quick Start -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +1. Fork this repository +2. Clone your forked repository +3. Navigate to the project directory +4. Install dependencies: -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. + ```bash + npm install + ``` -## Learn More +5. Start the development server: -To learn more about Next.js, take a look at the following resources: + ```bash + npm run dev + ``` -- [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. +6. Open `http://localhost:3000` in your browser -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## Customization -## Deploy on Vercel +### Personal Information -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. +Edit the `personal-info.md` file in the `content` directory to update your name, role, and other personal details. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +### Projects + +Add or modify projects in the `projects.md` file in the `content` directory. Each project should have a title, description, image, and link. + +### CV/Resume + +Update your experience, education, and skills in the `cv.md` file in the `content` directory. + +### Social Links + +Edit the `social-links.md` file in the `content` directory to add or modify your social media links. + +## Deployment + +You can easily deploy your portfolio using Vercel. Click the button below to deploy: + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fyourusername%2Fdevfolio) + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is open source and available under the [MIT License](LICENSE). + +## Credits + +Created by CharlyAutomatiza with ❤️ diff --git a/package-lock.json b/package-lock.json index 0f01ddd..e9f8c67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,21 @@ { - "name": "charlyautomatiza-portfolio", + "name": "devfolio", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "charlyautomatiza-portfolio", + "name": "devfolio", "version": "0.1.0", "dependencies": { "@radix-ui/react-slot": "^1.1.0", - "charlyautomatiza-portfolio": "file:", + "@swc/helpers": "^0.5.13", + "@types/jspdf": "^2.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "gray-matter": "^4.0.3", "gsap": "^3.12.5", + "jspdf": "^2.5.2", "lucide-react": "^0.454.0", "next": "15.0.2", "next-themes": "^0.3.0", @@ -45,6 +47,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", @@ -889,6 +903,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jspdf": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/jspdf/-/jspdf-2.0.0.tgz", + "integrity": "sha512-oonYDXI4GegGaG7FFVtriJ+Yqlh4YR3L3NVDiwCEBVG7sbya19SoGx4MW4kg1MCMRPgkbbFTck8YKJL8PrkDfA==", + "deprecated": "This is a stub types definition. jspdf provides its own type definitions, so you do not need this installed.", + "license": "MIT", + "dependencies": { + "jspdf": "*" + } + }, "node_modules/@types/node": { "version": "20.17.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.5.tgz", @@ -906,6 +930,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", @@ -1456,6 +1487,18 @@ "dev": true, "license": "MIT" }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1499,6 +1542,16 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1536,6 +1589,18 @@ "node": ">=8" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1607,6 +1672,33 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", + "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/canvg/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1624,10 +1716,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/charlyautomatiza-portfolio": { - "resolved": "", - "link": true - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1764,6 +1852,18 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1779,6 +1879,16 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1958,6 +2068,13 @@ "node": ">=6.0.0" } }, + "node_modules/dompurify": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.7.tgz", + "integrity": "sha512-2q4bEI+coQM8f5ez7kt2xclg1XsecaV9ASJk/54vwlfRRNQfDqJz2pzQ8t0Ix/ToBpXlVjrRIx7pFC/o8itG2Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2730,6 +2847,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3160,6 +3283,20 @@ "node": ">= 0.4" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3765,6 +3902,24 @@ "json5": "lib/cli.js" } }, + "node_modules/jspdf": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", + "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.5.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -4368,6 +4523,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4633,6 +4795,16 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -4710,6 +4882,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", @@ -4778,6 +4956,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -5045,6 +5233,16 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -5406,6 +5604,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tailwind-merge": { "version": "2.5.4", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz", @@ -5464,6 +5672,16 @@ "node": ">=6" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5703,6 +5921,16 @@ "dev": true, "license": "MIT" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 864bede..b952046 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "charlyautomatiza-portfolio", + "name": "devfolio", "version": "0.1.0", "private": true, "scripts": { @@ -10,11 +10,13 @@ }, "dependencies": { "@radix-ui/react-slot": "^1.1.0", - "charlyautomatiza-portfolio": "file:", + "@swc/helpers": "^0.5.13", + "@types/jspdf": "^2.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "gray-matter": "^4.0.3", "gsap": "^3.12.5", + "jspdf": "^2.5.2", "lucide-react": "^0.454.0", "next": "15.0.2", "next-themes": "^0.3.0", diff --git a/src/app/page.tsx b/src/app/page.tsx index d222e08..8df50b1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,22 +1,53 @@ import { getMarkdownContent } from '@/utils/markdown' import Portfolio from '@/components/Portfolio' import { Metadata } from 'next' +import fs from 'fs/promises' +import path from 'path' +import { createCVPdf } from '@/utils/pdfGenerator' +import { CVData, PersonalInfo, Project, SocialLinks } from '@/types' -export const metadata: Metadata = { - title: 'Portfolio', - description: 'My professional portfolio', +export async function generateMetadata(): Promise { + const personalInfoData = await getMarkdownContent('personal-info.md') + const personalInfo = personalInfoData.data as PersonalInfo + + return { + title: personalInfo.name, + description: `${personalInfo.name} - ${personalInfo.role}`, + } +} + +async function getCvPdfUrl() { + const cvPath = path.join(process.cwd(), 'public', 'cv.pdf') + try { + await fs.access(cvPath) + return '/cv.pdf' + } catch { + return undefined + } } export default async function Page() { const projectsData = await getMarkdownContent('projects.md') const cvData = await getMarkdownContent('cv.md') const personalInfoData = await getMarkdownContent('personal-info.md') + const socialLinksData = await getMarkdownContent('social-links.md') + + let cvPdfUrl = await getCvPdfUrl() + + if (!cvPdfUrl) { + // Create PDF if it doesn't exist + const pdfBuffer = await createCVPdf(cvData.data as CVData, personalInfoData.data as PersonalInfo) + await fs.writeFile(path.join(process.cwd(), 'public', 'cv.pdf'), Buffer.from(pdfBuffer)) + cvPdfUrl = '/cv.pdf' + } return ( ) } diff --git a/src/components/Portfolio.tsx b/src/components/Portfolio.tsx index 363248f..e53a06f 100644 --- a/src/components/Portfolio.tsx +++ b/src/components/Portfolio.tsx @@ -5,20 +5,14 @@ import { gsap } from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' -import { Github, Linkedin, Mail, FileText, ChevronDown, Moon, Sun, Menu, X } from 'lucide-react' +import { GithubIcon, LinkedinIcon, MailIcon, FileTextIcon, ChevronDownIcon, MoonIcon, SunIcon, MenuIcon, XIcon } from 'lucide-react' import { useTheme } from 'next-themes' import Image from 'next/image' -import { Project, CVData, PersonalInfo } from '@/types' +import { PortfolioProps } from '@/types' gsap.registerPlugin(ScrollTrigger) -interface PortfolioProps { - projects: Project[] - cvData: CVData - personalInfo: PersonalInfo -} - -export default function Portfolio({ projects = [], cvData = { experiences: [], education: [], skills: [] }, personalInfo = { name: 'John Doe', role: 'Software Engineer' } }: Readonly) { +export default function Portfolio({ projects, cvData, personalInfo, socialLinks, cvPdfUrl }: Readonly) { const [activeSection, setActiveSection] = useState('') const sectionRefs = useRef<{ [key: string]: React.RefObject }>({}) const [mounted, setMounted] = useState(false) @@ -81,6 +75,7 @@ export default function Portfolio({ projects = [], cvData = { experiences: [], e if (element) { element.scrollIntoView({ behavior: 'smooth' }) } + setMobileMenuOpen(false) } if (!mounted) { @@ -96,18 +91,18 @@ export default function Portfolio({ projects = [], cvData = { experiences: [], e
- -

{personalInfo.name}

+

{personalInfo.name}

-
{mobileMenuOpen && ( -
+
-
    {['home', 'portfolio', 'cv', 'contact'].map((section) => ( @@ -150,10 +146,7 @@ export default function Portfolio({ projects = [], cvData = { experiences: [], e ? 'bg-[#56B281] dark:bg-[#151E21] text-white' : 'text-[#2F3E44] dark:text-white hover:text-[#2F3E44]/80 dark:hover:text-white/80' }`} - onClick={() => { - scrollToSection(section) - setMobileMenuOpen(false) - }} + onClick={() => scrollToSection(section)} > {section.charAt(0).toUpperCase() + section.slice(1)} @@ -169,17 +162,24 @@ export default function Portfolio({ projects = [], cvData = { experiences: [], e ref={sectionRefs.current['home']} className="min-h-screen flex flex-col justify-center items-center text-center px-4 bg-gradient-to-b from-[#C7D8D9] to-[#91B8C1] dark:from-[#151E21] dark:to-[#121212]" > -

    {personalInfo.name}

    -

    {personalInfo.role}

    -
    +

    {personalInfo.name}

    +

    {personalInfo.role}

    +
    - + {cvPdfUrl && ( + + )}
    - scrollToSection('portfolio')} @@ -190,7 +190,7 @@ export default function Portfolio({ projects = [], cvData = { experiences: [], e ref={sectionRefs.current['portfolio']} className="min-h-screen py-20 px-4 bg-[#DED7C9] dark:bg-[#151E21]" > -

    Portfolio

    +

    Portfolio

    {projects.map((project, index) => ( @@ -219,7 +219,7 @@ export default function Portfolio({ projects = [], cvData = { experiences: [], e ref={sectionRefs.current['cv']} className="min-h-screen py-20 px-4 bg-[#91B8C1] dark:bg-[#121212]" > -

    Curriculum Vitae

    +

    Curriculum Vitae

    Experience

    @@ -261,8 +261,8 @@ export default function Portfolio({ projects = [], cvData = { experiences: [], e ref={sectionRefs.current['contact']} className="min-h-screen py-20 px-4 flex items-center justify-center bg-[#DED7C9] dark:bg-[#151E21]" > - -

    Get in Touch

    + +

    Get in Touch

    @@ -282,7 +281,7 @@ export default function Portfolio({ projects = [], cvData = { experiences: [], e
    @@ -305,20 +304,28 @@ export default function Portfolio({ projects = [], cvData = { experiences: [], e diff --git a/src/content/social-links.md b/src/content/social-links.md new file mode 100644 index 0000000..7e5f4ef --- /dev/null +++ b/src/content/social-links.md @@ -0,0 +1,6 @@ +--- +linkedin: https://www.linkedin.com/in/gautocarlos +github: https://github.com/charlyautomatiza +email: charlyautomatiza@gmail.com +cv: https://example.com/your-cv.pdf +--- \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 15ac3af..99328f4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,34 +1,50 @@ export interface Project { - title: string - description: string - image: string - link: string + title: string; + description: string; + image: string; + link: string; } export interface Experience { - title: string - company: string - period: string - description: string + title: string; + company: string; + period: string; + location: string; + description: string; } export interface Education { - degree: string - institution: string - year: string + degree: string; + institution: string; + year: string; } export interface Skill { - name: string + name: string; } export interface CVData { - experiences: Experience[] - education: Education[] - skills: Skill[] + experiences: Experience[]; + education: Education[]; + skills: Skill[]; } export interface PersonalInfo { - name: string - role: string + name: string; + role: string; +} + +export interface SocialLinks { + linkedin?: string; + github?: string; + email?: string; + cv?: string; +} + +export interface PortfolioProps { + projects: Project[]; + cvData: CVData; + personalInfo: PersonalInfo; + socialLinks: SocialLinks; + cvPdfUrl?: string; } diff --git a/src/utils/pdfGenerator.ts b/src/utils/pdfGenerator.ts new file mode 100644 index 0000000..b519fe9 --- /dev/null +++ b/src/utils/pdfGenerator.ts @@ -0,0 +1,81 @@ +import jsPDF from 'jspdf' +import { CVData, PersonalInfo } from '@/types' + +export async function createCVPdf(cvData: CVData, personalInfo: PersonalInfo) { + const doc = new jsPDF() + + // Header + doc.setFontSize(24) + doc.text(personalInfo.name, 105, 20, { align: 'center' }) + doc.setFontSize(16) + doc.text(personalInfo.role, 105, 30, { align: 'center' }) + + let yPos = 50 + + // Experiences + doc.setFontSize(18) + doc.text('Experience', 20, yPos) + yPos += 10 + + cvData.experiences.forEach((exp) => { + if (yPos > 270) { + doc.addPage() + yPos = 20 + } + doc.setFontSize(14) + doc.text(`${exp.title} at ${exp.company}`, 20, yPos) + yPos += 7 + doc.setFontSize(12) + doc.text(`${exp.period} | ${exp.location}`, 20, yPos) + yPos += 7 + doc.setFontSize(10) + const descriptionLines = doc.splitTextToSize(exp.description, 170) + doc.text(descriptionLines, 20, yPos) + yPos += 7 * descriptionLines.length + 5 + }) + + // Education + if (yPos > 270) { + doc.addPage() + yPos = 20 + } + doc.setFontSize(18) + doc.text('Education', 20, yPos) + yPos += 10 + + cvData.education.forEach((edu) => { + if (yPos > 270) { + doc.addPage() + yPos = 20 + } + doc.setFontSize(14) + doc.text(edu.degree, 20, yPos) + yPos += 7 + doc.setFontSize(12) + doc.text(`${edu.institution}, ${edu.year}`, 20, yPos) + yPos += 10 + }) + + // Skills + if (yPos > 270) { + doc.addPage() + yPos = 20 + } + doc.setFontSize(18) + doc.text('Skills', 20, yPos) + yPos += 10 + + const skillsPerLine = 3 + for (let i = 0; i < cvData.skills.length; i += skillsPerLine) { + if (yPos > 270) { + doc.addPage() + yPos = 20 + } + const rowSkills = cvData.skills.slice(i, i + skillsPerLine) + doc.setFontSize(12) + doc.text(rowSkills.map(skill => skill.name).join(' | '), 20, yPos) + yPos += 7 + } + + return doc.output('arraybuffer') +}