├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── screenshots
├── desktop-solution.webp
├── header.webp
├── mobile-solution.webp
└── tablet-solution.webp
├── src
├── App.jsx
├── assets
│ ├── bg-card-back.png
│ ├── bg-card-front.png
│ ├── bg-main-desktop.png
│ ├── bg-main-mobile.png
│ ├── card-logo.svg
│ ├── favicon-32x32.png
│ └── icon-complete.svg
├── components
│ ├── CardCredit.jsx
│ ├── CardForm.jsx
│ └── CardThanks.jsx
├── main.jsx
└── styles
│ ├── App.css
│ ├── CardForm.css
│ ├── CardThanks.css
│ └── index.css
└── vite.config.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": ["plugin:react/recommended", "standard"],
7 | "overrides": [],
8 | "parserOptions": {
9 | "ecmaVersion": "latest",
10 | "sourceType": "module"
11 | },
12 | "plugins": ["react"],
13 | "rules": {
14 | "indent": [1, "tab"],
15 | "no-tabs": 0,
16 | "jsx-quotes": [1, "prefer-single"],
17 | "react/react-in-jsx-scope": "off",
18 | "react/prop-types": "off",
19 | "no-unused-vars": 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | style-guide.md
27 | /design
28 | test.html
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Cosmo
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | # Frontend Mentor - Interactive card details form solution
9 |
10 | This is a solution to the [Interactive card details form challenge on Frontend Mentor](https://www.frontendmentor.io/challenges/interactive-card-details-form-XpS8cKZDWw). Frontend Mentor challenges help you improve your coding skills by building realistic projects.
11 |
12 | [Solution][solution-url] . [Live Page][live-page]
13 |
14 |
15 |
16 |
17 | Table of contents
18 |
19 | - [Overview](#overview)
20 | - [The challenge](#the-challenge)
21 | - [Screenshots](#screenshots)
22 | - [Links](#links)
23 | - [My process](#my-process)
24 | - [Built with](#built-with)
25 | - [What I learned](#what-i-learned)
26 | - [Author](#author)
27 |
28 |
29 |
30 | ## Overview
31 |
32 | ### The challenge
33 |
34 | Users should be able to:
35 |
36 | - Fill in the form and see the card details update in real-time
37 | - Receive error messages when the form is submitted if:
38 | - Any input field is empty
39 | - The card number, expiry date, or CVC fields are in the wrong format
40 | - View the optimal layout depending on their device's screen size
41 | - See hover, active, and focus states for interactive elements on the page
42 |
43 | #### Expected behaviour
44 |
45 | - Update the details on the card as the user fills in the fields
46 | - Validate the form fields when the form is submitted
47 | - If there are no errors, display the completed state
48 | - Reset the form when the user clicks "Continue" on the completed state
49 |
50 | ### Screenshots
51 |
52 |
53 |
54 |
55 |
57 |
58 |
59 |
61 |
62 |
63 |
65 |
66 |
67 |
68 |
69 | ### Links
70 |
71 | - [Solution][solution-url]
72 | - [Live Page][live-page]
73 |
74 | ## My process
75 |
76 | ### Built with
77 |
78 | - Semantic HTML5 markup
79 | - CSS custom properties
80 | - Flexbox
81 | - Desktop-first workflow
82 | - [React](https://reactjs.org/) - JS library
83 | - [Vite](https://interactive-card-details-form.vercel.app/)
84 |
85 | (back to top )
86 |
87 | ### What I learned
88 |
89 | This challenge was a great learning opportunity, it was also very useful to reinforce previous knowledge, and face problems that had not been presented to me before.
90 |
91 | (back to top )
92 |
93 | ## Author
94 |
95 | - Instagram - [@cosmo_art0](https://www.instagram.com/cosmo_art0/)
96 | - Frontend Mentor - [@CosmoArt](https://www.frontendmentor.io/profile/cosmoart)
97 | - Twitter - [@CosmoArt0](https://twitter.com/cosmoart0)
98 | - My personal page - [https://cosmoart.vercel.app](https://cosmoart.vercel.app)
99 |
100 | (back to top )
101 |
102 | [live-page]: https://interactive-card-details-form.vercel.app
103 | [solution-url]: https://www.frontendmentor.io/solutions/interactive-card-details-form-solution-tKY7SrfIs_
104 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Frontend Mentor | Interactive card details form
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "interactive-card-details-form-main",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite --host",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "react": "^18.2.0",
13 | "react-dom": "^18.2.0"
14 | },
15 | "devDependencies": {
16 | "@types/react": "^18.0.15",
17 | "@types/react-dom": "^18.0.6",
18 | "@vitejs/plugin-react": "^2.0.0",
19 | "eslint": "^8.35.0",
20 | "eslint-config-standard": "^17.0.0",
21 | "eslint-plugin-import": "^2.27.5",
22 | "eslint-plugin-n": "^15.6.1",
23 | "eslint-plugin-promise": "^6.1.1",
24 | "eslint-plugin-react": "^7.32.2",
25 | "vite": "^3.0.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/screenshots/desktop-solution.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmoart/Interactive-card-details-form/0996a1a6f388e29389fb26302046936eddcff01e/screenshots/desktop-solution.webp
--------------------------------------------------------------------------------
/screenshots/header.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmoart/Interactive-card-details-form/0996a1a6f388e29389fb26302046936eddcff01e/screenshots/header.webp
--------------------------------------------------------------------------------
/screenshots/mobile-solution.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmoart/Interactive-card-details-form/0996a1a6f388e29389fb26302046936eddcff01e/screenshots/mobile-solution.webp
--------------------------------------------------------------------------------
/screenshots/tablet-solution.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmoart/Interactive-card-details-form/0996a1a6f388e29389fb26302046936eddcff01e/screenshots/tablet-solution.webp
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import CardThanks from './components/CardThanks'
3 | import CardForm from './components/CardForm'
4 | import CardCredit from './components/CardCredit'
5 | import './styles/App.css'
6 |
7 | function App () {
8 | const [formData, setFormData] = useState({ name: null, number: null, mm: null, yy: null, cvc: null })
9 | const [validate, setValidate] = useState(false)
10 |
11 | const animateSlider = (validate) => {
12 | const axis = window.matchMedia('(max-width: 750px)').matches ? 'Y' : 'X'
13 | document.querySelector('.cardOverflow > div').style.transform = `translate${axis}(50${axis === 'Y' ? 'vh' : 'vw'})`
14 |
15 | document.body.classList.add('body-slider')
16 |
17 | setTimeout(() => {
18 | setValidate(validate)
19 | document.body.classList.remove('body-slider')
20 | document.querySelector('.cardOverflow > div').style.transform = 'translate(0)'
21 | }, 500)
22 | }
23 |
24 | return (
25 | <>
26 |
27 |
28 |
29 | {validate
30 | ?
31 | :
32 | }
33 |
34 |
35 |
39 | >
40 | )
41 | }
42 | export default App
43 |
--------------------------------------------------------------------------------
/src/assets/bg-card-back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmoart/Interactive-card-details-form/0996a1a6f388e29389fb26302046936eddcff01e/src/assets/bg-card-back.png
--------------------------------------------------------------------------------
/src/assets/bg-card-front.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmoart/Interactive-card-details-form/0996a1a6f388e29389fb26302046936eddcff01e/src/assets/bg-card-front.png
--------------------------------------------------------------------------------
/src/assets/bg-main-desktop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmoart/Interactive-card-details-form/0996a1a6f388e29389fb26302046936eddcff01e/src/assets/bg-main-desktop.png
--------------------------------------------------------------------------------
/src/assets/bg-main-mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmoart/Interactive-card-details-form/0996a1a6f388e29389fb26302046936eddcff01e/src/assets/bg-main-mobile.png
--------------------------------------------------------------------------------
/src/assets/card-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmoart/Interactive-card-details-form/0996a1a6f388e29389fb26302046936eddcff01e/src/assets/favicon-32x32.png
--------------------------------------------------------------------------------
/src/assets/icon-complete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/CardCredit.jsx:
--------------------------------------------------------------------------------
1 | export default function CreditCard({ formData }) {
2 | return (
3 |
14 | )
15 | }
--------------------------------------------------------------------------------
/src/components/CardForm.jsx:
--------------------------------------------------------------------------------
1 | import '../styles/CardForm.css'
2 |
3 | export default function CardForm ({ setFormData, formData, animateSlider }) {
4 | const handleInput = (e) => {
5 | const { name, value } = e.target
6 | if (name === 'number') e.target.value = value.replace(/\s/g, '').replace(/(.{4})/g, '$1 ').trim().slice(0, 19)
7 | if (name === 'mm' || name === 'yy') e.target.value = value.toString().replace(/[^0-9]/g, '').substring(0, 2)
8 | if (name === 'mm' && value > 12) e.target.value = '12'
9 | if (name === 'cvc') e.target.value = value.substring(0, 4)
10 |
11 | setFormData({ ...formData, [name]: e.target.value })
12 | }
13 |
14 | const handleError = (target, message = 'Error', type = 'add') => {
15 | if (type === 'add') {
16 | const submitBtn = document.querySelector('.btn-submit')
17 | submitBtn.classList.add('shake')
18 | submitBtn.addEventListener('animationend', () => submitBtn.classList.remove('shake'))
19 | }
20 |
21 | document.querySelector(`.label${target}`).nextElementSibling.innerHTML = message
22 | document.querySelector(`.label${target}`).nextElementSibling.classList[type === 'add' ? 'remove' : 'add']('info--hidden')
23 | document.querySelector(`[name="${target}"]`).classList[type]('input--error')
24 | }
25 |
26 | const handleSubmit = (e) => {
27 | e.preventDefault()
28 |
29 | for (const i in formData) {
30 | if (!formData[i]) {
31 | handleError(i, 'Can`t be blank')
32 | } else handleError(i, '', 'remove')
33 | }
34 |
35 | if (formData.number) {
36 | if (formData.number.length < 19) {
37 | handleError('number', 'Number is too short')
38 | } else if (formData.number.match(/[^0-9\s]/g)) {
39 | handleError('number', 'Wrong format, numbers only')
40 | } else handleError('number', '', 'remove')
41 | }
42 |
43 | if (formData.cvc) {
44 | if (formData.cvc.length < 3) {
45 | handleError('cvc', 'CVC is too short')
46 | } else handleError('cvc', '', 'remove')
47 | }
48 |
49 | if (!formData.mm) handleError('mm', 'Can`t be blank')
50 | if (!formData.yy) handleError('yy', 'Can`t be blank')
51 |
52 | if (document.querySelectorAll('.input--error').length === 0) animateSlider(true)
53 | }
54 |
55 | return (
56 |
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/CardThanks.jsx:
--------------------------------------------------------------------------------
1 | import '../styles/CardThanks.css'
2 | import iconComplete from '../assets/icon-complete.svg'
3 |
4 | export default function CardThanks ({ setFormData, animateSlider }) {
5 | const resetForm = () => {
6 | setFormData({ name: null, number: null, mm: null, yy: null, cvc: null })
7 | animateSlider(false)
8 | }
9 |
10 | return (
11 |
12 |
13 |
Thank you!
14 |
We've added your card details
15 |
Continue
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App'
4 | import './styles/index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')).render(
7 |
8 |
9 |
10 | )
11 |
--------------------------------------------------------------------------------
/src/styles/App.css:
--------------------------------------------------------------------------------
1 | .attribution {
2 | position: fixed;
3 | bottom: 0;
4 | right: 0;
5 | font-size: 12px;
6 | opacity: 0.5;
7 | }
8 | .attribution p {
9 | margin: 10px;
10 | }
11 | .cardFront,
12 | .cardBack {
13 | max-width: 28rem;
14 | height: 15.5rem;
15 | border-radius: 10px;
16 | background-size: cover;
17 | color: var(--White);
18 | position: relative;
19 | padding: 32px;
20 | }
21 |
22 | /* Card Front ========*/
23 | .cardFront {
24 | background-image: url('../assets/bg-card-front.png');
25 | background-size: cover;
26 | background-repeat: no-repeat;
27 | display: flex;
28 | flex-direction: column;
29 | justify-content: flex-end;
30 | margin: 0 auto 32px auto;
31 | }
32 |
33 | .cardFront div {
34 | display: flex;
35 | justify-content: space-between;
36 | width: 100%;
37 | word-spacing: 6px;
38 | }
39 |
40 | .cardFront > span {
41 | font-size: clamp(22px, 2.5vw, 27px);
42 | letter-spacing: 4px;
43 | margin-bottom: 25px;
44 | }
45 |
46 | .cardFront div span {
47 | font-size: 16px;
48 | }
49 |
50 | .cardFront div span:first-child {
51 | text-transform: uppercase;
52 | }
53 |
54 | /* Card Back =======*/
55 | .cardBack {
56 | background-image: url('../assets/bg-card-back.png');
57 | background-size: contain;
58 | background-size: 100% 100%;
59 | background-repeat: no-repeat;
60 | margin: 0 2rem 0 auto;
61 | }
62 |
63 | .cardBack span {
64 | position: absolute;
65 | right: 60px;
66 | top: 109px;
67 | font-size: 24px;
68 | }
69 |
70 | .cardFront::after {
71 | content: url('../assets/card-logo.svg');
72 | position: absolute;
73 | top: 27px;
74 | left: 27px;
75 | }
76 |
77 | /* ===== */
78 | .cardDeco {
79 | width: 54%;
80 | padding: 2rem;
81 | }
82 |
83 | .cardOverflow {
84 | width: 46%;
85 | overflow: hidden;
86 | }
87 |
88 | .cardOverflow > div {
89 | transition: transform 0.35s ease-in-out;
90 | }
91 |
92 | .btn-primary {
93 | background: var(--Verdarviolet);
94 | border: none;
95 | padding: 15px 20px;
96 | color: var(--White);
97 | cursor: pointer;
98 | border-radius: 6px;
99 | font-size: 18px;
100 | }
101 |
102 | .btn-primary:hover {
103 | opacity: 0.9;
104 | }
105 |
106 | .card-input {
107 | padding: 13px;
108 | border: 1px solid var(--Dargrayisviolet);
109 | border-radius: 7px;
110 | }
111 |
112 | .card-input::placeholder {
113 | opacity: 0.7;
114 | }
115 |
116 | @media screen and (max-width: 585px) {
117 | .cardFront {
118 | left: 0 !important;
119 | }
120 |
121 | .cardBack {
122 | right: 0 !important;
123 | }
124 | }
125 |
126 | @media screen and (max-width: 750px) {
127 | .cardFront,
128 | .cardBack {
129 | transform: scale(0.76) !important;
130 | }
131 |
132 | .cardDeco {
133 | width: 100%;
134 | }
135 |
136 | .cardFront,
137 | .cardBack {
138 | margin: 1rem auto;
139 | }
140 |
141 | .cardFront {
142 | margin: 1rem 2rem 1rem auto;
143 | position: absolute;
144 | width: inherit;
145 | z-index: 10;
146 | top: 8rem !important;
147 | }
148 |
149 | .cardFront {
150 | left: 3rem;
151 | }
152 |
153 | .cardBack {
154 | position: absolute;
155 | top: 2rem;
156 | right: 3rem;
157 | width: inherit;
158 | }
159 |
160 | .cardForm,
161 | .cardThanks {
162 | width: 100%;
163 | }
164 |
165 | .cardOverflow {
166 | width: 100%;
167 | }
168 | }
169 |
170 | @media screen and (max-width: 1400px) {
171 | .cardFront,
172 | .cardBack {
173 | transform: scale(0.85);
174 | }
175 | }
176 |
177 | @media screen and (max-width: 1010px) {
178 | .cardBack {
179 | margin: 0 auto;
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/styles/CardForm.css:
--------------------------------------------------------------------------------
1 | .info--hidden {
2 | height: 0 !important;
3 | }
4 |
5 | .input--error {
6 | border: 1px solid var(--Red) !important;
7 | }
8 |
9 | .shake {
10 | animation: shake 0.7s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
11 | }
12 |
13 | @keyframes shake {
14 | 10%,
15 | 90% {
16 | transform: translate3d(-1px, 0, 0);
17 | background: var(--Red);
18 | }
19 |
20 | 20%,
21 | 80% {
22 | transform: translate3d(2px, 0, 0);
23 | }
24 |
25 | 30%,
26 | 50%,
27 | 70% {
28 | transform: translate3d(-4px, 0, 0);
29 | }
30 |
31 | 40%,
32 | 60% {
33 | transform: translate3d(4px, 0, 0);
34 | }
35 | }
36 |
37 | .cardForm {
38 | display: flex;
39 | flex-direction: column;
40 | max-width: 24rem;
41 | margin-left: 10%;
42 | padding: 1rem;
43 | }
44 |
45 | .cardForm label {
46 | margin-bottom: 16px;
47 | gap: 14px;
48 | text-transform: uppercase;
49 | display: flex;
50 | flex-direction: column;
51 | color: var(--Verdarviolet);
52 | font-size: 15px;
53 | }
54 |
55 | .cardForm input:hover {
56 | border: 1px solid hsl(249, 99%, 64%);
57 | }
58 |
59 | .labelmm {
60 | width: 50%;
61 | }
62 |
63 | .labelcvc {
64 | width: 50%;
65 | }
66 |
67 | .labelmm div {
68 | display: flex;
69 | gap: 10px;
70 | }
71 |
72 | .labelmm input {
73 | width: 100%;
74 | }
75 |
76 | .cvc-mmyy {
77 | display: flex;
78 | margin-bottom: 26px;
79 | position: relative;
80 | gap: 10px;
81 | }
82 |
83 | .cvc-mmyy .info {
84 | position: absolute;
85 | bottom: -40px;
86 | transform: translateY(-25px);
87 | }
88 |
89 | .cvc-mmyy .info:last-child {
90 | right: 1rem;
91 | }
92 |
93 | .info {
94 | transition: height 0.3s ease-in-out;
95 | font-size: 13px;
96 | margin-top: 4px;
97 | margin-bottom: 15px;
98 | overflow: hidden;
99 | height: 20px;
100 | color: var(--Red);
101 | transform: translateY(-10px);
102 | margin: 0;
103 | }
104 |
105 | @media screen and (max-width: 585px) {
106 | .App::before {
107 | height: 20rem !important;
108 | }
109 | .cardForm {
110 | margin-top: 19rem !important;
111 | }
112 | }
113 |
114 | @media screen and (max-width: 750px) {
115 | .cardForm {
116 | padding: 2rem;
117 | margin-left: auto;
118 | margin-top: 20rem;
119 | max-width: 100%;
120 | }
121 |
122 | .App::before {
123 | content: '';
124 | height: 22rem;
125 | }
126 | }
127 |
128 | @media screen and (min-width: 1350px) {
129 | .cardForm {
130 | margin-left: 3.5rem;
131 | padding: 0;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/styles/CardThanks.css:
--------------------------------------------------------------------------------
1 | .cardThanks {
2 | text-align: center;
3 | color: var(--Verdarviolet);
4 | max-width: 30rem;
5 | margin-right: auto;
6 | }
7 |
8 | .cardThanks p:first-of-type {
9 | font-size: 28px;
10 | text-transform: uppercase;
11 | margin-bottom: 0;
12 | }
13 |
14 | .cardThanks p:last-of-type {
15 | opacity: 0.8;
16 | font-size: 16px;
17 | margin-top: 10px;
18 | margin-bottom: 30px;
19 | }
20 |
21 | .cardThanks .btn-primary {
22 | width: 60%;
23 | }
24 |
25 | @media screen and (max-width: 750px) {
26 | .cardThanks {
27 | margin: auto;
28 | margin-top: 19rem;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --Lineargradient: hsl(249, 99%, 64%) to hsl(278, 94%, 30%);
3 | --Red: hsl(0, 100%, 66%);
4 |
5 | --White: hsl(0, 0%, 100%);
6 | --Lighgrayishviolet: hsl(270, 3%, 87%);
7 | --Dargrayisviolet: hsl(279, 6%, 55%);
8 | --Verdarviolet: hsl(278, 68%, 11%);
9 | }
10 |
11 | body {
12 | font-family: 'Space Grotesk', sans-serif;
13 | background: var(--White);
14 | }
15 |
16 | body::after {
17 | content: '';
18 | background-image: url('../assets/bg-main-desktop.png');
19 | background-size: cover;
20 | width: 33.5%;
21 | height: 100vh;
22 | position: absolute;
23 | top: 0;
24 | left: 0;
25 | z-index: -100;
26 | transition: width 0.35s ease-in-out, height 0.35s ease-in-out;
27 | }
28 |
29 | .body-slider::after {
30 | width: 100%;
31 | }
32 |
33 | #root {
34 | display: flex;
35 | min-height: 100vh;
36 | align-items: center;
37 | justify-content: center;
38 | }
39 |
40 | @media screen and (max-width: 750px) {
41 | #root {
42 | flex-direction: column;
43 | }
44 |
45 | body::after {
46 | background-image: url('../assets/bg-main-mobile.png');
47 | height: 40%;
48 | width: 100%;
49 | }
50 |
51 | .body-slider::after {
52 | height: 100%;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()]
7 | })
8 |
--------------------------------------------------------------------------------