├── 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 |
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 |
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 |
96 |
97 |
98 | ) : null}
99 |
100 | {isEditMode ? (
101 |
102 | ) : (
103 |
104 | )}
105 |
106 | );
107 | };
108 |
109 | export default Form;
110 |
--------------------------------------------------------------------------------