feat: add compatibility page
This commit is contained in:
parent
32d1dca9b6
commit
32a326c8b9
@ -13,7 +13,8 @@ import {
|
||||
SubscriptionCheckout,
|
||||
SubscriptionReceipts,
|
||||
SubscriptionStatus,
|
||||
PaymentIntents
|
||||
PaymentIntents,
|
||||
AICompatCategories
|
||||
} from './resources'
|
||||
|
||||
const api = {
|
||||
@ -32,7 +33,8 @@ const api = {
|
||||
getSubscriptionStatus: createMethod<SubscriptionStatus.Payload, SubscriptionStatus.Response>(SubscriptionStatus.createRequest),
|
||||
getSubscriptionReceipt: createMethod<SubscriptionReceipts.GetPayload, SubscriptionReceipts.Response>(SubscriptionReceipts.createGetRequest),
|
||||
createSubscriptionReceipt: createMethod<SubscriptionReceipts.Payload, SubscriptionReceipts.Response>(SubscriptionReceipts.createRequest),
|
||||
createPaymentIntent: createMethod<PaymentIntents.Payload, PaymentIntents.Response>(PaymentIntents.createRequest)
|
||||
createPaymentIntent: createMethod<PaymentIntents.Payload, PaymentIntents.Response>(PaymentIntents.createRequest),
|
||||
getAiCompatCategories: createMethod<AICompatCategories.Payload, AICompatCategories.Response>(AICompatCategories.createRequest)
|
||||
}
|
||||
|
||||
export type ApiContextValue = typeof api
|
||||
|
||||
24
src/api/resources/AICompatCategories.ts
Normal file
24
src/api/resources/AICompatCategories.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import routes from "@/routes";
|
||||
import { getBaseHeaders } from "../utils";
|
||||
|
||||
export interface Payload {
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
compat_categories: CompatCategory[]
|
||||
}
|
||||
|
||||
export interface CompatCategory {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export const createRequest = ({ locale }: Payload): Request => {
|
||||
const url = new URL(routes.server.compatCategories());
|
||||
const query = new URLSearchParams({ locale });
|
||||
|
||||
url.search = query.toString();
|
||||
|
||||
return new Request(url, { method: "GET", headers: getBaseHeaders() });
|
||||
};
|
||||
@ -12,3 +12,4 @@ export * as SubscriptionCheckout from './UserSubscriptionCheckout'
|
||||
export * as SubscriptionStatus from './UserSubscriptionStatus'
|
||||
export * as SubscriptionReceipts from './UserSubscriptionReceipts'
|
||||
export * as PaymentIntents from './UserPaymentIntents'
|
||||
export * as AICompatCategories from './AICompatCategories'
|
||||
|
||||
102
src/components/Compatibility/index.tsx
Normal file
102
src/components/Compatibility/index.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import MainButton from "../MainButton";
|
||||
import Title from "../Title";
|
||||
import styles from "./styles.module.css";
|
||||
import { useCallback, useState } from "react";
|
||||
import NameInput from "./nameInput";
|
||||
import { DatePicker } from "../DateTimePicker";
|
||||
import { IDate, getDateAsString } from "@/services/date";
|
||||
import { AICompatCategories, useApi, useApiCall } from "@/api";
|
||||
|
||||
function CompatibilityPage(): JSX.Element {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
const [name, setName] = useState<string>('');
|
||||
const [date, setDate] = useState<string | IDate>('');
|
||||
const [compatCategory, setCompatCategory] = useState(2);
|
||||
const handleNext = () => console.log(name, date, compatCategory);
|
||||
|
||||
const api = useApi()
|
||||
const locale = i18n.language
|
||||
const loadData = useCallback(() => {
|
||||
return api.getAiCompatCategories({ locale })
|
||||
.then((resp: AICompatCategories.Response) => resp.compat_categories)
|
||||
}, [api, locale])
|
||||
const { data } = useApiCall<AICompatCategories.CompatCategory[]>(loadData)
|
||||
|
||||
const handleValidName = (name: string) => {
|
||||
setIsDisabled(name === '');
|
||||
setName(name)
|
||||
}
|
||||
|
||||
const handleValidDate = (date: IDate | string) => {
|
||||
setIsDisabled(date === '');
|
||||
setDate(date)
|
||||
}
|
||||
|
||||
const changeCompatCategory = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCompatCategory(parseInt(event.target.value))
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<section className={`${styles.page} page`}>
|
||||
<Title variant="h1" className={styles.title}>
|
||||
{t("compatibility")}
|
||||
</Title>
|
||||
<div className={styles.content}>
|
||||
<Title variant="h2" className={styles.iam}>
|
||||
{t("iAm")}
|
||||
</Title>
|
||||
<Title variant="h3" className={styles.plus}>
|
||||
+
|
||||
</Title>
|
||||
<div className={styles['inputs-container']}>
|
||||
<div className={styles['input-container__name-container']}>
|
||||
<NameInput
|
||||
name="name"
|
||||
value={name}
|
||||
placeholder={t('name')}
|
||||
onValid={handleValidName}
|
||||
onInvalid={() => setIsDisabled(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['input-container__date-container']}>
|
||||
<DatePicker
|
||||
name='birthdate'
|
||||
value={getDateAsString(date)}
|
||||
inputClassName={styles['date-input']}
|
||||
onValid={handleValidDate}
|
||||
onInvalid={() => setIsDisabled(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{data && data.length && (
|
||||
<div className={styles['compatibility-categories']}>
|
||||
{
|
||||
data.map((item, index) => (
|
||||
<div className="compatibility-categories__item" key={index}>
|
||||
<input className={`${styles["compatibility-categories__input"]} ${compatCategory === item.id ? styles["compatibility-categories__input--checked"] : ''}`}
|
||||
type="radio"
|
||||
name="compatCategory"
|
||||
id={String(item.id)}
|
||||
value={item.id}
|
||||
checked={compatCategory === item.id}
|
||||
onChange={changeCompatCategory} />
|
||||
<label htmlFor={String(item.id)}>
|
||||
{item.name}
|
||||
</label>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
<MainButton className={styles['check-btn']} onClick={handleNext} disabled={isDisabled}>
|
||||
{t("check")}
|
||||
</MainButton>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default CompatibilityPage;
|
||||
37
src/components/Compatibility/nameInput.tsx
Normal file
37
src/components/Compatibility/nameInput.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { FormField } from '@/types'
|
||||
import styles from './styles.module.css'
|
||||
|
||||
const isValidName = (name: string) => {
|
||||
return name.length > 0 && name.length < 30
|
||||
}
|
||||
|
||||
function NameInput(props: FormField<string>): JSX.Element {
|
||||
const { name, value, placeholder, onValid, onInvalid } = props
|
||||
const [userName, setUserName] = useState(value)
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserName(event.target.value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isValidName(userName)) {
|
||||
onValid(userName)
|
||||
} else {
|
||||
onInvalid()
|
||||
}
|
||||
}, [userName, onInvalid, onValid])
|
||||
|
||||
return (
|
||||
<div className={styles['name-input-container']}>
|
||||
<input
|
||||
name={name}
|
||||
type="text"
|
||||
value={userName}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder ?? ' '}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NameInput
|
||||
154
src/components/Compatibility/styles.module.css
Normal file
154
src/components/Compatibility/styles.module.css
Normal file
@ -0,0 +1,154 @@
|
||||
.page {
|
||||
position: relative;
|
||||
min-height: calc(100vh - 0px);
|
||||
max-height: -webkit-fill-available;
|
||||
flex: auto;
|
||||
background-color: #121212;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.iam,
|
||||
.plus {
|
||||
color: #ea445a;
|
||||
}
|
||||
|
||||
.iam {
|
||||
font-weight: 500;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 0;
|
||||
/* font-size: 24px; */
|
||||
}
|
||||
|
||||
.plus {
|
||||
font-size: 48px;
|
||||
font-weight: 100;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.name-input-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.input-container__date-container {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.name-input-container > input {
|
||||
background-color: #595452;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: solid 2px #ea445a;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.name-input-container > input:focus {
|
||||
border-color: #066fde;
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
.date-input > h3 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.date-input input {
|
||||
background-color: #595452;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: solid 2px #ea445a;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.date-input p {
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #83807e;
|
||||
}
|
||||
|
||||
.check-btn {
|
||||
border-radius: 30px;
|
||||
background-color: #ea445a;
|
||||
max-width: 200px;
|
||||
margin: 48px auto;
|
||||
}
|
||||
|
||||
.compatibility-categories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
margin-top: 72px;
|
||||
}
|
||||
|
||||
.compatibility-categories__input--checked,
|
||||
.compatibility-categories__input {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
.compatibility-categories__input--checked + label,
|
||||
.compatibility-categories__input + label {
|
||||
position: relative;
|
||||
padding-left: 48px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
line-height: 24px;
|
||||
display: inline-block;
|
||||
color: #fff;
|
||||
}
|
||||
.compatibility-categories__input--checked + label:before,
|
||||
.compatibility-categories__input + label:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 100%;
|
||||
}
|
||||
.compatibility-categories__input--checked + label:after,
|
||||
.compatibility-categories__input + label:after {
|
||||
content: "";
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-image: url(/check-mark-1.png);
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 6px;
|
||||
-webkit-transition: all 0.2s ease;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.compatibility-categories__input + label:after {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
.compatibility-categories__input--checked + label:after {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
.compatibility-categories__input--checked + label::before {
|
||||
background-color: #ea445a;
|
||||
border-color: #ea445a;
|
||||
}
|
||||
@ -10,7 +10,7 @@ type DateInputProps = Omit<FormField<DatePartValue>, 'onValid' | 'onInvalid'> &
|
||||
}
|
||||
|
||||
function DateInput(props: DateInputProps): JSX.Element {
|
||||
const { label, placeholder, name, value = '', max, maxLength, onChange } = props
|
||||
const { label, placeholder, name, inputClassName, value = '', max, maxLength, onChange } = props
|
||||
const validate = (value: number): boolean => value >= 0 && value <= max
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const datePart = e.target.value ? parseInt(e.target.value, 10) : 0
|
||||
@ -20,7 +20,7 @@ function DateInput(props: DateInputProps): JSX.Element {
|
||||
onChange(datePart > 0 ? normalize(datePart, maxLength) : '')
|
||||
}
|
||||
return (
|
||||
<div className="date-picker__field">
|
||||
<div className={`date-picker__field ${inputClassName ?? ''}`}>
|
||||
<h3 className='date-picker__field-label'>{label}</h3>
|
||||
<label className="date-picker__input">
|
||||
<input
|
||||
|
||||
@ -4,11 +4,13 @@ import { FormField } from '@/types'
|
||||
import DateInput from './DateInput'
|
||||
import ErrorText from '../ErrorText'
|
||||
import { stringify, getCurrentYear } from './utils'
|
||||
import { IDate, getDateAsString } from '@/services/date'
|
||||
|
||||
export function DatePicker(props: FormField<string>): JSX.Element {
|
||||
export function DatePicker(props: FormField<Date | IDate | string>): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const { name, value, onValid, onInvalid } = props
|
||||
const [initYear, initMonth, initDay] = value.split('-')
|
||||
const { name, value, inputClassName, onValid, onInvalid } = props
|
||||
const date = getDateAsString(value)
|
||||
const [initYear, initMonth, initDay] = date.split('-')
|
||||
const [year, setYear] = useState(initYear)
|
||||
const [month, setMonth] = useState(initMonth)
|
||||
const [day, setDay] = useState(initDay)
|
||||
@ -42,6 +44,7 @@ export function DatePicker(props: FormField<string>): JSX.Element {
|
||||
maxLength={4}
|
||||
label={t('year')}
|
||||
placeholder='YYYY'
|
||||
inputClassName={inputClassName}
|
||||
onChange={(year: string) => setYear(year)}
|
||||
/>
|
||||
<DateInput
|
||||
@ -51,6 +54,7 @@ export function DatePicker(props: FormField<string>): JSX.Element {
|
||||
maxLength={2}
|
||||
label={t('month')}
|
||||
placeholder='MM'
|
||||
inputClassName={inputClassName}
|
||||
onChange={(month: string) => setMonth(month)}
|
||||
/>
|
||||
<DateInput
|
||||
@ -60,6 +64,7 @@ export function DatePicker(props: FormField<string>): JSX.Element {
|
||||
maxLength={2}
|
||||
label={t('day')}
|
||||
placeholder='DD'
|
||||
inputClassName={inputClassName}
|
||||
onChange={(day: string) => setDay(day)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
.date-picker__container {
|
||||
grid-gap: 12px;
|
||||
background-color: #fff;
|
||||
background-color: transparent;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(3,1fr);
|
||||
|
||||
15
src/services/date/index.ts
Normal file
15
src/services/date/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export interface IDate {
|
||||
year: string;
|
||||
month: string;
|
||||
day: string;
|
||||
}
|
||||
|
||||
export const getDateAsString = (date: Date | IDate | string): string => {
|
||||
if (date instanceof Date) {
|
||||
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
|
||||
}
|
||||
if (typeof date === 'string') {
|
||||
return date;
|
||||
}
|
||||
return `${date.year}-${date.month}-${date.day}`;
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user