diff --git a/src/api/api.ts b/src/api/api.ts index 1a97cff..a141c63 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -14,7 +14,9 @@ import { SubscriptionReceipts, SubscriptionStatus, PaymentIntents, - AICompatCategories + AICompatCategories, + AICompats, + AIRequests } from './resources' const api = { @@ -34,7 +36,9 @@ const api = { getSubscriptionReceipt: createMethod(SubscriptionReceipts.createGetRequest), createSubscriptionReceipt: createMethod(SubscriptionReceipts.createRequest), createPaymentIntent: createMethod(PaymentIntents.createRequest), - getAiCompatCategories: createMethod(AICompatCategories.createRequest) + getAiCompatCategories: createMethod(AICompatCategories.createRequest), + getAiCompat: createMethod(AICompats.createRequest), + getAiRequest: createMethod(AIRequests.createRequest), } export type ApiContextValue = typeof api diff --git a/src/api/resources/AICompats.ts b/src/api/resources/AICompats.ts new file mode 100644 index 0000000..803d957 --- /dev/null +++ b/src/api/resources/AICompats.ts @@ -0,0 +1,45 @@ +import routes from "@/routes"; +import { getAuthHeaders } from "../utils"; + +export interface Payload { + data: { + category_id: number; + left_name: string; + left_bday: string; + right_name: string; + right_bday: string; + }; + token: string; +} + +export interface Response { + compat: ICompat; + meta: IMeta; +} + +export interface ICompat { + left_name: string; + left_bday: string; + right_name: string; + right_bday: string; + body: null | string; + body_pending: boolean; + body_check_path: string; +} + +export interface IMeta { + links: { + self: string; + }; +} + +export interface CompatCategory { + id: number; + name: string; +} + +export const createRequest = ({ token, data }: Payload): Request => { + const url = new URL(routes.server.compat()); + const body = JSON.stringify({ data }); + return new Request(url, { method: "POST", headers: getAuthHeaders(token), body }); +}; diff --git a/src/api/resources/AIRequests.ts b/src/api/resources/AIRequests.ts new file mode 100644 index 0000000..dfe1ca8 --- /dev/null +++ b/src/api/resources/AIRequests.ts @@ -0,0 +1,45 @@ +import { getAuthHeaders } from "../utils"; + +export interface Payload { + body_check_path: string; + token: string; +} + +export interface Response { + ai_request: IAiRequest; +} + +export interface IAiRequest { + id: string; + prompt_key: string; + created_at: string; + updated_at: string; + state: string; + is_reused: boolean; + finished_at: string; + job_id: null; + response: IAiResponse; +} + +export interface IAiResponse { + id: string; + inputs: IAiInputs; + created_at: string; + updated_at: string; + body: string; +} + +export interface IAiInputs { + left_name: string; + right_name: string; + stat_value: number; + stat_energy: string; + category_name: string; + left_bdayIso8601: string; + right_bdayIso8601: string; +} + +export const createRequest = ({ body_check_path, token }: Payload): Request => { + const url = new URL(`https://aura.wit.life${body_check_path}`); + return new Request(url, { method: "GET", headers: getAuthHeaders(token) }); +}; diff --git a/src/api/resources/index.ts b/src/api/resources/index.ts index 8e2f524..b5f5677 100644 --- a/src/api/resources/index.ts +++ b/src/api/resources/index.ts @@ -13,3 +13,5 @@ export * as SubscriptionStatus from './UserSubscriptionStatus' export * as SubscriptionReceipts from './UserSubscriptionReceipts' export * as PaymentIntents from './UserPaymentIntents' export * as AICompatCategories from './AICompatCategories' +export * as AICompats from './AICompats' +export * as AIRequests from './AIRequests' diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index f91e151..bdaaa09 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -26,6 +26,7 @@ import FeedbackPage from '../FeedbackPage' import CompatibilityPage from '../Compatibility' import BreathPage from '../BreathPage' import PriceListPage from '../PriceListPage' +import CompatResultPage from '../CompatResultPage' function App(): JSX.Element { const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState(false) @@ -50,6 +51,7 @@ function App(): JSX.Element { } /> } /> } /> + } /> } /> } /> }> diff --git a/src/components/BreathCircle/index.tsx b/src/components/BreathCircle/index.tsx index 1c7247d..3a1748f 100644 --- a/src/components/BreathCircle/index.tsx +++ b/src/components/BreathCircle/index.tsx @@ -1,6 +1,6 @@ import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' -// import routes from '@/routes' +import routes from '@/routes' import styles from './styles.module.css' import { useEffect, useState } from 'react' @@ -10,12 +10,11 @@ function BreathCircle(): JSX.Element { const { t } = useTranslation() const navigate = useNavigate() const [text, setText] = useState(t('')) - const [render, setRender] = useState(false) const [counter, setCounter] = useState(0) + const handleNext = () => navigate(routes.client.compatibility()) useEffect(() => { - // const handleNext = () => navigate(routes.client.compatibility()) Promise.resolve() .then(() => { setText(t('breathIn')) @@ -31,12 +30,11 @@ function BreathCircle(): JSX.Element { }) .then(() => { setCounter((prevState) => prevState + 1) - if (counter === 3) { - // handleNext() + if (counter === 5) { + handleNext() } - setRender((prevState) => !prevState) }) - }, [t, render, navigate, counter]) + }, [counter]) return (
diff --git a/src/components/CompatResultPage/index.tsx b/src/components/CompatResultPage/index.tsx new file mode 100644 index 0000000..320e6f4 --- /dev/null +++ b/src/components/CompatResultPage/index.tsx @@ -0,0 +1,68 @@ +import { useTranslation } from 'react-i18next' +import Title from '../Title' +import styles from './styles.module.css' +import { useSelector } from 'react-redux' +import { selectors } from '@/store' +import { useCallback, useState } from 'react' +import { AICompats, AIRequests, useApi, useApiCall } from '@/api' + + +function CompatResultPage(): JSX.Element { + const token = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjIzNjEyLCJpYXQiOjE2OTM0MTg5MTAsImV4cCI6MTcwMjA1ODkxMCwianRpIjoiNzg5MjkwYWItODg0YS00MGUyLTkyNjEtOWI2OGEyNjkwNmE0IiwiZW1haWwiOiJvdGhlckBleGFtcGxlLmNvbSIsInN0YXRlIjoicHJvdmVuIiwibG9jIjoiZW4iLCJ0eiI6LTI4ODAwLCJ0eXBlIjoiZW1haWwiLCJpc3MiOiJjb20ubGlmZS5hdXJhIn0.J2ocWIv5jKzuKMcwMgWMiNMyGg5qLlMAeln-bQm_9lw' + const { t } = useTranslation() + const api = useApi() + const rightUser = useSelector(selectors.selectRightUser) + const categoryId = useSelector(selectors.selectCategoryId) + const [text, setText] = useState('Loading...') + + const loadData = useCallback(async () => { + const right_bday = typeof rightUser.birthDate === 'string' ? rightUser.birthDate : `${rightUser.birthDate.year}-${rightUser.birthDate.month}-${rightUser.birthDate.day}` + const data: AICompats.Payload = { + data: { + left_name: 'John', + left_bday: '1970-01-01', + right_name: rightUser.name, + right_bday, + category_id: categoryId + }, + token + } + const aICompat = await api.getAiCompat(data) + if (aICompat.compat.body_pending) { + const loadAIRequest = async () => { + const aIRequest = await api.getAiRequest({ + body_check_path: aICompat.compat.body_check_path, + token + }) + if (aIRequest.ai_request.state !== 'ready') { + setTimeout(loadAIRequest, 3000) + } + setText(aIRequest?.ai_request?.response?.body || 'Loading...') + return aIRequest.ai_request + } + return await loadAIRequest() + } + setText(aICompat?.compat?.body || 'Loading...') + + return aICompat.compat + }, [api, rightUser, categoryId]) + + useApiCall(loadData) + + return ( +
+
+ {'46%'} + {t('you_and', {user: rightUser.name})} +
+
+ {t('sign')} +

+ {text} +

+
+
+ ) +} + +export default CompatResultPage diff --git a/src/components/CompatResultPage/styles.module.css b/src/components/CompatResultPage/styles.module.css new file mode 100644 index 0000000..9958151 --- /dev/null +++ b/src/components/CompatResultPage/styles.module.css @@ -0,0 +1,30 @@ +.page { + position: relative; + height: calc(100vh - 50px); + flex: auto; + max-height: -webkit-fill-available; + background-color: #000; + color: #fff; +} + +.title-container { + color: #e9445a; +} + +.percent { + margin-bottom: 0; +} + +.result-container { + width: 100%; +} + +.result-container__title { + width: 100%; + text-align: left; +} + +.result-container__text { + white-space:pre-wrap; + line-height: 1.2; +} \ No newline at end of file diff --git a/src/components/Compatibility/index.tsx b/src/components/Compatibility/index.tsx index add1a12..3cd8eaa 100644 --- a/src/components/Compatibility/index.tsx +++ b/src/components/Compatibility/index.tsx @@ -7,16 +7,31 @@ import NameInput from "./nameInput"; import { DatePicker } from "../DateTimePicker"; import { IDate, getDateAsString } from "@/services/date"; import { AICompatCategories, useApi, useApiCall } from "@/api"; +import { useNavigate } from "react-router-dom"; +import routes from "@/routes"; +import { useDispatch } from "react-redux"; +import { actions } from "@/store"; function CompatibilityPage(): JSX.Element { const { t, i18n } = useTranslation(); + const navigate = useNavigate() + const dispatch = useDispatch() const [isDisabled, setIsDisabled] = useState(true); const [isDisabledName, setIsDisabledName] = useState(true); const [isDisabledDate, setIsDisabledDate] = useState(true); const [name, setName] = useState(''); const [date, setDate] = useState(''); const [compatCategory, setCompatCategory] = useState(2); - const handleNext = () => console.log(name, date, compatCategory); + const handleNext = () => { + dispatch(actions.compatibility.update({ + rightUser: { + name, + birthDate: date + }, + categoryId: compatCategory + })) + navigate(routes.client.compatibilityResult()) + }; const api = useApi() const locale = i18n.language diff --git a/src/components/PriceItem/styles.module.css b/src/components/PriceItem/styles.module.css index b432182..611f93b 100644 --- a/src/components/PriceItem/styles.module.css +++ b/src/components/PriceItem/styles.module.css @@ -9,6 +9,7 @@ border-radius: 10px; border: solid #d6d2d2 2px; font-weight: 600; + cursor: pointer; } .active { diff --git a/src/locales/dev.ts b/src/locales/dev.ts index 2e252e3..aa555fe 100644 --- a/src/locales/dev.ts +++ b/src/locales/dev.ts @@ -76,5 +76,7 @@ export default { should_not_get: " shouldn't get in the way of finding something for you", money: "Money", people_joined_today: " people joined today", + you_and: "You and ", + sign: "Sign", }, } diff --git a/src/routes.ts b/src/routes.ts index 384aadf..cf73899 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -21,6 +21,7 @@ const routes = { static: () => [host, 'static', ':typeId'].join('/'), legal: (type: string) => [host, 'static', type].join('/'), compatibility: () => [host, 'compatibility'].join('/'), + compatibilityResult: () => [host, 'compatibility', 'result'].join('/'), breath: () => [host, 'breath'].join('/'), priceList: () => [host, 'price-list'].join('/'), }, @@ -41,6 +42,7 @@ const routes = { subscriptionReceipts: () => [apiHost, prefix, 'user', 'subscription_receipts.json'].join('/'), subscriptionReceipt: (id: string) => [apiHost, prefix, 'user', 'subscription_receipts', `${id}.json`].join('/'), compatCategories: () => [apiHost, prefix, 'ai', 'compat_categories.json'].join('/'), + compat: () => [apiHost, prefix, 'ai', 'compats.json'].join('/'), }, } @@ -53,6 +55,7 @@ export const entrypoints = [ routes.client.attention(), routes.client.feedback(), routes.client.breath(), + routes.client.compatibilityResult(), ] export const isEntrypoint = (path: string) => entrypoints.includes(path) export const isNotEntrypoint = (path: string) => !isEntrypoint(path) @@ -72,6 +75,7 @@ export const withoutFooterRoutes = [ routes.client.compatibility(), routes.client.breath(), routes.client.priceList(), + routes.client.compatibilityResult(), ] export const hasNoFooter = (path: string) => !withoutFooterRoutes.includes(path) diff --git a/src/store/compatibility.ts b/src/store/compatibility.ts new file mode 100644 index 0000000..7774ea9 --- /dev/null +++ b/src/store/compatibility.ts @@ -0,0 +1,43 @@ +import { IDate } from "@/services/date"; +import { createSlice, createSelector } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; + +interface ICompatibility { + rightUser: IUser; + categoryId: number; +} + +interface IUser { + name: string; + birthDate: string | IDate; +} + +const initialState: ICompatibility = { + rightUser: { + name: "", + birthDate: "", + }, + categoryId: 1, +}; + +const compatibilitySlice = createSlice({ + name: "compatibility", + initialState, + reducers: { + update(state, action: PayloadAction>) { + return { ...state, ...action.payload }; + }, + }, + extraReducers: (builder) => builder.addCase("reset", () => initialState), +}); + +export const { actions } = compatibilitySlice; +export const selectRightUser = createSelector( + (state: { compatibility: ICompatibility }) => state.compatibility.rightUser, + (compatibility) => compatibility +); +export const selectCategoryId = createSelector( + (state: { compatibility: ICompatibility }) => state.compatibility.categoryId, + (compatibility) => compatibility +); +export default compatibilitySlice.reducer; diff --git a/src/store/index.ts b/src/store/index.ts index caa7b88..f9f4187 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -6,12 +6,14 @@ import aura, { actions as auraActions } from './aura' import payment, { actions as paymentActions } from './payment' import subscriptionPlans, { actions as subscriptionPlasActions, selectPlanById } from './subscriptionPlan' import status, { actions as userStatusActions, selectStatus } from './status' +import compatibility, { actions as compatibilityActions } from './compatibility' import { loadStore, backupStore } from './storageHelper' import { selectAuraCoordinates } from './aura' import { selectSelectedPrice } from './payment' +import { selectRightUser, selectCategoryId } from './compatibility' const preloadedState = loadStore() -export const reducer = combineReducers({ token, user, form, status, subscriptionPlans, aura, payment }) +export const reducer = combineReducers({ token, user, form, status, subscriptionPlans, aura, payment, compatibility }) export const actions = { token: tokenActions, user: userActions, @@ -19,6 +21,7 @@ export const actions = { status: userStatusActions, subscriptionPlan: subscriptionPlasActions, aura: auraActions, + compatibility: compatibilityActions, payment: paymentActions, reset: createAction('reset'), } @@ -28,6 +31,8 @@ export const selectors = { selectStatus, selectPlanById, selectAuraCoordinates, + selectRightUser, + selectCategoryId, selectSelectedPrice, ...formSelectors, }