From dba36ae458f182baae134d2740a97308f50e9822 Mon Sep 17 00:00:00 2001 From: Umar Adilov <99314948+adilovcode@users.noreply.github.com> Date: Tue, 29 Apr 2025 00:13:50 +0500 Subject: [PATCH] Introduced API integration with taylor db --- .env.example | 3 + .gitignore | 2 +- package.json | 5 +- pnpm-lock.yaml | 25 ++++++- src/app/api-utlities/@types/index.ts | 50 ++++++++++++++ src/app/api-utlities/presenters/index.ts | 48 +++++++++++++ src/app/api-utlities/requests/common.ts | 69 +++++++++++++++++++ .../requests/main-page.request.ts | 13 ++++ .../api-utlities/utilities/taylor.client.ts | 25 +++++++ src/app/api/pages/main/route.ts | 24 +++++++ tsconfig.json | 3 +- 11 files changed, 261 insertions(+), 6 deletions(-) create mode 100644 .env.example create mode 100644 src/app/api-utlities/@types/index.ts create mode 100644 src/app/api-utlities/presenters/index.ts create mode 100644 src/app/api-utlities/requests/common.ts create mode 100644 src/app/api-utlities/requests/main-page.request.ts create mode 100644 src/app/api-utlities/utilities/taylor.client.ts create mode 100644 src/app/api/pages/main/route.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..22fe331 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +TAYLOR_API_ENDPOINT= +TAYLOR_API_TOKEN= +TAYLOR_MEDIA_URL= \ No newline at end of file diff --git a/.gitignore b/.gitignore index f6813ec..a5d4edc 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,7 @@ yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) -.env* +.env # vercel .vercel diff --git a/package.json b/package.json index 6ffdbf6..8971d74 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "clsx": "^2.1.1", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", + "json-to-graphql-query": "^2.3.0", + "lodash": "^4.17.21", "lucide-react": "^0.501.0", "next": "15.3.1", "next-themes": "^0.4.6", @@ -48,7 +50,8 @@ "@types/eslint": "^9.6.1", "@types/eslint-config-prettier": "^6.11.3", "@types/eslint-plugin-tailwindcss": "^3.17.0", - "@types/node": "^20", + "@types/lodash": "^4.17.16", + "@types/node": "^20.17.30", "@types/react": "^19", "@types/react-dom": "^19", "@typescript-eslint/parser": "^8.30.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f6c3d0..2a28b61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,12 @@ importers: embla-carousel-react: specifier: ^8.6.0 version: 8.6.0(react@19.1.0) + json-to-graphql-query: + specifier: ^2.3.0 + version: 2.3.0 + lodash: + specifier: ^4.17.21 + version: 4.17.21 lucide-react: specifier: ^0.501.0 version: 0.501.0(react@19.1.0) @@ -117,8 +123,11 @@ importers: '@types/eslint-plugin-tailwindcss': specifier: ^3.17.0 version: 3.17.0 + '@types/lodash': + specifier: ^4.17.16 + version: 4.17.16 '@types/node': - specifier: ^20 + specifier: ^20.17.30 version: 20.17.30 '@types/react': specifier: ^19 @@ -1055,6 +1064,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/lodash@4.17.16': + resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==} + '@types/node@20.17.30': resolution: {integrity: sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==} @@ -1921,6 +1933,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-to-graphql-query@2.3.0: + resolution: {integrity: sha512-khZtaLLQ0HllFec+t89ZWduUZ0rmne/OpRm/39hyZUWDHNx9Yk4DgQzDtMeqd8zj2g5opBD4GHrdtH0JzKnN2g==} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -3462,6 +3477,8 @@ snapshots: '@types/json5@0.0.29': {} + '@types/lodash@4.17.16': {} + '@types/node@20.17.30': dependencies: undici-types: 6.19.8 @@ -4025,7 +4042,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.25.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import-x@4.10.6(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-plugin-import@2.31.0)(eslint@9.25.0(jiti@2.4.2)))(eslint@9.25.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -4067,7 +4084,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.25.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.25.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import-x@4.10.6(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint-plugin-import@2.31.0)(eslint@9.25.0(jiti@2.4.2)))(eslint@9.25.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -4515,6 +4532,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-to-graphql-query@2.3.0: {} + json5@1.0.2: dependencies: minimist: 1.2.8 diff --git a/src/app/api-utlities/@types/index.ts b/src/app/api-utlities/@types/index.ts new file mode 100644 index 0000000..705be9f --- /dev/null +++ b/src/app/api-utlities/@types/index.ts @@ -0,0 +1,50 @@ +export type Root = { records: T[] }; + +export interface Image { + url: string; + name: string; +} + +export interface Select { + name: string; +} + +export type Discount = Root<{ + _name: string; + _opisanie: string; + _do: string; + _foto: Image[]; +}>; + +export type Job = Root<{ + id: number; + _name: string; + _type: Select[]; + _localtio: Select[]; + _tags: Select[]; +}>; + +export type Partner = Root<{ + id: number; + _name: string; + _image: Image[]; +}>; + +export type Station = Root<{ + _name: string; + _opisanie: string; + _adress: string; + _chasyRaboty: Select; + _lat: number; + _long: number; + _avtomojka: boolean; + _dtCopy: boolean; + _ai92Copy: boolean; + _ai95Copy: boolean; + _z100Copy: boolean; + _propanCopy: boolean; + _zaryadnayaStanci: boolean; + _miniMarketCop: boolean; + _region: Select; + _foto: Image[]; +}>; diff --git a/src/app/api-utlities/presenters/index.ts b/src/app/api-utlities/presenters/index.ts new file mode 100644 index 0000000..1333fd2 --- /dev/null +++ b/src/app/api-utlities/presenters/index.ts @@ -0,0 +1,48 @@ +import { isEmpty } from 'lodash'; + +import { Discount, Image, Job, Partner, Station } from '../@types'; + +export const presentImage = (images: Image[]) => + isEmpty(images) ? null : `${process.env.TAYLOR_MEDIA_URL}/${images[0].url}`; + +export const presentPartners = (partners: Partner) => + partners.records.map((record) => ({ + name: record._name, + poster: presentImage(record._image), + })); + +export const presentJobs = (jobs: Job) => + jobs.records.map((job) => ({ + name: job._name, + tags: job._tags.map((tag) => tag.name), + location: !isEmpty(job._localtio) ? job._localtio[0].name : null, + type: !isEmpty(job._type) ? job._type[0].name : null, + })); + +export const presentDiscounts = (discounts: Discount) => + discounts.records.map((discount) => ({ + name: discount._name, + description: discount._opisanie, + expiresAt: discount._do, + image: presentImage(discount._foto), + })); + +export const presentStations = (stations: Station) => + stations.records.map((station: any) => ({ + name: station._name, + description: station._opisanie, + address: station._adress, + workingHours: station._chasyRaboty?.name || null, + latitude: station._lat, + longitude: station._long, + carWash: station._avtomojka || false, + ai92: station._dtCopy || false, + ai95: station._ai92Copy || false, + z100: station._ai95Copy || false, + propan: station._z100Copy || false, + electricCharge: station._propanCopy || false, + miniMarket: station._zaryadnayaStanci || false, + toilet: station._miniMarketCop || false, + region: !isEmpty(station._region) ? station._region[0].name : null, + image: presentImage(station._foto), + })); diff --git a/src/app/api-utlities/requests/common.ts b/src/app/api-utlities/requests/common.ts new file mode 100644 index 0000000..f49887a --- /dev/null +++ b/src/app/api-utlities/requests/common.ts @@ -0,0 +1,69 @@ +export const stationsRequest = { + _azs: { + records: { + _name: true, + _opisanie: true, + _adress: true, + _chasyRaboty: { + name: true, + }, + _lat: true, + _long: true, + _avtomojka: true, + _dtCopy: true, // ai92 + _ai92Copy: true, // ai95 + _ai95Copy: true, // z100 + _z100Copy: true, // propan + _propanCopy: true, // electricCharge + _zaryadnayaStanci: true, // miniMarket + _miniMarketCop: true, // toilet + _region: { + name: true, + }, + _foto: { + url: true, + }, + }, + }, +}; + +export const partnersRequest = { + _partners: { + records: { + _name: true, + _image: { + url: true, + }, + }, + }, +}; + +export const jobsRequest = { + _vacancies: { + records: { + _name: true, + _tags: { + name: true, + }, + _type: { + name: true, + }, + _localtio: { + name: true, + }, + }, + }, +}; + +export const discountsRequest = { + _akcii: { + records: { + _name: true, + _opisanie: true, + _do: true, + _foto: { + url: true, + }, + }, + }, +}; diff --git a/src/app/api-utlities/requests/main-page.request.ts b/src/app/api-utlities/requests/main-page.request.ts new file mode 100644 index 0000000..aa7770b --- /dev/null +++ b/src/app/api-utlities/requests/main-page.request.ts @@ -0,0 +1,13 @@ +import { + discountsRequest, + jobsRequest, + partnersRequest, + stationsRequest, +} from './common'; + +export const mainPageRequest = { + ...partnersRequest, + ...jobsRequest, + ...discountsRequest, + ...stationsRequest, +}; diff --git a/src/app/api-utlities/utilities/taylor.client.ts b/src/app/api-utlities/utilities/taylor.client.ts new file mode 100644 index 0000000..7247e4a --- /dev/null +++ b/src/app/api-utlities/utilities/taylor.client.ts @@ -0,0 +1,25 @@ +import { jsonToGraphQLQuery } from 'json-to-graphql-query'; + +export const requestTaylor = async (query: object, variables?: object) => { + const body = JSON.stringify({ + query: jsonToGraphQLQuery({ query }), + variables, + }); + + const response = await fetch(process.env.TAYLOR_API_ENDPOINT || '', { + body, + method: 'POST', + headers: { + Authorization: process.env.TAYLOR_API_TOKEN || '', + 'Content-type': 'application/json', + }, + }); + + const parsedResponse = await response.json(); + + if (parsedResponse.errors) { + throw parsedResponse.errors; + } + + return parsedResponse; +}; diff --git a/src/app/api/pages/main/route.ts b/src/app/api/pages/main/route.ts new file mode 100644 index 0000000..f17286f --- /dev/null +++ b/src/app/api/pages/main/route.ts @@ -0,0 +1,24 @@ +import { + presentDiscounts, + presentJobs, + presentPartners, + presentStations, +} from '@/app/api-utlities/presenters'; +import { mainPageRequest } from '@/app/api-utlities/requests/main-page.request'; +import { requestTaylor } from '@/app/api-utlities/utilities/taylor.client'; + +export async function GET(request: Request) { + const response = await requestTaylor(mainPageRequest); + + return new Response( + JSON.stringify({ + partners: presentPartners(response.data._partners), + jobs: presentJobs(response.data._vacancies), + discounts: presentDiscounts(response.data._akcii), + stations: presentStations(response.data._azs), + }), + { + headers: { 'Content-Type': 'application/json' }, + }, + ); +} diff --git a/tsconfig.json b/tsconfig.json index 73918f4..4f354b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "isolatedModules": true, "jsx": "preserve", "incremental": true, + "types": ["node"], "plugins": [ { "name": "next" @@ -27,5 +28,5 @@ } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules"], }