In this blog post I describe the refactoring process that took place in our product landing page Kubernetic, in order to get a more clean UI. The whole process took 5 days and was a complete rewrite of the landing page, including the trial signup form and stripe integration for payments.
The main reason of the refactor was to test out TailwindCSS framework and their utility-first design. I'm not sure if they coined the term but that was the first time I encountered it and wanted to try it out and see the benefits / inconveniences in a real-world use case. The landing page is a small website that desperately needed a lift-up so it fitted the description. You can check the final code of the landing page on GitHub.
On the following sections I will describe each decision and give a how-to guide to reproduce the setup starting from scratch to deploying in production.
NextJS Starter
Since we do a fresh start with the landing page, it was time to use NextJS instead of Create React App (CRA).
In React website there is a recommended toolchains section where they describe CRA as best fit if you're learning React or building a single-page app, while NextJS is best fit for server-rendered website with NodeJS.
Probably this landing page would fit in the CRA definition since we don't use NodeJS for server-side, but still I found myself more comfortable with NextJS guiding me with an opinionated way to build stuff (e.g. pages structure), and the integration during deployment to production or with TailWindCSS which we'll discuss further below.
For NextJS there is a quick starter template you can use to bootstrap your repo:
$ npx create-next-app nextjs-blog --use-npm --example \
https://github.com/vercel/next-learn-starter/tree/master/learn-starter
Once you have the repository you can run the app using npm run dev
and open the http://localhost:3000.
NextJS + TailwindCSS Starter
TailwindCSS is essentially a PostCSS plugin, so in order to integrate it you need to install PostCSS first, a tool for transforming CSS with JavaScript. There is a great guide for integrating NextJS with TailwindCSS here which I definitely would recommend to read and do the exercise yourself so that you can better understand the concepts and mechanisms behind the curtain. For faster bootstrapping though you can use the following starter instead that already comes prepared:
$ npx create-next-app nextjs-blog --use-npm --example \
https://github.com/vercel/next.js/tree/canary/examples/with-tailwindcss
Now run npm run dev and open http://localhost:3000 on your browser:
Enable TypeScript
There are always strong opinions between javascript vs typescript, choose the one you're more comfortable with, I personally prefer typescript from javascript because it is a strongly typed superset, which can provide a compilation-time validation of the types, and the IDE (my current favourite is Visual Studio Code) is providing helpful insight when I'm coding.
In case you want to enable typescript, NextJS provide a nice onboarding process. First, create an empty tsconfig.json
file in the root of your project:
touch tsconfig.json
Once created run npm run dev
and follow the instructions to install the dependencies:
npm run dev
# You'll see instructions like these:
#
# Please install typescript, @types/react, and @types/node by running:
#
# yarn add --dev typescript @types/react @types/node
#
# ...
Once the dependencies are installed, on the next run the following configuration files are auto-generated for you tsconfig.json
, next-env.d.ts
. Now you can start converting .js
files to .tsx
use typescript in the project.
Pages Structure
One of the things I really liked on NextJS is the predefined Pages layout.
In a nutshell if you create for example pages/about.tsx
that exports a React component, it will be accessible at /about
.
It also supports pages with dynamic routes. For example, if you create a file called pages/posts/[id].tsx
, then it will be accessible at posts/1
, posts/2
, etc.
In our landing repo the pages structure is the following:
-
index.tsx
- The landing page. -
enterprise/trial.tsx
- Signup form for trial of Kubernetic Enterprise, sent to Netlify Forms. -
payment/checkout.tsx
- Form for payment of Kubernetic Desktop, sent to Stripe. -
payment/success.tsx
- The redirect page from successful Stripe payments.
tsconfig.json
I'm not very fan of fiddling with the tsconfig.json
as I like to keep it as lean as possible, but there is one change I like, adding baseURL
and the corresponding paths (tsconfig reference):
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@components/*": ["components/*"],
"@utils/*": ["utils/*"],
"@styles/*": ["styles/*"]
},
...
}
}
Now your imports are relative to the root directory instead of being relative to current directory. So the index.tsx
file can now be updated to the following:
# OLD import without defined baseUrl:
# import Nav from '../components/nav'
# NEW import with defined baseUrl:
import Nav from '@components/nav'
export default function IndexPage() {
return (
<div>
<Nav />
<div className="py-20">
<h1 className="text-5xl text-center text-accent-1">
Next.js + Tailwind CSS
</h1>
</div>
</div>
)
}
If you get tired of imports always looking like "../" or "./". Or needing to change as you move files, this is a great way to fix that.
Call-To-Action Button (CTA)
The CTA button has a principal download link with a nice-looking dropdown menu that displays option for different OS. I've added a slight shadow on hover and a transition movement of 1-px in 0.3secs which gives the illusion of button popup.
All this is very easy to do with TailWindCSS and it gave me a pixel perfect freedom that I didn't feel before. This is exactly what I was looking for with the utility-first design without entering the here-be-dragons CSS world. Arguably, you could still say it's CSS, but I'd say it's a bit more abstract, and it sits somewhere between CSS and pre-cooked UI frameworks such as Bootstrap or Material UI.
The actual CTAButton can be found here. First create the SVG icons of the CTA button as a separate component components/Icons.tsx
:
export function AppleIcon() {
return (<svg className="fill-current place-self-center align-middle w-4 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 315" version="1.1" >
<g>
<path d="M213.803394,167.030943 C214.2452,214.609646 255.542482,230.442639 256,230.644727 C255.650812,231.761357 249.401383,253.208293 234.24263,275.361446 C221.138555,294.513969 207.538253,313.596333 186.113759,313.991545 C165.062051,314.379442 158.292752,301.507828 134.22469,301.507828 C110.163898,301.507828 102.642899,313.596301 82.7151126,314.379442 C62.0350407,315.16201 46.2873831,293.668525 33.0744079,274.586162 C6.07529317,235.552544 -14.5576169,164.286328 13.147166,116.18047 C26.9103111,92.2909053 51.5060917,77.1630356 78.2026125,76.7751096 C98.5099145,76.3877456 117.677594,90.4371851 130.091705,90.4371851 C142.497945,90.4371851 165.790755,73.5415029 190.277627,76.0228474 C200.528668,76.4495055 229.303509,80.1636878 247.780625,107.209389 C246.291825,108.132333 213.44635,127.253405 213.803394,167.030988 M174.239142,50.1987033 C185.218331,36.9088319 192.607958,18.4081019 190.591988,0 C174.766312,0.636050225 155.629514,10.5457909 144.278109,23.8283506 C134.10507,35.5906758 125.195775,54.4170275 127.599657,72.4607932 C145.239231,73.8255433 163.259413,63.4970262 174.239142,50.1987249"></path>
</g>
</svg>
)
}
export function DropdownIcon() {
return (<svg className="fill-current -mr-1 -ml-1 h-5 w-5 rounded-md" viewBox="0 0 20 20">
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
</svg>
)
}
export function WinIcon() {
return (
<svg className="fill-current w-4 mr-2" xmlns="http://www.w3.org/2000/svg" version="1.1"
viewBox="-2.61977004 -2.61977004 92.56520808 92.83416708">
<path
d="M 0,12.40183 35.68737,7.5416 35.70297,41.96435 0.03321,42.16748 z m 35.67037,33.52906 0.0277,34.45332 -35.66989,-4.9041 -0.002,-29.77972 z M 39.99644,6.90595 87.31462,0 l 0,41.527 -47.31818,0.37565 z M 87.32567,46.25471 87.31457,87.59463 39.9964,80.91625 39.9301,46.17767 z" />
</svg>
)
}
export function LinuxIcon() {
return (
<svg className="fill-current w-4 mr-2" xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 266 312">
<g transform="translate(-3.3359375,285.2793)">
<path d="M132-206c0,1-1,1-1,1h-1c-1,0-1-1-2-2,0,0-1-1-1-2s0-1,1-1l2,1c1,1,2,2,2,3m-18-10c0-5-2-8-5-8,0,0,0,1-1,1v2h3c0,2,1,3,1,5h2m35-5c2,0,3,2,4,5h2c-1-1-1-2-1-3s0-2-1-3-2-2-3-2c0,0-1,1-2,1,0,1,1,1,1,2m-30,16c-1,0-1,0-1-1s0-2,1-3c2,0,3-1,3-1,1,0,1,1,1,1,0,1-1,2-3,4h-1m-11-1c-4-2-5-5-5-10,0-3,0-5,2-7,1-2,3-3,5-3s3,1,5,3c1,3,2,6,2,9v1,1h1v-1c1,0,1-2,1-6,0-3,0-6-2-9s-4-5-8-5c-3,0-6,2-7,5-2,4-2.4,7-2.4,12,0,4,1.4,8,5.4,12,1-1,2-1,3-2m125,141c1,0,1-0.4,1-1.3,0-2.2-1-4.8-4-7.7-3-3-8-4.9-14-5.7-1-0.1-2-0.1-2-0.1-1-0.2-1-0.2-2-0.2-1-0.1-3-0.3-4-0.5,3-9.3,4-17.5,4-24.7,0-10-2-17-6-23s-8-9-13-10c-1,1-1,1-1,2,5,2,10,6,13,12,3,7,4,13,4,20,0,5.6-1,13.9-5,24.5-4,1.6-8,5.3-11,11.1,0,0.9,0,1.4,1,1.4,0,0,1-0.9,2-2.6,2-1.7,3-3.4,5-5.1,3-1.7,5-2.6,8-2.6,5,0,10,0.7,13,2.1,4,1.3,6,2.7,7,4.3,1,1.5,2,2.9,3,4.2,0,1.3,1,1.9,1,1.9m-92-145c-1-1-1-3-1-5,0-4,0-6,2-9,2-2,4-3,6-3,3,0,5,2,7,4,1,3,2,5,2,8,0,5-2,8-6,9,0,0,1,1,2,1,2,0,3,1,5,2,1-6,2-10,2-15,0-6-1-10-3-13-3-3-6-4-10-4-3,0-6,1-9,3-2,3-3,5-3,8,0,5,1,9,3,13,1,0,2,1,3,1m12,16c-13,9-23,13-31,13-7,0-14-3-20-8,1,2,2,4,3,5l6,6c4,4,9,6,14,6,7,0,15-4,25-11l9-6c2-2,4-4,4-7,0-1,0-2-1-2-1-2-6-5-16-8-9-4-16-6-20-6-3,0-8,2-15,6-6,4-10,8-10,12,0,0,1,1,2,3,6,5,12,8,18,8,8,0,18-4,31-14v2c1,0,1,1,1,1m23,202c4,7.52,11,11.3,19,11.3,2,0,4-0.3,6-0.9,2-0.4,4-1.1,5-1.9,1-0.7,2-1.4,3-2.2,2-0.7,2-1.2,3-1.7l17-14.7c4-3.19,8-5.98,13-8.4,4-2.4,8-4,10-4.9,3-0.8,5-2,7-3.6,1-1.5,2-3.4,2-5.8,0-2.9-2-5.1-4-6.7s-4-2.7-6-3.4-4-2.3-7-5c-2-2.6-4-6.2-5-10.9l-1-5.8c-1-2.7-1-4.7-2-5.8,0-0.3,0-0.4-1-0.4s-3,0.9-4,2.6c-2,1.7-4,3.6-6,5.6-1,2-4,3.8-6,5.5-3,1.7-6,2.6-8,2.6-8,0-12-2.2-15-6.5-2-3.2-3-6.9-4-11.1-2-1.7-3-2.6-5-2.6-5,0-7,5.2-7,15.7v3.3,11.6,8.9,4.3,3c0,0.9-1,2.9-1,6-1,3.1-1,6.62-1,10.6l-2,11.1v0.17m-145-5.29c9.3,1.36,20,4.27,32.1,8.71,12.1,4.4,19.5,6.7,22.2,6.7,7,0,12.8-3.1,17.6-9.09,1-1.94,1-4.22,1-6.84,0-9.45-5.7-21.4-17.1-35.9l-6.8-9.1c-1.4-1.9-3.1-4.8-5.3-8.7-2.1-3.9-4-6.9-5.5-9-1.3-2.3-3.4-4.6-6.1-6.9-2.6-2.3-5.6-3.8-8.9-4.6-4.2,0.8-7.1,2.2-8.5,4.1s-2.2,4-2.4,6.2c-0.3,2.1-0.9,3.5-1.9,4.2-1,0.6-2.7,1.1-5,1.6-0.5,0-1.4,0-2.7,0.1h-2.7c-5.3,0-8.9,0.6-10.8,1.6-2.5,2.9-3.8,6.2-3.8,9.7,0,1.6,0.4,4.3,1.2,8.1,0.8,3.7,1.2,6.7,1.2,8.8,0,4.1-1.2,8.2-3.7,12.3-2.5,4.3-3.8,7.5-3.8,9.78,1,3.88,7.6,6.61,19.7,8.21m33.3-90.9c0-6.9,1.8-14.5,5.5-23.5,3.6-9,7.2-15,10.7-19-0.2-1-0.7-1-1.5-1l-1-1c-2.9,3-6.4,10-10.6,20-4.2,9-6.4,17.3-6.4,23.4,0,4.5,1.1,8.4,3.1,11.8,2.2,3.3,7.5,8.1,15.9,14.2l10.6,6.9c11.3,9.8,17.3,16.6,17.3,20.6,0,2.1-1,4.2-4,6.5-2,2.4-4.7,3.6-7,3.6-0.2,0-0.3,0.2-0.3,0.7,0,0.1,1,2.1,3.1,6,4.2,5.7,13.2,8.5,25.2,8.5,22,0,39-9,52-27,0-5,0-8.1-1-9.4v-3.7c0-6.5,1-11.4,3-14.6s4-4.7,7-4.7c2,0,4,0.7,6,2.2,1-7.7,1-14.4,1-20.4,0-9.1,0-16.6-2-23.6-1-6-3-11-5-15-2-3-4-6-6-9s-3-6-5-9c-1-4-2-7-2-12-3-5-5-10-8-15-2-5-4-10-6-14l-9,7c-10,7-18,10-25,10-6,0-11-1-14-5l-6-5c0,3-1,7-3,11l-6.3,12c-2.8,7-4.3,11-4.6,14-0.4,2-0.7,4-0.9,4l-7.5,15c-8.1,15-12.2,28.9-12.2,40.4,0,2.3,0.2,4.7,0.6,7.1-4.5-3.1-6.7-7.4-6.7-13m71.6,94.6c-13,0-23,1.76-30,5.25v-0.3c-5,6-10.6,9.1-18.4,9.1-4.9,0-12.6-1.9-23-5.7-10.5-3.6-19.8-6.36-27.9-8.18-0.8-0.23-2.6-0.57-5.5-1.03-2.8-0.45-5.4-0.91-7.7-1.37-2.1-0.45-4.5-1.13-7.1-2.05-2.5-0.79-4.5-1.82-6-3.07-1.38-1.26-2.06-2.68-2.06-4.27,0-1.6,0.34-3.31,1.02-5.13,0.64-1.1,1.34-2.2,2.04-3.2,0.7-1.1,1.3-2.1,1.7-3.1,0.6-0.9,1-1.8,1.4-2.8,0.4-0.9,0.8-1.8,1-2.9,0.2-1,0.4-2,0.4-3s-0.4-4-1.2-9.3c-0.8-5.2-1.2-8.5-1.2-9.9,0-4.4,1-7.9,3.2-10.4s4.3-3.8,6.5-3.8h11.5c0.9,0,2.3-0.5,4.4-1.7,0.7-1.6,1.3-2.9,1.7-4.1,0.5-1.2,0.7-2.1,0.9-2.5,0.2-0.6,0.4-1.2,0.6-1.7,0.4-0.7,0.9-1.5,1.6-2.3-0.8-1-1.2-2.3-1.2-3.9,0-1.1,0-2.1,0.2-2.7,0-3.6,1.7-8.7,5.3-15.4l3.5-6.3c2.9-5.4,5.1-9.4,6.7-13.4,1.7-4,3.5-10,5.5-18,1.6-7,5.4-14,11.4-21l7.5-9c5.2-6,8.6-11,10.5-15s2.9-9,2.9-13c0-2-0.5-8-1.6-18-1-10-1.5-20-1.5-29,0-7,0.6-12,1.9-17s3.6-10,7-14c3-4,7-8,13-10s13-3,21-3c3,0,6,0,9,1,3,0,7,1,12,3,4,2,8,4,11,7,4,3,7,8,10,13,2,6,4,12,5,20,1,5,1,10,2,17,0,6,1,10,1,13,1,3,1,7,2,12,1,4,2,8,4,11,2,4,4,8,7,12,3,5,7,10,11,16,9,10,16,21,20,32,5,10,8,23,8,36.9,0,6.9-1,13.6-3,20.1,2,0,3,0.8,4,2.2s2,4.4,3,9.1l1,7.4c1,2.2,2,4.3,5,6.1,2,1.8,4,3.3,7,4.5,2,1,5,2.4,7,4.2,2,2,3,4.1,3,6.3,0,3.4-1,5.9-3,7.7-2,2-4,3.4-7,4.3-2,1-6,3-12,5.82-5,2.96-10,6.55-15,10.8l-10,8.51c-4,3.9-8,6.7-11,8.4-3,1.8-7,2.7-11,2.7l-7-0.8c-8-2.1-13-6.1-16-12.2-16-1.94-29-2.9-37-2.9" />
</g>
</svg >
)
}
Once the Icons are created you can create the CTA button components/CTAButton.tsx
:
import Link from 'next/link'
import { useState } from "react"
import { AppleIcon, DropdownIcon, LinuxIcon, WinIcon } from "./Icons"
export default function CTAButton() {
const [isOpen, updateIsOpen] = useState(false)
return (
<>
<div className="inline-flex">
<div className="relative">
<div className="btn-popup inline-flex w-56 divide-x divide-green-600 hover:shadow-lg">
<Link href="https://www.kubernetic.com/">
<button className="btn btn-green inline-flex w-48 rounded-l px-3 py-3 pl-4">
<AppleIcon />
<span>Download for Mac</span>
</button>
</Link>
<button aria-label="choose-os" className="btn btn-green inline-flex transition rounded-r ease-in-out duration-150 px-3 py-3"
onClick={() => updateIsOpen(!isOpen)}>
<DropdownIcon />
</button>
</div>
{isOpen && <DropdownMenu />}
</div>
</div>
</>
)
}
const DropdownMenu = () => (
<div className="absolute">
<ul className="w-56 ml-1 p-2 mt-2 text-gray-600 bg-white border border-gray-100 rounded-lg shadow-md min-w-max-content right-0" aria-label="submenu">
<DropdownMenuItem icon={<WinIcon />} text="Download for Windows" to="https://www.kubernetic.com/" />
<DropdownMenuItem icon={<LinuxIcon />} text="Download for Linux" to="https://www.kubernetic.com/" />
</ul>
</div>
)
type DropdownMenuProps = { icon: any, text: string, to: string }
function DropdownMenuItem({ icon, text, to }: DropdownMenuProps) {
return (
<Link href={to}>
<li>
<a className="inline-flex items-center cursor-pointer w-full px-2 py-2 text-sm font-medium transition-colors duration-150 rounded-md hover:bg-gray-200 hover:text-gray-800" type="button">
{icon}
<span>{text}</span>
</a>
</li>
</Link>
)
}
Lastly, in order to be able to re-use the styling of the buttons elsewhere I've defined it in the index.css
instead of inlining it on each component:
@tailwind base;
/* Write your own custom base styles here */
/* Start purging... */
@tailwind components;
/* Stop purging. */
/* Write your own custom component styles here */
.btn-blue {
@apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
}
/* Start purging... */
@tailwind utilities;
/* Stop purging. */
/* Your own custom utilities */
/* Button */
.btn {
@apply whitespace-no-wrap text-base font-medium items-center justify-center cursor-pointer;
}
.btn:focus {
@apply outline-none shadow-outline;
}
.btn:hover {
@apply shadow-lg;
}
/* Button Popup */
.btn-popup {
@apply transition duration-300 ease-in-out transform;
}
.btn-popup:hover {
@apply -translate-y-px;
}
/* Button Green */
.btn-green {
@apply text-white bg-green-500;
}
.btn-green:focus {
@apply border-green-700;
}
.btn-green:hover {
@apply text-white bg-green-400;
}
.btn-green:active {
@apply bg-green-700;
}
Update your pages/ndex.tsx
to include the CTAButton:
import CTAButton from '@components/CTAButton'
import Nav from '@components/nav'
export default function IndexPage() {
return (
<div>
<Nav />
<div className="py-20">
<h1 className="text-center">
<CTAButton/>
</h1>
</div>
</div>
)
}
You should now be able to see your own CTA Button:
Deploy with Netlify
Previously our landing page was deployed on Google Storage Bucket which was served by Google Cloud CDN. The deployment was done manually, now we use Netlify and can't look back anymore. Here are some features that we currently use:
- Pushes on
master
repository are automatically published on website. - Automatic HTTPS certificates with Let's Encrypt.
- Pushes on
develop
branch are automatically published on dedicated URL. - Other branches and GitHub Pull Requests can also get dedicated URLs.
Netlify supports NextJS out-of-the-box so not much was necessary for the setup. Everything can be version-controlled under the netlify.toml
file (Personally I'm not very fan of the TOML format, and prefer YAML although I know a lot of people don't like it much, but that's just a personal preference), if you have environment variables that are sensitive you can configure them on their UI instead. In our case everything is publishable (yes the Stripe keys are the public ones - don't be sneaky):
[build]
command = "npm run build && npm run export"
publish = "out"
[[plugins]]
package = "netlify-plugin-cache-nextjs"
[context.production.environment]
NEXT_PUBLIC_LICENSESERVER_URL = ...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = ...
[context.staging.environment]
NEXT_PUBLIC_LICENSESERVER_URL = ...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = ...
[context.branch-deploy.environment]
NEXT_PUBLIC_LICENSESERVER_URL = ...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = ...
- The
build
section defines the build command and output directory. - The
plugins
we've setup a cache for NetxtJS to make builds faster. - The
environment
sections define different variables that are injected during build depending the branch. While onmaster
branch we use the production variables, ondevelop
branch we test Stripe integration using their test environment. The same variables are also setup in theenv.[local|production|development|test]
files so that we use them outside of Netlify (e.g. a local running instance).env.local
is not added in the version control so that it can be configured locally by each developer. The variables need to start with prefixNEXT_PUBLIC_
so that they are accessible from the browser itself (NextJS doc).
Of course deployment can also be done with Vercel, the authors of NextJS, there was no particular reason why I chose Netlify instead of Vercel, make sure to check out both before deciding, I'm personally happy with Netlify so I don't have a reason for now to switch.
Optimisations (Lighthouse)
Once deployed I took a bit of time to make sure the site is optimised properly for web, using lighthouse. Once I finished the optimisations here the result was pretty good:
One of the things I noticed there was the images I used on the landing page could be compressed better by serving them in next-gen formats. I then used cwebp CLI for static PNG and ezgif.com for animated PNGs.
Conclusion
NextJS is definitely worth to check it out if you're into React, even if you build a simple landing page it provides a great opinionated way to organize pages and components.
The TailwindCSS utility-first design is really cool, I can now do one-of designs on specific components without the need to name each element class on CSS. Although this should be used with caution in bigger projects, to avoid duplication of designs, thankfully custom components can be easily created and re-used as shown in the CSS file.
Lastly, with the image optimisations I gained 1 sec in First-Contentful-Paint time which can improve sales.
Top comments (4)
Can you elaborate on why you used React for a simple landing page? As far as I can see, the whole page can be built with next to no JS at all.
Certainly, I'm not advocating to use this for any type of landing page, especially if you're specialising on landing pages, starting out or you haven't worked with React yet.
Actually the previous version was exactly that (HTML + JS), but for my case it made sense for the following reasons:
I already know React since Kubernetic desktop client is react+electron based.
Building landing pages is not part of my day job, it is more important to have unified tooling, so reusing React to the landing page is a plus, not an obstacle.
The website is not an SPA, it currently has 4 pages, so creating components to reuse (e.g. Header + Footer) makes is more manageable, and React helps with that, surely there are other templating mechanisms, but then we go back to reason 1.
I didn't explain in detail the Stripe payment, but one of the pages goes to Stripe, I needed a dev environment to test payments before production, this setup provided a dev environment on Netlify to test it with before pushing to production.
The main reason that triggered the refactor was the last one, I needed a way to test the payments with confidence before pushing to production, where money is involved.
The form is calculating if TAX should be added. I live in Spain - so EU rules apply - I check the user country and the VAT ID (EU business TAX ID number), this is done twice (frontend and backend for security) so I needed a way to test all scenarios before pushing to production.
Yeah, I agree with you and I think it's not necessary to use Reactjs for simple webpage like this. What do you think?
I think is a valid use for academic reasons, but I think is an overkill for a landing page, maybe there are more reasons IDK