├── README.md ├── src ├── App.jsx ├── main.jsx ├── components │ ├── Button │ │ ├── index.jsx │ │ └── Button.css │ ├── Input │ │ ├── index.jsx │ │ └── Input.css │ ├── Education │ │ └── index.jsx │ ├── PersonalInfo │ │ └── index.jsx │ ├── Experience │ │ └── index.jsx │ └── Form │ │ └── index.jsx └── index.css ├── vite.config.js ├── .gitignore ├── index.html ├── public └── favicon.svg ├── .eslintrc.cjs └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # CV Application 2 | 3 | Simple React project for The Odin Project 4 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import Form from "./components/Form"; 2 | 3 | const App = () => { 4 | return
; 5 | }; 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Apply Here! 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | C 6 | V 8 | 9 | -------------------------------------------------------------------------------- /src/components/Button/index.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import "./Button.css"; 3 | 4 | const Button = ({ label, onClick, variant }) => { 5 | variant = variant || "primary"; 6 | return ( 7 | 10 | ); 11 | }; 12 | 13 | Button.propTypes = { 14 | label: PropTypes.string.isRequired, 15 | onClick: PropTypes.func.isRequired, 16 | variant: PropTypes.string, 17 | }; 18 | 19 | export default Button; 20 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cv-application", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.2.15", 18 | "@types/react-dom": "^18.2.7", 19 | "@vitejs/plugin-react": "^4.0.3", 20 | "eslint": "^8.45.0", 21 | "eslint-plugin-react": "^7.32.2", 22 | "eslint-plugin-react-hooks": "^4.6.0", 23 | "eslint-plugin-react-refresh": "^0.4.3", 24 | "vite": "^4.4.5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Input/index.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import "./Input.css"; 3 | 4 | const Input = ({ 5 | name, 6 | placeholder, 7 | value, 8 | onChange, 9 | index, 10 | section, 11 | isEditMode, 12 | }) => { 13 | const inputStyles = isEditMode ? "editable" : "readonly"; 14 | 15 | return ( 16 | onChange(e, index, section)} 23 | /> 24 | ); 25 | }; 26 | 27 | Input.propTypes = { 28 | name: PropTypes.string.isRequired, 29 | placeholder: PropTypes.string.isRequired, 30 | value: PropTypes.string.isRequired, 31 | onChange: PropTypes.func.isRequired, 32 | index: PropTypes.number, 33 | section: PropTypes.string, 34 | isEditMode: PropTypes.bool.isRequired, 35 | }; 36 | 37 | export default Input; 38 | -------------------------------------------------------------------------------- /src/components/Button/Button.css: -------------------------------------------------------------------------------- 1 | .button { 2 | border-radius: 8px; 3 | border: 1px solid transparent; 4 | padding: 0.6em 1.2em; 5 | font-size: 1em; 6 | font-weight: 500; 7 | font-family: inherit; 8 | background-color: #1a1a1a; 9 | cursor: pointer; 10 | transition: border-color 0.25s; 11 | } 12 | 13 | @media (prefers-color-scheme: light) { 14 | .button { 15 | background-color: #f9f9f9; 16 | } 17 | } 18 | 19 | 20 | .button--primary { 21 | background-color: #1a1a1a; 22 | } 23 | 24 | .button--danger { 25 | background-color: #ff4d4d; 26 | } 27 | 28 | .button--main { 29 | font-size: 2em; 30 | font-weight: 750 31 | } 32 | 33 | .button--primary:hover, .button--main:hover { 34 | border-color: #646cff; 35 | } 36 | 37 | .button--primary:focus, 38 | .button--primary:focus-visible, 39 | .button--main:focus, 40 | .button--main:focus-visible { 41 | outline: 4px auto -webkit-focus-ring-color; 42 | } 43 | -------------------------------------------------------------------------------- /src/components/Input/Input.css: -------------------------------------------------------------------------------- 1 | input[type="text"], input[type="email"] { 2 | padding: 0.8em 1em; 3 | /* Bigger padding for spacious feel */ 4 | font-size: 1.2em; 5 | /* Increased font size */ 6 | border: 1px solid transparent; 7 | border-radius: 8px; 8 | width: 100%; 9 | /* Take full width of its container * 10 | margin-bottom: 0.5em; 11 | /* Space between inputs */ 12 | box-sizing: border-box; 13 | /* Ensure padding doesn't increase width */ 14 | transition: border-color 0.25s; 15 | /* Smooth transition */ 16 | background-color: rgba(255, 255, 255, 0.08); 17 | /* Slight background to make it pop */ 18 | } 19 | 20 | /* Effects only for editable inputs */ 21 | input[type="text"]:not(.readonly):hover, input[type="email"]:not(.readonly):hover { 22 | border-color: #535bf2; 23 | outline: none; 24 | } 25 | 26 | input[type="text"]:not(.readonly):focus, input[type="email"]:not(.readonly):focus { 27 | border-color: #535bf2; 28 | outline: none; 29 | } 30 | 31 | /* Styling for readonly inputs */ 32 | input[type="text"].readonly, input[type="email"].readonly { 33 | background: none; 34 | cursor: default; 35 | /* Suppress hover and focus effects */ 36 | pointer-events: none; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Education/index.jsx: -------------------------------------------------------------------------------- 1 | import Input from "../Input"; 2 | import Button from "../Button"; 3 | 4 | const Education = ({ 5 | educationData, 6 | handleInputChange, 7 | isEditMode, 8 | handleClick, 9 | }) => { 10 | return educationData.map((edu, index) => ( 11 |
12 | 21 | 30 | 39 | {isEditMode ? ( 40 |
48 | )); 49 | }; 50 | 51 | export default Education; 52 | -------------------------------------------------------------------------------- /src/components/PersonalInfo/index.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | import Input from "../Input"; 4 | 5 | const PersonalInfo = ({ formData, handleInputChange, isEditMode }) => { 6 | return ( 7 |
8 |
9 | 16 | 23 |
24 | 31 | 38 |
39 |
40 | ); 41 | }; 42 | 43 | PersonalInfo.propTypes = { 44 | formData: PropTypes.object.isRequired, 45 | handleInputChange: PropTypes.func.isRequired, 46 | isEditMode: PropTypes.bool.isRequired, 47 | }; 48 | 49 | export default PersonalInfo; 50 | -------------------------------------------------------------------------------- /src/components/Experience/index.jsx: -------------------------------------------------------------------------------- 1 | import Input from "../Input"; 2 | import Button from "../Button"; 3 | 4 | const Experience = ({ 5 | experienceData, 6 | handleInputChange, 7 | isEditMode, 8 | handleClick, 9 | }) => { 10 | return experienceData.map((exp, index) => ( 11 |
12 | 21 | 30 | 39 | 48 | {isEditMode ? ( 49 |
57 | )); 58 | }; 59 | 60 | export default Experience; 61 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | margin-top: 4rem; 3 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 4 | line-height: 1.5; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | @media (prefers-color-scheme: light) { 19 | :root { 20 | color: #213547; 21 | background-color: #ffffff; 22 | } 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: flex-start center; 29 | justify-content: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | .container { 35 | display: flex; 36 | flex-direction: column; 37 | justify-content: flex-start; 38 | gap: 4px; 39 | align-items: center; 40 | width: 70%; 41 | margin: 0 auto; 42 | } 43 | 44 | /* for elements to display side-by-side */ 45 | .pair-container { 46 | display: flex; 47 | justify-content: space-between; 48 | gap: 4px; 49 | width: 100%; 50 | } 51 | 52 | .pair-container>* { 53 | flex: 1; 54 | } 55 | 56 | /* adjustments for mobile */ 57 | @media (max-width: 768px) { 58 | .container { 59 | width: 90%; 60 | } 61 | 62 | /* pairs are stacked on mobile */ 63 | .pair-container { 64 | flex-direction: column; 65 | } 66 | } 67 | 68 | .form-container { 69 | display: flex; 70 | flex-direction: column; 71 | gap: 4px; 72 | width: 100%; 73 | } 74 | 75 | 76 | /* animation */ 77 | @keyframes fadeIn { 78 | from { 79 | opacity: 0; 80 | } 81 | 82 | to { 83 | opacity: 1; 84 | } 85 | } 86 | 87 | .added-item { 88 | animation: fadeIn 0.4s ease-out; 89 | } 90 | -------------------------------------------------------------------------------- /src/components/Form/index.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import Button from "../Button"; 4 | import Education from "../Education"; 5 | import Experience from "../Experience"; 6 | import PersonalInfo from "../PersonalInfo"; 7 | 8 | const Form = () => { 9 | const [formData, setFormData] = useState({ 10 | firstName: "", 11 | lastName: "", 12 | email: "", 13 | phone: "", 14 | education: [], 15 | experience: [], 16 | }); 17 | 18 | const [isEditMode, setIsEditMode] = useState(true); 19 | 20 | const addEducation = () => { 21 | setFormData((prevState) => ({ 22 | ...prevState, 23 | education: [...prevState.education, { school: "", study: "", date: "" }], 24 | })); 25 | }; 26 | 27 | const removeEducation = (index) => { 28 | setFormData((prevState) => ({ 29 | ...prevState, 30 | education: prevState.education.filter((_, i) => i !== index), 31 | })); 32 | }; 33 | 34 | const addExperience = () => { 35 | setFormData((prevState) => ({ 36 | ...prevState, 37 | experience: [ 38 | ...prevState.experience, 39 | { company: "", position: "", responsibilities: "", date: "" }, 40 | ], 41 | })); 42 | }; 43 | 44 | const removeExperience = (index) => { 45 | setFormData((prevState) => ({ 46 | ...prevState, 47 | experience: prevState.experience.filter((_, i) => i !== index), 48 | })); 49 | }; 50 | 51 | const handleInputChange = (event, index, section) => { 52 | const { name, value } = event.target; 53 | if (section === "education" || section === "experience") { 54 | const newData = [...formData[section]]; 55 | newData[index][name] = value; 56 | setFormData((prevState) => ({ ...prevState, [section]: newData })); 57 | } else { 58 | setFormData((prevState) => ({ ...prevState, [name]: value })); 59 | } 60 | }; 61 | 62 | const handleSubmit = () => { 63 | setIsEditMode(false); 64 | console.log(formData); 65 | }; 66 | 67 | const handleEdit = () => { 68 | setIsEditMode(true); 69 | }; 70 | 71 | return ( 72 |
73 | 78 | 79 | 85 | 86 | 92 | 93 | {isEditMode ? ( 94 |
95 |
98 | ) : null} 99 | 100 | {isEditMode ? ( 101 |
106 | ); 107 | }; 108 | 109 | export default Form; 110 | --------------------------------------------------------------------------------