diff --git a/src/api/backend.ts b/src/api/backend.ts new file mode 100644 index 0000000..38b4507 --- /dev/null +++ b/src/api/backend.ts @@ -0,0 +1,122 @@ +import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; +import { getTokenCookie } from "@utils/auth"; + +export const HOST = `${process.env.NEXT_PUBLIC_API_URL}`; + +const axiosInstance: AxiosInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, +}); + +export enum APIPath { + TAGS = "/tags/:id", + EVENTS = "/events/:id", + FEED = "/feed/:id", + JOBADS = "/jobads/:id", + SIGNUPS = "/signup/:id", + SIGNUP_FORMS = "/signupForm/:id", + AUTH_TOKEN = "/api-token-auth", + AUTH_TOKEN_VERIFY = "/api-token-verify", +} + +export type API = { + path: APIPath; + urlParams?: { + id?: string | number; + }; + queryParams?: { + limit?: number; + offset?: number; + since?: Date; + }; + authenticated?: boolean; +}; + +type Headers = { + Authorization?: string; +}; + +const getAuthHeader = (): string => { + const jwt = getTokenCookie(); + return `JWT ${jwt}`; +}; + +const getHeaders = (auth?: boolean): Headers => { + if (auth) { + return { + Authorization: getAuthHeader(), + }; + } + return {}; +}; + +const fillUrlParams = (apiPath: APIPath, params: API["urlParams"] = {}): string => { + const path = apiPath + .split("/") + .filter(Boolean) + .map((urlComponent) => { + // fill in each placeholder component like ':id' with value from params + if (urlComponent.startsWith(":")) { + const key = urlComponent.substring(1); + const value = params[key]; + return value; + } + return urlComponent; + }) + .join("/"); + // code above strips leading '/' from path + return `/${path}`; +}; + +async function callBackendAPI( + path: APIPath, + urlParams: API["urlParams"] = {}, + queryParams: API["queryParams"] = {}, + method: AxiosRequestConfig["method"], + headers: Headers, + requestBody: RequestType, +): Promise { + const url = fillUrlParams(path, urlParams); + const request: AxiosRequestConfig = { + url, + method, + headers, + params: queryParams, + data: requestBody, + responseType: "json", + }; + const response = await axiosInstance.request(request); + + const arrayResp = (response.data as { results?: ResponseType }); + if (Array.isArray(arrayResp.results)) { + return arrayResp.results; + } + return response.data; +} + +export async function getBackendAPI({ + path, urlParams, queryParams, authenticated, +}: API): Promise { + const headers = getHeaders(authenticated); + return callBackendAPI(path, urlParams, queryParams, "GET", headers, undefined); +} + +export async function postBackendAPI({ + path, urlParams, queryParams, authenticated, +}: API, body: RequestType): Promise { + const headers = getHeaders(authenticated); + return callBackendAPI(path, urlParams, queryParams, "POST", headers, body); +} + +export async function putBackendAPI({ + path, urlParams, queryParams, authenticated, +}: API, body: RequestType): Promise { + const headers = getHeaders(authenticated); + return callBackendAPI(path, urlParams, queryParams, "PUT", headers, body); +} + +export async function deleteBackendAPI({ + path, urlParams, queryParams, authenticated, +}: API): Promise { + const headers = getHeaders(authenticated); + return callBackendAPI(path, urlParams, queryParams, "DELETE", headers, undefined); +} diff --git a/src/api/eventApi.ts b/src/api/eventApi.ts index b4074ad..f6f81fe 100644 --- a/src/api/eventApi.ts +++ b/src/api/eventApi.ts @@ -1,11 +1,10 @@ /* eslint-disable no-console */ -import axios from "axios"; import Event from "@models/Event"; -import { getAuthHeader } from "@utils/auth"; +import { + APIPath, deleteBackendAPI, getBackendAPI, postBackendAPI, putBackendAPI, +} from "./backend"; -export const URL = `${process.env.NEXT_PUBLIC_API_URL}/events/`; - -export interface Options { +interface Options { limit?: number; offset?: number; auth?: boolean; @@ -15,33 +14,28 @@ export interface Options { class EventApi { static async getEvent(id: number, auth = false): Promise { try { - const headers = auth ? { Authorization: getAuthHeader() } : null; - const resp = await axios.get(`${URL}${id}/`, { - headers, + return await getBackendAPI({ + path: APIPath.EVENTS, urlParams: { id }, authenticated: auth, }); - return resp.data; } catch (err) { console.error(err); throw err; } } - static async getEvents(options: Options = {}): Promise { - const { - since, limit, offset, auth, - } = options; + static async getEvents({ + since, limit, offset, auth, + }: Options = {}): Promise { try { - const params = { - since, - limit, - offset, - }; - const headers = auth ? { Authorization: getAuthHeader() } : null; - const resp = await axios.get(`${URL}`, { - headers, - params, + return await getBackendAPI({ + path: APIPath.EVENTS, + queryParams: { + since, + limit, + offset, + }, + authenticated: auth, }); - return resp.data.results; } catch (err) { console.error(err); throw err; @@ -50,12 +44,9 @@ class EventApi { static async createEvent(data: Event): Promise { try { - const resp = await axios.post(URL, data, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + return await postBackendAPI({ + path: APIPath.EVENTS, authenticated: true, + }, data); } catch (err) { console.error(err); throw err; @@ -64,27 +55,18 @@ class EventApi { static async updateEvent(data: Event): Promise { try { - const putUrl = `${URL}${data.id}/`; - const resp = await axios.put(putUrl, data, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + return await putBackendAPI({ + path: APIPath.EVENTS, urlParams: { id: data.id }, authenticated: true, + }, data); } catch (err) { console.error(err); throw err; } } - static async deleteEvent(id: number) { + static async deleteEvent(id: number): Promise { try { - const resp = await axios.delete(`${URL}${id}`, { - headers: { - Authorization: getAuthHeader(), - }, - }); - return resp.data; + await deleteBackendAPI<{ message: "OK" }>({ path: APIPath.EVENTS, urlParams: { id } }); } catch (err) { console.error(err); throw err; diff --git a/src/hooks/useFetchBackend.ts b/src/hooks/useFetchBackend.ts new file mode 100644 index 0000000..e6c1308 --- /dev/null +++ b/src/hooks/useFetchBackend.ts @@ -0,0 +1,27 @@ +import useSWR from "swr"; +import { APIPath, getBackendAPI } from "@api/backend"; + +function useFetchBackend({ + apiPath: path, + fallbackData, + options, +}: { + apiPath: APIPath, + fallbackData?: DataType, + options?: { + limit?: number; + auth?: boolean; + } +}): { + data?: DataType, + error?: any + } { + const fetcher = (limit: number, authenticated: boolean) => getBackendAPI({ path, queryParams: { limit }, authenticated }); + const { data, error } = useSWR([options?.limit, options?.auth], fetcher, { fallbackData }); + return { + data, + error, + }; +} + +export default useFetchBackend; diff --git a/src/hooks/useFetchEvents.ts b/src/hooks/useFetchEvents.ts deleted file mode 100644 index 2a810ba..0000000 --- a/src/hooks/useFetchEvents.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useRef } from "react"; -import useSWR from "swr"; -import isDeepEqual from "fast-deep-equal/react"; -import axios, { AxiosRequestConfig } from "axios"; -import Event from "@models/Event"; -import { getAuthHeader } from "@utils/auth"; -import { URL, Options } from "@api/eventApi"; - -const fetcher = (url: string, config: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data); - -const generateFetchParams = (id = "", options: Options = {}) => { - const url = `${URL}${id}`; - const { - auth, since, limit, offset, - } = options; - - return { - url, - config: { - params: { - since, - limit, - offset, - }, - headers: auth ? { Authorization: getAuthHeader() } : null, - }, - }; -}; - -interface FetchArguments { - fallbackData?: Event | Event[], - id?: string; - options?: Options -} - -const useFetchEvents = ({ - fallbackData, - id = "", - options = {}, -}: FetchArguments) => { - const { url, config } = generateFetchParams(id, options); - - // Use ref, since config dependency is non-primitive => without this we have infinite fetch loop - const configRef = useRef(config); - if (!isDeepEqual(configRef.current, config)) { - configRef.current = config; - } - - const { data, error } = useSWR([url, configRef.current], fetcher, { fallbackData }); - return { - data: data?.results || data, - error, - }; -}; - -export default useFetchEvents; diff --git a/src/hooks/useFetchFeed.ts b/src/hooks/useFetchFeed.ts deleted file mode 100644 index cf0fe2a..0000000 --- a/src/hooks/useFetchFeed.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useRef } from "react"; -import useSWR from "swr"; -import isDeepEqual from "fast-deep-equal/react"; -import axios, { AxiosRequestConfig } from "axios"; -import Post from "@models/Feed"; -import { getAuthHeader } from "@utils/auth"; -import { URL, Options } from "@api/feedApi"; - -const feedFetcher = (url: string, config?: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data); - -const generateFetchParams = (id = "", options: Options = {}) => { - const url = `${URL}${id}`; - const { auth, limit, offset } = options; - - return { - url, - config: { - params: { - limit, - offset, - }, - headers: auth ? { Authorization: getAuthHeader() } : null, - }, - }; -}; - -interface FetchArguments { - fallbackData?: Post | Post[], - id?: string; - options?: Options -} - -const useFetchFeed = ({ - fallbackData, - id = "", - options = {}, -}: FetchArguments) => { - const { url, config } = generateFetchParams(id, options); - - // Use ref, since config dependency is non-primitive => without this we have infinite fetch loop - const configRef = useRef(config); - if (!isDeepEqual(configRef.current, config)) { - configRef.current = config; - } - - const { data, error } = useSWR([url, configRef.current], feedFetcher, { fallbackData }); - return { - data: data?.results || data, - error, - }; -}; - -export default useFetchFeed; diff --git a/src/hooks/useFetchJobAds.ts b/src/hooks/useFetchJobAds.ts deleted file mode 100644 index b422832..0000000 --- a/src/hooks/useFetchJobAds.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useRef } from "react"; -import useSWR from "swr"; -import isDeepEqual from "fast-deep-equal/react"; -import axios, { AxiosRequestConfig } from "axios"; -import JobAd from "@models/JobAd"; -import { getAuthHeader } from "@utils/auth"; -import { URL, Options } from "@api/jobAdApi"; - -const jobAdFetcher = (url: string, config?: AxiosRequestConfig) => axios.get(url, config).then((res) => res.data); - -const generateFetchParams = (id = "", options: Options = {}) => { - const url = `${URL}${id}`; - const { - since, limit, offset, auth, - } = options; - - return { - url, - config: { - params: { - since, - limit, - offset, - }, - headers: auth ? { Authorization: getAuthHeader() } : null, - }, - }; -}; - -interface FetchArguments { - fallbackData?: JobAd | JobAd[], - id?: string; - options?: Options; -} - -const useFetchJobAds = ({ - fallbackData, - id = "", - options = {}, -}: FetchArguments) => { - const { url, config } = generateFetchParams(id, options); - - // Use ref, since config dependency is non-primitive => without this we have infinite fetch loop - const configRef = useRef(config); - if (!isDeepEqual(configRef.current, config)) { - configRef.current = config; - } - - const { data, error } = useSWR([url, configRef.current], jobAdFetcher, { fallbackData }); - return { - data: data?.results || data, - error, - }; -}; - -export default useFetchJobAds; diff --git a/src/pages/admin/events/index.tsx b/src/pages/admin/events/index.tsx index edfc3f4..6cdddd0 100644 --- a/src/pages/admin/events/index.tsx +++ b/src/pages/admin/events/index.tsx @@ -8,7 +8,8 @@ import { Button, Link } from "@components/index"; import AddLink from "@components/AddLink"; import Event from "@models/Event"; import EventApi from "@api/eventApi"; -import useFetchEvents from "@hooks/useFetchEvents"; +import useFetchBackend from "@hooks/useFetchBackend"; +import { APIPath } from "@api/backend"; const URL = "/admin/events"; @@ -65,7 +66,7 @@ const renderData = (events: Event[]) => { }; const AdminEventPage: NextPage = () => { - const { data } = useFetchEvents({ options: { auth: true } }); + const { data } = useFetchBackend({ apiPath: APIPath.EVENTS, options: { auth: true } }); return (

Events

diff --git a/src/pages/admin/feed/index.tsx b/src/pages/admin/feed/index.tsx index 793f27b..30092e5 100644 --- a/src/pages/admin/feed/index.tsx +++ b/src/pages/admin/feed/index.tsx @@ -8,7 +8,8 @@ import { Button, Link } from "@components/index"; import AddLink from "@components/AddLink"; import Post from "@models/Feed"; import PostApi from "@api/feedApi"; -import useFetchFeed from "@hooks/useFetchFeed"; +import useFetchBackend from "@hooks/useFetchBackend"; +import { APIPath } from "@api/backend"; const URL = "/admin/feed"; @@ -65,7 +66,7 @@ const renderData = (feed: Post[]) => { }; const AdminFeedPage: NextPage = () => { - const { data } = useFetchFeed({ options: { auth: true } }); + const { data } = useFetchBackend({ apiPath: APIPath.FEED, options: { auth: true } }); return (

Feed

diff --git a/src/pages/admin/jobads/index.tsx b/src/pages/admin/jobads/index.tsx index cc6e7fa..3de3931 100644 --- a/src/pages/admin/jobads/index.tsx +++ b/src/pages/admin/jobads/index.tsx @@ -7,8 +7,9 @@ import AdminListCommon from "@views/admin/AdminListCommon"; import { Button, Link } from "@components/index"; import AddLink from "@components/AddLink"; import JobAd from "@models/JobAd"; -import useFetchJobAds from "@hooks/useFetchJobAds"; +import useFetchBackend from "@hooks/useFetchBackend"; import JobAdApi from "@api/jobAdApi"; +import { APIPath } from "@api/backend"; const URL = "/admin/jobads"; @@ -69,7 +70,7 @@ const renderData = (jobAds: JobAd[]) => { }; const AdminJobAdPage: NextPage = () => { - const { data } = useFetchJobAds({ options: { auth: true } }); + const { data } = useFetchBackend({ apiPath: APIPath.JOBADS, options: { auth: true } }); return (

Job advertisements

diff --git a/src/pages/in_english.tsx b/src/pages/in_english.tsx index d1cfdef..db62e76 100644 --- a/src/pages/in_english.tsx +++ b/src/pages/in_english.tsx @@ -3,12 +3,12 @@ import { NextPage, GetStaticProps } from "next"; import Head from "next/head"; import Event from "@models/Event"; import EventApi from "@api/eventApi"; -import useFetchEvents from "@hooks/useFetchEvents"; +import useFetchBackend from "@hooks/useFetchBackend"; import Post from "@models/Feed"; import FeedApi from "@api/feedApi"; -import useFetchFeed from "@hooks/useFetchFeed"; import InEnglishPageView from "@views/InEnglishPage/InEnglishPageView"; import PageWrapper from "@views/common/PageWrapper"; +import { APIPath } from "@api/backend"; const eventOptions = { limit: 4, @@ -24,8 +24,8 @@ interface InitialProps { } const InEnglishPage: NextPage = ({ initialEvents, initialFeed }) => { - const eventResult = useFetchEvents({ fallbackData: initialEvents, options: eventOptions }); - const feedResult = useFetchFeed({ fallbackData: initialFeed, options: feedOptions }); + const eventResult = useFetchBackend({ apiPath: APIPath.EVENTS, fallbackData: initialEvents, options: eventOptions }); + const feedResult = useFetchBackend({ apiPath: APIPath.FEED, fallbackData: initialFeed, options: feedOptions }); return ( <> @@ -33,7 +33,7 @@ const InEnglishPage: NextPage = ({ initialEvents, initialFeed }) = - + ); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index b18dc68..309ee90 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -3,12 +3,12 @@ import { NextPage, GetStaticProps } from "next"; import Head from "next/head"; import Event from "@models/Event"; import EventApi from "@api/eventApi"; -import useFetchEvents from "@hooks/useFetchEvents"; +import useFetchBackend from "@hooks/useFetchBackend"; import Post from "@models/Feed"; import FeedApi from "@api/feedApi"; -import useFetchFeed from "@hooks/useFetchFeed"; import FrontPageView from "@views/FrontPage/FrontPageView"; import PageWrapper from "@views/common/PageWrapper"; +import { APIPath } from "@api/backend"; const eventOptions = { limit: 4, @@ -24,8 +24,8 @@ interface InitialProps { } const FrontPage: NextPage = ({ initialEvents, initialFeed }) => { - const eventResult = useFetchEvents({ fallbackData: initialEvents, options: eventOptions }); - const feedResult = useFetchFeed({ fallbackData: initialFeed, options: feedOptions }); + const eventResult = useFetchBackend({ apiPath: APIPath.EVENTS, fallbackData: initialEvents, options: eventOptions }); + const feedResult = useFetchBackend({ apiPath: APIPath.FEED, fallbackData: initialFeed, options: feedOptions }); return ( <> @@ -33,7 +33,7 @@ const FrontPage: NextPage = ({ initialEvents, initialFeed }) => { - + ); diff --git a/src/pages/kilta/toiminta.tsx b/src/pages/kilta/toiminta.tsx index e0efd46..3a35b82 100644 --- a/src/pages/kilta/toiminta.tsx +++ b/src/pages/kilta/toiminta.tsx @@ -3,12 +3,12 @@ import { NextPage, GetStaticProps } from "next"; import Head from "next/head"; import Event from "@models/Event"; import EventApi from "@api/eventApi"; -import useFetchEvents from "@hooks/useFetchEvents"; +import useFetchBackend from "@hooks/useFetchBackend"; import Post from "@models/Feed"; import FeedApi from "@api/feedApi"; -import useFetchFeed from "@hooks/useFetchFeed"; import ActualPageView from "@views/ActualPage/ActualPageView"; import PageWrapper from "@views/common/PageWrapper"; +import { APIPath } from "@api/backend"; interface InitialProps { initialEvents: Event[]; @@ -16,8 +16,8 @@ interface InitialProps { } const ActualPage: NextPage = ({ initialEvents, initialFeed }) => { - const eventResult = useFetchEvents({ fallbackData: initialEvents }); - const feedResult = useFetchFeed({ fallbackData: initialFeed }); + const eventResult = useFetchBackend({ apiPath: APIPath.EVENTS, fallbackData: initialEvents }); + const feedResult = useFetchBackend({ apiPath: APIPath.FEED, fallbackData: initialFeed }); return ( <> diff --git a/src/pages/yritysyhteistyo.tsx b/src/pages/yritysyhteistyo.tsx index 5eed22b..b81c759 100644 --- a/src/pages/yritysyhteistyo.tsx +++ b/src/pages/yritysyhteistyo.tsx @@ -3,9 +3,10 @@ import { NextPage, GetStaticProps } from "next"; import Head from "next/head"; import JobAd from "@models/JobAd"; import JobAdApi from "@api/jobAdApi"; -import useFetchJobAds from "@hooks/useFetchJobAds"; +import useFetchBackend from "@hooks/useFetchBackend"; import CorporatePageView from "@views/CorporatePage/CorporatePageView"; import PageWrapper from "@views/common/PageWrapper"; +import { APIPath } from "@api/backend"; interface InitialProps { initialJobAds: JobAd[]; @@ -13,7 +14,7 @@ interface InitialProps { const CorporatePage: NextPage = ({ initialJobAds }) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { data, error } = useFetchJobAds({ fallbackData: initialJobAds }); + const { data, error } = useFetchBackend({ apiPath: APIPath.JOBADS, fallbackData: initialJobAds }); return ( <>