Building an Ionic App with Grafbase Api.

ยท

10 min read

Welcome to another exciting adventure into the world of coding.

Today we are going to be building an Ionic todo app with grafbase and React.

Grafbase is an exciting solution for developers to create and deploy mobile/ web apps with ease and speed. It also helps to create Api's easily for use, you can check their documentation out in this link https://hshno.de/grafbase-signup.

To get Started, click on the get Started button in the grafbase hompage, you will need to signin if you already have an account, and if you don't you will have to create one, after successfully signing in it will take you to their starter guide where you will install their cli using:

command
npx grafbase init todoapp

The todoapp is the name of the app, which you can change to the name of your app. Next, we are going to be building the schema for our todo app. From the code below you can see that we defined the todo type and the todoList type. According to the documentation you will have to manually install the grafbase sdk using the command "npm install @grafbase/sdk".

import { g, auth, config } from '@grafbase/sdk';

// Welcome to Grafbase!
// Define your data models, integrate auth, permission rules, custom resolvers, search, and more with Grafbase.
// Integrate Auth
// https://grafbase.com/docs/auth
//
// const authProvider = auth.OpenIDConnect({
//   issuer: process.env.ISSUER_URL ?? ''
// })
//
// Define Data Models
// https://grafbase.com/docs/database



const Todo = g.model('Todo', {
  title: g.string().length({ min: 4, max: 255 }),
  notes: g.string().optional(),
  complete: g.boolean().default(false),
});

const TodoList = g.model('TodoList', {
  title: g.string().length({ min: 4, max: 50 }),
  todos: g.relation(Todo).optional().list(),
});




  // Extend models with resolvers
  // https://grafbase.com/docs/edge-gateway/resolvers
  // gravatar: g.url().resolver('user/gravatar')


export default config({
  schema: g
  // Integrate Auth
  // https://grafbase.com/docs/auth
  // auth: {
  //   providers: [authProvider],
  //   rules: (rules) => {
  //     rules.private()
  //   }
  // }
})

Then we run the api with the command: "npx grafbase dev", this will give you that endpoint and , which you can use to go to the graphiql playground where you will test the api using mutationa and queries.

Next, we create the React app, this is basically the commands you will need to create the Ionic-React app:

command
$ npm install -g @ionic/cli

$ ionic start myApp tabs --type react

$ ionic serve

You will install the cli in your root folder using the first command, then create you ionic-react app using the second command, and in ionic your app name should always start with a small letter, then you can start the ionic-react app with "ionic serve". This will start your app on a localhost link which will be shown when it is ready.

After that, you will start buiding your Todo app Ui with ionic web components and React hooks. From the code below, you can see the Home Page: We are using the useQuery, gql and useMutation hooks gotten from apollo/client to access the data from the graphql api.

import {
  IonButton,
  IonCard,
  IonCardContent,
  IonCardHeader,
  IonCardTitle,
  IonContent,
  IonHeader,
  IonIcon,
  IonPage,
  IonTitle,
  IonToolbar,
  useIonAlert,
  useIonLoading,
} from '@ionic/react'
import { useQuery, gql, useMutation } from '@apollo/client'
import { add } from 'ionicons/icons'

export interface TodoList {
  id: string
  title: string
  todos: { edges: { node: Todo }[] }
}

export interface Todo {
  id: string
  title: string
  notes: string
  complete: boolean
}

// Retrieve collection with Live Queries
export const TODOLIST_QUERY = gql`
  query @live {
    todoListCollection(first: 10) {
      edges {
        node {
          id
          title
          todos(first: 100) {
            edges {
              node {
                id
              }
            }
          }
        }
      }
    }
  }
`

const CREATE_LIST_MUTATION = gql`
  mutation CreateList($title: String!) {
    todoListCreate(input: { title: $title, todos: [] }) {
      todoList {
        id
        title
        todos(first: 100) {
          edges {
            node {
              id
            }
          }
        }
      }
    }
  }
`
const Home: React.FC = () => {
  // Query our collection on load
  const { data, loading, error } = useQuery<{
    todoListCollection: { edges: { node: TodoList }[] }
  }>(TODOLIST_QUERY)
  const [createList] = useMutation(CREATE_LIST_MUTATION)
  const [presentAlert] = useIonAlert()
  const [showLoading, hideLoading] = useIonLoading()

  const addList = () => {
    presentAlert({
      header: 'Please enter a name for your new list',
      buttons: ['Create List'],
      inputs: [
        {
          name: 'name',
          placeholder: 'Tasklist',
          min: 2,
        },
      ],
      onDidDismiss: async (ev: CustomEvent) => {
        await showLoading()
        console.log('run mutation..')

        // Run the mutation
        const res = await createList({
          variables: { title: ev.detail.data.values.name },
        })
        console.log('๐Ÿš€ ~ file: Home.tsx:97 ~ onDidDismiss: ~ res', res)

        hideLoading()
      },
    })
  }

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar color="primary">
          <IonTitle>My Todos</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent className="ion-padding">
        {data && !loading && !error && (
          <>
            {data?.todoListCollection?.edges?.map(({ node }) => (
              <IonCard button routerLink={`/home/${node?.id}`} key={node?.id}>
                <IonCardHeader>
                  <IonCardTitle>{node?.title}</IonCardTitle>
                </IonCardHeader>
                <IonCardContent>
                  Tasks: {node?.todos?.edges.length ?? 0}
                </IonCardContent>
              </IonCard>
            ))}
          </>
        )}

        <IonButton expand="block" onClick={() => addList()}>
          <IonIcon icon={add} slot="start"></IonIcon>
          Create new List
        </IonButton>
      </IonContent>
    </IonPage>
  )
}

export default Home

We also have the TodoList which is written below:

import {
  IonBackButton,
  IonButton,
  IonButtons,
  IonContent,
  IonHeader,
  IonIcon,
  IonInput,
  IonItem,
  IonItemOption,
  IonItemOptions,
  IonItemSliding,
  IonLabel,
  IonList,
  IonPage,
  IonTitle,
  IonToolbar,
  useIonLoading,
  useIonRouter,
} from '@ionic/react'
import React, { useRef, useState } from 'react'
import { RouteComponentProps } from 'react-router'
import { useQuery, gql, useMutation } from '@apollo/client'
import { checkmarkDoneOutline, trashBinOutline } from 'ionicons/icons'
import { Todo, TODOLIST_QUERY } from './Home'

interface TodolistPageProps
  extends RouteComponentProps<{
    id: string
  }> {}

const TODOLIST_DETAILS_QUERY = gql`
  query GetTodolistDetails($id: ID!) {
    todoList(by: { id: $id }) {
      title
      id
      todos(first: 100) {
        edges {
          node {
            id
            title
            notes
            complete
          }
        }
      }
    }
  }
`

const CREATE_TODO_MUTATION = gql`
  mutation CreateTodo($title: String!) {
    todoCreate(input: { title: $title, complete: false, notes: "" }) {
      todo {
        id
      }
    }
  }
`

const UPDATE_TODOLIST_MUTATION = gql`
  mutation UpdateTodolist($listID: ID!, $todoID: ID!) {
    todoListUpdate(by: { id: $listID }, input: { todos: [{ link: $todoID }] }) {
      todoList {
        title
        todos(first: 100) {
          edges {
            node {
              id
            }
          }
        }
      }
    }
  }
`

const DELETE_TODO_MUTATION = gql`
  mutation DeleteTodo($id: ID!) {
    todoDelete(by: { id: $id }) {
      deletedId
    }
  }
`

const UPDATE_TODO_MUTATION = gql`
  mutation UpdateTodo($id: ID!, $complete: Boolean) {
    todoUpdate(by: { id: $id }, input: { complete: $complete }) {
      todo {
        title
        complete
        id
      }
    }
  }
`

const DELETE_TODOLIST_MUTATION = gql`
  mutation DeleteTodolist($id: ID!) {
    todoListDelete(by: { id: $id }) {
      deletedId
    }
  }
`

const Todolist: React.FC<TodolistPageProps> = ({ match }) => {
  const { data, loading, error } = useQuery<{
    todoList: { id: String; title: String; todos: { edges: { node: Todo }[] } }
  }>(TODOLIST_DETAILS_QUERY, {
    variables: { id: match.params.id },
  })
  const [updateTodolist] = useMutation(UPDATE_TODOLIST_MUTATION)
  const [deleteTodolist] = useMutation(DELETE_TODOLIST_MUTATION)

  const [createTodo] = useMutation(CREATE_TODO_MUTATION)
  const [deleteTodo] = useMutation(DELETE_TODO_MUTATION)
  const [updateTodo] = useMutation(UPDATE_TODO_MUTATION)

  const [title, setTitle] = useState('')
  const listRef = useRef<HTMLIonListElement>(null)
  const inputRef = useRef<HTMLIonInputElement>(null)
  const ionRouter = useIonRouter()

  const [showLoading, hideLoading] = useIonLoading()

  const addTodo = async () => {
    showLoading()
    // Create a todo
    const createTodoResult = await createTodo({
      variables: { title },
    })

    // Access the result ID
    const todoId = createTodoResult.data.todoCreate.todo.id

    // Update the todolikst to include the todo
    await updateTodolist({
      variables: { listID: match.params.id, todoID: todoId },
      refetchQueries: [TODOLIST_DETAILS_QUERY],
    })

    inputRef.current!.value = ''
    hideLoading()
  }

  const deleteTodoById = (id: string) => {
    deleteTodo({
      variables: { id: id },
      refetchQueries: [TODOLIST_DETAILS_QUERY],
    })
  }

  const updateTodoById = async (id: string) => {
    await updateTodo({
      variables: { id: id, complete: true },
      refetchQueries: [TODOLIST_DETAILS_QUERY],
    })
    listRef.current?.closeSlidingItems()
  }

  const deleteList = () => {
    deleteTodolist({
      variables: { id: match.params.id },
      refetchQueries: [TODOLIST_QUERY],
    })
    ionRouter.goBack()
  }

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar color="secondary">
          <IonButtons slot="start">
            <IonBackButton defaultHref="/home" />
          </IonButtons>
          <IonTitle>{data?.todoList.title}</IonTitle>
          <IonButtons slot="end">
            <IonButton onClick={() => deleteList()}>
              <IonIcon icon={trashBinOutline} slot="icon-only" />
            </IonButton>
          </IonButtons>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        <IonItem>
          <IonInput
            ref={inputRef}
            placeholder="Use Grafbase"
            onIonChange={(e: any) => setTitle(e.target.value)}
          ></IonInput>
          <IonButton expand="full" slot="end" onClick={() => addTodo()}>
            Add Task
          </IonButton>
        </IonItem>
        {data && !loading && !error && (
          <IonList ref={listRef}>
            {data?.todoList?.todos?.edges.map(({ node }) => (
              <IonItemSliding key={node.id}>
                <IonItem>
                  <span
                    style={{
                      textDecoration: node.complete ? 'line-through' : '',
                      opacity: node.complete ? 0.4 : 1,
                    }}
                  >
                    {node.title}
                  </span>
                </IonItem>

                <IonItemOptions side="start">
                  {!node.complete && (
                    <IonItemOption
                      color="success"
                      onClick={() => updateTodoById(node.id)}
                    >
                      <IonIcon
                        icon={checkmarkDoneOutline}
                        slot="icon-only"
                      ></IonIcon>
                    </IonItemOption>
                  )}
                </IonItemOptions>

                <IonItemOptions side="end">
                  <IonItemOption
                    color="danger"
                    onClick={() => deleteTodoById(node.id)}
                  >
                    <IonIcon icon={trashBinOutline} slot="icon-only"></IonIcon>
                  </IonItemOption>
                </IonItemOptions>
              </IonItemSliding>
            ))}
          </IonList>
        )}
        {data && data?.todoList?.todos?.edges.length === 0 && (
          <div
            className="ion-padding"
            style={{ textAlign: 'center', fontSize: 'large' }}
          >
            <IonLabel color="medium">Add your first item now!</IonLabel>
          </div>
        )}
      </IonContent>
    </IonPage>
  )
}

export default Todolist

This is what our App.tsx will contain:

import { Redirect, Route } from 'react-router-dom';
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import Home from './pages/Home';

/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css'

/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css'
import '@ionic/react/css/structure.css'
import '@ionic/react/css/typography.css'

/* Optional CSS utils that can be commented out */
import '@ionic/react/css/padding.css'
import '@ionic/react/css/float-elements.css'
import '@ionic/react/css/text-alignment.css'
import '@ionic/react/css/text-transformation.css'
import '@ionic/react/css/flex-utils.css'
import '@ionic/react/css/display.css'

/* Theme variables */
import './theme/variables.css'
import Todolist from './pages/Todolist'

setupIonicReact()

const App: React.FC = () => (
  <IonApp>
    <IonReactRouter>
      <IonRouterOutlet>
        <Route exact path="/home">
          <Home />
        </Route>
        <Route exact path="/home/:id" component={Todolist} />
        <Route exact path="/">
          <Redirect to="/home" />
        </Route>
      </IonRouterOutlet>
    </IonReactRouter>
  </IonApp>
)

export default App;

Then to finish it up in the index.tsx

 import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import * as serviceWorkerRegistration from './serviceWorkerRegistration'
import reportWebVitals from './reportWebVitals'
import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  split,
  HttpLink,
} from '@apollo/client'
import { isLiveQuery, SSELink } from '@grafbase/apollo-link'
import { getOperationAST } from 'graphql'
import { SignJWT } from 'jose'

//Example of genreating a JWT:
// const secret = new Uint8Array(
//   'mysupersecretk3y!'.split('').map((c) => c.charCodeAt(0)),
// )

// const getToken = () => {
//   return new SignJWT({ sub: 'user_1234', groups: [] })
//     .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
//     .setIssuer('https://devdactic.com')
//     .setIssuedAt()
//     .setExpirationTime('2h')
//     .sign(secret)
// }

// getToken().then((token) => {
//   console.log(token);
// })

// const GRAFBASE_API_URL = 'http://127.0.0.1:4000/graphql'
const GRAFBASE_API_URL =
  'https://todo-main-dkingofcode.grafbase.app/graphql'

// Use JWT in a real app or API Key for testing with x-api-key
const JWT_TOKEN =
  'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMzQiLCJncm91cHMiOltdLCJpc3MiOiJodHRwczovL2RldmRhY3RpYy5jb20iLCJpYXQiOjE2OTE5MzAxMjMsImV4cCI6MTY5MTkzNzMyM30.DbaFWX1ROsOAubsZBpQdKDwaJsimY5-WNkQyXihHuls'
export const createApolloLink = () => {
  const sseLink = new SSELink({
    uri: GRAFBASE_API_URL,
    headers: {
      authorization: JWT_TOKEN,
    },
  })

  const httpLink = new HttpLink({
    uri: GRAFBASE_API_URL,
    headers: {
      authorization: JWT_TOKEN,
    },
  })

  return split(
    ({ query, operationName, variables }) =>
      isLiveQuery(getOperationAST(query, operationName), variables),
    sseLink,
    httpLink,
  )
}

const link = createApolloLink()

const client = new ApolloClient({
  link,
  cache: new InMemoryCache(),
})

const container = document.getElementById('root')
const root = createRoot(container!)
root.render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </React.StrictMode>,
)

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://cra.link/PWA
serviceWorkerRegistration.unregister()

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()

Now you will need to deploy the grafbase api you first created by creating a git repository, first you will need to got to the grafbase root directory in your terminal and type:

command
git init

Then after initialising your root directory, you will add all your code to the stagin area using this command:

command
git add .

After adding all your code, you will commit this code using the command:

command
git commit -m "first schema"

You can change the commit message if you want to, then you got to your github account and create a new repository and then you copy the url link given to you, then you connect it to your directory using this command:

command
git remote add origin [remote-url]

Then you push your code to github using this command:

command
git push -u origin master

You can change your remote branch to main before pushing if you want to, it's up to you.

Then since you have successfully uploaded your code to github, then you can go to grafbase and connect your githhub account to grafbase, after that you will type the name of your grafbase api repository you just uploaded and search for it on grafbase, if you have connected your github account you could just search for the repositiory straight away. Then when you have found it, you will have to import the repository to grafbase using the import button right next to the repo name. Then you will be directed to a deployment page, where you can deploy your api.

You can now change the grafbase api url in the index.tsx to the grafbase api url given to you after you have deployed. To get it you click on the connect button, which will show you your url and your api key.

ย