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
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)
}
Command to migrate changes to database
npx prisma migrate dev --name initialize_db
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({});
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
});
// 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 }
});
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,
},
},
})
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>
imports for the component main HomePage Component
import { Camera, CameraResultType, ImageOptions } from "@capacitor/camera";
import { alertController } from "@ionic/vue";
import { ref } from "vue";
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",
},
],
});
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;
query the database using the api route we created
const { data, pending, error, refresh } = await useAsyncData<ImagePostArray>(
"posts",
() => $fetch(`${API_URL}/getAllPosts`)
);
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,
});
}
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;
};
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,
});
}
};
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>
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,
});
};
Capacitor Configuration and Setup
$ npm install --save @capacitor/core @capacitor/cli
$ npx cap init
Add your platform that you want to use, Android or IOS
$ npx cap add android
$ npx cap add ios
Additional Configuration
Since we are using the camera we will need to install the camera component
npm install @capacitor/camera
npx cap sync
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" />
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>
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],
}
},
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
- Look at the nuxt 3 documentation to learn more.
- Look at the Nuxt Ionic to learn more.
- See associated video Using Nuxt-Ionic: Full Stack Mobile With Prisma SQLite & Ionic Framework w/ Capacitor
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
โฆRelated Links
- Module - https://ionic.roe.dev/
- VS Code Plugin - https://marketplace.visualstudio.com/items?itemName=ionic.ionic
- Ionic Vue - https://ionicframework.com/vue
- Capacitor - https://capacitorjs.com/docs/apis/camera
- Prisma - https://www.prisma.io/
- Volar Typescript Issue [FIXED] - https://github.com/ionic-team/ionic-framework/issues/24169#ionic
- Source Code - https://github.com/aaronksaunders/nuxt-ionic-prisma-camera-app/
Top comments (0)