Going Serverless with VueJS Divya Sasidharan

HELLO MY NAME IS Hellot Divya Sasidharan

HELLO MY NAME IS Divya Sasidharan Hellot @shortdiv

The Ordinary World The call to adventure Refusal Meeting the Mentor Crossing the threshold Tests, Allies, Enemies Innermost Cave Ordeal Reward The road back Resurrection Return with the Elixir

The Ordinary World The call to adventure Refusal Meeting the Mentor Crossing the threshold Tests, Allies, Enemies Innermost Cave Ordeal Reward The road back Resurrection Return with the Elixir DEPARTURE

The Ordinary World The call to adventure Refusal DEPARTURE Meeting the Mentor Crossing the threshold Tests, Allies, Enemies Innermost Cave Ordeal Reward The road back Resurrection Return with the Elixir INITIATION

The Ordinary World The call to adventure Refusal DEPARTURE Meeting the Mentor Crossing the threshold Tests, Allies, Enemies Innermost Cave INITIATION Ordeal Reward The road back Resurrection Return with the Elixir RETURN

Act 1 Departure

The Ordinary World DATE SCENE ACT 04/30 1 1

The Ordinary World DATE SCENE ACT 04/30 1 1

The Ordinary World DATE SCENE ACT 04/30 1 1

✅ Sammy Serverus

Call to Adventure DATE SCENE ACT 04/30 2 1

Call to Adventure DATE SCENE ACT 04/30 2 1

Call to Adventure DATE SCENE ACT 04/30 2 1

Orange News 1. Some news (github.com) 100000 points by somedude 3 hours ago | hide | 100000 comments 2. Some news (github.com) 100000 points by somedude 3 hours ago | hide | 1 comments 3. Some news (github.com) 100000 points by somedude 3 hours ago | hide | 100000 comments 4. Some news (github.com) 100000 points by somedude 3 hours ago | hide | 100000 comments 5. Some news (github.com) 100000 points by somedude 3 hours ago | hide | 100000 comments 6. 7. 8. 9. 10. 11. 12. 13. Some news (github.com) 100000 points by somedude 3 hours ago | hide | 100000 comments Some news (github.com) 100000 points by somedude 3 hours ago | hide | 100000 comments Some news (github.com) 100000 points by somedude 3 hours ago | hide | 100000 comments Some news (github.com) 100000 points by somedude 3 hours ago | hide | 100000 comments Some news (github.com) 100000 points by somedude 3 hours ago | hide | 100000 comments Some news (github.com) 100000 points by somedude 3 hours ago | hide | 100000 comments Some news (github.com) 100000 points by somedude 3 hours ago | hide | 100000 comments Some news (github.com) 100000 points by somedude 3 hours ago | hide | 100000 comments 14. Some news (github.com) 100000 points by somedude 3 hours ago | hide | 100000 comments 15. Some news (github.com)

chipie.com

Refusal DATE SCENE ACT 04/30 3 1

Refusal DATE SCENE ACT 04/30 3 1

Refusal DATE SCENE ACT 04/30 3 1

We’re gonna need a bigger server rack

😩😩😩😩

Meeting the Mentor DATE SCENE ACT 04/30 4 1

Meeting the Mentor DATE SCENE ACT 04/30 4 1

Meeting the Mentor DATE SCENE ACT 04/30 4 1

Swami Scale-a-lot

Man who run servers without servers, accomplish many things

Crossing the Threshold DATE SCENE ACT 04/30 5 1

Crossing the Threshold DATE SCENE ACT 04/30 5 1

Crossing the Threshold DATE SCENE ACT 04/30 5 1

Act 2 Initiation

Tests, Allies, Enemies DATE SCENE ACT 04/30 1 2

Tests, Allies, Enemies DATE SCENE ACT 04/30 1 2

Tests, Allies, Enemies DATE SCENE ACT 04/30 1 2

|—————-| | THERE | | ARE | | STILL | | SERVERS | | IN | | SERVERLESS| |—————-| (__/) || (•ㅅ•) || / づ

Innermost Cave DATE SCENE ACT 04/30 2 2

Innermost Cave DATE SCENE ACT 04/30 2 2

Innermost Cave DATE SCENE ACT 04/30 2 2

1 const express = require(‘express’), yelp = require(‘yelp-fusion’), 2 port = 8000, 3 app = express(); 4 5 6 app.use((req, res, next) => { res.header(“Access-Control-Allow-Origin”, “*”); 7 res.header(“Access-Control-Allow-Headers”, “Origin, X-Requested 8 -with, Content-Type, Accept”); 9 next(); 10 }) 11 12 13 app.get(‘/yelpit/:term/:location’, (req, res) => { const clientId = CLIENT_ID; 14 const clientSecret = CLIENT_SECRET; 15 16 yelp.accessToken(clientId, clientSecret) 17 .then((response) => { 18 const client = yelp.client(response.jsonBody.access_token); 19 const searchRequest = { 20 term: req.params.term, 21 limit: 50, 22 location: req.params.location 23 } 24 client.search(searchRequest).then(response => { 25 const firstResult = response.jsonBody.businesses; 26 res.send(firstResult) 27

1 const express = require(‘express’), yelp = require(‘yelp-fusion’), 2 port = 8000, 3 app = express(); 4 5 6 app.use((req, res, next) => { res.header(“Access-Control-Allow-Origin”, “*”); 7 res.header(“Access-Control-Allow-Headers”, “Origin, X-Requested 8 -with, Content-Type, Accept”); 9 next(); 10 }) 11 12 13 app.get(‘/yelpit/:term/:location’, (req, res) => { const clientId = CLIENT_ID; 14 const clientSecret = CLIENT_SECRET; 15 16 yelp.accessToken(clientId, clientSecret) 17 .then((response) => { 18 const client = yelp.client(response.jsonBody.access_token); 19 const searchRequest = { 20 term: req.params.term, 21 limit: 50, 22 location: req.params.location 23 } 24 client.search(searchRequest).then(response => { 25 const firstResult = response.jsonBody.businesses; 26 res.send(firstResult) 27

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 }) app.get(‘/yelpit/:term/:location’, (req, res) => { const clientId = CLIENT_ID; const clientSecret = CLIENT_SECRET; }) yelp.accessToken(clientId, clientSecret) .then((response) => { const client = yelp.client(response.jsonBody.access_token); const searchRequest = { term: req.params.term, limit: 50, location: req.params.location } client.search(searchRequest).then(response => { const firstResult = response.jsonBody.businesses; res.send(firstResult) }) }) app.listen(port, ‘localhost’, function(err) { if(err) {console.log(err)} console.info(Listening on port ${port}) });

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 module.exports = { devServer: { proxy: { ‘^/yelpit/*’: { target: ‘http://localhost:8000/yelpit’, secure: false } } } }

The Chilling Adventures of Functions as a Service |—————-| | THERE | | ARE | | STILL | | SERVERS | | IN | | SERVERLESS| |—————-| (_/) || (•ㅅ•) || / づ |—————-| | THERE | | ARE | | STILL | | SERVERS | | IN | | SERVERLESS| |—————-| (_/) || (•ㅅ•) || / づ |—————-| | THERE | | ARE | | STILL | | SERVERS | | IN | | SERVERLESS| |—————-| (_/) || (•ㅅ•) || / づ |—————-| | THERE | | ARE | | STILL | | SERVERS | | IN | | SERVERLESS| |—————-| (_/) || (•ㅅ•) || / づ

exports.handler = function(event, context, callback) { console.log(event) callback(null, { statusCode: 200, body: JSON.stringify({ msg: “Hello, World!” }) }) } functions/hello.js

yarn add netlify-cli

netlify functions:create

[build] command = “yarn build” functions = “functions” publish = “dist” netlify.toml

/.netlify/functions/{function_name}

/.netlify/functions/hello

/.netlify/functions/yelp-it

1 const yelp = require(“yelp-fusion”); 2 3 exports.handler = function(event, context, callback) { const apiKey = “API_KEY”; 4 5 const client = yelp.client(apiKey); 6 7 const { term, location } = event.queryStringParameters; 8 9 const searchRequest = { 10 term: term, 11 location: location 12 }; 13 14 var fetchFromYelp = async function() { 15 try { 16 let res = await client.search(searchRequest); 17 callback(null, { 18 statusCode: 200, 19 body: JSON.stringify({ 20 results: res 21 }) 22 }); 23 } catch (err) { 24 callback(null, { 25 statusCode: 200, 26 body: JSON.stringify({ 27 err: err 28

9 const searchRequest = { 10 term: term, 11 location: location 12 }; 13 14 var fetchFromYelp = async function() { 15 try { 16 let res = await client.search(searchRequest); 17 callback(null, { 18 statusCode: 200, 19 body: JSON.stringify({ 20 results: res 21 }) 22 }); 23 } catch (err) { 24 callback(null, { 25 statusCode: 200, 26 body: JSON.stringify({ 27 err: err 28 }) 29 }); 30 } 31 }; 32 33 fetchFromYelp(); 34 35 }; 36

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template> <div class=“home”> <div id=”sidebar” v-if=”dataLoaded”> <Slider :yelpData=“yelpData.features” /> </div> </div> </template>

<script> export default { name: “home”, mounted() { axios .get(“/.netlify/functions/yelp-it”, { params: { location: “chicago,il”, term: “pizza” } }) .then(async res => { res = JSON.parse(res.data.results.body); const results = this.geojsonify(res.businesses); this.dataLoaded = true; this.yelpData = results; },

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template> <div class=“home”> <div id=”sidebar” v-if=”dataLoaded”> <Slider :yelpData=“yelpData.features” /> </div> </div> </template>

<script> export default { name: “home”, mounted() { axios .get(“/.netlify/functions/yelp-it”, { params: { location: “chicago,il”, term: “pizza” } }) .then(async res => { res = JSON.parse(res.data.results.body); const results = this.geojsonify(res.businesses); this.dataLoaded = true; this.yelpData = results; },

11 <script> 12 export default { 13 name: “home”, 14 mounted() { 15 axios 16 .get(“/.netlify/functions/yelp-it”, { 17 params: { 18 location: “chicago,il”, 19 term: “pizza” 20 } 21 22 }) 23 .then(async res => { 24 res = JSON.parse(res.data.results.body); 25 const results = this.geojsonify(res.businesses); 26 this.dataLoaded = true; 27 this.yelpData = results; 28 }, 29 methods: { 30 geojsonify(response) { 31 let features = []; 32 33 response.map(item => { 34 features.push({ 35 type: “Feature”, 36 geometry: { 37 type: “Point”, 38 coordinates: [item.coordinates.longitude, 39 item.coordinates.latitude] 40 },

reviews.js export const state = { … } export const mutations = { … } export const actions = { … } export const getters = { … } src/state/reviews.js

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 export const state = { yelpData: null }; export const mutations = { SET_YELP_REVIEWS(state, value) { state.yelpData = value }, } export const actions = { getYelp({ commit }, value) { var geojsonify = function(data) { … } } } axios .get(“/.netlify/functions/yelp-it”, { params: { location: “chicago,il”, term: “pizza” } }) .then(async res => { res = JSON.parse(res.data.results.body); const results = geojsonify(res.businesses); commit(“SET_YELP_REVIEWS”, res.docs) })

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 export const state = { yelpData: null }; export const mutations = { SET_YELP_REVIEWS(state, value) { state.yelpData = value } } export const actions = { getYelp({ commit }, value) { var geojsonify = function(data) { … } } } axios .get(“/.netlify/functions/yelp-it”, { params: { location: “chicago,il”, term: “pizza” } }) .then(async res => { res = JSON.parse(res.data.results.body); const results = geojsonify(res.businesses); commit(“SET_YELP_REVIEWS”, res.docs) })

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 export const state = { yelpData: null }; export const mutations = { SET_YELP_REVIEWS(state, value) { state.yelpData = value }, } export const actions = { getYelp({ commit }, value) { var geojsonify = function(data) { … } } } axios .get(“/.netlify/functions/yelp-it”, { params: { location: “chicago,il”, term: “pizza” } }) .then(async res => { res = JSON.parse(res.data.results.body); const results = geojsonify(res.businesses); commit(“SET_YELP_REVIEWS”, res.docs) })

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 export const state = { yelpData: null }; export const mutations = { SET_YELP_REVIEWS(state, value) { state.yelpData = value }, } export const actions = { getYelp({ commit }, value) { C var geojsonify = function(data) { … } } } axios .get(“/.netlify/functions/yelp-it”, { params: { location: “chicago,il”, term: “pizza” } }) .then(async res => { res = JSON.parse(res.data.results.body); const results = geojsonify(res.businesses); commit(“SET_YELP_REVIEWS”,Cres.docs) })

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 export const state = { yelpData: null }; export const mutations = { SET_YELP_REVIEWS(state, value) { state.yelpData = value C }, } export const actions = { getYelp({ commit }, value) { var geojsonify = function(data) { … } } } axios .get(“/.netlify/functions/yelp-it”, { params: { location: “chicago,il”, term: “pizza” } }) .then(async res => { res = JSON.parse(res.data.results.body); const results = geojsonify(res.businesses); commit(“SET_YELP_REVIEWS”, res.docs) })

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <template> <div class=“home”> <div id=”sidebar” v-if=”dataLoaded”> <Slider :yelpData=”yelpData.features” /> </div> </div> </template> <script> import { mapState, mapActions } from “vuex”; export default { name: “home”, computed: { …mapState(“yelp”, [“yelpData”]) }, methods: { …mapActions(“reviews”, [“getYelp”]), }, mounted() { this.getYelp() } } </script>

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <template> <div class=“home”> <div id=”sidebar” v-if=”dataLoaded”> <Slider :yelpData=”yelpData.features” /> </div> </div> </template> <script> import { mapState, mapActions } from “vuex”; export default { name: “home”, computed: { …mapState(“yelp”, [“yelpData”]) }, methods: { …mapActions(“reviews”, [“getYelp”]), }, mounted() { this.getYelp() } } </script>

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <template> <div class=“home”> <div id=”sidebar” v-if=”dataLoaded”> <Slider :yelpData=”yelpData.features” /> </div> </div> </template> <script> import { mapState, mapActions } from “vuex”; export default { name: “home”, computed: { …mapState(“yelp”, [“yelpData”]) }, methods: { …mapActions(“reviews”, [“getYelp”]), }, mounted() { this.getYelp() } } </script>

reviews.js export const state = { … } export const mutations = { … } export const actions = { … } export const getters = { … } src/state/reviews.js

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 firebase.initializeApp(config); const db = firebase.firestore(); export const state = { chiPieReviews: {} }; export const mutations = { SET_CHIPIE_REVIEWS(state, value) { var geojsonify = function() {} var t = {} value.forEach(val => { t[val.id] = val.data() }) debugger state.chiPieReviews = t } }; export const actions = { createRating({ commit }, value) { return new Promise((resolve, reject) => { var user = db .collection(“users”) .doc(value.id); user .collection(“pizza-places”) .doc(value.name)

74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 }, setRatings({ dispatch }, value) { return new Promise((resolve, reject) => { var user = db .collection(“users”) .doc(value.id); var docRef = user.collection(“pizza-places”).doc(value.name); docRef .get() .then(doc => { if (doc.exists) { resolve(“oh yeah”); dispatch(“updateRating”, value); } else { dispatch(“createRating”, value); resolve(“empty”); } }) .catch(err => { reject(err); }); }); }, getRatings({ commit }, value) { return new Promise((resolve, reject) => { var user = db .collection(“users”) .doc(value);

Ordeal DATE SCENE ACT 04/30 3 2

Ordeal DATE SCENE ACT 04/30 3 2

Ordeal DATE SCENE ACT 04/30 3 2

Netlify Identity GoTrue

auth.js export const state = { … } export const mutations = { … } export const actions = { … } export const getters = { … } src/state/auth.js

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import GoTrue from “gotrue-js”; import axios from “axios”; const auth = new GoTrue({ APIUrl: “https://chipie.netlify.com/.netlify/identity”, audience: “”, setCookie: false }); export const state = { currentUser: getSavedState(“auth.currentUser”), loading: false, token: null, notifications: [] }; export const mutations = { SET_CURRENT_USER(state, value) { state.currentUser = value; saveState(“auth.currentUser”, value); }, TOGGLE_LOAD(state) { state.loading = !state.loading; } }; export const getters = {

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import GoTrue from “gotrue-js”; C import axios from “axios”; const auth = new GoTrue({ APIUrl: “https://chipie.netlify.com/.netlify/identity”, audience: “”, C setCookie: false }); export const state = { currentUser: getSavedState(“auth.currentUser”), loading: false, token: null, notifications: [] }; export const mutations = { SET_CURRENT_USER(state, value) { state.currentUser = value; saveState(“auth.currentUser”, value); }, TOGGLE_LOAD(state) { state.loading = !state.loading; } }; export const getters = {

31 32 export const actions = { 33 init() { 34 localStorage.removeItem(“auth.currentUser”); 35 }, 36 validate({ commit, state }) { 37 if (!state.currentUser) return Promise.resolve(null); 38 const user = auth.currentUser(); 39 commit(“SET_CURRENT_USER”, user); 40 return user; 41 }, 42 attemptLogin({ commit, dispatch }, credentials) { 43 return new Promise((resolve, reject) => { 44 dispatch(“attemptConfirmation”, credentials).then(() => { 45 auth 46 .login(credentials.email, credentials.password) 47 .then(response => { 48 resolve(response); 49 commit(“SET_CURRENT_USER”, response); 50 }) 51 .catch(error => { 52 reject(error.json); 53 }); 54 }); 55 }); 56 }, 57 58 attemptConfirmation({ commit, dispatch }, credentials) { 59 return new Promise((resolve, reject) => { 60 if (!credentials.token) {

31 32 export const actions = { 33 init() { 34 localStorage.removeItem(“auth.currentUser”); 35 }, 36 validate({ commit, state }) { 37 if (!state.currentUser) return Promise.resolve(null); 38 const user = auth.currentUser(); 39 commit(“SET_CURRENT_USER”, user); 40 return user; 41 }, 42 attemptLogin({ commit, dispatch }, credentials) { 43 return new Promise((resolve, reject) => { 44 dispatch(“attemptConfirmation”, credentials).then(() => { C 45 auth C 46 .login(credentials.email, credentials.password) 47 .then(response => { 48 resolve(response); 49 commit(“SET_CURRENT_USER”, response); C 50 }) 51 .catch(error => { 52 reject(error.json); 53 }); 54 }); 55 }); 56 }, 57 58 attemptConfirmation({ commit, dispatch }, credentials) { 59 return new Promise((resolve, reject) => { 60 if (!credentials.token) {

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 export const mutations = { SET_CURRENT_USER(state, value)C{ state.currentUser = value; C saveState(“auth.currentUser”, value); }, TOGGLE_LOAD(state) { state.loading = !state.loading; } }; export const getters = { isLoggedIn(state) { return !!state.currentUser; } }; export const actions = { init() { localStorage.removeItem(“auth.currentUser”); }, validate({ commit, state }) { if (!state.currentUser) return Promise.resolve(null); const user = auth.currentUser(); commit(“SET_CURRENT_USER”, user); return user; }, attemptLogin({ commit, dispatch }, credentials) { return new Promise((resolve, reject) => {

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template> <div class=”login-screen”> <div class=”account-login”> <form @submit.prevent=”login()”> <label> <span>Email:</span> <input type=“text” placeholder=“name” v-model=“loginCreds.email” /> </label> <label> <span>Password:</span> <input type=”password” placeholder=”password” v-model=“loginCreds.password” /> </label> <button type=”submit” class=“account-button”>Login</button> </form> </div> </div> </template> <script> import { mapActions } from “vuex”;

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template> <div class=”login-screen”> <div class=”account-login”> <form @submit.prevent=”login()”> <label> <span>Email:</span> <input type=“text” placeholder=“name” v-model=“loginCreds.email” /> </label> <label> <span>Password:</span> <input type=”password” placeholder=”password” v-model=“loginCreds.password” /> </label> <button type=”submit” class=“account-button”>Login</button> </form> </div> </div> </template> <script> import { mapActions } from “vuex”;

26 27 <script> 28 import { mapActions } from “vuex”; 29 30 export default { name: “LoginAccount”, 31 data() { 32 return { 33 isNewUser: true, 34 loginCreds: { 35 email: null, 36 password: null 37 } 38 } 39 }, 40 methods: { 41 …mapActions(“auth”, [“attemptLogin”]), 42 transferToDashboard() { 43 this.$router.push(this.$route.query.redirect || “/”); 44 }, 45 login() { 46 let token = decodeURIComponent(window.location.search) 47 .substring(1) 48 .split(“confirmation_token=”)[1]; 49 this.attemptLogin({ token, …this.loginCreds }) 50 .then(res => { 51 this.transferToDashboard(); 52 }) 53 .catch(err => { 54 console.log(err);

40 }, 41 methods: { 42 …mapActions(“auth”, [“attemptLogin”]), 43 transferToDashboard() { 44 this.$router.push(this.$route.query.redirect || “/”); 45 }, 46 login() { 47 let token = decodeURIComponent(window.location.search) 48 .substring(1) 49 .split(“confirmation_token=”)[1]; 50 this.attemptLogin({ token, …this.loginCreds }) 51 .then(res => { 52 this.transferToDashboard(); 53 }) 54 .catch(err => { 55 console.log(err); 56 }); 57 }, 58 } 59 } 60 </script> 61 62 63

40 }, 41 methods: { 42 …mapActions(“auth”, [“attemptLogin”]), 43 transferToDashboard() { 44 this.$router.push(this.$route.query.redirect || “/”); 45 }, 46 login() { 47 let token = decodeURIComponent(window.location.search) 48 .substring(1) 49 .split(“confirmation_token=”)[1]; 50 this.attemptLogin({ token, …this.loginCreds }) 51 .then(res => { 52 this.transferToDashboard(); 53 console.log(res); 54 }) 55 .catch(err => { 56 console.log(err); 57 }); 58 }, 59 } 60 } 61 </script> 62 63

Reward DATE SCENE ACT 04/30 4 2

Reward DATE SCENE ACT 04/30 4 2

Reward DATE SCENE ACT 04/30 4 2

Password????

Act 3 Return

The Road Back DATE SCENE ACT 04/30 1 3

The Road Back DATE SCENE ACT 04/30 1 3

The Road Back DATE SCENE ACT 04/30 1 3

There is no cloud, just someone else’s server

Resurrection/ Return with Elixir DATE SCENE ACT 04/30 2 3

Resurrection/ Return with Elixir DATE SCENE ACT 04/30 2 3

Resurrection/ Return with Elixir DATE SCENE ACT 04/30 2 3

👏 👏 👏 👏 👏 👏 ⚡ ⚡ ⚡ 👏 👏 👏 👏

chipie.netlify.live

% github.com/shortdiv/chipie Divya Sasidharan @shortdiv