diff --git a/package-lock.json b/package-lock.json index 370d418..8e58e38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,10 @@ "dom-to-image": "^2.6.0", "file-saver": "^2.0.5", "react": "^18.2.0", + "react-curved-text": "^3.0.1", "react-dom": "^18.2.0", "react-nice-avatar": "^1.5.0", + "react-router-dom": "^6.23.1", "react-scripts": "5.0.1", "sass": "^1.77.1", "typescript": "^4.9.5", @@ -34,6 +36,7 @@ "@types/chroma-js": "^2.4.4", "@types/dom-to-image": "^2.6.7", "@types/file-saver": "^2.0.7", + "@types/react-curved-text": "^2.0.3", "tailwindcss": "^3.4.3" } }, @@ -3799,6 +3802,14 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4688,6 +4699,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-curved-text": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/react-curved-text/-/react-curved-text-2.0.3.tgz", + "integrity": "sha512-J7yy3DJX+I+jQWk5r8IZaVyOLi0bsirD7k0yq2X6IwYTNSCVXfiEYYYq0Yoy4ZErdC2YV2nku+OQIxapPfG/Tg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.2.25", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.25.tgz", @@ -7457,6 +7477,12 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dommatrix": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dommatrix/-/dommatrix-1.0.3.tgz", + "integrity": "sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==", + "deprecated": "dommatrix is no longer maintained. Please use @thednp/dommatrix." + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -15249,6 +15275,18 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/react-curved-text": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/react-curved-text/-/react-curved-text-3.0.1.tgz", + "integrity": "sha512-tHRDAfiVa8bVMTadRdHfGoO/PjPgLF42RODAWoQlc2PNnwTDBh0hbSAZtNiNSmd8C3gB2d0rlNd841UXb0/rTQ==", + "dependencies": { + "svg-path-commander": "1.0.5" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -15409,6 +15447,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "dependencies": { + "@remix-run/router": "1.16.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "dependencies": { + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -16986,6 +17054,14 @@ "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" }, + "node_modules/svg-path-commander": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/svg-path-commander/-/svg-path-commander-1.0.5.tgz", + "integrity": "sha512-hhQfARVXoPrwwe4DNPWM4hjQLK7rTxwQ+TUc5mxoe5g0k5eStc4SPnKqmivlm/dzZ98bI+yDyKye92n2t+oOiQ==", + "dependencies": { + "dommatrix": "^1.0.3" + } + }, "node_modules/svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", diff --git a/package.json b/package.json index 4bd90aa..f2be072 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,10 @@ "dom-to-image": "^2.6.0", "file-saver": "^2.0.5", "react": "^18.2.0", + "react-curved-text": "^3.0.1", "react-dom": "^18.2.0", "react-nice-avatar": "^1.5.0", + "react-router-dom": "^6.23.1", "react-scripts": "5.0.1", "sass": "^1.77.1", "typescript": "^4.9.5", @@ -53,6 +55,7 @@ "@types/chroma-js": "^2.4.4", "@types/dom-to-image": "^2.6.7", "@types/file-saver": "^2.0.7", + "@types/react-curved-text": "^2.0.3", "tailwindcss": "^3.4.3" } } diff --git a/src/App/App.tsx b/src/App/App.tsx index 3217d28..4fcbd31 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -1,4 +1,5 @@ import React, { Component } from "react"; +import { Routes, Route } from "react-router-dom"; import AvatarList from "./components/AvatarList/index"; import AvatarEditor from "./components/AvatarEditor/index"; import Footer from "./components/Footer"; @@ -7,11 +8,15 @@ import { saveAs } from "file-saver"; import ReactNiceAvatar, { genConfig } from "./config/index"; import Header from "./components/Header"; import "./index.scss"; +import Form from "./components/Form"; +import Arrow from "./components/Arrow"; +import AboutUs from "./components/AboutUs"; interface AppState { config: { [key: string]: any }; shape: AvatarShape; avatarId: string; + avatarImageDataUrl: string | null; } type AvatarShape = "circle" | "rounded" | "square"; @@ -24,8 +29,11 @@ class App extends Component<{}, AppState> { isGradient: Boolean(Math.round(Math.random())), }), shape: "circle", - avatarId: "myAvatar", // Declare avatarId here + avatarId: "myAvatar", + avatarImageDataUrl: null, }; + this.handleFormSubmit = this.handleFormSubmit.bind(this); + this.captureAvatarImage = this.captureAvatarImage.bind(this); } selectConfig(config: { [key: string]: any }) { @@ -33,21 +41,29 @@ class App extends Component<{}, AppState> { } updateConfig(key: string, value: any) { - // Specify type for value const { config } = this.state; config[key] = value; this.setState({ config }); } updateShape(shape: AvatarShape) { - // Specify type for shape this.setState({ shape }); } async download() { const scale = 2; const node = document.getElementById(this.state.avatarId); - if (node) { + + if (!node) { + console.error( + "Element with ID", + this.state.avatarId, + "not found in the DOM." + ); + return; + } + + try { const blob = await domtoimage.toBlob(node, { height: node.offsetHeight * scale, style: { @@ -59,7 +75,13 @@ class App extends Component<{}, AppState> { width: node.offsetWidth * scale, }); - saveAs(blob, "avatar.png"); + if (blob) { + saveAs(blob, "avatar.png"); + } else { + console.error("Blob is null or undefined."); + } + } catch (error) { + console.error("Error generating image blob:", error); } } @@ -69,38 +91,84 @@ class App extends Component<{}, AppState> { }); } + async captureAvatarImage() { + const node = document.getElementById(this.state.avatarId); + if (node) { + const dataUrl = await domtoimage.toPng(node); + console.log(dataUrl); + this.setState({ avatarImageDataUrl: dataUrl }, () => { + console.log("State updated:", this.state.avatarImageDataUrl); + }); + } else console.log("node is not exist"); + } + + handleFormSubmit( + formData: { name: string; region: string; role: string }, + imageDataUrl: string + ) { + console.log("Form submitted", formData, imageDataUrl); + } + render() { - const { config, shape } = this.state; + const { config, shape, avatarImageDataUrl } = this.state; return ( -
-
+
+
-
- + } /> + +
+ +
+ + ) => + this.onInputKeyUp(e) + } + /> + + +
+ +
+ + } /> -
- - ) => - this.onInputKeyUp(e) - } - /> + {}} + setSelectedRole={() => {}} + avatarImageDataUrl={avatarImageDataUrl} + /> + } + /> +
- - -
); diff --git a/src/App/components/AboutUs.tsx b/src/App/components/AboutUs.tsx new file mode 100644 index 0000000..3afe8a5 --- /dev/null +++ b/src/App/components/AboutUs.tsx @@ -0,0 +1,35 @@ +import React from "react"; + +const AboutUs: React.FC = () => { + return ( +
+
+

About Us

+

+ Welcome to our avatar generation website! This project was initiated as + part of our learning journey in software development at CodeYourFuture. +

+

+ The primary objective of this website is to provide a platform for + individuals who cannot use their original pictures to generate avatars. + Throughout this project, We have been utilizing TypeScript to ensure + cleaner and more understandable code, facilitating easier maintenance + and scalability. +

+

+ Initially, we worked on refactoring the existing codebase, which was + written in legacy code. This process allowed us to gain in-depth + understanding of the project, fix bugs, and apply necessary updates. + Now, we are actively working on building new features to enhance the + website and achieve its objectives. +

+

+ This experience has been invaluable in teaching us how to approach new + projects written by others, identify and resolve issues effectively, and + seamlessly integrate new features into existing codebases. +

+
+ ); +}; + +export default AboutUs; diff --git a/src/App/components/Arrow.tsx b/src/App/components/Arrow.tsx new file mode 100644 index 0000000..3383f4c --- /dev/null +++ b/src/App/components/Arrow.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { useNavigate } from "react-router-dom"; + +type ArrowProps = { + width?: number; + height?: number; + fillColor?: string; + onCaptureAvatar: () => void; // Add a new prop for capturing avatar image +}; + +const Arrow = ({ + width = 100, + height = 50, + fillColor = "black", + onCaptureAvatar, +}: ArrowProps) => { + const navigate = useNavigate(); + + const handleClick = async (event: React.MouseEvent) => { + event.preventDefault(); + await onCaptureAvatar(); // Capture the avatar image + navigate("/form"); + }; + + return ( + + + + + + ); +}; + +export default Arrow; diff --git a/src/App/components/Form.tsx b/src/App/components/Form.tsx index 6c80b1a..cdaa2fc 100644 --- a/src/App/components/Form.tsx +++ b/src/App/components/Form.tsx @@ -1,8 +1,22 @@ -import React, { useState } from "react"; -import { TextField, Button, Grid, Card } from "@mui/material"; +import ReactCurvedText from 'react-curved-text'; +import React, { useState, useEffect } from "react"; +import { + Button, + Grid, + Card, + Select, + MenuItem, + InputLabel, + FormControl, + SelectChangeEvent, +} from "@mui/material"; +import ReactDOM from 'react-dom'; interface FormProps { onSubmit: (formData: FormData, imageDataUrl: string) => void; + avatarImageDataUrl: string | null; + setSelectedRegion: React.Dispatch>; + setSelectedRole: React.Dispatch>; } interface FormData { @@ -11,82 +25,195 @@ interface FormData { role: string; } -const Form: React.FC = ({ onSubmit }) => { +const Form: React.FC = ({ + onSubmit, + avatarImageDataUrl, + setSelectedRegion, + setSelectedRole, +}) => { const [formData, setFormData] = useState({ name: "", region: "", role: "", }); - const [imageDataUrl, setImageDataUrl] = useState(null); - const handleChange = (e: React.ChangeEvent) => { + const pictureByClickingArrow = () => { + if (avatarImageDataUrl) { + fetch(avatarImageDataUrl) + .then((response) => response.blob()) + .then((blob) => { + const dataURL = URL.createObjectURL(blob); + setImageDataUrl(dataURL); + const pictureDiv = document.getElementById("picture"); + if (pictureDiv) { + pictureDiv.innerHTML = ""; + const img = document.createElement("img"); + img.src = dataURL; + img.style.position = "relative"; + pictureDiv.appendChild(img); + } + onSubmit(formData, dataURL); + }); + } else { + console.error("No avatar image URL provided."); + } + }; + + useEffect(() => { + pictureByClickingArrow(); + }, []); // Empty dependency array ensures this runs only once when the component mounts + + const handleChange = ( + e: React.ChangeEvent | SelectChangeEvent + ) => { const { name, value } = e.target; setFormData({ ...formData, [name]: value }); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); + if (avatarImageDataUrl) { + fetch(avatarImageDataUrl) + .then((response) => response.blob()) + .then((blob) => { + const dataURL = URL.createObjectURL(blob); + setImageDataUrl(dataURL); + + const pictureDiv = document.getElementById("picture"); + if (pictureDiv) { + pictureDiv.innerHTML = ""; + + // Role + const roleP = document.createElement("p"); + roleP.innerText = formData.role; + roleP.style.position = "absolute"; + roleP.style.top = "1px"; + roleP.style.left = "0px"; + roleP.style.right = "0px"; + roleP.style.color = "black"; + roleP.style.backgroundColor = "transparent"; + + // Image + const img = document.createElement("img"); + img.src = dataURL; + img.style.position = "relative"; + img.style.border = "40px solid red"; + img.style.borderRadius = "160px"; + img.style.width = "256px"; + img.style.height = "256px"; + + // Create a wrapper div for ReactCurvedText + const curvedTextDiv = document.createElement("div"); + curvedTextDiv.id = "curvedTextDiv"; + curvedTextDiv.style.position = "absolute"; + curvedTextDiv.style.top = "0"; + curvedTextDiv.style.left = "0"; + curvedTextDiv.style.width = "100%"; + curvedTextDiv.style.height = "100%"; + curvedTextDiv.style.pointerEvents = "none"; + + pictureDiv.style.position = "relative"; + pictureDiv.style.width = "256px"; // + pictureDiv.style.height = "256px"; + + pictureDiv.appendChild(img); + pictureDiv.appendChild(roleP); + pictureDiv.appendChild(curvedTextDiv); + + // Render the ReactCurvedText component into the wrapper div + ReactDOM.render( + , + curvedTextDiv + ); - // Generate image - const canvas = document.createElement("canvas"); - canvas.width = 400; - canvas.height = 200; - const ctx = canvas.getContext("2d"); - if (ctx) { - ctx.fillStyle = "#ffffff"; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = "#000000"; - ctx.font = "20px Arial"; - ctx.fillText(`Name: ${formData.name}`, 10, 30); - ctx.fillText(`Region: ${formData.region}`, 10, 60); - ctx.fillText(`Role: ${formData.role}`, 10, 90); - const dataUrl = canvas.toDataURL("image/png"); - setImageDataUrl(dataUrl); - onSubmit(formData, dataUrl); // Call the onSubmit callback with form data and image data URL + // + ReactDOM.render( + , + roleP + ); + } + + onSubmit(formData, dataURL); + }); + } else { + console.error("No avatar image URL provided."); } }; + return ( -
- - + + + - - - - + + Region + + - + + Role + + - - - + + +
); }; diff --git a/src/App/components/ImageGenerator.tsx b/src/App/components/ImageGenerator.tsx index c6e29b7..17be7f8 100644 --- a/src/App/components/ImageGenerator.tsx +++ b/src/App/components/ImageGenerator.tsx @@ -2,11 +2,15 @@ import React from "react"; import { Button, Card, CardActions, CardContent } from "@mui/material"; interface ImageGeneratorProps { - generatedImageDataUrl: string | null; // Change prop name to generatedImageDataUrl + generatedImageDataUrl: string; + selectedRegion: string; + selectedRole: string; } const ImageGenerator: React.FC = ({ generatedImageDataUrl, + selectedRegion, + selectedRole, }) => { // Use generatedImageDataUrl prop const handleDownload = () => { @@ -25,9 +29,21 @@ const ImageGenerator: React.FC = ({ <> Generated +

+ {selectedRegion} +

diff --git a/src/App/index.scss b/src/App/index.scss index 9dbd0e7..4115491 100644 --- a/src/App/index.scss +++ b/src/App/index.scss @@ -1,6 +1,6 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; .App { min-height: 100vh; @@ -11,3 +11,19 @@ .inputField { background: rgba(255, 255, 255, 0.1); } + +@keyframes moveRight { + 0% { + transform: translateX(0px); + } + 50% { + transform: translateX(-20px); + } + 100% { + transform: translateX(30px); + } +} + +.animate-right { + animation: moveRight 2s infinite; +} diff --git a/src/AvatarForm.tsx b/src/AvatarForm.tsx new file mode 100644 index 0000000..7a4ce7d --- /dev/null +++ b/src/AvatarForm.tsx @@ -0,0 +1,50 @@ +import React, { useState } from "react"; +import { Grid } from "@mui/material"; +import ImageGenerator from "./App/components/ImageGenerator"; +import Header from "./App/components/Header"; +import Form from "./App/components/Form"; +import Footer from "./App/components/Footer"; + +const App: React.FC = () => { + const [formData, setFormData] = useState(null); + const [selectedRole, setSelectedRole] = useState(""); + const [selectedRegion, setSelectedRegion] = useState(""); + const [generatedImageDataUrl, setGeneratedImageDataUrl] = useState< + string | null + >(null); + + const handleSubmit = (data: any, imageDataUrl: string) => { + setFormData(data); + setGeneratedImageDataUrl(imageDataUrl); + }; + + return ( +
+
+
+ + +
+ + + {generatedImageDataUrl && ( + + )} + + +
+
+
+ ); +}; + +export default App; diff --git a/src/index.scss b/src/index.scss index c41e8fe..3d9bd37 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,5 +1,4 @@ @import "../src/scss/iconfont"; - @import "../src/scss/_fonts.scss"; html { diff --git a/src/index.tsx b/src/index.tsx index 6eb01ab..7853277 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { BrowserRouter as Router } from "react-router-dom"; import ReactDOM from "react-dom/client"; import "./index.scss"; import App from "./App/App"; @@ -8,12 +9,9 @@ const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement ); root.render( - + - + ); -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); diff --git a/tsconfig.json b/tsconfig.json index 17cb279..edd0aa0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "types": ["react", "react-dom", "react-router-dom"] }, "include": ["src", "tailwind.config.js"] }