Using Nuxt-Ionic: Prisma SQLite & Ionic Framework w/ Capacitor

Aaron K Saunders - Jul 1 '22 - - Dev Community

Logos of tech

This is a follow-up to my video on using nuxt-ionic to build and deploy a mobile application using Ionic Framework Vue Components and Capacitor for deploying to native devices.

Link to the first video Intro To Nuxt Ionic

In this post we will add a Camera using Capacitor Camera Plugin, add a backend with using Prisma with SQLite as our database, deploy to mobile device and run the server creating a full-stack mobile experience using VueJS Nuxt 3 and Ionic Framework

The Video

Installing Prisma

npm install prisma@latest typescript ts-node @types/node --save-dev
npm install @prisma/client@latest
npx prisma init --datasource-provider sqlite
Enter fullscreen mode Exit fullscreen mode

Server Related Actions

Create the database models for the application. Notice I have specified the database url directly in the file and not used an .env file.

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./mydata.db"
}

model ImagePost {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title     String   
  content   String?
  image     String?
  published Boolean  @default(false)
}
Enter fullscreen mode Exit fullscreen mode

Command to migrate changes to database

npx prisma migrate dev --name initialize_db
Enter fullscreen mode Exit fullscreen mode

Since we are using Nuxt for our backend we will create two api routes in the server/api/ directory; one for getting all of ImagePost records and another for adding them.

Prisma client code

// server/utils/prisma-client.ts

import prismaClient from '@prisma/client';

const { PrismaClient } = prismaClient;

export default new PrismaClient({});
Enter fullscreen mode Exit fullscreen mode

Server API for getting all of the ImagePosts is below. We are using the prisma client we initialized above.

// server/api/getAllPosts

import dbClient from '../utils/prisma-client';

export default defineEventHandler(async ({ req }) => {

  const data = await dbClient.imagePost.findMany();
  return  data 
});
Enter fullscreen mode Exit fullscreen mode
// server/api/addPost

import dbClient from '../utils/prisma-client';

export default defineEventHandler(async (event) => {
    const body = await useBody(event)
    console.log(body);

    const resp = await dbClient.imagePost.create({
        data: {
            title: body.title,
            content:  body.content,
            image:  body.image,
        }
    });
    return { resp }
});
Enter fullscreen mode Exit fullscreen mode

You will modify your runtime configuration to support the url for the api when you are running locally and developing in the browser which is development; and when testing on device/phone which is production production in this configuration.

// nuxt.config.ts

import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      API_URL: process.env.NODE_ENV === "development" ? "/api" : 'http://192.168.1.56:3000/api',
    }
  },
  modules: ['nuxt-ionic'],
  ssr: true,
  meta: [{
    name: "viewport",
    content: "viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
  }
  ],
  ionic: {
    integrations: {
      //
      // pwa: true,
      // router: true,
    },
    css: {
      // basic: false,
      // core: false,
      utilities: true,
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Client Related Actions

Code Changes in HomePage.vue

template

<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>Nuxt Ionic Prisma Photo Demo</ion-title>
        <ion-buttons slot="end">
          <ion-button>LOGOUT</ion-button>
        </ion-buttons>
      </ion-toolbar>
    </ion-header>
    <ion-content class="ion-padding">

      <!-- indicators -->
      <ion-loading :is-open="pending" message="LOADING..."></ion-loading>
      <ion-loading :is-open="saving" message="SAVING..."></ion-loading>

            <!-- modal to get title and content before saving -->
      <image-post-input
        :showImagePostInput="showImagePostInput"
        :imageURL="imageURL"
        @image-post-submit="doSave"
        @image-post-cancel="showImagePostInput = false"
      />
      <p>
        Sample app with Nuxt for server and client mobile app. Prisma for saving the data
        to database and Ionic / Capacitor for device capabilities
      </p>

            <!-- click to take photo and save to database -->
      <ion-button @click="doCamera"> CREATE IMAGE POST </ion-button>

      <!-- loop through records in database -->
      <ion-card v-for="item in data" :key="item?.id">
        <ion-card-header>
          <ion-card-title>{{ item?.title }}</ion-card-title>
          <ion-card-subtitle>{{ item?.content }}</ion-card-subtitle>
        </ion-card-header>
        <ion-card-content v-if="item?.image">
          <ion-img :src="(item?.image as any)" />
        </ion-card-content>
      </ion-card>
    </ion-content>

  </ion-page>
</template>
Enter fullscreen mode Exit fullscreen mode

imports for the component main HomePage Component

import { Camera, CameraResultType, ImageOptions } from "@capacitor/camera";
import { alertController } from "@ionic/vue";
import { ref } from "vue";
Enter fullscreen mode Exit fullscreen mode

extra code added to the head of the page to load ionic pwa components, this helps us to use a devices camera when running the app as a PWA

useHead({
  script: [
    {
      async: true,
      crossorigin: "anonymous",
      type: "module",
      src:
        "https://unpkg.com/@ionic/pwa-elements@latest/dist/ionicpwaelements/ionicpwaelements.esm.js",
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

local properties used in the component

// router
const ionRouter = useIonRouter();

// ref holding the image from the camera
const imageURL = ref<string | null>(null);

// flag for rendering the saving indicator ui element
const saving = ref<boolean>(false);

// flag for rendering the ImagePostInput Modal
const showImagePostInput = ref<boolean>(false);

// api url from config to support dev and prod api calls
const API_URL = useRuntimeConfig().public.API_URL;
Enter fullscreen mode Exit fullscreen mode

query the database using the api route we created

const { data, pending, error, refresh } = await useAsyncData<ImagePostArray>(
  "posts",
  () => $fetch(`${API_URL}/getAllPosts`)
);
Enter fullscreen mode Exit fullscreen mode

We using the pending to indicate we are loading, data is the result from the query, error is used to show an alert if there is a problem and refresh is used to reload the data after we add a new ImagePost

Alert code for when there is an error and the function we use for displaying an alert since it appears multiple times in the application

const doAlert = (options: { header: string; message: string }) => {
  return alertController
    .create({ buttons: ["OK"], ...options })
    .then((alert) => alert.present());
};

// display error if necessary
if (error?.value) {
  doAlert({
    header: "Error Loading Data",
    message: (error?.value as Error)?.message,
  });
}
Enter fullscreen mode Exit fullscreen mode

This is the function to take the picture, it is called when button is clicked in the template. If an image is captured, we set the ref imageURL which will render the image in the input form. the last thing we do is set the boolean flag to show the modal component ImagePostInput

const doCamera = async () => {
  const image = await Camera.getPhoto({
    quality: 90,
    // allowEditing: true,
    correctOrientation: true,
    width: 400,
    resultType: CameraResultType.Base64,
  });

  imageURL.value = `data:${image.format};base64,${image.base64String}`;

  // show dialog to confirm image
  showImagePostInput.value = true;
};
Enter fullscreen mode Exit fullscreen mode

function for saving the ImagePost to the data base using the server api route we created. it is called if the submit event is emitted from the dialog

const doSave = async (
  { title, content }: { title: string; content: string }
) => {
  // hide the input form
  showImagePostInput.value = false;

  // show the saving indicator
  saving.value = true;

  try {
    const dataToSave: Partial<ImagePost> = {
      title,
      content,
      image: imageURL.value,
      published: true,
    };

    await $fetch(`${API_URL}/post`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(dataToSave),
    });
    imageURL.value = null;

    // reload the data for the UI
    await refresh();

    // hide saving ui element
    saving.value = false;

    // display alert to indicate successful save
    doAlert({
      header: "Saving Image Post",
      message: "Image saved successfully",
    });
  } catch (error) {
    saving.value = false;
    doAlert({
      header: "Error",
      message: error.message,
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

ImagePostInput Component - Capture additional Data and Save To Database

Input form for capturing additional information about the photo, this is trigger to be opened after the user takes a photo with the camera

<template>
  <IonModal :is-open="showImagePostInput" 
            v-on:ion-modal-did-dismiss="closeModal">
    <IonHeader>
      <IonToolbar>
        <IonTitle>Image Post Input</IonTitle>
      </IonToolbar>
    </IonHeader>
    <IonContent class="ion-padding">
      <IonItem>
        <IonLabel position="floating">Title</IonLabel>
        <IonInput v-model="data.title" />
      </IonItem>
      <IonItem>
        <IonLabel position="floating">Content</IonLabel>
        <IonInput v-model="data.content" />
      </IonItem>
      <div v-if="imageURL" style="margin: auto; width: 50%; margin-top: 18px">
        <ion-img :src="imageURL" />
      </div>
      <div style="float: right; margin: 12px">
        <IonButton @click="closeModal" color="danger">CANCEL</IonButton>
        <IonButton @click="onSubmit">SAVE</IonButton>
      </div>
    </IonContent>
  </IonModal>
</template>
Enter fullscreen mode Exit fullscreen mode

Code section

defineProps({
  // flag to show/hide the modal
  showImagePostInput: {
    type: Boolean,
    default: false,
  },
  // image URL data
  imageURL: {
    type: String,
    default: "",
  },
});

// events emmitted by the component
const emit = defineEmits<{
  // event to close the modal
  (event: "image-post-cancel"): void;
  // event to save the data
  (
    event: "image-post-submit",
    { title, content }: { title: string; content: string }
  ): void;
}>();

// data from the component form
const data = ref({
  title: "",
  content: "",
});

/**
 * close modal take no action
 */
const closeModal = () => {
  emit("image-post-cancel");
};

/**
 * close modal and pass form data
 */
const onSubmit = () => {
  emit("image-post-submit", {
    title: data.value.title,
    content: data.value.content,
  });
};
Enter fullscreen mode Exit fullscreen mode

Capacitor Configuration and Setup

$ npm install --save @capacitor/core @capacitor/cli
$ npx cap init
Enter fullscreen mode Exit fullscreen mode

Add your platform that you want to use, Android or IOS

$ npx cap add android
$ npx cap add ios
Enter fullscreen mode Exit fullscreen mode

Additional Configuration

Since we are using the camera we will need to install the camera component

npm install @capacitor/camera
npx cap sync
Enter fullscreen mode Exit fullscreen mode

See link for instruction to install the capacitor camera plugin Camera Capacitor Plugin API - Capacitor (capacitorjs.com)

Add appropriate permissions in Android and IOS to access the camera

Android Changes - AndroidManifest.xml

<!-- Permissions -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Enter fullscreen mode Exit fullscreen mode

IOS Changes in Info.plist

  <key>NSPhotoLibraryUsageDescription</key>
    <string>Photo Useage Description</string>
    <key>NSPhotoLibraryAddUsageDescription</key>
    <string>Photo Library Add Description</string>
    <key>NSCameraUsageDescription</key>
    <string>Useage Description</string>
Enter fullscreen mode Exit fullscreen mode

Running The Code

This code should now run fine as a PWA and provide a camera for you to take a photo and save to the data base.

if you want to deploy to a mobile device you need to have the backend running on a known ip address and then make sure you set that ip address in the nuxt configuration

runtimeConfig: {
    public: {
      API_URL: process.env.NODE_ENV === "development" ? "/api" : [YOUR SERVER IP ADDRESS],
    }
  },
Enter fullscreen mode Exit fullscreen mode

now you can deploy the app to you mobile device and then start up your nuxt server and you should be good to go.

Source Code

Nuxt 3 Prisma Ionic Framework Capacitor Camera Mobile App using Nuxt Ionic Module

Description

Using Nuxt-Ionic: Full Stack Mobile With Prisma SQLite & Ionic Framework w/ Capacitor #nuxt #ionic #prisma

This is a follow-up to my video on using nuxt-ionic to build and deploy a mobile application using Ionic Framework Vue Components and Capacitor for deploying to native devices.

In this post, we will add a Camera using Capacitor Camera Plugin, add a backend using Prisma with SQLite as our database, deploy it to a mobile device and run the server creating a full-stack mobile experience using VueJS Nuxt 3 and Ionic Framework

Setup

Make sure to install the dependencies:

# yarn
yarn install
# npm
npm
Enter fullscreen mode Exit fullscreen mode

Related Links

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .