Connect the website to the API

This commit is contained in:
Gender Shrapnel 2019-10-27 13:41:16 +01:00
parent ab791efe42
commit 32e8dfa4bb
8 changed files with 66 additions and 38 deletions

View File

@ -1,34 +1,38 @@
import { DateTime } from "luxon"; import { APIError, Task } from "./types";
export interface Task { export const getTasks = async (
id: number; limit: number = 10,
name: string; offset: number = 0
due: DateTime; ): Promise<{ items: Task[]; count: number }> => {
} const url = new URL("tasks", API_URL);
url.searchParams.set("limit", limit.toString());
url.searchParams.set("offset", offset.toString());
const tasks: Task[] = [...Array(5).keys()].map(n => ({ const response = await fetch(url.toString());
id: n, if (!response.ok) {
name: `Task ${n}`, throw new APIError("Task fetch failed");
due: DateTime.local().plus({ weeks: n }) }
}));
export const getTasks = async (): Promise<Task[]> => const json = await response.json();
new Promise<Task[]>((resolve, reject) => if (!Number.isSafeInteger(json.count) || !Array.isArray(json.tasks)) {
setTimeout(() => resolve(tasks), 3000) throw new APIError("Invalid response from server.");
); }
const count: number = json.count;
const items: Task[] = (json.tasks as any[]).map(t => {
if (
typeof t !== "object" ||
!Number.isSafeInteger(t.id) ||
typeof t.name !== "string"
) {
throw new APIError("Invalid response from server.");
}
export const schedule = async (task: Omit<Task, "id">): Promise<Task> => return {
new Promise<Task>((resolve, reject) => id: t.id,
setTimeout(() => { name: t.name,
const id = Math.max(...tasks.map(t => t.id)) + 1; schedule: { type: "Plain" }
tasks[id] = { ...task, id }; };
resolve(tasks[id]); });
}, 3000)
);
export const unschedule = async (taskId: number): Promise<void> => return { items, count };
new Promise<void>((resolve, reject) => };
setTimeout(() => {
delete tasks[taskId];
}, 3000)
);

View File

@ -0,0 +1,8 @@
export class APIError extends Error {}
export interface Task {
id: number;
name: string;
schedule: {
type: "Plain";
};
}

1
src/frontend/globals.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare const API_URL: string;

View File

@ -1,5 +1,6 @@
import { getTasks } from "@kredens/frontend/api/tasks";
import { Task as APITask } from "@kredens/frontend/api/types";
import { all, call, put, takeEvery } from "redux-saga/effects"; import { all, call, put, takeEvery } from "redux-saga/effects";
import { getTasks, Task as APITask } from "../api/tasks";
import { taskFetchError, taskFetchOk, taskFetchStart } from "./tasks/actions"; import { taskFetchError, taskFetchOk, taskFetchStart } from "./tasks/actions";
import { Task, TaskQuery, TaskScheduleType } from "./tasks/types"; import { Task, TaskQuery, TaskScheduleType } from "./tasks/types";
@ -7,22 +8,26 @@ export function* fetchTasksSaga(query: TaskQuery = { limit: 10 }) {
yield put(taskFetchStart(query)); yield put(taskFetchStart(query));
try { try {
const tasks: APITask[] = yield call(getTasks); const tasks: { items: APITask[]; count: number } = yield call(
getTasks,
query.limit,
query.offset
);
yield put( yield put(
taskFetchOk( taskFetchOk(
query, query,
tasks tasks.items
.map<[string, Task]>(t => [ .map<[string, Task]>(t => [
t.id.toString(), t.id.toString(),
{ {
name: t.name, name: t.name,
schedule: { schedule: {
type: TaskScheduleType.Once, type: TaskScheduleType[t.schedule.type]
due: t.due.toISO()
} }
} }
]) ])
.reduce((res, [id, task]) => ({ ...res, [id]: task }), {}) .reduce((res, [id, task]) => ({ ...res, [id]: task }), {}),
tasks.count
) )
); );
} catch (error) { } catch (error) {

View File

@ -26,12 +26,14 @@ export function taskFetchStart(query: TaskQuery): TasksActionType {
export function taskFetchOk( export function taskFetchOk(
query: TaskQuery, query: TaskQuery,
results: { [key: string]: Task } results: { [key: string]: Task },
count: number
): TasksActionType { ): TasksActionType {
return { return {
type: TasksAction.TASKS_FETCH_OK, type: TasksAction.TASKS_FETCH_OK,
query, query,
results, results,
count,
fetched: DateTime.utc().toISO() fetched: DateTime.utc().toISO()
}; };
} }

View File

@ -3,6 +3,7 @@ import { TasksAction, TasksActionType, TasksState } from "./types";
const initialState: TasksState = { const initialState: TasksState = {
items: {}, items: {},
count: 0,
queries: {} queries: {}
}; };

View File

@ -21,8 +21,8 @@ export interface Task {
export interface TaskQuery { export interface TaskQuery {
query?: string; query?: string;
after?: string;
limit: number; limit: number;
offset?: number;
} }
interface TaskQueryOk { interface TaskQueryOk {
@ -48,6 +48,7 @@ export interface TasksState {
items: { items: {
[key: string]: Task; [key: string]: Task;
}; };
count: number;
queries: { queries: {
[key: string]: TaskQueryResult; [key: string]: TaskQueryResult;
}; };
@ -82,6 +83,7 @@ interface TaskFetchOkAction {
type: TasksAction.TASKS_FETCH_OK; type: TasksAction.TASKS_FETCH_OK;
query: TaskQuery; query: TaskQuery;
results: { [key: string]: Task }; results: { [key: string]: Task };
count: number;
fetched: string; fetched: string;
} }

View File

@ -24,7 +24,12 @@ const config: webpack.Configuration = {
alias: { alias: {
"@kredens": path.resolve(__dirname, "src/") "@kredens": path.resolve(__dirname, "src/")
} }
} },
plugins: [
new webpack.DefinePlugin({
API_URL: JSON.stringify("http://localhost:3000/api/")
})
]
}; };
export default config; export default config;