Building a serverless Web app on AWS with Amplify, CDK(python) and Nuxt(Part 3)

I love creating and writing about the creation of software.
Search for a command to run...

I love creating and writing about the creation of software.
No comments yet. Be the first to comment.
This series would feature all posts about building serverless applications with AWS amplify and CDK
Hey wassup, Welcome! I'm Rosius In this blog series, I will be showing you how to build a serverless newspaper web application and host it to the cloud with complete continuous Integration/ continuous deployment pipelines. We will be using AWS ser...
Watch the complete Video on Youtube https://www.youtube.com/watch?v=kKX-8L_R2XM Prerequisites Before proceeding make sure you have these dependencies Docker Python 3.11 and above AWS CLI Full access to a foundation model. For this workshop, we'll...

Introduction In a previous workshop, Building event-driven applications with AWS sam and python we built an e-commerce ordering service using AWS SAM, API Gateway, Lambda and Python. Here's the solutions architecture. In that workshop, whenever a ne...

Github Repository https://github.com/EducloudHQ/introduction_to_gen_ai Skill up with serverless on Educloud https://www.educloud.academy/content/da99ad07-7efa-41e7-ba50-b18e6b89e10d This post is an introductory lesson to building AI-enhanced serverle...

Introduction Serverless computing is a cloud-based approach to application development that eliminates the need for provisioning and managing servers. Instead, developers focus on writing code that is triggered by events such as HTTP requests or data...

https://github.com/trey-rosius/sam_stepfunctions Hey!!! How you doing? In this post, we’ll look at how to build a workflow, using SAM as IaC, Appsync, and python. Prerequisites Install AWS Cli (https://docs.aws.amazon.com/cli/latest/userguide/cli-c...

Hi, Welcome to Part 3 of this post series.
In Part 1, we covered the steps to get started in deploying an Amplify Application using AWS CDK.
In Part 2, we added a backend to the application with Amplify tools and Amplify CLI. We added categories such as authentication, API, and Storage.
In this part, we will start building the application from the login page.
Install these 2 modules before proceeding
npm install @aws-amplify/ui-components
@aws-amplify/ui-vue
Create a file called loginpage.vue inside the pages directory and type in this code
<template>
<div class="container">
<div class="content">
<div class="left">
<h2 class="article-title article-title--large">
<a href="#" class="article-link">The Newspaper for <mark class="mark mark--primary">Those</mark> who can <mark class="mark mark--secondary">Read</mark></a>
</h2>
</div>
<div class="right">
<amplify-authenticator v-if="authState !== 'signedin'" class="signIn" />
<div class="authenticated" v-if="authState === 'signedin' && user">
<update-profile :email="user.attributes.email"
:userId="user.attributes.sub" :username="user.username" />
<button @click="signOut">Sign Out</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { onAuthUIStateChange } from '@aws-amplify/ui-components'
import { Auth } from 'aws-amplify';
import UpdateProfile from '../components/UpdateProfile.vue';
export default {
components:{UpdateProfile},
created() {
onAuthUIStateChange((authState, authData) => {
this.authState = authState;
this.user = authData;
console.log(this.user);
})
},
data() {
return { user: undefined, authState: undefined }
},
methods: {
signOut(){
console.log("clicked sign out");
Auth.signOut();
this.authState = undefined;
}
},
beforeDestroy() {
return onAuthUIStateChange;
}
}
</script>
amplify-authenticator provides ready-made UI components for building the authentication pages of our application faster.
We first verify if the user is signed in, by checking the authState.
If the user is signed in, grab the user details and send them as props to another page called updateprofile.vue.
If not, display the login screen. Here's how the login page looks like.

<template>
<div class="update-profile">
<input type="file" @change="onFileChange" />
<div id="preview">
<img class= "profilepic" v-if="url" :src="url" />
</div>
<div class="sub-load">
<Loading v-if="loading"/>
<input v-else type="submit" value="Upload Image" class="btn" @click="upload">
</div>
</div>
</template>
<script>
import { API } from 'aws-amplify';
import { Storage } from 'aws-amplify';
import { v4 as uuidv4 } from 'uuid';
import profile_icon from '@/assets/profile.png';
import {createUser} from '../src/graphql/mutations';
export default {
mounted(){
this.first_name = this.username;
this.email = this.email;
this.userId = this.userId;
console.log(this.email);
},
props:{
username:String,
email:String,
userId:String,
},
data(){
return{
first_name:String,
url:profile_icon,
filename:String,
loading:false,
filePath:String
}
},
methods:{
async addUserToDb(){
const uuid = uuidv4();
const {username,email,filename,userId} = this;
const signedURL = await Storage.get(filename);
const user = {id:userId,username,email,profilePicUrl:signedURL,userType:"ADMIN"};
console.log("signed url"+signedURL);
await API.graphql({
query:createUser,
variables:{input:user},
}).then((result) =>{
this.loading = false;
console.log("result is"+result);
})
this.loading = false;
console.log("successfully uploaded");
this.$router.push({name:'index',params:{id: uuid}});
},
onFileChange(e) {
const file = e.target.files[0];
console.log(file);
this.filename =file.name;
this.filePath = file;
this.url = URL.createObjectURL(file);
},
upload(e) {
console.log(this.filename);
this.loading = true;
const result = Storage.put(this.filename, this.filePath, {
contentType:this.filePath.type,
progressCallback(progress) {
if(`${progress.loaded}` === `${progress.total}` ){
}
console.log(`Uploaded: ${progress.loaded}/${progress.total}`);
},
}).then((result) =>{
this.addUserToDb();
})
}
}
}
</script>
Here's what's happening. Firstly, we get the passed-in user data(username, email and userId) as props. Then we give the user the possibility to select an image as a profile picture. When the upload button is clicked, the image is uploaded to an s3 bucket, the pre-signed URL is gotten and saved to the user table through the createUser mutation of our GraphQl API.

<template>
<div class="container">
<div class="content">
<div class="left">
<h2 class="article-title article-title--large">
<a href="#" class="article-link">Talk <mark class="mark mark--tertiary">That</mark> Talk</a>
</h2>
</div>
<div class="right">
<div v-if="image" class="img-container">
<input class="pickImage" type="file" @change="onFileChange" />
<div id="preview-pic">
<img class= "postpic" v-if="url" :src="url" />
</div>
</div>
<div class="checkbox">
<input type="checkbox" name="noImage" id="noImage" class="noImageCheck" @click="showImagePicker">
<label for="noImage" class="noImage"> No Image</label>
</div>
<input type="text" class="postTitle" placeholder= "Post title" v-model="postTitle" name="postTitle" id="postTitle">
<textarea placeholder="What's new ?" v-model="postText" class="article__post" rows="4" cols="10"/><br>
<Loading v-if="loading" />
<input type="submit" v-else-if="image" value="Create Article" class="article__btn" @click="addPostWithImage">
<input type="submit" v-else value="Create Article" class="article__btn" @click="addPostWithoutImage">
</div>
</div>
</div>
</template>
<script>
import { onAuthUIStateChange } from '@aws-amplify/ui-components'
import { Auth,API,Storage } from 'aws-amplify';
import add_image from '@/assets/add_pic.svg';
import { v4 as uuidv4 } from 'uuid';
import {createPost} from '../src/graphql/mutations';
export default {
mounted() {
console.log('Params: ', this.$route.params);
this.userId = this.$route.params.userId;
console.log("user id is"+this.userId);
},
data(){
return {
postTitle:"",
postText:'',
loading:false,
image:true,
url:add_image,
filename:String,
filePath:String,
}
},
methods:{
showImagePicker(){
this.image = !this.image;
},
async addPostToDb(){
const uuid = uuidv4();
const {userId,postText,filename,postTitle,image} = this;
const signedURL = await Storage.get(filename);
const post = {id:uuid,userId:userId,postText,title:postTitle,postType:"BUSINESS",postStatus:"CREATED",postImageUrl:signedURL,image};
console.log("signed url"+signedURL);
await API.graphql({
query:createPost,
variables:{input:post},
}).then((result) =>{
console.log("result is"+result);
this.loading = false;
})
console.log("successfully uploaded");
this.postText = "";
this.loading = false;
//this.$emit('close');
},
onFileChange(e) {
const file = e.target.files[0];
console.log(file);
this.filename =file.name;
this.filePath = file;
this.url = URL.createObjectURL(file);
},
addPostWithImage(e) {
console.log("clicked");
this.loading = true;
const result = Storage.put(this.filename, this.filePath, {
contentType:this.filePath.type,
progressCallback(progress) {
if(`${progress.loaded}` === `${progress.total}` ){
}
console.log(`Uploaded: ${progress.loaded}/${progress.total}`);
},
}).then((result) =>{
this.addPostToDb();
})
},
async addPostWithoutImage(e){
const uuid = uuidv4();
const {userId,postText,postTitle,image} = this;
const post = {id:uuid,userId:userId,postText,title:postTitle,postType:"BUSINESS",postStatus:"CREATED",image};
await API.graphql({
query:createPost,
variables:{input:post},
}).then((result) =>{
console.log("result is"+result);
this.loading = false;
})
console.log("successfully uploaded");
this.postText = "";
this.loading = false;
}
}
}
</script>
This piece of code is similar to the updateprofile code.
You give the user the possibility to upload a post with or without an image. You save the post in the post table using createPost mutation .

Index page In the pages folder, open up the index.vue file and type in the following code
<template>
<div>
<header class="header responsive-wrapper">
<div class="header-left">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="233.445" height="52.805" viewBox="0 0 233.445 52.805">
<defs>
<clipPath id="clip-fox-news-logo">
<rect width="233.445" height="52.805" />
</clipPath>
</defs>
<g id="fox-news-logo" clip-path="url(#clip-fox-news-logo)">
<g id="Group_1" data-name="Group 1" transform="translate(-2179.585 601.093)">
<path id="Path_5" data-name="Path 5" d="M4.185-29.205H22.59v4.59H9.5v7.7h11.97v4.545H9.5V0H4.185Zm27.675,6.75a12.117,12.117,0,0,1,4.14.7,8.656,8.656,0,0,1,3.33,2.138q3.105,3.15,3.105,8.73,0,5.49-3.105,8.64A9.921,9.921,0,0,1,31.86.63,10.222,10.222,0,0,1,24.3-2.25Q21.2-5.355,21.2-10.89q0-5.625,3.105-8.73A10.217,10.217,0,0,1,31.86-22.455Zm5.265,11.52a8.372,8.372,0,0,0-1.35-5.2,4.5,4.5,0,0,0-3.757-1.733,5.013,5.013,0,0,0-3.937,1.733,7.654,7.654,0,0,0-1.53,5.2,8.27,8.27,0,0,0,1.372,5.2A4.534,4.534,0,0,0,31.68-4.005,4.989,4.989,0,0,0,35.6-5.738,7.654,7.654,0,0,0,37.125-10.935ZM64.62,0H58.14l-5.4-7.02L47.7,0H41.175l8.5-10.89-8.5-11.025H47.7l5.355,7.02,5.085-7.02h6.48L56.16-10.89ZM78.48-29.205l12.87,20.7v-20.7h5.31V0h-5.4L77.985-21.42V0h-5.31V-29.205Zm37.71,9.945a9.966,9.966,0,0,1,1.688,3.33,12.23,12.23,0,0,1,.563,3.465,20.182,20.182,0,0,1-.18,2.835H104.13a7.606,7.606,0,0,0,1.305,4.32,4.268,4.268,0,0,0,3.69,1.665q3.285,0,3.915-2.88H118.4A7.849,7.849,0,0,1,115.38-1.35,9.93,9.93,0,0,1,109.125.63a9.489,9.489,0,0,1-7.245-3.15,11.96,11.96,0,0,1-3.015-8.415,11.822,11.822,0,0,1,3.015-8.46,9.529,9.529,0,0,1,7.245-3.06A8.486,8.486,0,0,1,116.19-19.26Zm-7.02,1.035a4.385,4.385,0,0,0-3.51,1.463,6.578,6.578,0,0,0-1.485,3.758h9.315q0-3.555-2.25-4.77A4.4,4.4,0,0,0,109.17-18.225Zm13.455-3.645,4.41,15.615,4.185-14.4h4.815L140.22-6.21l4.455-15.66h5.31L142.785,0h-5.22l-4.05-13.1L129.555,0H124.29l-7.245-21.87Zm31.05,15.21q.09,3.105,4.545,3.1a4.934,4.934,0,0,0,2.813-.743,2.1,2.1,0,0,0,1.1-1.733,1.847,1.847,0,0,0-.675-1.53,6.639,6.639,0,0,0-2.115-.99l-4.77-1.71q-5.31-1.845-5.31-6.03a5.172,5.172,0,0,1,2.5-4.477,11.8,11.8,0,0,1,6.7-1.687,10.044,10.044,0,0,1,6.48,1.845,6,6,0,0,1,2.362,4.635h-4.995a2.191,2.191,0,0,0-1.035-1.71,4.856,4.856,0,0,0-2.7-.63,4.6,4.6,0,0,0-2.723.7,1.974,1.974,0,0,0-.967,1.6q0,1.575,2.655,2.34l5.175,1.935a10.264,10.264,0,0,1,3.938,2.182A4.576,4.576,0,0,1,168.03-6.12a5.8,5.8,0,0,1-2.565,4.68,11.1,11.1,0,0,1-7.02,2.07,11.753,11.753,0,0,1-7.02-1.913,6.353,6.353,0,0,1-2.7-5.377Z" transform="translate(2245 -561)" />
<path id="Path_1" data-name="Path 1" d="M2207.653-46.775c-.029,5.98,9.656-7.959,11.9-8.762a45.226,45.226,0,0,1,9.4-2.275,19.066,19.066,0,0,1,2.663-.116c4.366.13-3.9-18.154-3.9-18.154l-8.853.386s-6.824,3.109-7.306,9.208,1.265,4.691-.822,10.951c-1.45,4.351-2.432,3.276-3.084,5.217A25.988,25.988,0,0,0,2207.653-46.775Z" transform="translate(-0.475 -503)" />
<path id="Path_2" data-name="Path 2" d="M2232.822-46.775c.029,5.98-9.656-7.959-11.9-8.762a45.226,45.226,0,0,0-9.4-2.275,19.066,19.066,0,0,0-2.663-.116c-4.366.13,3.9-18.154,3.9-18.154l8.853.386s6.824,3.109,7.306,9.208-1.265,4.691.822,10.951c1.45,4.351,2.432,3.276,3.084,5.217A25.988,25.988,0,0,1,2232.822-46.775Z" transform="translate(-28 -503)" />
<circle id="Ellipse_1" data-name="Ellipse 1" cx="1" cy="1" r="1" transform="translate(2205 -559)" />
<path id="Path_3" data-name="Path 3" d="M2216.209-91.186c.2,1.456,9.045,4.389,11.518,8.66s3.761-15.174,2.68-16.427S2216.012-92.642,2216.209-91.186Z" transform="translate(0 -502)" />
<path id="Path_4" data-name="Path 4" d="M2230.791-91.186c-.2,1.456-9.045,4.389-11.518,8.66s-3.761-15.174-2.68-16.427S2230.988-92.642,2230.791-91.186Z" transform="translate(-34 -502)" />
</g>
</g>
</svg>
</div>
<div class="header-middle">
<a href="#" class="header-link">Call Us (237) 678 323 387</a>
<span>/</span>
<a href="#" class="header-link">rosius@ghost.com</a>
</div>
<div class="header-right">
<nav class="header-nav">
<a href="#" class="header-link" v-if="signedIn" @click="signUserOut">Logout</a>
<NuxtLink to="/loginpage" class="header-link" v-else> Login </NuxtLink>
<nuxt-link :to="{name:'createArticle',params:{userId:user.id}}" class="header-link header-link--button">Write Article</nuxt-link>
</nav>
<img class="article-author-img" :src="user.profilePicUrl" alt="Profile Picture">
</div>
</header>
<main class="responsive-wrapper">
<div class="page-title">
<h1>Latest Updates</h1>
</div>
<div class="magazine-layout">
<div class="magazine-column">
<article class="article">
<h2 class="article-title article-title--large">
<a href="#" class="article-link">The First Signs of <mark class="mark mark--primary">Alcoholic Liver</mark> Damage Are Not in the Liver</a>
</h2>
<div class="article-excerpt">
<p>The combination of my father's death and my personal back ground lit a fire in me to know more</p>
<p>He was admitted to the hospital on June 24, 2016.</p>
</div>
<div class="article-author">
<div class="article-author-img">
<img src="https://assets.codepen.io/285131/author-3.png" />
</div>
<div class="article-author-info">
<dl>
<dt>David Sherof</dt>
<dd>Reporter</dd>
</dl>
</div>
</div>
</article>
</div>
<div class="magazine-column" v-for="post in posts" :key="post.id">
<article v-if="post.image" class="article">
<figure class="article-img">
<img :src="post.postImageUrl" />
</figure>
<mark class="mark mark--secondary article-title">{{post.postType}}</mark>
<h2 class="article-title article-title--medium">
<a href="#" class="article-link">{{post.title}}</a>
</h2>
<div class="article-excerpt">
<p>{{post.postText}}</p>
</div>
<div class="article-author">
<div class="article-author-img">
<img :src="post.user.profilePicUrl" />
</div>
<div class="article-author-info">
<dl>
<dt>{{post.user.username}}</dt>
<dd>{{post.user.userType}}</dd>
</dl>
</div>
</div>
</article>
<article v-else class="article">
<h2 class="article-title article-title--large">
<a href="#" class="article-link"> <mark class="mark mark--primary">{{post.title}}</mark> </a>
</h2>
<div class="article-excerpt">
{{post.postText}}
</div>
<div class="article-author">
<div class="article-author-img">
<img :src="post.user.profilePicUrl" />
</div>
<div class="article-author-info">
<dl>
<dt>{{post.user.username}}</dt>
<dd>{{post.user.userType}}</dd>
</dl>
</div>
</div>
</article>
</div>
<div class="magazine-column">
<article class="article">
<figure class="article-img">
<img src="https://images.unsplash.com/photo-1512521743077-a42eeaaa963c?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1351&q=80" />
</figure>
<h2 class="article-title article-title--small">
<a href="#" class="article-link">To Become <mark class="mark mark--secondary">Happier</mark>, Ask Yourself These Two Questions Every Night</a>
</h2>
<div class="article-creditation">
<p>By Jonathan O'Connell</p>
</div>
</article>
<article class="article">
<figure class="article-img">
<img src="https://images.unsplash.com/photo-1569234817121-a2552baf4fd4?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1350&q=80" />
</figure>
<h2 class="article-title article-title--small">
<a href="#" class="article-link">10 Things I Stole From People Smarter Than Me</a>
</h2>
<div class="article-creditation">
<p>By Jonathan O'Connell</p>
</div>
</article>
</div>
<div class="magazine-column">
<article class="article">
<h2 class="article-title article-title--medium">
<a href="#" class="article-link">Traveller Visiting Ice Cave With Amazing Eye-Catching Scenes</a>
</h2>
<div class="article-excerpt">
<p>Slack has become indispensible for many businesses operation remotely during the pandemic. Here's what the acquisition could mean for users...</p>
</div>
<div class="article-author">
<div class="article-author-img">
<img src="https://assets.codepen.io/285131/author-2.png" />
</div>
<div class="article-author-info">
<dl>
<dt>James Robert</dt>
<dd>Editor</dd>
</dl>
</div>
</div>
</article>
<article class="article">
<small class="article-category">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28" height="22" viewBox="0 0 28 22">
<defs>
<clipPath id="clip-headphones">
<rect width="28" height="22"/>
</clipPath>
</defs>
<g id="headphones" clip-path="url(#clip-headphones)">
<g id="Group_2" data-name="Group 2" transform="translate(-3680 -609)">
<path id="Subtraction_1" data-name="Subtraction 1" d="M5,12H5A5.274,5.274,0,0,1,0,6.5,5.274,5.274,0,0,1,5,1V12Z" transform="translate(3680 619)"/>
<rect id="Rectangle_18" data-name="Rectangle 18" width="2" height="11" transform="translate(3686 620)"/>
<path id="Subtraction_5" data-name="Subtraction 5" d="M1.243,12H.045C.015,11.67,0,11.334,0,11A11,11,0,0,1,18.778,3.222,10.925,10.925,0,0,1,22,11c0,.331-.015.667-.045,1h-1.2a14.108,14.108,0,0,0-2.685-6.241A8.982,8.982,0,0,0,11,2,8.982,8.982,0,0,0,3.929,5.759,14.109,14.109,0,0,0,1.243,12Z" transform="translate(3683 609)"/>
<path id="Subtraction_6" data-name="Subtraction 6" d="M5,0H5A5.274,5.274,0,0,0,0,5.5,5.274,5.274,0,0,0,5,11V0Z" transform="translate(3708 631) rotate(180)"/>
<rect id="Rectangle_23" data-name="Rectangle 23" width="2" height="11" transform="translate(3700 620)"/>
<path id="Path_8" data-name="Path 8" d="M.156-.031,2.125-1.687,2,2H0Z" transform="translate(3683 619)"/>
<path id="Path_9" data-name="Path 9" d="M1.969-.031,0-1.687.125,2h2Z" transform="translate(3702.875 619)"/>
</g>
</g>
</svg>
<span>Post Reports / Podcast</span>
</small>
<h2 class="article-title article-title--medium">
<a href="#" class="article-link">Things to Do After 6 P.M Will Enrich <mark class="mark mark--tertiary">Your Life</mark></a>
</h2>
<div class="article-podcast-player">
<button class="podcast-play-button">
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" fill="#000000" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"></rect><path d="M232.3125,114.34375,88.34375,26.35937A15.99781,15.99781,0,0,0,64,40.00781V215.99219a16.00521,16.00521,0,0,0,24.34375,13.64843L232.3125,141.65625a16.00727,16.00727,0,0,0,0-27.3125Z"></path></svg>
</button>
<div class="podcast-progression">
</div>
<span class="podcast-time">23:45</span>
</div>
<div class="article-podcast-info">
Apple Podcasts, Google Podcasts, Stitcher
</div>
<div class="article-author">
<div class="article-author-img">
<img src="https://assets.codepen.io/285131/author-3.png" />
</div>
<div class="article-author-info">
<dl>
<dt>David Sherof</dt>
<dd>Reporter</dd>
</dl>
</div>
</div>
</article>
</div>
</div>
</main>
</div>
</template>
<script>
import { onAuthUIStateChange } from '@aws-amplify/ui-components'
import { Auth,API } from 'aws-amplify';
import {onCreatePost} from '../src/graphql/subscriptions';
import {getUser,listPosts} from '../src/graphql/queries';
export default {
data() {
return { user: Object,
posts:[],
authState: undefined,
signedIn:false }
},
async beforeCreate() {
try {
const user = await Auth.currentAuthenticatedUser();
console.log(user.attributes.sub);
this.getUserDetails(user.attributes.sub);
this.signedIn = true
} catch (err) {
this.signedIn = false
}
},
created() {
this.getPosts();
//this.subscribe();
},
methods: {
async getUserDetails(userId){
const userData = await API.graphql({
query:getUser,
variables:{id: userId}
});
this.user =userData.data.getUser;
console.log(this.user);
},
signUserOut(){
console.log("clicked sign out");
Auth.signOut();
this.$router.push({name:'loginpage'});
} ,
async getPosts(){
const posts = await API.graphql({
query:listPosts
});
this.posts = posts.data.listPosts.items;
console.log(posts.data.listPosts);
},
},
}
</script>
<style lang="scss" scoped>
.profile-pic{
object-fit: cover;
width: 40px;
height: 40px;
}
</style>
Firstly, we determine the authentication state of the application. We check to see if any user is currectly signed in .
try {
const user = await Auth.currentAuthenticatedUser();
console.log(user.attributes.sub);
this.getUserDetails(user.attributes.sub);
this.signedIn = true
} catch (err) {
this.signedIn = false
}
},
If true, we display the logout button while getting the user details. If false, we display the login button.
We also retrieve and display the list of posts in our posts table from the dynamoDB.
async getPosts(){
const posts = await API.graphql({
query:listPosts
});
this.posts = posts.data.listPosts.items;
console.log(posts.data.listPosts);
},
},
Thanks for taking out the time to check this out. I really appreciate it.
I'll love to know what you think about this piece.
Stay Safe ❤️