Created
October 24, 2025 12:01
-
-
Save sunmeat/90a57c72def5f76a59e135c915c07274 to your computer and use it in GitHub Desktop.
spring + react
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| App.jsx: | |
| import {useState} from 'react' | |
| import './App.css' | |
| function App() { | |
| const [formData, setFormData] = useState({ | |
| name: '', | |
| email: '', | |
| phone: '', | |
| subject: '', | |
| message: '' | |
| }) | |
| const [response, setResponse] = useState('') | |
| const [loading, setLoading] = useState(false) | |
| const handleChange = (e) => { | |
| setFormData({ | |
| ...formData, | |
| [e.target.name]: e.target.value | |
| }) | |
| } | |
| const handleSubmit = async (e) => { | |
| e.preventDefault() | |
| setLoading(true) | |
| setResponse('') | |
| try { | |
| const res = await fetch('http://localhost:8080/api/submit', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(formData), | |
| }) | |
| if (res.ok) { | |
| const message = await res.text() | |
| setResponse(message) | |
| setFormData({ | |
| name: '', | |
| email: '', | |
| phone: '', | |
| subject: '', | |
| message: '' | |
| }) | |
| } else { | |
| throw new Error('Помилка сервера') | |
| } | |
| } catch (error) { | |
| setResponse('Помилка: ' + error.message) | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| return ( | |
| <div className="app-container"> | |
| <div className="form-card"> | |
| <h1 className="form-title">Контактна форма</h1> | |
| <form onSubmit={handleSubmit} className="form"> | |
| <div className="input-group"> | |
| <label htmlFor="name" className="label">Ім'я</label> | |
| <input | |
| type="text" | |
| id="name" | |
| name="name" | |
| value={formData.name} | |
| onChange={handleChange} | |
| required | |
| className="input" | |
| disabled={loading} | |
| /> | |
| </div> | |
| <div className="input-group"> | |
| <label htmlFor="email" className="label">Email</label> | |
| <input | |
| type="email" | |
| id="email" | |
| name="email" | |
| value={formData.email} | |
| onChange={handleChange} | |
| required | |
| className="input" | |
| disabled={loading} | |
| /> | |
| </div> | |
| <div className="input-group"> | |
| <label htmlFor="phone" className="label">Телефон</label> | |
| <input | |
| type="tel" | |
| id="phone" | |
| name="phone" | |
| value={formData.phone} | |
| onChange={handleChange} | |
| className="input" | |
| disabled={loading} | |
| /> | |
| </div> | |
| <div className="input-group"> | |
| <label htmlFor="subject" className="label">Тема</label> | |
| <input | |
| type="text" | |
| id="subject" | |
| name="subject" | |
| value={formData.subject} | |
| onChange={handleChange} | |
| required | |
| className="input" | |
| disabled={loading} | |
| /> | |
| </div> | |
| <div className="input-group"> | |
| <label htmlFor="message" className="label">Повідомлення</label> | |
| <textarea | |
| id="message" | |
| name="message" | |
| value={formData.message} | |
| onChange={handleChange} | |
| required | |
| rows="5" | |
| className="textarea" | |
| disabled={loading} | |
| /> | |
| </div> | |
| <button type="submit" className={`submit-btn ${loading ? 'disabled' : ''}`} disabled={loading}> | |
| {loading ? 'Відправка...' : 'Відправити'} | |
| </button> | |
| {response && ( | |
| <div className={`response ${response.includes('Помилка') ? 'error' : 'success'}`}> | |
| {response} | |
| </div> | |
| )} | |
| </form> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| export default App | |
| ================================================================================================================================= | |
| App.css: (файл index.css має бути пустим!) | |
| :root { | |
| --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| --secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| --success-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); | |
| --error-gradient: linear-gradient(135deg, #fa709a 0%, #fee140 100%); | |
| --bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| --card-bg: rgba(255, 255, 255, 0.95); | |
| --border-color: #e1e5e9; | |
| --text-primary: #333; | |
| --text-secondary: #555; | |
| --shadow: 0 20px 40px rgba(0, 0, 0, 0.1); | |
| --shadow-hover: 0 25px 50px rgba(0, 0, 0, 0.15); | |
| --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| line-height: 1.6; | |
| color: var(--text-primary); | |
| } | |
| .app-container { | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: var(--bg-gradient); | |
| padding: 20px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .app-container::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><defs><radialGradient id="a" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="rgba(255,255,255,0.1)"/><stop offset="100%" stop-color="rgba(255,255,255,0)"/></radialGradient></defs><circle cx="200" cy="200" r="100" fill="url(%23a)"/><circle cx="800" cy="300" r="150" fill="url(%23a)"/><circle cx="400" cy="800" r="120" fill="url(%23a)"/></svg>'); | |
| pointer-events: none; | |
| animation: float 20s ease-in-out infinite; | |
| } | |
| @keyframes float { | |
| 0%, 100% { | |
| transform: translateY(0px) rotate(0deg); | |
| } | |
| 50% { | |
| transform: translateY(-20px) rotate(180deg); | |
| } | |
| } | |
| .form-card { | |
| background: var(--card-bg); | |
| backdrop-filter: blur(20px); | |
| border-radius: 24px; | |
| padding: 48px 40px; | |
| width: 100%; | |
| max-width: 480px; | |
| box-shadow: var(--shadow); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| position: relative; | |
| z-index: 1; | |
| animation: slideUp 0.6s ease-out; | |
| } | |
| @keyframes slideUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(30px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .form-title { | |
| text-align: center; | |
| color: var(--text-primary); | |
| margin-bottom: 36px; | |
| font-size: 28px; | |
| font-weight: 700; | |
| letter-spacing: -0.5px; | |
| position: relative; | |
| } | |
| .form-title::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: -10px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 60px; | |
| height: 3px; | |
| background: var(--primary-gradient); | |
| border-radius: 2px; | |
| } | |
| .form { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 24px; | |
| } | |
| .input-group { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .label { | |
| display: block; | |
| margin-bottom: 8px; | |
| color: var(--text-secondary); | |
| font-size: 14px; | |
| font-weight: 500; | |
| letter-spacing: 0.5px; | |
| transition: var(--transition); | |
| } | |
| .input, | |
| .textarea { | |
| width: 100%; | |
| padding: 14px 18px; | |
| border: 2px solid var(--border-color); | |
| border-radius: 16px; | |
| font-size: 16px; | |
| font-family: inherit; | |
| background: rgba(255, 255, 255, 0.8); | |
| transition: var(--transition); | |
| outline: none; | |
| resize: vertical; | |
| } | |
| .input:focus, | |
| .textarea:focus { | |
| border-color: #667eea; | |
| box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1); | |
| background: white; | |
| transform: translateY(-1px); | |
| } | |
| .textarea { | |
| min-height: 120px; | |
| font-family: inherit; | |
| } | |
| .submit-btn { | |
| width: 100%; | |
| padding: 16px; | |
| background: var(--primary-gradient); | |
| color: white; | |
| border: none; | |
| border-radius: 16px; | |
| font-size: 16px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| margin-top: 8px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .submit-btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); | |
| transition: left 0.5s; | |
| } | |
| .submit-btn:hover:not(.disabled)::before { | |
| left: 100%; | |
| } | |
| .submit-btn:hover:not(.disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-hover); | |
| } | |
| .submit-btn.disabled { | |
| opacity: 0.7; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .response { | |
| padding: 14px 18px; | |
| border-radius: 16px; | |
| text-align: center; | |
| font-size: 14px; | |
| font-weight: 500; | |
| margin-top: 16px; | |
| animation: fadeIn 0.4s ease-out; | |
| border: 1px solid transparent; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: scale(0.95); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| } | |
| .response.success { | |
| background: var(--success-gradient); | |
| color: #0a6c00; | |
| border-color: rgba(79, 172, 254, 0.3); | |
| } | |
| .response.error { | |
| background: var(--error-gradient); | |
| color: #721c24; | |
| border-color: rgba(250, 112, 154, 0.3); | |
| } | |
| @media (max-width: 480px) { | |
| .form-card { | |
| padding: 32px 24px; | |
| margin: 10px; | |
| } | |
| .form-title { | |
| font-size: 24px; | |
| margin-bottom: 28px; | |
| } | |
| .input, | |
| .textarea { | |
| padding: 12px 16px; | |
| } | |
| } | |
| ================================================================================================================================= | |
| ReactApplication.java: | |
| package com.sunmeat.react; | |
| import org.springframework.boot.SpringApplication; | |
| import org.springframework.boot.autoconfigure.SpringBootApplication; | |
| import org.springframework.web.bind.annotation.CrossOrigin; | |
| import org.springframework.web.bind.annotation.PostMapping; | |
| import org.springframework.web.bind.annotation.RequestBody; | |
| import org.springframework.web.bind.annotation.RestController; | |
| import java.util.Map; | |
| @SpringBootApplication | |
| public class ReactApplication { | |
| public static void main(String[] args) { | |
| SpringApplication.run(ReactApplication.class, args); | |
| } | |
| } | |
| @RestController | |
| @CrossOrigin(origins = "http://localhost:5173") | |
| class SubmitController { | |
| @PostMapping("/api/submit") | |
| public String submitData(@RequestBody Map<String, String> data) { | |
| System.out.println("Отримано дані з форми (з React на 5173):"); | |
| data.forEach((key, value) -> System.out.println(key + ": " + value)); | |
| return "Дані отримано на сервері! (Перевірте консоль)"; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment