Skip to content

An over-engineered todo application built for the P2-Anteaters team, featuring a beautiful UI with React, full authentication and todo system with Python (SQLAlchemy).

Notifications You must be signed in to change notification settings

paul-bokelman/over-engineered-todo-application

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Todo application

project built for the P2-Anteaters team

How its made

Our project is comprised of two standalone sections, the server side and the client side. We did this in order to use React on the frontend in order to make a beautiful & functional website in record time.

Server side (Flask)

The first section of our project is our main server written in Python with the Flask lightweight framework. Our server was built to be a complete API so all actions are handled remotely through API requests from the client. This approach allows us to use a javascript framework like react for the frontend, without this approach it wouldn't be efficient.

The base of our server has two database models which are the User and Todos models that are bound through a one-to-many relationship which means that a User may have many Todos but a Todos may not have many User. Below are the model snippets.

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(15), unique=True)
    email = db.Column(db.String(50), unique=True)
    password = db.Column(db.String(80))
    todos = db.relationship('Todos', backref='author')


class Todos(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(120), nullable=False)
    desc = db.Column(db.Text, nullable=False)
    completed = db.Column(db.Boolean)
    date_added = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

Although these models would be rendered useless without server functionality so we added some API routes so that the client side of the application could interact with relational database. Below are an explanation of just 2 of the 8 routes.

Adding Todo

A crucial part of our application was to allow the logged in user to add a "todo", otherwise we wouldn't have anything. Similar to all of the other routes in the app.py we accomplished this by creating an API route that can we requested through the client side.

@app.route('/add_todo/<username>', methods=['POST'])
def add_todo(username):
    d = request.get_json()
    user = User.query.filter_by(username=username).first()
    todo = Todos(name=d['name'], desc=d['desc'], completed=d['completed'], author=user)
    db.session.add(todo)
    db.session.commit()
    return 'Added Todo', 201

This route in particular takes a username parameter as well as a JSON object from the incoming POST request. This incoming JSON is then deconstructed by the individual keys after the corresponding user object matching the username parameter is found, these deconstructed keys are then committed into the database through the current users corresponding Id.

Users Todos

In order to display all of the users individual "todos" onto the screen we first need the data to be accessed by the client side. This is why we have a route that returns all of a users "todos".

@app.route('/todos/<username>', methods=['GET'])
def todos(username):
    for user in User.query.filter_by(username=username):
        todos = []
        for todo in user.todos:
            todos.append({'id':todo.id, 'name': todo.name, 'desc': todo.desc, 'completed': todo.completed, 'date_added': todo.date_added})
        response = jsonify({'todos': todos})
        return response
    return 'Request failed, make sure user exists (case sensitive).', 404

This route takes a single parameter which is the username parameter, similar to the /add_todo route. We then use the username string to find the users particular corresponding object in the database, this object contains all type of information about the user although through dot notation we can loop over all of the users todos. All of the todos are then appended to an array, converted to acceptable JSON then returned back to the client. In the case that the user doesn't exist there is a fallback return that simply lets the client know that the server couldn't find the entry.

Client side (React)

The client side (frontend) of the application was built entirely in React, and without the server is completely functionless although servable for frontend functionality. The biggest advantage that React provides as opposed to traditional front-end development is that you can build functional components which are practically snippets of the ui that can all be combined in order to build a faster and cleaner application. Another major advantage of using React for frontend development is the access to the massive ecosystem of libraries to help aid you in the development process, this project is no exception and is utilizing lots of libraries.

Application Structure

The client side is divided into 2 major sections, The "server-optional" and the "server-dependant" sections. As the names imply they both function as namely implied. For each section there will be a snippet and a more in depth look into the sub sections.

Server dependant

The server optional section of the client is the section of the application where the integrity and functionality of the Flask server really shines, these areas in the client are where data is relayed back and fourth between the server and the client in order to accomplish a full stack application.

The server dependant section is comprised of the auth and dashboard sub-sections. These two sub-sections interact with the server differently although interact regardless.

  • Auth section - This section has all of the forms and on-demand request functions associated with the authentication process. This page in particular is a deeply nested tree of components which each serve their purpose. In the nested Form.jsx component there are clear server request functions, here are the signup and login functions. Each of these functions accept a deconstructed object with different required values, those values are then converted to JSON format and "posted" to the corresponding route for server validation.
const handleSignup = async ({ username, email, password }) => {
  const data = {
    username,
    email,
    password,
  };
  const response = await axios({
    method: "post",
    url: "https://antsapi.nighthawkcodingsociety.com/signup",
    data,
  }).catch((error) => error.response);
  if (response.status === 201) {
    setLoggingIn(true);
  } else {
    console.log(response.status);
  }
};

const handleLogin = async ({ username, password, remember }) => {
  const data = {
    username,
    password,
    remember: remember === undefined ? false : remember,
  };
  console.log(data);
  setLoading(true);
  const response = await axios({
    method: "post",
    url: "https://antsapi.nighthawkcodingsociety.com/login",
    data,
  }).catch((error) => error.response);
  if (response.status === 200) {
    setUsername(username);
    logIn();
    history.push({
      pathname: "/dashboard",
      state: { username: username, loggedIn: true },
    });
  } else {
    console.log(response.status);
  }
  setLoading(false);
};
  • Dashboard section - The dashboard is the meat of the project. This page has by far the most functionality as well as interaction with the server. To oversimplify things, there are two main things that are happening within this page, the first is the fetching of the users todos this functionality is handled by using the username of the user logged in stored by the state to get the corresponding todos by concatenating the username in the /todos route. The second major part of this page is the adding of todos for a particular user. This function simply gets the input data from the forms in the "adding" card in the dashboard then uses that data to make a post request through axios and adds an entry to the database for the current user.
// fetching of the users todos
const { isLoading, isError, data, error, refetch } = useQuery("todos", () =>
  axios.get(
    `https:/https://antsapi.nighthawkcodingsociety.com/todos/${state.username}`
  )
);
// adding of todos for a particular user
const addTodo = async ({ name, desc }) => {
  const data = {
    name,
    desc,
    completed: false,
  };
  console.log(data);
  const response = await axios({
    method: "post",
    url: `https://https://antsapi.nighthawkcodingsociety.com/add_todo/${state.username}`,
    data,
  }).catch((error) => error.response);
  if (response.status === 201) {
    console.log("todo added");
    refetch();
  } else {
    console.log(response.status);
  }
};
Server optional

The server optional section of the client is arguably the less important section of the two considering that it doesn't interact with the server in any fashion. The major sub-sections of this section include the home page as well as the members sections.

  • Home Page - By far the most style intensive, the home page has multiple intertwined components that are configured to display the different sections of the home page. Each "feature" section on the homepage is made up from a single component, this component takes multiple props to determine the variety in the particular section.
// Home page jsx to pass props to Feature component
<Feature
  icon="easy"
  tag={["Fundamentally easy", 1, "text-green-400"]} //green
  header="We prioritize simplicity."
>
  Generally todo applications these days have very cluttered user interfaces and
  advertisements in every corner, we aim to change that. Our ui was built
  passion and the people in mind and makes everything from viewing your todos to
  adding new ones a breeze.
  <div className="text-gray-300 text-left mt-8">
    <Link to="/auth" className="inline-block text-center">
      <div className="h-36 w-52 group px-12 py-8 rounded-2xl border-2 bg-white hover:shadow-xl hover:text-green-500 hover:border-green-500 hover:border-4 duration-200">
        <FaRegIdBadge className="text-5xl mx-auto" />
        <h1 className="mt-2 font-semibold text-xl">Register</h1>
      </div>
    </Link>
    <div className="inline-block group px-6 py-8 text-3xl bg-white">
      <TiArrowRightThick />
    </div>
    <Link to="/auth" className="inline-block text-center">
      <div className="relative h-36 w-52 group px-12 py-8 rounded-2xl border-2 bg-white hover:shadow-xl hover:text-green-400 hover:border-green-400 hover:border-4 duration-200">
        <HiOutlineUser className="text-5xl mx-auto" />
        <h1 className="mt-2 font-semibold text-xl">Login</h1>
      </div>
    </Link>
    <div className="inline-block group px-6 py-8 text-3xl bg-white">
      <TiArrowRightThick />
    </div>
    <Link to="/dashboard" className="inline-block text-center">
      <div className="relative h-36 w-52 group px-12 py-8 rounded-2xl border-2 bg-white hover:shadow-xl hover:text-green-300 hover:border-green-300 hover:border-4 duration-200">
        <BsCardChecklist className="text-5xl mx-auto" />
        <h1 className="mt-2 font-semibold text-xl">Todos</h1>
      </div>
    </Link>
  </div>
</Feature>
// Feature component that takes deconstructed props from parent
export const Feature = ({ icon, tag, header, children }) => {
  const getIcon = () => {
    switch (icon) {
      case "easy":
        return <BiAward />;
      case "speed":
        return <BsLightningFill />;
      case "team":
        return <RiTeamFill />;
    }
  };

  const primary =
    tag[1] === 1
      ? "from-green-400"
      : tag[1] === 2
      ? "from-yellow-400" //blue
      : "from-blue-400";
  const secondary =
    tag[1] === 1
      ? "to-green-600"
      : tag[1] === 2
      ? "to-yellow-600" //blue
      : "to-blue-600";

  return (
    <div className="mt-36">
      <div
        className={`w-12 h-12 rounded-xl bg-gradient-to-br flex items-center justify-center ${primary} ${secondary} text-2xl text-white`}
      >
        {getIcon()}
      </div>
      <h4 className={`mt-4 uppercase ${tag[2]} font-semibold text-lg`}>
        {tag[0]}
      </h4>
      <h3 className="text-6xl font-bold">{header}</h3>
      <p className="mt-2 max-w-4xl text-xl text-gray-600 font-medium leading-8 mb-6">
        {children[0]}
      </p>
      <div>{children[1]}</div>
    </div>
  );
};
  • Members sections - The members section is a very hard section to simply represent through 1 singular file, this is because the members section is comprised of many different components that conditionally render based on the name param. This works by having the initial button with the select options redirect the user to a route that checks the specific param, depending on the param a certain member-specific component will be rendered with their corresponding information.
// Members select page
export const Members = () => {
  const history = useHistory();
  return (
    <div>
      <Formik
        initialValues={{ member: "Mackenzie" }}
        onSubmit={(values) => {
          history.push(`/member/${values.member}`);
        }}
      >
        {() => (
          <Form>
            <FormWrapper>
              <Field
                name="member"
                component="select"
                className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
              >
                <option value="Mackenzie">Mackenzie</option>
                <option value="Anthony">Anthony</option>
                <option value="Pedro">Pedro</option>
                <option value="Naweid">Naweid</option>
                <option value="Cherry">Cherry</option>
              </Field>
              <SubmitButton text="Go" />
            </FormWrapper>
          </Form>
        )}
      </Formik>
    </div>
  );
};
// Redirect page for specific component
export const MemberTemplate = () => {
  let { member } = useParams();
  return (
    <div>
      {member === "Mackenzie" ? (
        <Kenzie />
      ) : member === "Anthony" ? (
        <Anthony />
      ) : member === "Pedro" ? (
        <Pedro />
      ) : member === "Naweid" ? (
        <Naweid />
      ) : member === "Cherry" ? (
        <Cherry />
      ) : (
        <h1>member not found</h1>
      )}
      <BubbleSort />
    </div>
  );
};
// Example of member component
export const Anthony = () => {
  const [res, setRes] = useState(cube(2));
  return (
    <div>
      <Heading name="Anthony" />
      <Label>Cube Formula:</Label>
      <Formik
        initialValues={{ x: "" }}
        onSubmit={(values) => {
          const { x } = values;
          setRes(cube(x));
        }}
      >
        {() => (
          <Form>
            <FormWrapper>
              <InputForm label="x" name="x" type="number" />
              <SubmitButton text="Submit" />
            </FormWrapper>
          </Form>
        )}
      </Formik>
      <Answer>{res}</Answer>
    </div>
  );
};

Libraries

As previously mentioned the client side uses lots of libraries, below is a list of the Major libraries and their purpose/contribution in the project.

  • Axios - Used to make HTTP requests to the server side of the application with ease.

  • React Query - Responsible for managing state as well as caching data from server responses.

  • Tailwind CSS - Ease of styling for full application.

Deployment

Our client side deployment was done on Netlify, this means that we have two different deployments with no strings attached. The reason we chose to deploy the client side on Netlify instead of the raspberry pi is because on Netlify everything is git-flow oriented which means that all you need to do to deploy a client application built on node is simply login and choose the repo.

Technical Requirements

  • Python
  • SQLAlchemy
  • Flask
  • React
  • Raspberry Pi
  • HTML/CSS

About

An over-engineered todo application built for the P2-Anteaters team, featuring a beautiful UI with React, full authentication and todo system with Python (SQLAlchemy).

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published