feat: add compatibility page

This commit is contained in:
gofnnp 2023-08-25 04:01:46 +04:00
parent 32d1dca9b6
commit 32a326c8b9
10 changed files with 348 additions and 8 deletions

View File

@ -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

View 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() });
};

View File

@ -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'

View 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;

View 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

View 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;
}

View File

@ -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

View File

@ -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>

View File

@ -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);

View 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}`;
};