├── .gitignore ├── LICENSE ├── README.md ├── contactapi-backend ├── HELP.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── contactapi │ │ │ │ ├── Application.java │ │ │ │ ├── config │ │ │ │ └── CorsConfig.java │ │ │ │ ├── constant │ │ │ │ └── Constant.java │ │ │ │ ├── domain │ │ │ │ └── Contact.java │ │ │ │ ├── repo │ │ │ │ └── ContactRepo.java │ │ │ │ ├── resource │ │ │ │ └── ContactResource.java │ │ │ │ └── service │ │ │ │ └── ContactService.java │ │ └── resources │ │ │ └── application.yml │ └── test │ │ └── java │ │ └── com │ │ └── example │ │ └── contactapi │ │ └── ApplicationTests.java └── target │ └── classes │ ├── application.yml │ └── com │ └── example │ └── contactapi │ ├── Application.class │ ├── config │ └── CorsConfig.class │ ├── constant │ └── Constant.class │ ├── domain │ └── Contact.class │ ├── repo │ └── ContactRepo.class │ ├── resource │ └── ContactResource.class │ └── service │ └── ContactService.class ├── contactapi-frontend ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── api │ ├── ContactService.js │ └── ToastService.js │ ├── components │ ├── Contact.js │ ├── ContactDetail.js │ ├── ContactList.js │ └── Header.js │ ├── index.css │ └── index.js └── uploads ├── 26159e21-6cc7-487e-bdf7-6a628ae2784c.jpg ├── 32c98695-6b7a-4626-a50c-1e70f7513a11.avif ├── 32d61fc7-0d7c-4b71-8ee7-1ab560679378.avif ├── 35978471-e75b-4c90-932b-39b8946d4d67.avif ├── 4228b2b1-0259-4452-9451-80ef6333231c.jpg ├── 46d6452e-6842-4f08-a5e4-e32b1c294e84.avif ├── 4aa2c3ce-e1c5-4944-88b4-09c3136966b4.avif ├── 65d0856c-a3a2-438a-9dd9-ba53028a4e39.jpg ├── a6c4cd78-54c1-401b-97f2-a895c394afb5.avif ├── ad4b7e64-bd77-4c02-8eef-273c1732fabc.avif └── ff2d235a-d078-431a-85e2-38c5bd3e794c.avif /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 HARIHARAN S 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📱 Contact Manager Full Stack Application 2 | 3 | A modern and efficient contact management system built with Java Spring Boot backend and React frontend. This application provides a seamless experience for managing contacts with features like CRUD operations, search functionality, and a responsive user interface. 4 | 5 | ## 🚀 Features 6 | 7 | - ✨ Create, Read, Update, and Delete contacts 8 | - 🔍 Search and filter contacts 9 | - 📱 Responsive design for all devices 10 | - 🔒 Secure API endpoints 11 | - 🎨 Modern and intuitive user interface 12 | - 📊 Efficient data management 13 | - 🔄 Real-time updates 14 | 15 | ## 🛠️ Tech Stack 16 | 17 | ### Backend 18 | - Java 17 19 | - Spring Boot 20 | - Spring Data JPA 21 | - MySQL Database 22 | - Maven 23 | 24 | ### Frontend 25 | - React.js 26 | - Material-UI 27 | - Axios 28 | - React Router 29 | - Redux Toolkit 30 | 31 | ## 📁 Project Structure 32 | 33 | ``` 34 | 📦 contact-manager-fullstack-java 35 | ├── 📂 contactapi-backend 36 | │ ├── 📂 src 37 | │ │ ├── 📂 main 38 | │ │ │ ├── 📂 java 39 | │ │ │ │ └── 📂 com 40 | │ │ │ │ └── 📂 example 41 | │ │ │ │ └── 📂 contactapi 42 | │ │ │ │ ├── 📂 config 43 | │ │ │ │ │ └── 📄 CorsConfig.java 44 | │ │ │ │ ├── 📂 constant 45 | │ │ │ │ ├── 📂 domain 46 | │ │ │ │ │ └── 📄 Contact.java 47 | │ │ │ │ ├── 📂 repo 48 | │ │ │ │ │ └── 📄 ContactRepo.java 49 | │ │ │ │ ├── 📂 resource 50 | │ │ │ │ │ └── 📄 ContactResource.java 51 | │ │ │ │ ├── 📂 service 52 | │ │ │ │ │ └── 📄 ContactService.java 53 | │ │ │ │ └── 📄 Application.java 54 | │ │ │ └── 📂 resources 55 | │ │ └── 📂 test 56 | │ ├── 📂 target 57 | │ ├── 📂 .mvn 58 | │ ├── 📄 pom.xml 59 | │ ├── 📄 mvnw 60 | │ └── 📄 mvnw.cmd 61 | │ 62 | ├── 📂 contactapi-frontend 63 | │ ├── 📂 src 64 | │ │ ├── 📂 api 65 | │ │ │ ├── 📄 ContactService.js 66 | │ │ │ └── 📄 ToastService.js 67 | │ │ ├── 📂 components 68 | │ │ │ ├── 📄 Contact.js 69 | │ │ │ ├── 📄 ContactDetail.js 70 | │ │ │ ├── 📄 ContactList.js 71 | │ │ │ └── 📄 Header.js 72 | │ │ ├── 📄 App.js 73 | │ │ ├── 📄 index.js 74 | │ │ └── 📄 index.css 75 | │ ├── 📂 public 76 | │ ├── 📄 package.json 77 | │ └── 📄 package-lock.json 78 | │ 79 | └── 📂 uploads 80 | ``` 81 | 82 | ### Backend Structure Details 83 | - **config/** 84 | - `CorsConfig.java` - CORS configuration for cross-origin requests 85 | - **domain/** 86 | - `Contact.java` - Contact entity class with properties and annotations 87 | - **repo/** 88 | - `ContactRepo.java` - JPA repository interface for Contact entity 89 | - **resource/** 90 | - `ContactResource.java` - REST controller with CRUD endpoints 91 | - **service/** 92 | - `ContactService.java` - Business logic implementation for contact operations 93 | - **Application.java** - Main application entry point 94 | 95 | ### Frontend Structure Details 96 | - **api/** 97 | - `ContactService.js` - API integration for contact operations 98 | - `ToastService.js` - Toast notification service 99 | - **components/** 100 | - `Contact.js` - Individual contact component 101 | - `ContactDetail.js` - Contact details view component 102 | - `ContactList.js` - List of contacts component 103 | - `Header.js` - Application header component 104 | - **App.js** - Main application component 105 | - **index.js** - Application entry point 106 | - **index.css** - Global styles 107 | 108 | ## 🚀 Getting Started 109 | 110 | ### Prerequisites 111 | - Java 17 or higher 112 | - Node.js 14 or higher 113 | - MySQL 8.0 or higher 114 | - Maven 115 | - npm or yarn 116 | 117 | ### Backend Setup 118 | 1. Navigate to the backend directory: 119 | ```bash 120 | cd contactapi-backend 121 | ``` 122 | 2. Install dependencies: 123 | ```bash 124 | mvn install 125 | ``` 126 | 3. Configure database in `application.properties` 127 | 4. Run the application: 128 | ```bash 129 | mvn spring-boot:run 130 | ``` 131 | 132 | ### Frontend Setup 133 | 1. Navigate to the frontend directory: 134 | ```bash 135 | cd contactapi-frontend 136 | ``` 137 | 2. Install dependencies: 138 | ```bash 139 | npm install 140 | ``` 141 | 3. Start the development server: 142 | ```bash 143 | npm start 144 | ``` 145 | 146 | ## 🤝 Contributing 147 | 148 | We welcome contributions to improve the Contact Manager application! Here's how you can contribute: 149 | 150 | 1. Fork the repository 151 | 2. Create a new branch (`git checkout -b feature/amazing-feature`) 152 | 3. Make your changes 153 | 4. Commit your changes (`git commit -m 'Add some amazing feature'`) 154 | 5. Push to the branch (`git push origin feature/amazing-feature`) 155 | 6. Open a Pull Request 156 | 157 | Please make sure to update tests as appropriate and adhere to the existing coding style. 158 | 159 | ## 📝 License 160 | 161 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 162 | 163 | ## 👥 Authors 164 | 165 | - **HARIHARANS24** - *Initial work* - [GitHub Profile](https://github.com/HARIHARANS24) 166 | 167 | ## 🙏 Acknowledgments 168 | 169 | - Spring Boot team for the amazing framework 170 | - React team for the frontend library 171 | - All contributors who have helped shape this project 172 | -------------------------------------------------------------------------------- /contactapi-backend/HELP.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ### Reference Documentation 4 | For further reference, please consider the following sections: 5 | 6 | * [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) 7 | * [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.5.0-RC1/maven-plugin) 8 | * [Create an OCI image](https://docs.spring.io/spring-boot/3.5.0-RC1/maven-plugin/build-image.html) 9 | * [Spring Web](https://docs.spring.io/spring-boot/3.5.0-RC1/reference/web/servlet.html) 10 | * [Spring Data JPA](https://docs.spring.io/spring-boot/3.5.0-RC1/reference/data/sql.html#data.sql.jpa-and-spring-data) 11 | 12 | ### Guides 13 | The following guides illustrate how to use some features concretely: 14 | 15 | * [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) 16 | * [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) 17 | * [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) 18 | * [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) 19 | 20 | ### Maven Parent overrides 21 | 22 | Due to Maven's design, elements are inherited from the parent POM to the project POM. 23 | While most of the inheritance is fine, it also inherits unwanted elements like `` and `` from the parent. 24 | To prevent this, the project POM contains empty overrides for these elements. 25 | If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides. 26 | 27 | -------------------------------------------------------------------------------- /contactapi-backend/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /contactapi-backend/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /contactapi-backend/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.5.0-RC1 9 | 10 | 11 | com.example 12 | contactapi 13 | 0.0.1-SNAPSHOT 14 | contactapi 15 | Demo project for Spring Boot 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 21 31 | 32 | 33 | 34 | org.hibernate.orm 35 | hibernate-core 36 | 6.3.1.Final 37 | 38 | 39 | org.postgresql 40 | postgresql 41 | 42.7.5 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-data-jpa 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-web 50 | 51 | 52 | org.postgresql 53 | postgresql 54 | runtime 55 | 56 | 57 | org.projectlombok 58 | lombok 59 | true 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-starter-test 64 | test 65 | 66 | 67 | 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-compiler-plugin 73 | 74 | 75 | 76 | org.projectlombok 77 | lombok 78 | 79 | 80 | 81 | 82 | 83 | org.springframework.boot 84 | spring-boot-maven-plugin 85 | 86 | 87 | 88 | org.projectlombok 89 | lombok 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | spring-milestones 99 | Spring Milestones 100 | https://repo.spring.io/milestone 101 | 102 | false 103 | 104 | 105 | 106 | 107 | 108 | spring-milestones 109 | Spring Milestones 110 | https://repo.spring.io/milestone 111 | 112 | false 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /contactapi-backend/src/main/java/com/example/contactapi/Application.java: -------------------------------------------------------------------------------- 1 | package com.example.contactapi; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /contactapi-backend/src/main/java/com/example/contactapi/config/CorsConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.contactapi.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.cors.CorsConfiguration; 6 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 7 | import org.springframework.web.filter.CorsFilter; 8 | 9 | import java.util.List; 10 | 11 | import static com.example.contactapi.constant.Constant.X_REQUESTED_WITH; 12 | import static org.springframework.http.HttpHeaders.ACCEPT; 13 | import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS; 14 | import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN; 15 | import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS; 16 | import static org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD; 17 | import static org.springframework.http.HttpHeaders.AUTHORIZATION; 18 | import static org.springframework.http.HttpHeaders.CONTENT_TYPE; 19 | import static org.springframework.http.HttpHeaders.ORIGIN; 20 | import static org.springframework.http.HttpMethod.DELETE; 21 | import static org.springframework.http.HttpMethod.GET; 22 | import static org.springframework.http.HttpMethod.OPTIONS; 23 | import static org.springframework.http.HttpMethod.PATCH; 24 | import static org.springframework.http.HttpMethod.POST; 25 | import static org.springframework.http.HttpMethod.PUT; 26 | 27 | @Configuration 28 | public class CorsConfig { 29 | 30 | @Bean 31 | public CorsFilter corsFilter(){ 32 | var urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(); 33 | var corsConfiguration = new CorsConfiguration(); 34 | corsConfiguration.setAllowCredentials(true); 35 | corsConfiguration.setAllowedOrigins(List.of("http://localhost:3000", "http://localhost:4200")); 36 | corsConfiguration.setAllowedHeaders(List.of(ORIGIN, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE, ACCEPT, AUTHORIZATION, X_REQUESTED_WITH, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_ALLOW_CREDENTIALS)); 37 | corsConfiguration.setExposedHeaders(List.of(ORIGIN, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE, ACCEPT, AUTHORIZATION, X_REQUESTED_WITH, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_ALLOW_CREDENTIALS)); 38 | corsConfiguration.setAllowedMethods(List.of(GET.name(), POST.name(), PUT.name(), PATCH.name(), DELETE.name(), OPTIONS.name())); 39 | urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration); 40 | return new CorsFilter(urlBasedCorsConfigurationSource); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /contactapi-backend/src/main/java/com/example/contactapi/constant/Constant.java: -------------------------------------------------------------------------------- 1 | package com.example.contactapi.constant; 2 | 3 | public class Constant { 4 | public static final String PHOTO_DIRECTORY = System.getProperty("user.home") + "/Downloads/uploads/"; 5 | public static final String X_REQUESTED_WITH = "X-Requested-With"; 6 | } 7 | -------------------------------------------------------------------------------- /contactapi-backend/src/main/java/com/example/contactapi/domain/Contact.java: -------------------------------------------------------------------------------- 1 | package com.example.contactapi.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.Id; 7 | import jakarta.persistence.Table; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import lombok.Setter; 12 | import org.hibernate.annotations.UuidGenerator; 13 | import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_DEFAULT; 14 | 15 | @Entity 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | @JsonInclude(NON_DEFAULT) 21 | @Table(name = "contacts") 22 | public class Contact { 23 | @Id 24 | @UuidGenerator 25 | @Column(name = "id", unique = true, updatable = false) 26 | private String id; 27 | private String name; 28 | private String email; 29 | private String title; 30 | private String phone; 31 | private String address; 32 | private String status; 33 | private String photoUrl; 34 | } -------------------------------------------------------------------------------- /contactapi-backend/src/main/java/com/example/contactapi/repo/ContactRepo.java: -------------------------------------------------------------------------------- 1 | package com.example.contactapi.repo; 2 | 3 | import com.example.contactapi.domain.Contact; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | import java.util.Optional; 7 | 8 | @Repository 9 | public interface ContactRepo extends JpaRepository { 10 | Optional findById(String id); 11 | } 12 | -------------------------------------------------------------------------------- /contactapi-backend/src/main/java/com/example/contactapi/resource/ContactResource.java: -------------------------------------------------------------------------------- 1 | package com.example.contactapi.resource; 2 | 3 | import com.example.contactapi.domain.Contact; 4 | import com.example.contactapi.service.ContactService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.PutMapping; 12 | import org.springframework.web.bind.annotation.RequestBody; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RequestParam; 15 | import org.springframework.web.bind.annotation.RestController; 16 | import org.springframework.web.multipart.MultipartFile; 17 | 18 | import java.io.IOException; 19 | import java.net.URI; 20 | import java.nio.file.Files; 21 | import java.nio.file.Paths; 22 | 23 | import static com.example.contactapi.constant.Constant.PHOTO_DIRECTORY; 24 | import static org.springframework.http.MediaType.IMAGE_JPEG_VALUE; 25 | import static org.springframework.http.MediaType.IMAGE_PNG_VALUE; 26 | 27 | @RestController 28 | @RequestMapping("/contacts") 29 | @RequiredArgsConstructor 30 | public class ContactResource { 31 | private final ContactService contactService; 32 | 33 | @PostMapping 34 | public ResponseEntity createContact(@RequestBody Contact contact) { 35 | //return ResponseEntity.ok().body(contactService.createContact(contact)); 36 | return ResponseEntity.created(URI.create("/contacts/userID")).body(contactService.createContact(contact)); 37 | } 38 | 39 | @GetMapping 40 | public ResponseEntity> getContacts(@RequestParam(value = "page", defaultValue = "0") int page, 41 | @RequestParam(value = "size", defaultValue = "10") int size) { 42 | return ResponseEntity.ok().body(contactService.getAllContacts(page, size)); 43 | } 44 | 45 | @GetMapping("/{id}") 46 | public ResponseEntity getContact(@PathVariable(value = "id") String id) { 47 | return ResponseEntity.ok().body(contactService.getContact(id)); 48 | } 49 | 50 | @PutMapping("/photo") 51 | public ResponseEntity uploadPhoto(@RequestParam("id") String id, @RequestParam("file")MultipartFile file) { 52 | return ResponseEntity.ok().body(contactService.uploadPhoto(id, file)); 53 | } 54 | 55 | 56 | 57 | @GetMapping(path = "/image/{filename}", produces = { IMAGE_PNG_VALUE, IMAGE_JPEG_VALUE }) 58 | public byte[] getPhoto(@PathVariable("filename") String filename) throws IOException { 59 | return Files.readAllBytes(Paths.get(PHOTO_DIRECTORY + filename)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /contactapi-backend/src/main/java/com/example/contactapi/service/ContactService.java: -------------------------------------------------------------------------------- 1 | package com.example.contactapi.service; 2 | 3 | 4 | import com.example.contactapi.domain.Contact; 5 | import com.example.contactapi.repo.ContactRepo; 6 | import jakarta.transaction.Transactional; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.data.domain.Page; 10 | import org.springframework.data.domain.PageRequest; 11 | import org.springframework.data.domain.Sort; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.web.multipart.MultipartFile; 14 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 15 | 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.nio.file.Paths; 19 | import java.util.Optional; 20 | import java.util.function.BiFunction; 21 | import java.util.function.Function; 22 | 23 | import static com.example.contactapi.constant.Constant.PHOTO_DIRECTORY; 24 | import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 25 | 26 | @Service 27 | @Slf4j 28 | @Transactional(rollbackOn = Exception.class) 29 | @RequiredArgsConstructor 30 | public class ContactService { 31 | private final ContactRepo contactRepo; 32 | 33 | public Page getAllContacts(int page, int size) { 34 | return contactRepo.findAll(PageRequest.of(page, size, Sort.by("name"))); 35 | } 36 | 37 | public Contact getContact(String id) { 38 | return contactRepo.findById(id).orElseThrow(() -> new RuntimeException("Contact not found")); 39 | } 40 | 41 | public Contact createContact(Contact contact) { 42 | return contactRepo.save(contact); 43 | } 44 | 45 | public void deleteContact(Contact contact) { 46 | // Assignment 47 | } 48 | 49 | public String uploadPhoto(String id, MultipartFile file) { 50 | log.info("Saving picture for user ID: {}", id); 51 | Contact contact = getContact(id); 52 | String photoUrl = photoFunction.apply(id, file); 53 | contact.setPhotoUrl(photoUrl); 54 | contactRepo.save(contact); 55 | return photoUrl; 56 | } 57 | 58 | private final Function fileExtension = filename -> Optional.of(filename).filter(name -> name.contains(".")) 59 | .map(name -> "." + name.substring(filename.lastIndexOf(".") + 1)).orElse(".png"); 60 | 61 | private final BiFunction photoFunction = (id, image) -> { 62 | String filename = id + fileExtension.apply(image.getOriginalFilename()); 63 | try { 64 | Path fileStorageLocation = Paths.get(PHOTO_DIRECTORY).toAbsolutePath().normalize(); 65 | if(!Files.exists(fileStorageLocation)) { Files.createDirectories(fileStorageLocation); } 66 | Files.copy(image.getInputStream(), fileStorageLocation.resolve(filename), REPLACE_EXISTING); 67 | return ServletUriComponentsBuilder 68 | .fromCurrentContextPath() 69 | .path("/contacts/image/" + filename).toUriString(); 70 | }catch (Exception exception) { 71 | throw new RuntimeException("Unable to save image"); 72 | } 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /contactapi-backend/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: 4 | username: 5 | password: 6 | jpa: 7 | database-platform: org.hibernate.dialect.PosgreSQLInnoDBDialect 8 | generate-ddl: true 9 | show-sql: true 10 | hibernate: 11 | ddl-auto: update 12 | properties: 13 | hibernate: 14 | globally_quoted_identifiers: true 15 | dialect: org.hibernate.dialect.PostgreSQLDialect 16 | format_sql: true 17 | servlet: 18 | multipart: 19 | enabled: true 20 | max-file-size: 1000MB 21 | max-request-size: 1000MB 22 | mvc: 23 | throw-exception-if-no-handler-found: true 24 | async: 25 | request-timeout: 3600000 26 | server: 27 | port: 8080 28 | error: 29 | path: /user/error 30 | whitelabel: 31 | enabled: false 32 | -------------------------------------------------------------------------------- /contactapi-backend/src/test/java/com/example/contactapi/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.contactapi; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /contactapi-backend/target/classes/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:postgresql://127.0.0.1:5432/postgres 4 | username: postgres 5 | password: root 6 | jpa: 7 | database-platform: org.hibernate.dialect.PosgreSQLInnoDBDialect 8 | generate-ddl: true 9 | show-sql: true 10 | hibernate: 11 | ddl-auto: update 12 | properties: 13 | hibernate: 14 | globally_quoted_identifiers: true 15 | dialect: org.hibernate.dialect.PostgreSQLDialect 16 | format_sql: true 17 | servlet: 18 | multipart: 19 | enabled: true 20 | max-file-size: 1000MB 21 | max-request-size: 1000MB 22 | mvc: 23 | throw-exception-if-no-handler-found: true 24 | async: 25 | request-timeout: 3600000 26 | server: 27 | port: 8080 28 | error: 29 | path: /user/error 30 | whitelabel: 31 | enabled: false 32 | -------------------------------------------------------------------------------- /contactapi-backend/target/classes/com/example/contactapi/Application.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/contactapi-backend/target/classes/com/example/contactapi/Application.class -------------------------------------------------------------------------------- /contactapi-backend/target/classes/com/example/contactapi/config/CorsConfig.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/contactapi-backend/target/classes/com/example/contactapi/config/CorsConfig.class -------------------------------------------------------------------------------- /contactapi-backend/target/classes/com/example/contactapi/constant/Constant.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/contactapi-backend/target/classes/com/example/contactapi/constant/Constant.class -------------------------------------------------------------------------------- /contactapi-backend/target/classes/com/example/contactapi/domain/Contact.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/contactapi-backend/target/classes/com/example/contactapi/domain/Contact.class -------------------------------------------------------------------------------- /contactapi-backend/target/classes/com/example/contactapi/repo/ContactRepo.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/contactapi-backend/target/classes/com/example/contactapi/repo/ContactRepo.class -------------------------------------------------------------------------------- /contactapi-backend/target/classes/com/example/contactapi/resource/ContactResource.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/contactapi-backend/target/classes/com/example/contactapi/resource/ContactResource.class -------------------------------------------------------------------------------- /contactapi-backend/target/classes/com/example/contactapi/service/ContactService.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/contactapi-backend/target/classes/com/example/contactapi/service/ContactService.class -------------------------------------------------------------------------------- /contactapi-frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /contactapi-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contactapi-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/dom": "^10.4.0", 7 | "@testing-library/jest-dom": "^6.6.3", 8 | "@testing-library/react": "^16.3.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^1.9.0", 11 | "react": "^19.1.0", 12 | "react-dom": "^19.1.0", 13 | "react-router-dom": "^7.6.0", 14 | "react-scripts": "5.0.1", 15 | "react-toastify": "^11.0.5", 16 | "web-vitals": "^2.1.4" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /contactapi-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/contactapi-frontend/public/favicon.ico -------------------------------------------------------------------------------- /contactapi-frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /contactapi-frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/contactapi-frontend/public/logo192.png -------------------------------------------------------------------------------- /contactapi-frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/contactapi-frontend/public/logo512.png -------------------------------------------------------------------------------- /contactapi-frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /contactapi-frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /contactapi-frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import 'react-toastify/dist/ReactToastify.css'; 3 | import Header from './components/Header' 4 | import ContactList from './components/ContactList' 5 | import { getContacts, saveContact, udpatePhoto } from './api/ContactService'; 6 | import { Routes, Route, Navigate } from 'react-router-dom'; 7 | import ContactDetail from './components/ContactDetail'; 8 | import { toastError } from './api/ToastService'; 9 | import { ToastContainer } from 'react-toastify'; 10 | 11 | function App() { 12 | const modalRef = useRef(); 13 | const fileRef = useRef(); 14 | const [data, setData] = useState({}); 15 | const [currentPage, setCurrentPage] = useState(0); 16 | const [file, setFile] = useState(undefined); 17 | const [values, setValues] = useState({ 18 | name: '', 19 | email: '', 20 | phone: '', 21 | address: '', 22 | title: '', 23 | status: '', 24 | }); 25 | 26 | const getAllContacts = async (page = 0, size = 10) => { 27 | try { 28 | setCurrentPage(page); 29 | const { data } = await getContacts(page, size); 30 | setData(data); 31 | console.log(data); 32 | } catch (error) { 33 | console.log(error); 34 | toastError(error.message); 35 | } 36 | }; 37 | 38 | const onChange = (event) => { 39 | setValues({ ...values, [event.target.name]: event.target.value }); 40 | }; 41 | 42 | const handleNewContact = async (event) => { 43 | event.preventDefault(); 44 | try { 45 | const { data } = await saveContact(values); 46 | const formData = new FormData(); 47 | formData.append('file', file, file.name); 48 | formData.append('id', data.id); 49 | const { data: photoUrl } = await udpatePhoto(formData); 50 | toggleModal(false); 51 | setFile(undefined); 52 | fileRef.current.value = null; 53 | setValues({ 54 | name: '', 55 | email: '', 56 | phone: '', 57 | address: '', 58 | title: '', 59 | status: '', 60 | }) 61 | getAllContacts(); 62 | } catch (error) { 63 | console.log(error); 64 | toastError(error.message); 65 | } 66 | }; 67 | 68 | const updateContact = async (contact) => { 69 | try { 70 | const { data } = await saveContact(contact); 71 | console.log(data); 72 | } catch (error) { 73 | console.log(error); 74 | toastError(error.message); 75 | } 76 | }; 77 | 78 | const updateImage = async (formData) => { 79 | try { 80 | const { data: photoUrl } = await udpatePhoto(formData); 81 | } catch (error) { 82 | console.log(error); 83 | toastError(error.message); 84 | } 85 | }; 86 | 87 | const toggleModal = show => show ? modalRef.current.showModal() : modalRef.current.close(); 88 | 89 | useEffect(() => { 90 | getAllContacts(); 91 | }, []); 92 | 93 | return ( 94 | <> 95 |
96 |
97 |
98 | 99 | } /> 100 | } /> 101 | } /> 102 | 103 |
104 |
105 | 106 | {/* Modal */} 107 | 108 |
109 |

New Contact

110 | toggleModal(false)} className="bi bi-x-lg"> 111 |
112 |
113 |
114 |
115 |
116 |
117 | Name 118 | 119 |
120 |
121 | Email 122 | 123 |
124 |
125 | Title 126 | 127 |
128 |
129 | Phone Number 130 | 131 |
132 |
133 | Address 134 | 135 |
136 |
137 | Account Status 138 | 139 |
140 |
141 | Profile Photo 142 | setFile(event.target.files[0])} ref={fileRef} name='photo' required /> 143 |
144 |
145 |
146 | 147 | 148 |
149 |
150 |
151 |
152 | 153 | 154 | ); 155 | } 156 | 157 | export default App; -------------------------------------------------------------------------------- /contactapi-frontend/src/api/ContactService.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const API_URL = 'http://localhost:8080/contacts'; 4 | 5 | export async function saveContact(contact) { 6 | return await axios.post(API_URL, contact); 7 | } 8 | 9 | export async function getContacts(page = 0, size = 10) { 10 | return await axios.get(`${API_URL}?page=${page}&size=${size}`); 11 | } 12 | 13 | export async function getContact(id) { 14 | return await axios.get(`${API_URL}/${id}`); 15 | } 16 | 17 | export async function udpateContact(contact) { 18 | return await axios.post(API_URL, contact); 19 | } 20 | 21 | export async function udpatePhoto(formData) { 22 | return await axios.put(`${API_URL}/photo`, formData); 23 | } 24 | 25 | export async function deleteContact(id) { 26 | return await axios.delete(`${API_URL}/${id}`); 27 | } -------------------------------------------------------------------------------- /contactapi-frontend/src/api/ToastService.js: -------------------------------------------------------------------------------- 1 | import { toast, ToastContainer, POSITION } from 'react-toastify'; 2 | import 'react-toastify/dist/ReactToastify.css'; 3 | 4 | const toastConfig = { 5 | position: "top-right", 6 | autoClose: 1500, 7 | hideProgressBar: false, 8 | closeOnClick: true, 9 | pauseOnHover: true, 10 | draggable: true, 11 | progress: undefined, 12 | theme: "light" 13 | }; 14 | 15 | 16 | export function toastInfo(message) { 17 | toast.info(message, toastConfig); 18 | } 19 | 20 | export function toastSuccess(message) { 21 | toast.success(message, toastConfig); 22 | } 23 | 24 | export function toastWarning(message) { 25 | toast.warn(message, toastConfig); 26 | } 27 | 28 | export function toastError(message) { 29 | toast.error(message, toastConfig); 30 | } 31 | -------------------------------------------------------------------------------- /contactapi-frontend/src/components/Contact.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | const Contact = ({ contact }) => { 5 | return ( 6 | 7 |
8 |
9 | {contact.name} 10 |
11 |
12 |

{contact.name.substring(0, 15)}

13 |

{contact.title}

14 |
15 |
16 |
17 |

{contact.email.substring(0, 20)}

18 |

{contact.address}

19 |

{contact.phone}

20 |

{contact.status === 'Active' ? : 21 | } {contact.status}

22 |
23 | 24 | ) 25 | } 26 | 27 | export default Contact -------------------------------------------------------------------------------- /contactapi-frontend/src/components/ContactDetail.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { Link, useParams } from 'react-router-dom'; 3 | import { getContact } from '../api/ContactService'; 4 | import { toastError, toastSuccess } from '../api/ToastService'; 5 | 6 | const ContactDetail = ({ updateContact, updateImage }) => { 7 | const inputRef = useRef(); 8 | const [contact, setContact] = useState({ 9 | id: '', 10 | name: '', 11 | email: '', 12 | phone: '', 13 | address: '', 14 | title: '', 15 | status: '', 16 | photoUrl: '' 17 | }); 18 | 19 | const { id } = useParams(); 20 | 21 | const fetchContact = async (id) => { 22 | try { 23 | const { data } = await getContact(id); 24 | setContact(data); 25 | console.log(data); 26 | //toastSuccess('Contact retrieved'); 27 | } catch (error) { 28 | console.log(error); 29 | toastError(error.message); 30 | } 31 | }; 32 | 33 | const selectImage = () => { 34 | inputRef.current.click(); 35 | }; 36 | 37 | const udpatePhoto = async (file) => { 38 | try { 39 | const formData = new FormData(); 40 | formData.append('file', file, file.name); 41 | formData.append('id', id); 42 | await updateImage(formData); 43 | setContact((prev) => ({ ...prev, photoUrl: `${prev.photoUrl}?updated_at=${new Date().getTime()}` })); 44 | toastSuccess('Photo updated'); 45 | } catch (error) { 46 | console.log(error); 47 | toastError(error.message); 48 | } 49 | }; 50 | 51 | const onChange = (event) => { 52 | setContact({ ...contact, [event.target.name]: event.target.value }); 53 | }; 54 | 55 | const onUpdateContact = async (event) => { 56 | event.preventDefault(); 57 | await updateContact(contact); 58 | fetchContact(id); 59 | toastSuccess('Contact Updated'); 60 | }; 61 | 62 | useEffect(() => { 63 | fetchContact(id); 64 | }, []); 65 | 66 | return ( 67 | <> 68 | Back to list 69 |
70 |
71 | {`Profile 72 |
73 |

{contact.name}

74 |

JPG, GIF, or PNG. Max size of 10MG

75 | 76 |
77 |
78 |
79 |
80 |
81 |
82 | 83 |
84 | Name 85 | 86 |
87 |
88 | Email 89 | 90 |
91 |
92 | Phone 93 | 94 |
95 |
96 | Address 97 | 98 |
99 |
100 | Title 101 | 102 |
103 |
104 | Status 105 | 106 |
107 |
108 |
109 | 110 |
111 |
112 |
113 |
114 |
115 | 116 |
117 | udpatePhoto(event.target.files[0])} name='file' accept='image/*' /> 118 |
119 | 120 | ) 121 | } 122 | 123 | export default ContactDetail; -------------------------------------------------------------------------------- /contactapi-frontend/src/components/ContactList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Contact from './Contact'; 3 | 4 | const ContactList = ({ data, currentPage, getAllContacts }) => { 5 | const handlePageClick = (page) => { 6 | if (page >= 0 && page < data.totalPages) { 7 | getAllContacts(page); 8 | } 9 | }; 10 | 11 | return ( 12 |
13 | 66 | 67 | {data?.content?.length === 0 && ( 68 |
No Contacts. Please add a new contact.
69 | )} 70 | 71 |
    72 | {data?.content?.length > 0 && 73 | data.content.map((contact) => ( 74 | 75 | ))} 76 |
77 | 78 | {data?.content?.length > 0 && data?.totalPages > 1 && ( 79 |
80 | 87 | 88 | {[...Array(data.totalPages).keys()].map((page) => ( 89 | 96 | ))} 97 | 98 | 105 |
106 | )} 107 |
108 | ); 109 | }; 110 | 111 | export default ContactList; 112 | -------------------------------------------------------------------------------- /contactapi-frontend/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Header = ({ toggleModal, nbOfContacts }) => { 4 | return ( 5 |
6 |
7 |

Contact List ({nbOfContacts})

8 | 11 |
12 |
13 | ) 14 | } 15 | 16 | export default Header -------------------------------------------------------------------------------- /contactapi-frontend/src/index.css: -------------------------------------------------------------------------------- 1 | /* GLOBAL VARIABLES */ 2 | :root { 3 | --selective-blue: hsl(210, 69%, 25%); 4 | --selective-blue-2: hsl(210, 69%, 19%); 5 | --selective-blue-3: hsl(210, 69%, 35%); 6 | --sky-blue: hsl(220, 48%, 90%); 7 | --light-gray: hsl(233, 100%, 98%); 8 | --light-gray-2: hsl(233, 100%, 88%); 9 | --dark-gray: hsl(225, 20%, 48%); 10 | --white: hsl(0, 0%, 100%); 11 | --spooky-black-1: hsl(0, 0%, 9%); 12 | --spooky-black-2: hsl(180, 3%, 7%); 13 | --spooky-black-3: hsla(0, 0%, 0%, 0.06); 14 | 15 | --border-color: #e7eaee; 16 | --border-color2: hsl(214, 17%, 92%); 17 | 18 | 19 | --body-bg: #f8f9fa; 20 | 21 | --quick-silver: hsl(0, 0%, 65%); 22 | --radical-red: hsl(351, 83%, 61%); 23 | --isabelline: hsl(36, 33%, 94%); 24 | --gray-x-11: hsl(0, 0%, 73%); 25 | --kappel_15: hsla(170, 75%, 41%, 0.15); 26 | --platinum: hsl(0, 0%, 90%); 27 | --gray-web: hsl(0, 0%, 50%); 28 | --black_80: hsla(0, 0%, 0%, 0.8); 29 | --white_50: hsla(0, 0%, 100%, 0.5); 30 | --black_50: hsla(0, 0%, 0%, 0.5); 31 | --black_30: hsla(0, 0%, 0%, 0.3); 32 | --kappel: hsl(170, 75%, 41%); 33 | --kappel: hsl(195, 71%, 31%); 34 | 35 | /** 36 | * gradient color 37 | */ 38 | 39 | --gradient: linear-gradient(-90deg, hsl(151, 58%, 46%) 0%, hsl(170, 75%, 41%) 100%); 40 | 41 | /** 42 | * typography 43 | */ 44 | 45 | --ff-league_spartan: 'League Spartan', sans-serif; 46 | --ff-poppins: 'Poppins', sans-serif; 47 | 48 | --fs-1: 4.2rem; 49 | --fs-2: 3.2rem; 50 | --fs-3: 2.3rem; 51 | --fs-4: 1.8rem; 52 | --fs-5: 1.5rem; 53 | --fs-6: 1.4rem; 54 | --fs-7: 1.3rem; 55 | --fs-8: 1.1rem; 56 | 57 | --fw-500: 500; 58 | --fw-600: 600; 59 | --fw-400: 400; 60 | 61 | /* Line height */ 62 | --lh-1: 1.5; 63 | 64 | /** 65 | * spacing 66 | */ 67 | 68 | --section-padding: 75px; 69 | 70 | /** 71 | * shadow 72 | */ 73 | 74 | --shadow-1: 0 6px 15px 0 hsla(0, 0%, 0%, 0.05); 75 | --shadow-2: 0 10px 30px hsla(0, 0%, 0%, 0.06); 76 | --shadow-3: 0 10px 50px 0 hsla(220, 53%, 22%, 0.1); 77 | 78 | /** 79 | * radius 80 | */ 81 | 82 | --radius-pill: 500px; 83 | --radius-circle: 50%; 84 | --radius-3: 3px; 85 | --radius-5: 5px; 86 | --radius-10: 10px; 87 | 88 | /** 89 | * transition 90 | */ 91 | 92 | --transition: 0.25s ease; 93 | --transition-2: 0.5s ease; 94 | --cubic-in: cubic-bezier(0.51, 0.03, 0.64, 0.28); 95 | --cubic-out: cubic-bezier(0.33, 0.85, 0.4, 0.96); 96 | 97 | } 98 | 99 | /* RESET */ 100 | *, 101 | *::before, 102 | *::after { 103 | box-sizing: border-box; 104 | margin: 0; 105 | padding: 0; 106 | } 107 | 108 | /* SCROLL BAR */ 109 | 110 | ::-webkit-scrollbar { 111 | width: 8px; 112 | } 113 | 114 | ::-webkit-scrollbar-track { 115 | background: hsl(0, 0%, 95%); 116 | } 117 | 118 | ::-webkit-scrollbar-thumb { 119 | background: hsl(0, 0%, 80%); 120 | border-radius: 5px; 121 | } 122 | 123 | ::-webkit-scrollbar-thumb:hover { 124 | background: hsl(0, 0%, 70%); 125 | } 126 | 127 | body { 128 | font-family: var(--ff-poppins); 129 | background-color: var(--white); 130 | line-height: var(--lh-1); 131 | -webkit-font-smoothing: antialiased; 132 | } 133 | 134 | img, 135 | picture, 136 | video, 137 | canvas, 138 | svg { 139 | display: block; 140 | max-width: 100%; 141 | } 142 | 143 | a, 144 | img, 145 | span, 146 | data, 147 | time, 148 | input, 149 | button, 150 | textarea, 151 | select { 152 | font: inherit; 153 | } 154 | 155 | li { 156 | list-style: none; 157 | } 158 | 159 | a { 160 | text-decoration: none; 161 | } 162 | 163 | button { 164 | background: none; 165 | border: none; 166 | cursor: pointer; 167 | text-align: center; 168 | font: inherit; 169 | } 170 | 171 | i { 172 | font-size: 1.2rem; 173 | } 174 | 175 | p, 176 | h1, 177 | h3, 178 | h4, 179 | h5, 180 | h6, 181 | a { 182 | color: var(--spooky-black-1); 183 | } 184 | 185 | i { 186 | vertical-align: middle; 187 | /* font-size: 20px; */ 188 | } 189 | 190 | /* RESET END */ 191 | 192 | /* UTILITIES */ 193 | 194 | .container { 195 | width: min(1200px, 100% - 2rem); 196 | margin-inline: auto; 197 | /* background-color: lightpink; */ 198 | overflow: hidden; 199 | } 200 | 201 | .header .container { 202 | display: flex; 203 | flex-direction: row; 204 | justify-content: space-between; 205 | align-items: center; 206 | flex-wrap: wrap; 207 | gap: 1rem; 208 | margin-top: 3rem; 209 | } 210 | 211 | .btn { 212 | color: var(--white); 213 | background-color: var(--selective-blue); 214 | font-size: .8rem; 215 | padding: 3px 10px; 216 | white-space: normal; 217 | border-radius: var(--radius-5); 218 | box-shadow: 0 0 2px var(--black_80); 219 | transition: 0.2s ease-out; 220 | } 221 | 222 | .btn-danger { 223 | background-color: var(--radical-red); 224 | } 225 | 226 | .btn:hover { 227 | background-color: var(--selective-blue-3); 228 | } 229 | 230 | .btn-danger:hover { 231 | background-color: hsl(351, 81%, 65%); 232 | } 233 | 234 | .bi-plus-square { 235 | margin-right: 5px; 236 | } 237 | 238 | .disabled { 239 | pointer-events: none; 240 | opacity: .6; 241 | } 242 | 243 | .main { 244 | margin-top: 1.3rem; 245 | margin-left: 0px; 246 | margin-right: 0px; 247 | } 248 | 249 | .contact__list { 250 | display: grid; 251 | grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); 252 | gap: 1rem; 253 | } 254 | 255 | .contact__item { 256 | width: 100%; 257 | background-color: #ecf1f1; 258 | padding: .88rem; 259 | border-radius: var(--radius-5); 260 | cursor: pointer; 261 | min-height: max-content; 262 | /* max-width: 280px; */ 263 | } 264 | 265 | .contact__header { 266 | display: flex; 267 | flex-direction: row; 268 | justify-content: flex-start; 269 | align-items: center; 270 | flex-wrap: wrap; 271 | column-gap: 12px; 272 | } 273 | 274 | .contact__image { 275 | width: 50px; 276 | } 277 | 278 | .contact__image img { 279 | width: 100%; 280 | border: 3px solid var(--selective-blue); 281 | aspect-ratio: 1 / 1; 282 | object-fit: cover; 283 | object-position: center; 284 | border-radius: var(--radius-circle); 285 | } 286 | 287 | .contact_name { 288 | color: #3c3f3f; 289 | font-size: 1.25rem; 290 | text-align: center; 291 | font-weight: 600; 292 | } 293 | 294 | .contact_title { 295 | font-size: 11px; 296 | background-color: #dfe7e8; 297 | color: var(--kappel); 298 | text-align: center; 299 | border-radius: 13px; 300 | font-weight: 600; 301 | padding: 2px; 302 | } 303 | 304 | .contact__body { 305 | margin-top: 1rem; 306 | } 307 | 308 | /* .contact__body>p:not(:last-child) { 309 | margin-bottom: .35rem; 310 | } */ 311 | 312 | .contact__body>p { 313 | font-size: 14px; 314 | margin-bottom: .35rem; 315 | margin-left: 8px; 316 | font-weight: 500; 317 | } 318 | 319 | .contact__body>p i { 320 | color: #75777a; 321 | font-size: 20px; 322 | margin-right: 8px; 323 | -webkit-text-stroke: .5px; 324 | } 325 | 326 | .contact__body>p i::before { 327 | color: var(--kappel); 328 | background-color: #dfe7e8; 329 | padding: 7px; 330 | border-radius: var(--radius-circle); 331 | } 332 | 333 | .pagination { 334 | margin-top: 2rem; 335 | display: flex; 336 | justify-content: center; 337 | margin-bottom: 10rem; 338 | } 339 | 340 | .pagination a { 341 | color: black; 342 | padding: 5px 10px; 343 | text-decoration: none; 344 | transition: background-color .3s; 345 | border: 1px solid #ddd; 346 | cursor: pointer; 347 | } 348 | 349 | .pagination a:first-child { 350 | border-top-left-radius: var(--radius-5); 351 | border-bottom-left-radius: var(--radius-5); 352 | } 353 | 354 | .pagination a:last-child { 355 | border-top-right-radius: var(--radius-5); 356 | border-bottom-right-radius: var(--radius-5); 357 | } 358 | 359 | .pagination a.active { 360 | background-color: var(--selective-blue); 361 | color: white; 362 | border: 1px solid var(--selective-blue); 363 | } 364 | 365 | .pagination a:hover:not(.active) { 366 | background-color: #ecf1f1; 367 | } 368 | 369 | /* PROFILE */ 370 | 371 | .profile { 372 | display: grid; 373 | grid-template-columns: 360px 1fr; 374 | /* grid-template-columns: repeat(auto-fit, minmax(1fr, 1fr)); */ 375 | align-items: start; 376 | gap: 1rem; 377 | margin-top: 2rem; 378 | } 379 | 380 | .profile__details { 381 | display: flex; 382 | flex-direction: row; 383 | justify-content: flex-start; 384 | align-items: center; 385 | flex-wrap: wrap; 386 | gap: 1rem; 387 | background-color: #ecf1f1; 388 | padding: 10px; 389 | border-radius: var(--radius-5); 390 | } 391 | 392 | .profile__details img { 393 | width: 120px; 394 | border-radius: var(--radius-5); 395 | border: 3px solid var(--selective-blue); 396 | aspect-ratio: 1 / 1; 397 | object-fit: cover; 398 | object-position: center; 399 | } 400 | 401 | .profile__name { 402 | color: #3c3f3f; 403 | font-size: 1rem; 404 | font-weight: 600; 405 | margin-bottom: .2rem; 406 | } 407 | 408 | .profile__muted { 409 | font-size: 12px; 410 | color: #75777a; 411 | margin-bottom: 1rem; 412 | } 413 | 414 | .profile__settings { 415 | background-color: #ecf1f1; 416 | padding: 10px; 417 | border-radius: var(--radius-5); 418 | } 419 | 420 | .divider { 421 | /* margin-top: 1rem; */ 422 | height: 2px; 423 | background: hsla(210, 10%, 23%, 0.07); 424 | } 425 | 426 | /* Modal */ 427 | .modal { 428 | position: fixed !important; 429 | top: 45%; 430 | left: 50%; 431 | transform: translate(-50%, -50%); 432 | padding: 1.3rem; 433 | width: 100%; 434 | max-width: 550px; 435 | user-select: text; 436 | visibility: visible; 437 | overflow: hidden; 438 | border: none; 439 | border-radius: 5px; 440 | height: max-content; 441 | } 442 | 443 | .modal::backdrop { 444 | background-color: rgb(0 0 0 / .7); 445 | opacity: .5; 446 | } 447 | 448 | .modal__header { 449 | display: flex; 450 | justify-content: space-between; 451 | margin-bottom: .8rem; 452 | } 453 | 454 | .modal__header i { 455 | cursor: pointer; 456 | } 457 | 458 | .modal__header i::before { 459 | border-radius: 50%; 460 | padding: .5rem; 461 | background-color: hsl(0, 0%, 90%); 462 | } 463 | 464 | .modal__header i:hover::before { 465 | background-color: hsl(0, 0%, 90%); 466 | } 467 | 468 | .modal__body { 469 | /* overflow: hidden; */ 470 | overflow-y: hidden; 471 | /* Hide vertical scrollbar */ 472 | overflow-x: hidden; 473 | /* Hide horizontal scrollbar */ 474 | } 475 | 476 | .modal__footer { 477 | display: flex; 478 | justify-content: space-between; 479 | flex-wrap: wrap; 480 | bottom: 1.5rem; 481 | margin-top: 15px; 482 | } 483 | 484 | .modal__footer .btn { 485 | padding: 7px 15px; 486 | } 487 | 488 | dialog[open] { 489 | animation: fadeIn .8s ease normal; 490 | } 491 | 492 | @keyframes fadeIn { 493 | from { 494 | opacity: 0; 495 | } 496 | 497 | to { 498 | opacity: 1; 499 | } 500 | } 501 | 502 | 503 | /* Media Queries */ 504 | 505 | @media (max-width: 900px) { 506 | .profile { 507 | grid-template-columns: repeat(auto-fit, minmax(80%, 1fr)); 508 | } 509 | } 510 | 511 | @media (max-width: 389px) { 512 | .profile__details { 513 | justify-content: center; 514 | place-self: center; 515 | width: 100%; 516 | } 517 | 518 | .profile__settings { 519 | place-self: center; 520 | } 521 | 522 | .profile__metadata { 523 | text-align: center; 524 | } 525 | } 526 | 527 | 528 | /* IMPORTED STYLES */ 529 | .user-details { 530 | display: flex; 531 | flex-wrap: wrap; 532 | justify-content: space-between; 533 | margin: 20px 0 12px 0; 534 | /* min-height: 600px; */ 535 | } 536 | 537 | .input-box { 538 | margin-bottom: 15px; 539 | width: calc(100% / 2 - 20px); 540 | } 541 | 542 | .details { 543 | display: block; 544 | font-weight: 500; 545 | margin-bottom: 5px; 546 | } 547 | 548 | .input-box input { 549 | height: 45px; 550 | width: 100%; 551 | outline: none; 552 | font-size: 16px; 553 | border-radius: 5px; 554 | padding-left: 15px; 555 | border: 1px solid #e6e1e1; 556 | transition: all 0.3s ease; 557 | } 558 | 559 | .input-box input:is(:focus, :valid) { 560 | border-color: #b9b7ba; 561 | } 562 | 563 | input[type=file]::file-selector-button { 564 | color: var(--white); 565 | background-color: var(--selective-blue); 566 | font-size: .8rem; 567 | padding: 7px 15px; 568 | white-space: normal; 569 | border-radius: var(--radius-5); 570 | border: none; 571 | box-shadow: 0 0 2px var(--black_80); 572 | transition: 0.2s ease-out; 573 | /* content: ""; */ 574 | } 575 | 576 | input[type=file]::file-selector-button:hover { 577 | background-color: var(--selective-blue-3); 578 | } 579 | 580 | .form_footer { 581 | display: flex; 582 | justify-content: space-between; 583 | align-items: center; 584 | margin-top: 35px; 585 | } 586 | 587 | .form_footer .btn { 588 | padding: 7px 15px; 589 | } 590 | 591 | @media(max-width: 584px) { 592 | 593 | /* .container{ 594 | max-width: 100%; 595 | } */ 596 | form .user-details .input-box { 597 | margin-bottom: 15px; 598 | width: 100%; 599 | } 600 | 601 | form .category { 602 | width: 100%; 603 | } 604 | 605 | /* .user-details { 606 | max-height: 300px; 607 | overflow-y: scroll; 608 | } */ 609 | 610 | .user-details::-webkit-scrollbar { 611 | width: 5px; 612 | } 613 | } 614 | 615 | @media(max-width: 459px) { 616 | /* .container .content .category{ 617 | flex-direction: column; 618 | } */ 619 | } -------------------------------------------------------------------------------- /contactapi-frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | // 10 | 11 | 12 | 13 | // 14 | ); -------------------------------------------------------------------------------- /uploads/26159e21-6cc7-487e-bdf7-6a628ae2784c.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/uploads/26159e21-6cc7-487e-bdf7-6a628ae2784c.jpg -------------------------------------------------------------------------------- /uploads/32c98695-6b7a-4626-a50c-1e70f7513a11.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/uploads/32c98695-6b7a-4626-a50c-1e70f7513a11.avif -------------------------------------------------------------------------------- /uploads/32d61fc7-0d7c-4b71-8ee7-1ab560679378.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/uploads/32d61fc7-0d7c-4b71-8ee7-1ab560679378.avif -------------------------------------------------------------------------------- /uploads/35978471-e75b-4c90-932b-39b8946d4d67.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/uploads/35978471-e75b-4c90-932b-39b8946d4d67.avif -------------------------------------------------------------------------------- /uploads/4228b2b1-0259-4452-9451-80ef6333231c.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/uploads/4228b2b1-0259-4452-9451-80ef6333231c.jpg -------------------------------------------------------------------------------- /uploads/46d6452e-6842-4f08-a5e4-e32b1c294e84.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/uploads/46d6452e-6842-4f08-a5e4-e32b1c294e84.avif -------------------------------------------------------------------------------- /uploads/4aa2c3ce-e1c5-4944-88b4-09c3136966b4.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/uploads/4aa2c3ce-e1c5-4944-88b4-09c3136966b4.avif -------------------------------------------------------------------------------- /uploads/65d0856c-a3a2-438a-9dd9-ba53028a4e39.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/uploads/65d0856c-a3a2-438a-9dd9-ba53028a4e39.jpg -------------------------------------------------------------------------------- /uploads/a6c4cd78-54c1-401b-97f2-a895c394afb5.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/uploads/a6c4cd78-54c1-401b-97f2-a895c394afb5.avif -------------------------------------------------------------------------------- /uploads/ad4b7e64-bd77-4c02-8eef-273c1732fabc.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/uploads/ad4b7e64-bd77-4c02-8eef-273c1732fabc.avif -------------------------------------------------------------------------------- /uploads/ff2d235a-d078-431a-85e2-38c5bd3e794c.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HARIHARANS24/contact-manager-fullstack-java/dd3134d3e726ad89ad0cbb55e704d41e5b0573b5/uploads/ff2d235a-d078-431a-85e2-38c5bd3e794c.avif --------------------------------------------------------------------------------