Skip to content
This repository has been archived by the owner on Jan 20, 2024. It is now read-only.

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
tdemin committed Dec 26, 2019
2 parents a0663b0 + efb4134 commit 9168bca
Show file tree
Hide file tree
Showing 14 changed files with 1,292 additions and 932 deletions.
32 changes: 16 additions & 16 deletions package.json
@@ -1,39 +1,39 @@
{
"name": "amber_web",
"version": "0.0.6",
"version": "0.0.7",
"private": true,
"dependencies": {
"@types/jest": "^24.0.23",
"@types/node": "^12.12.12",
"@types/react": "^16.9.13",
"@types/jest": "^24.X",
"@types/node": "^12.12.21",
"@types/react": "^16.9.17",
"@types/react-dom": "^16.9.4",
"@types/react-redux": "^7.1.2",
"@types/react-router-dom": "^5.1.2",
"@types/react-redux": "^7.1.5",
"@types/react-router-dom": "^5.1.3",
"axios": "^0.19.0",
"bulma": "^0.8.0",
"node-sass": "^4.12.0",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"node-sass": "^4.13.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-localization": "^1.0.15",
"react-redux": "^7.1.1",
"react-router-dom": "^5.0.1",
"react-scripts": "3.2.0",
"react-redux": "^7.1.3",
"react-router-dom": "^5.1.2",
"react-scripts": "3.3.0",
"redux": "^4.0.4",
"redux-devtools-extension": "^2.13.8",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"typescript": "3.7.2"
"typescript": "3.7.4"
},
"devDependencies": {
"eslint": "^6.2.2",
"prettier": "^1.18.2"
"eslint": "^6.8.0",
"prettier": "^1.19.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"beautify": "prettier --write src/**",
"beautify": "prettier --write src/**/*.tsx src/**/*.ts",
"lint": "eslint src/**/*.ts src/**/*.tsx"
},
"browserslist": {
Expand Down
24 changes: 20 additions & 4 deletions src/actions/auth.ts
Expand Up @@ -4,6 +4,8 @@ import { Dispatch } from "redux";

import Actions from "./list";
import { AuthAction } from "../typings/actions";
import { serializeAuthData } from "../helpers/api";
import { SuccessAction, FailAction } from "../typings/api";

const tokenHeader = "X-Auth-Token";

Expand All @@ -14,10 +16,7 @@ const tokenHeader = "X-Auth-Token";
* @param pass Password in plain text
*/
export const login = (user: string, pass: string) => (dispatch: Dispatch) => {
req.post("/login", {
name: user,
password: pass,
}).then(
req.post("/login", serializeAuthData(user, pass)).then(
(res: AxiosResponse) => {
setToken(res.data.token);
dispatch({
Expand Down Expand Up @@ -47,6 +46,23 @@ export const logout = () => (dispatch: Dispatch) => {
});
};

/**
* Signup function. Performs an HTTP POST request, calls the provided functions
* on success/fail.
* @param user Username
* @param pass Password in plain text
* @param sh Function to be called on success
* @param fh Function to be called on fail
*/
export const signup = (
user: string,
pass: string,
sh: SuccessAction,
fh: FailAction
) => {
req.post("/signup", serializeAuthData(user, pass)).then(sh, fh);
};

/**
* Sets an auth token in the app's Axios instance to be used in every request
* from now on.
Expand Down
16 changes: 16 additions & 0 deletions src/actions/misc.ts
@@ -0,0 +1,16 @@
import req from "../axios";
import { VersionData } from "../typings/api";

const EmptyData = {
signup: false,
version: "unknown",
};

export const getServerVersion = async () => {
let versionData: VersionData = EmptyData;
await req.get("/version").then(
(r) => (versionData = r.data),
() => (versionData = EmptyData)
);
return versionData;
};
9 changes: 3 additions & 6 deletions src/actions/tasks.ts
Expand Up @@ -38,10 +38,8 @@ const resolveUpdates = (merge: TaskMergeResult, dispatch: Dispatch) => {
export const refetchTasks = (localTasks: Task[]) => (dispatch: Dispatch) => {
req.get("/task").then(
(res: AxiosResponse) => {
const remoteTasks = res.data["tasks"].map((x: TaskRecord) =>
taskFromRecord(x)
) as Task[];
const merge = mergeTasks(remoteTasks, localTasks);
const rm = (res.data as TaskRecord[]).map((x) => taskFromRecord(x));
const merge = mergeTasks(rm, localTasks);
resolveUpdates(merge, dispatch);
},
() => {
Expand All @@ -59,8 +57,7 @@ export const refetchTasks = (localTasks: Task[]) => (dispatch: Dispatch) => {
export const createTask = (task: Task) => (dispatch: Dispatch) => {
req.post("/task", taskToRecord(task)).then(
(res: AxiosResponse) => {
const { id } = res.data;
task.ID = id;
task.ID = res.data;
dispatch({
type: Actions.TaskCreate,
data: task,
Expand Down
9 changes: 7 additions & 2 deletions src/const.ts
@@ -1,11 +1,16 @@
export const baseURI: string =
process.env.REACT_APP_APIURI || "https://amber.h.tdem.in/api";
process.env.REACT_APP_APIURI || "https://amber.h.tdem.in/api/v0";

export const appVersion: string = "0.0.6";
export const appVersion: string = "0.0.7";
export const appFullName: string = "Amber Web";
export const appName: string = "amber_web";
export const appAuthor: string = "Timur Demin";
export const appHomePage: string = "https://git.tdem.in/tdemin/amber_web";

export const amberFullName: string = "Amber Server";
export const amberHomePage: string = "https://git.tdem.in/tdemin/amber";

/** Network timeout (in ms) as used by Axios. */
export const networkTimeout: number = 5000;
/** Delay used for UI stuff like purging tasks recursively. */
export const uiDelay: number = 1000;
15 changes: 15 additions & 0 deletions src/helpers/api.ts
@@ -0,0 +1,15 @@
import { AuthData } from "../typings/api";

/**
* Serializes authentication data into a JSON object to be pushed to the API
* server. Returns the resulting object.
*/
export const serializeAuthData = (
username: string,
password: string
): AuthData => ({
username,
password,
});

export default serializeAuthData;
29 changes: 11 additions & 18 deletions src/reducers/tasks.ts
Expand Up @@ -2,9 +2,8 @@ import Actions from "../actions/list";
import { TaskAction } from "../typings/actions";
import { Task } from "../typings/tasks";

// be sure to account for that the sort occurs IN PLACE and this
// does NOT return a new array
const sort = (tasks: Task[]) => tasks.sort((a, b) => a.ID - b.ID);
/** Sorts a task array in ascending order. Returns a new sorted array. */
const sort = (tasks: Task[]) => [...tasks.sort((a, b) => a.ID - b.ID)];

export interface TaskState {
tasks: Task[];
Expand All @@ -18,42 +17,36 @@ export const taskReducer = (
case Actions.TasksFetch:
return { tasks: sort(action.data as Task[]) };
case Actions.TasksFetchError:
return { tasks: [...sort(state.tasks)] };
return { tasks: sort(state.tasks) };
case Actions.TaskCreate: {
state.tasks.push(action.data as Task);
return { tasks: [...sort(state.tasks)] };
return { tasks: sort(state.tasks) };
}
case Actions.TaskCreateError: {
const newTask = action.data as Task;
newTask.ToSync = true;
state.tasks.push(newTask);
return { tasks: [...sort(state.tasks)] };
return { tasks: sort(state.tasks) };
}
case Actions.TaskDelete: {
return {
tasks: [
...sort(
state.tasks.filter(
(task) => task.ID !== (action.data as Task).ID
)
),
],
};
const IDToRemove = (action.data as Task).ID;
const tasks = state.tasks.filter((task) => task.ID !== IDToRemove);
return { tasks: sort(tasks) };
}
case Actions.TaskDeleteError: {
const index = state.tasks.findIndex(
(task) => task.ID === (action.data as Task).ID
);
state.tasks[index].ToRemove = true;
return { tasks: [...sort(state.tasks)] };
return { tasks: sort(state.tasks) };
}
case Actions.TaskUpdate: {
const index = state.tasks.findIndex(
(task) => task.ID === (action.data as Task).ID
);
state.tasks[index] = action.data as Task;
state.tasks[index].LastMod = Date.now();
return { tasks: [...sort(state.tasks)] };
return { tasks: sort(state.tasks) };
}
case Actions.TaskUpdateError: {
const index = state.tasks.findIndex(
Expand All @@ -62,7 +55,7 @@ export const taskReducer = (
state.tasks[index] = action.data as Task;
state.tasks[index].ToSync = true;
state.tasks[index].LastMod = Date.now();
return { tasks: [...sort(state.tasks)] };
return { tasks: sort(state.tasks) };
}
default:
return state;
Expand Down
14 changes: 14 additions & 0 deletions src/typings/api.ts
@@ -0,0 +1,14 @@
import { AxiosResponse, AxiosError } from "axios";

export type SuccessAction = (s: AxiosResponse) => void;
export type FailAction = (e: AxiosError) => void;

export type AuthData = {
username: string;
password: string;
};

export type VersionData = {
version: string;
signup: boolean;
};
3 changes: 2 additions & 1 deletion src/views/assets/locales.ts
@@ -1,6 +1,6 @@
import LocalizedStrings from "react-localization";

import { appVersion, appFullName, appAuthor } from "../../const";
import { appVersion, appFullName, appAuthor, amberFullName } from "../../const";

export default new LocalizedStrings({
en: {
Expand Down Expand Up @@ -35,5 +35,6 @@ export default new LocalizedStrings({
task_toggleBtnCompleted: "Completed",
task_toggleBtnPending: "Pending",
app_versionString: `${appFullName} v${appVersion} by ${appAuthor}`,
amber_versionString: `${amberFullName} v`,
},
});
35 changes: 21 additions & 14 deletions src/views/components/footer.tsx
@@ -1,21 +1,28 @@
import React from "react";
import React, { useState } from "react";

import Level from "./bulma/level";

import { appHomePage } from "../../const";
import { getServerVersion } from "../../actions/misc";
import { appHomePage, amberHomePage } from "../../const";
import strings from "../assets/locales";

/** Static footer with no dynamic code. */
export const Footer: React.FC = () => (
<footer>
<Level level>
<Level levelItem>
<a className="text link" href={appHomePage}>
{strings.app_versionString}
</a>
export const Footer: React.FC = () => {
const [version, setVersion] = useState("unknown");
getServerVersion().then((r) => setVersion(r.version));
return (
<footer>
<Level level>
<Level levelItem className="footer_links">
<a className="text link" href={appHomePage}>
{`${strings.app_versionString}`}
</a>
<a className="text link" href={amberHomePage}>
{`${strings.amber_versionString}${version}`}
</a>
</Level>
</Level>
</Level>
</footer>
);
</footer>
);
};

export default Footer;
export default React.memo(Footer);
3 changes: 3 additions & 0 deletions src/views/mainView.tsx
Expand Up @@ -17,6 +17,7 @@ import { AnyAction } from "../typings/actions";
import { Task } from "../typings/tasks";
import { Store } from "../typings/store";

import { uiDelay } from "../const";
import strings from "./assets/locales";

const mapStateToProps = (state: Store) => ({
Expand Down Expand Up @@ -47,6 +48,8 @@ class MainView extends React.Component<Props, State> {
.length === 0 && task.Completed
);
danglingTasks.forEach((task) => this.props.dispatch(deleteTask(task)));
// more tasks possibly left to go?
danglingTasks.length > 0 && setTimeout(() => this.prune(), uiDelay);
};
updateSearch = (e: React.FormEvent<HTMLInputElement>) =>
this.setState({
Expand Down
29 changes: 19 additions & 10 deletions src/views/signupForm.tsx
@@ -1,8 +1,6 @@
import React from "react";
import { RouteComponentProps as RCP } from "react-router-dom";
import { AxiosError, AxiosResponse } from "axios";

import req from "../axios";
import { AxiosError } from "axios";

import Container from "./components/bulma/container";
import Button from "./components/bulma/button";
Expand All @@ -13,6 +11,7 @@ import Field from "./components/bulma/field";
import Message from "./components/message";

import strings from "./assets/locales";
import { signup as callAPISignup } from "../actions/auth";

const successRedirectDelay = 5000;

Expand Down Expand Up @@ -49,14 +48,24 @@ class SignupForm extends React.PureComponent<RCP, State> {
};
signup = () => {
this.setState({ status: Status.IN_PROCESS });
let data = { name: this.state.name, password: this.state.password };
req.post("/signup", data).then(
callAPISignup(
this.state.name,
this.state.password,
() => this.setState({ status: Status.SUCCESS }),
(e: AxiosError) =>
this.setState({
status: Status.FAILED,
httpCode: (e.response as AxiosResponse).status,
})
(e: AxiosError) => {
if (e.response) {
this.setState({
status: Status.FAILED,
httpCode: e.response?.status,
});
} else {
// fallback for possible CORS errors
this.setState({
status: Status.FAILED,
httpCode: Errors.FORBIDDEN,
});
}
}
);
};
goBack = () => this.props.history.push("/");
Expand Down
5 changes: 5 additions & 0 deletions src/views/styles/common.scss
Expand Up @@ -67,6 +67,11 @@ div.searchBox {
}
}

.footer_links {
display: flex;
flex-direction: column;
}

@media (max-width: $small) {
div.headerText {
display: none;
Expand Down

0 comments on commit 9168bca

Please sign in to comment.