From 3e9cb7f23332269bf22ee74f1e551107922a35ad Mon Sep 17 00:00:00 2001 From: "Aidar Shaikhutdin @makeweb.space" Date: Wed, 10 May 2023 15:53:21 +0600 Subject: [PATCH] feat: add api provider --- src/api/ApiContext.ts | 24 +++++ src/api/api.ts | 24 +++++ src/api/index.ts | 6 ++ src/api/resources/AssetCategories.ts | 28 ++++++ src/api/resources/Assets.ts | 50 ++++++++++ src/api/resources/Auras.ts | 54 +++++++++++ src/api/resources/AuthTokens.ts | 37 +++++++ src/api/resources/Elements.ts | 35 +++++++ src/api/resources/User.ts | 123 ++++++++++++++++++++++++ src/api/resources/UserDailyForecasts.ts | 30 ++++++ src/api/resources/index.ts | 7 ++ src/api/types.ts | 18 ++++ src/api/useApi.ts | 4 + src/api/utils.ts | 28 ++++++ src/auth/AuthContext.ts | 18 +--- src/auth/AuthProvider.tsx | 56 +++-------- src/components/MainButton/index.tsx | 9 +- src/components/MainButton/styles.css | 12 ++- src/init.tsx | 22 ++--- src/routes.ts | 8 +- src/store/token.ts | 2 +- src/store/user.ts | 2 +- src/types.ts | 77 --------------- 23 files changed, 516 insertions(+), 158 deletions(-) create mode 100644 src/api/ApiContext.ts create mode 100644 src/api/api.ts create mode 100644 src/api/index.ts create mode 100644 src/api/resources/AssetCategories.ts create mode 100644 src/api/resources/Assets.ts create mode 100644 src/api/resources/Auras.ts create mode 100644 src/api/resources/AuthTokens.ts create mode 100644 src/api/resources/Elements.ts create mode 100644 src/api/resources/User.ts create mode 100644 src/api/resources/UserDailyForecasts.ts create mode 100644 src/api/resources/index.ts create mode 100644 src/api/types.ts create mode 100644 src/api/useApi.ts create mode 100644 src/api/utils.ts diff --git a/src/api/ApiContext.ts b/src/api/ApiContext.ts new file mode 100644 index 0000000..bccb55c --- /dev/null +++ b/src/api/ApiContext.ts @@ -0,0 +1,24 @@ +import { createContext } from 'react' +import { createMethod } from './utils' +import { + User, + Auras, + Elements, + AuthTokens, + Assets, + AssetCategories, + DailyForecasts, +} from './resources' + +export interface ApiContextValue { + auth: ReturnType> + getElements: ReturnType> + getUser: ReturnType> + updateUser: ReturnType> + getAssets: ReturnType> + getAssetCategories: ReturnType> + getDailyForecasts: ReturnType> + getAuras: ReturnType> +} + +export const ApiContext = createContext({} as ApiContextValue) diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 0000000..a09c175 --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,24 @@ +import { ApiContextValue } from './ApiContext' +import { createMethod } from './utils' +import { + User, + Auras, + Elements, + AuthTokens, + Assets, + AssetCategories, + DailyForecasts, +} from './resources' + +export function createApi(): ApiContextValue { + return { + auth: createMethod(AuthTokens.createRequest), + getElements: createMethod(Elements.createRequest), + getUser: createMethod(User.createGetRequest), + updateUser: createMethod(User.createPatchRequest), + getAssets: createMethod(Assets.createRequest), + getAssetCategories: createMethod(AssetCategories.createRequest), + getDailyForecasts: createMethod(DailyForecasts.createRequest), + getAuras: createMethod(Auras.createRequest), + } +} diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..b1ee7c6 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,6 @@ +export * from './useApi' +export * from './ApiContext' +export * from './api' +export * from './types' +export type { User } from './resources/User' +export type { Payload as SignUpPayload } from './resources/AuthTokens' diff --git a/src/api/resources/AssetCategories.ts b/src/api/resources/AssetCategories.ts new file mode 100644 index 0000000..c7e1dbb --- /dev/null +++ b/src/api/resources/AssetCategories.ts @@ -0,0 +1,28 @@ +import routes from "../../routes" +import { AuthToken } from "../types" +import { getAuthHeaders } from "../utils" + +export interface Payload { + locale: string + token: AuthToken +} + +export interface Response { + asset_categories: AssetCategory[] +} + +export interface AssetCategory { + id: number + name: string + slug: string + path: string +} + +export const createRequest = ({ locale, token }: Payload): Request => { + const url = new URL(routes.server.assetCategories()) + const query = new URLSearchParams({ locale }) + + url.search = query.toString() + + return new Request(url, { method: 'GET', headers: getAuthHeaders(token) }) +} diff --git a/src/api/resources/Assets.ts b/src/api/resources/Assets.ts new file mode 100644 index 0000000..538b738 --- /dev/null +++ b/src/api/resources/Assets.ts @@ -0,0 +1,50 @@ +import routes from "../../routes" +import { AuthToken } from "../types" +import { AssetCategory } from "./AssetCategories" +import { getAuthHeaders } from "../utils" + +export interface Payload { + token: AuthToken + category: string + page?: number + perPage?: number +} + +export interface Response { + asset_category: AssetCategory + assets: Asset[] +} + +export interface Asset { + id: string + url: string + asset_data: AssetData +} + +export interface AssetData { + id: string + storage: string + metadata: AssetMetadata +} + +export interface AssetMetadata { + size: number + width: number + height: number + filename: string + mime_type: string +} + +export const createRequest = ({ token, category, page, perPage }: Payload): Request => { + const url = new URL(routes.server.assets(category)) + const query = new URLSearchParams() + + if (page && perPage) { + query.append('page', page.toString()) + query.append('per_page', perPage.toString()) + } + + url.search = query.toString() + + return new Request(url, { method: 'GET', headers: getAuthHeaders(token) }) +} diff --git a/src/api/resources/Auras.ts b/src/api/resources/Auras.ts new file mode 100644 index 0000000..9bbd89d --- /dev/null +++ b/src/api/resources/Auras.ts @@ -0,0 +1,54 @@ +import routes from "../../routes" +import { AuthToken } from "../types" +import { getAuthHeaders } from "../utils" + +export interface Payload { + token: AuthToken +} + +export interface Response { + user_aura: UserAura +} + +export interface UserAura { + updated_at: string + viewed_at: string | null + aurapic: string | null + stats: UserAuraStat[] + config: UserAuraConfig +} + +export interface UserAuraStat { + stat: string + value: number + label: string +} + +export interface UserAuraConfig { + birthRate: number + imageURL: string + particleIntensity: number + particleIntensityVariation: number + particleLifeSpan: number + particleSize: number + particleSizeVariation: number + particleVelocity: number + speedFactor: number + spreadingAngle: number + stretchFactor: number + holes: [{ + from: number + to: number + }] + animations: { + [key: string]: { + keyTimes: number[] + values: number[] + } + } +} + +export const createRequest = ({ token }: Payload): Request => { + const url = new URL(routes.server.auras()) + return new Request(url, { method: 'GET', headers: getAuthHeaders(token) }) +} diff --git a/src/api/resources/AuthTokens.ts b/src/api/resources/AuthTokens.ts new file mode 100644 index 0000000..59c9300 --- /dev/null +++ b/src/api/resources/AuthTokens.ts @@ -0,0 +1,37 @@ +import routes from "../../routes" +import { AuthToken } from "../types" +import { User } from "./User" +import { getBaseHeaders } from "../utils" + +export interface Payload { + email: string + timezone: string + locale: string +} + +export interface Response { + auth: { + token: AuthToken + payload: JwtPayload + user: User + } +} + +export interface JwtPayload { + sub: number + email: string + loc: string + tz: number + state: string + iat: number + exp: number + jti: string + type: string + iss: string +} + +export const createRequest = ({ locale, timezone, email }: Payload): Request => { + const url = new URL(routes.server.token()) + const body = JSON.stringify({ auth: { locale, timezone, email }}) + return new Request(url, { method: 'POST', headers: getBaseHeaders(), body }) +} diff --git a/src/api/resources/Elements.ts b/src/api/resources/Elements.ts new file mode 100644 index 0000000..ef4ff22 --- /dev/null +++ b/src/api/resources/Elements.ts @@ -0,0 +1,35 @@ +import routes from "../../routes" +import { getBaseHeaders } from "../utils" + +export interface Payload { + locale: string +} + +export interface Response { + data: { + variant: string + groups: ElementGroup[] + } +} + +export interface ElementGroup { + name: string + items: ElementGroupItem[] +} + +export interface ElementGroupItem { + type: string + href: string + title: string + url_slug: string + [key: string]: string +} + +export const createRequest = ({ locale }: Payload): Request => { + const url = new URL(routes.server.elements()) + const query = new URLSearchParams({ locale }) + + url.search = query.toString() + + return new Request(url, { method: 'GET', headers: getBaseHeaders() }) +} diff --git a/src/api/resources/User.ts b/src/api/resources/User.ts new file mode 100644 index 0000000..dda2cb8 --- /dev/null +++ b/src/api/resources/User.ts @@ -0,0 +1,123 @@ +import routes from "../../routes" +import { AuthToken } from "../types" +import { getAuthHeaders } from "../utils" + +export interface GetPayload { + token: AuthToken +} + +export interface PatchPayload extends GetPayload { + user: Partial +} + +export interface Response { + user: User + meta?: { + links: { + self: string + } + } +} + +export interface UserPatch { + locale: string + timezone: string + profile_attributes: Partial & { + birthplace_id: null + birthplace_attributes: { + address?: string + coords?: string + }, + remote_userpic_url: string + }> + daily_push_subs_attributes: [{ + time: string + daily_push_id: string + }] +} + +export interface User { + id: string | null | undefined + username: string | null + email: string + locale: string + state: string + timezone: string + new_registration: boolean + stat: { + last_online_at: string | null + prev_online_at: string | null + } + profile: UserProfile + daily_push_subs: Subscription[] +} + +export interface UserProfile { + full_name: string | null + gender: string | null + birthday: string | null + birthplace: UserBirhplace | null + age: UserAge | null + sign: UserSign | null + userpic: UserPic | null + userpic_mime_type: string | undefined + relationship_status: string + human_relationship_status: string +} + +export interface UserAge { + years: number + days: number +} + +export interface UserSign { + house: number + ruler: string + dates: { + start: { + month: number + day: number + } + end: { + month: number + day: number + } + } + sign: string + char: string + polarity: string + modality: string + triplicity: string +} + +export interface UserPic { + th: string + th2x: string + lg: string +} + +export interface UserBirhplace { + id: string + address: string + coords: string +} + +export interface Subscription { + id: string + daily_push_id: string + time: string + updated_at: string + created_at: string + last_sent_at: string | null +} + +export const createGetRequest = ({ token }: GetPayload): Request => { + const url = new URL(routes.server.user()) + return new Request(url, { method: 'GET', headers: getAuthHeaders(token) }) +} + +export const createPatchRequest = ({ token, user }: PatchPayload): Request => { + const url = new URL(routes.server.user()) + const body = JSON.stringify({ user }) + return new Request(url, { method: 'PATCH', headers: getAuthHeaders(token), body }) +} diff --git a/src/api/resources/UserDailyForecasts.ts b/src/api/resources/UserDailyForecasts.ts new file mode 100644 index 0000000..b511fdd --- /dev/null +++ b/src/api/resources/UserDailyForecasts.ts @@ -0,0 +1,30 @@ +import routes from "../../routes" +import { AuthToken } from "../types" +import { getAuthHeaders } from "../utils" + +export interface Payload { + token: AuthToken +} + +export interface Response { + user_daily_forecast: UserDailyForecast +} + +export interface UserDailyForecast { + day: string + updated_at: string + viewed_at: string | null + sign_id: number + forecasts: Forecast[] +} + +export interface Forecast { + category_name: string + category: string + body: string +} + +export const createRequest = ({ token }: Payload): Request => { + const url = new URL(routes.server.dailyForecasts()) + return new Request(url, { method: 'GET', headers: getAuthHeaders(token) }) +} diff --git a/src/api/resources/index.ts b/src/api/resources/index.ts new file mode 100644 index 0000000..b1519cd --- /dev/null +++ b/src/api/resources/index.ts @@ -0,0 +1,7 @@ +export * as Assets from './Assets' +export * as AssetCategories from './AssetCategories' +export * as User from './User' +export * as DailyForecasts from './UserDailyForecasts' +export * as Auras from './Auras' +export * as Elements from './Elements' +export * as AuthTokens from './AuthTokens' diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..cc83771 --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,18 @@ +export interface AuthError { + title: string + detail: string + source: { + pointer: string + parameter: string + } +} + +export interface ApiError { + base: string[] +} + +export interface ErrorResponse { + errors: AuthError[] | ApiError +} + +export type AuthToken = string diff --git a/src/api/useApi.ts b/src/api/useApi.ts new file mode 100644 index 0000000..706e0c5 --- /dev/null +++ b/src/api/useApi.ts @@ -0,0 +1,4 @@ +import { useContext } from 'react' +import { ApiContext } from './ApiContext' + +export const useApi = () => useContext(ApiContext) diff --git a/src/api/utils.ts b/src/api/utils.ts new file mode 100644 index 0000000..f9cf60b --- /dev/null +++ b/src/api/utils.ts @@ -0,0 +1,28 @@ +import { AuthToken } from "./types" +import { ErrorResponse } from "./types" + +export function createMethod(createRequest: (payload: P) => Request) { + return async (payload: P): Promise => { + const request = createRequest(payload) + const response = await fetch(request) + const data: R & ErrorResponse = await response.json() + + if (response.ok) return data + + const error = Array.isArray(data.errors) ? data.errors[0]?.title : data.errors.base[0] + + throw new Error(error) + } +} + +export function getBaseHeaders(): Headers { + return new Headers({ + 'Content-Type': 'application/json', + }) +} + +export function getAuthHeaders(token: AuthToken): Headers { + const headers = getBaseHeaders() + headers.append('Authorization', `Bearer ${token}`) + return headers +} diff --git a/src/auth/AuthContext.ts b/src/auth/AuthContext.ts index c47592c..528b138 100644 --- a/src/auth/AuthContext.ts +++ b/src/auth/AuthContext.ts @@ -1,5 +1,5 @@ import { createContext } from 'react' -import { AuthToken, User } from '../types' +import { AuthToken, User, SignUpPayload } from '../api' export interface AuthContextValue { user: User | null @@ -9,18 +9,4 @@ export interface AuthContextValue { addBirthday: (birthday: string, token: AuthToken) => Promise } -export interface SignUpPayload { - email: string - timezone: string - locale: string -} - -const initialContext: AuthContextValue = { - token: '', - user: null, - logout: () => undefined, - signUp: () => Promise.resolve(''), - addBirthday: () => Promise.resolve(), -} - -export const AuthContext = createContext(initialContext) +export const AuthContext = createContext({} as AuthContextValue) diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx index ce9bd58..63a33e3 100644 --- a/src/auth/AuthProvider.tsx +++ b/src/auth/AuthProvider.tsx @@ -1,57 +1,23 @@ import { useDispatch, useSelector } from 'react-redux' -import { AuthContext, SignUpPayload } from './AuthContext' import { RootState, actions } from '../store' -import { AuthToken } from '../types' -import routes from '../routes' +import { useApi, AuthToken, SignUpPayload } from '../api' +import { AuthContext } from './AuthContext' export function AuthProvider({ children }: React.PropsWithChildren): JSX.Element { + const api = useApi() const dispatch = useDispatch() const token = useSelector((state: RootState) => state.token) const user = useSelector((state: RootState) => state.user) - const signUp = async ({ email, timezone, locale }: SignUpPayload): Promise => { - const response = await fetch(routes.server.token(), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ auth: { email, timezone, locale } }) - }) - if (response.ok) { - const { auth: { token, user } } = await response.json() - dispatch(actions.token.update(token)) - dispatch(actions.user.update(user)) - return token - } else { - const { errors } = await response.json() - if (Array.isArray(errors)) { - const [error] = errors - throw new Error(error.title) - } else { - const { base: [error] } = errors - throw new Error(error) - } - } + const signUp = async (payload: SignUpPayload): Promise => { + const { auth: { token, user } } = await api.auth(payload) + dispatch(actions.token.update(token)) + dispatch(actions.user.update(user)) + return token } const addBirthday = async (birthday: string, token: AuthToken): Promise => { - const response = await fetch(routes.server.user(), { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify({ user: { profile_attributes: { birthday } } }) - }) - if (response.ok) { - const { user } = await response.json() - dispatch(actions.user.update(user)) - } else { - const { errors } = await response.json() - if (Array.isArray(errors)) { - const [error] = errors - throw new Error(error.title) - } else { - const { base: [error] } = errors - throw new Error(error) - } - } + const payload = { user: { profile_attributes: { birthday } }, token } + const { user } = await api.updateUser(payload) + dispatch(actions.user.update(user)) } const logout = () => dispatch(actions.reset()) const auth = { signUp, logout, addBirthday, token, user: user.id ? user : null } diff --git a/src/components/MainButton/index.tsx b/src/components/MainButton/index.tsx index f933479..ea77564 100644 --- a/src/components/MainButton/index.tsx +++ b/src/components/MainButton/index.tsx @@ -1,10 +1,13 @@ import './styles.css' -type ButtonProps = React.ButtonHTMLAttributes; +type ButtonProps = React.ButtonHTMLAttributes & { + color?: 'black' | 'blue' +}; -function MainButton({ className, children, ...props}: ButtonProps): JSX.Element { - const combinedClassNames = ['main-btn', className].filter(Boolean).join(' ') +function MainButton({ className, children, color, ...props}: ButtonProps): JSX.Element { + const colorClass = color ? `main-btn--${color}` : 'main-btn--black' + const combinedClassNames = ['main-btn', colorClass, className].filter(Boolean).join(' ') return } diff --git a/src/components/MainButton/styles.css b/src/components/MainButton/styles.css index c5779ff..68fd81d 100644 --- a/src/components/MainButton/styles.css +++ b/src/components/MainButton/styles.css @@ -1,9 +1,7 @@ .main-btn { align-items: center; - background: #000; border: none; border-radius: 20px; - color: #fff; cursor: pointer; display: flex; font-size: 18px; @@ -22,3 +20,13 @@ .main-btn:disabled { opacity: 50%; } + +.main-btn--black { + background: #000; + color: #fff; +} + +.main-btn--blue { + background: #306ed7; + color: #fff; +} \ No newline at end of file diff --git a/src/init.tsx b/src/init.tsx index bfb2028..ecb745f 100644 --- a/src/init.tsx +++ b/src/init.tsx @@ -1,30 +1,30 @@ import React from 'react' -import { BrowserRouter } from 'react-router-dom' import i18next from 'i18next' +import { BrowserRouter } from 'react-router-dom' import { I18nextProvider, initReactI18next } from 'react-i18next' import { Provider } from 'react-redux' import { store } from './store' -import resources from './locales' -import routes from './routes' import { AuthProvider } from './auth' +import { ApiContext, createApi } from './api' +import resources, { getClientLocale } from './locales' import App from './components/App' const init = async () => { - const response = await fetch(routes.server.translations()) - const data = await response.json() - const defaultLanguage = data.meta.locale - // TODO: add translations from data.translations + const api = createApi() + const lng = getClientLocale() const i18nextInstance = i18next.createInstance() - const options = { lng: defaultLanguage, resources } + const options = { lng, resources } await i18nextInstance.use(initReactI18next).init(options) return ( - - - + + + + + diff --git a/src/routes.ts b/src/routes.ts index eaad6b8..a3fd361 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -10,13 +10,17 @@ const routes = { emailEnter: () => [host, 'email'].join('/'), subscription: () => [host, 'subscription'].join('/'), createProfile: () => [host, 'profile', 'create'].join('/'), + paymentMethod: () => [host, 'payment', 'method'].join('/'), + wallpaper: () => [host, 'wallpaper'].join('/'), }, server: { - locales: () => [apiHost, prefix, 'locales.json'].join('/'), - translations: () => [apiHost, prefix, 't.json'].join('/'), elements: () => [apiHost, prefix, 'elements.json'].join('/'), user: () => [apiHost, prefix, 'user.json'].join('/'), token: () => [apiHost, prefix, 'auth', 'token.json'].join('/'), + assets: (category: string) => [apiHost, prefix, 'assets', 'categories', `${category}.json`].join('/'), + assetCategories: () => [apiHost, prefix, 'assets', 'categories.json'].join('/'), + dailyForecasts: () => [apiHost, prefix, 'user', 'daily_forecasts.json'].join('/'), + auras: () => [apiHost, prefix, 'user', 'aura.json'].join('/'), }, } diff --git a/src/store/token.ts b/src/store/token.ts index c5b17b5..2eb3941 100644 --- a/src/store/token.ts +++ b/src/store/token.ts @@ -1,6 +1,6 @@ import { createSlice } from '@reduxjs/toolkit' import type { PayloadAction } from '@reduxjs/toolkit' -import type { AuthToken } from '../types' +import type { AuthToken } from '../api' const initialState: AuthToken = '' diff --git a/src/store/user.ts b/src/store/user.ts index 08bbc9a..41f9547 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -1,6 +1,6 @@ import { createSlice } from '@reduxjs/toolkit' import type { PayloadAction } from '@reduxjs/toolkit' -import type { User } from '../types' +import type { User } from '../api' import { getClientLocale, getClientTimezone } from '../locales' const initialState: User = { diff --git a/src/types.ts b/src/types.ts index a9fd553..d84a30d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,80 +12,3 @@ export interface SignupForm { birthdate: string birthtime: string } - -export type AuthToken = string - -export interface User { - id: string | null | undefined - username: string | null - email: string - locale: string - state: string - timezone: string - new_registration: boolean - stat: { - last_online_at: string | null - prev_online_at: string | null - } - profile: UserProfile - daily_push_subs: Subscription[] -} - -export interface UserProfile { - full_name: string | null - gender: string | null - birthday: string | null - birthplace: UserBirhplace | null - age: UserAge | null - sign: UserSign | null - userpic: UserPic | null - userpic_mime_type: string | undefined - relationship_status: string - human_relationship_status: string -} - -export interface UserAge { - years: number - days: number -} - -export interface UserSign { - house: number - ruler: string - dates: { - start: { - month: number - day: number - } - end: { - month: number - day: number - } - } - sign: string - char: string - polarity: string - modality: string - triplicity: string -} - -export interface UserPic { - th: string - th2x: string - lg: string -} - -export interface UserBirhplace { - id: string - address: string - coords: string -} - -export interface Subscription { - id: string - daily_push_id: string - time: string - updated_at: string - created_at: string - last_sent_at: string | null -}